diff --git a/cmd/evm/blockrunner.go b/cmd/evm/blockrunner.go
index 0275c019bc..d5cd8d8e3d 100644
--- a/cmd/evm/blockrunner.go
+++ b/cmd/evm/blockrunner.go
@@ -86,7 +86,7 @@ func blockTestCmd(ctx *cli.Context) error {
continue
}
test := tests[name]
- if err := test.Run(false, rawdb.HashScheme, tracer, func(res error, chain *core.BlockChain) {
+ if err := test.Run(false, rawdb.HashScheme, false, tracer, func(res error, chain *core.BlockChain) {
if ctx.Bool(DumpFlag.Name) {
if state, _ := chain.State(); state != nil {
fmt.Println(string(state.Dump(nil)))
diff --git a/core/block_validator.go b/core/block_validator.go
index 3d49f4e6a3..75f7f8a94b 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -20,8 +20,10 @@ import (
"errors"
"fmt"
+ "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
@@ -34,14 +36,12 @@ import (
type BlockValidator struct {
config *params.ChainConfig // Chain configuration options
bc *BlockChain // Canonical block chain
- engine consensus.Engine // Consensus engine used for validating
}
// NewBlockValidator returns a new block validator which is safe for re-use
-func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engine consensus.Engine) *BlockValidator {
+func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain) *BlockValidator {
validator := &BlockValidator{
config: config,
- engine: engine,
bc: blockchain,
}
return validator
@@ -59,7 +59,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
// Header validity is known at this point. Here we verify that uncles, transactions
// and withdrawals given in the block body match the header.
header := block.Header()
- if err := v.engine.VerifyUncles(v.bc, block); err != nil {
+ if err := v.bc.engine.VerifyUncles(v.bc, block); err != nil {
return err
}
if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash {
@@ -121,7 +121,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
// ValidateState validates the various changes that happen after a state transition,
// such as amount of used gas, the receipt roots and the state root itself.
-func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64) error {
+func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64, stateless bool) error {
header := block.Header()
if block.GasUsed() != usedGas {
return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas)
@@ -132,6 +132,11 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
if rbloom != header.Bloom {
return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom)
}
+ // In stateless mode, return early because the receipt and state root are not
+ // provided through the witness, rather the cross validator needs to return it.
+ if stateless {
+ return nil
+ }
// The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]]))
receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil))
if receiptSha != header.ReceiptHash {
@@ -145,6 +150,28 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
return nil
}
+// ValidateWitness cross validates a block execution with stateless remote clients.
+//
+// Normally we'd distribute the block witness to remote cross validators, wait
+// for them to respond and then merge the results. For now, however, it's only
+// Geth, so do an internal stateless run.
+func (v *BlockValidator) ValidateWitness(witness *stateless.Witness, receiptRoot common.Hash, stateRoot common.Hash) error {
+ // Run the cross client stateless execution
+ // TODO(karalabe): Self-stateless for now, swap with other clients
+ crossReceiptRoot, crossStateRoot, err := ExecuteStateless(v.config, witness)
+ if err != nil {
+ return fmt.Errorf("stateless execution failed: %v", err)
+ }
+ // Stateless cross execution suceeeded, validate the withheld computed fields
+ if crossReceiptRoot != receiptRoot {
+ return fmt.Errorf("cross validator receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, receiptRoot)
+ }
+ if crossStateRoot != stateRoot {
+ return fmt.Errorf("cross validator state root mismatch (cross: %x local: %x)", crossStateRoot, stateRoot)
+ }
+ return nil
+}
+
// CalcGasLimit computes the gas limit of the next block after parent. It aims
// to keep the baseline gas close to the provided target, and increase it towards
// the target if the baseline gas is lower.
diff --git a/core/blockchain.go b/core/blockchain.go
index ac4eb1c47e..05ebfd18b8 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -37,6 +37,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/snapshot"
+ "github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -302,18 +303,18 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
vmConfig: vmConfig,
logger: vmConfig.Tracer,
}
- bc.flushInterval.Store(int64(cacheConfig.TrieTimeLimit))
- bc.forker = NewForkChoice(bc, shouldPreserve)
- bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb)
- bc.validator = NewBlockValidator(chainConfig, bc, engine)
- bc.prefetcher = newStatePrefetcher(chainConfig, bc, engine)
- bc.processor = NewStateProcessor(chainConfig, bc, engine)
-
var err error
bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped)
if err != nil {
return nil, err
}
+ bc.flushInterval.Store(int64(cacheConfig.TrieTimeLimit))
+ bc.forker = NewForkChoice(bc, shouldPreserve)
+ bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb)
+ bc.validator = NewBlockValidator(chainConfig, bc)
+ bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
+ bc.processor = NewStateProcessor(chainConfig, bc.hc)
+
bc.genesisBlock = bc.GetBlockByNumber(0)
if bc.genesisBlock == nil {
return nil, ErrNoGenesis
@@ -1809,7 +1810,14 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
// while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction.
if bc.chainConfig.IsByzantium(block.Number()) {
- statedb.StartPrefetcher("chain", !bc.vmConfig.EnableWitnessCollection)
+ var witness *stateless.Witness
+ if bc.vmConfig.EnableWitnessCollection {
+ witness, err = stateless.NewWitness(bc, block)
+ if err != nil {
+ return it.index, err
+ }
+ }
+ statedb.StartPrefetcher("chain", witness)
}
activeState = statedb
@@ -1924,11 +1932,18 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s
ptime := time.Since(pstart)
vstart := time.Now()
- if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil {
+ if err := bc.validator.ValidateState(block, statedb, receipts, usedGas, false); err != nil {
bc.reportBlock(block, receipts, err)
return nil, err
}
vtime := time.Since(vstart)
+
+ if witness := statedb.Witness(); witness != nil {
+ if err = bc.validator.ValidateWitness(witness, block.ReceiptHash(), block.Root()); err != nil {
+ bc.reportBlock(block, receipts, err)
+ return nil, fmt.Errorf("cross verification failed: %v", err)
+ }
+ }
proctime := time.Since(start) // processing + validation
// Update the metrics touched during block processing and validation
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index e4bc3e09a6..4f28c6f5e6 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -168,8 +168,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error {
blockchain.reportBlock(block, receipts, err)
return err
}
- err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas)
- if err != nil {
+ if err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas, false); err != nil {
blockchain.reportBlock(block, receipts, err)
return err
}
diff --git a/core/state/database.go b/core/state/database.go
index d71f8f34b6..d54417d2f9 100644
--- a/core/state/database.go
+++ b/core/state/database.go
@@ -125,6 +125,10 @@ type Trie interface {
// be created with new root and updated trie database for following usage
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet)
+ // Witness returns a set containing all trie nodes that have been accessed.
+ // The returned map could be nil if the witness is empty.
+ Witness() map[string]struct{}
+
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
// starts at the key after the given start key. And error will be returned
// if fails to create node iterator.
diff --git a/core/state/state_object.go b/core/state/state_object.go
index 5c1dab53dc..880b715b4b 100644
--- a/core/state/state_object.go
+++ b/core/state/state_object.go
@@ -323,11 +323,17 @@ func (s *stateObject) finalise() {
//
// It assumes all the dirty storage slots have been finalized before.
func (s *stateObject) updateTrie() (Trie, error) {
- // Short circuit if nothing changed, don't bother with hashing anything
+ // Short circuit if nothing was accessed, don't trigger a prefetcher warning
if len(s.uncommittedStorage) == 0 {
- return s.trie, nil
+ // Nothing was written, so we could stop early. Unless we have both reads
+ // and witness collection enabled, in which case we need to fetch the trie.
+ if s.db.witness == nil || len(s.originStorage) == 0 {
+ return s.trie, nil
+ }
}
- // Retrieve a pretecher populated trie, or fall back to the database
+ // Retrieve a pretecher populated trie, or fall back to the database. This will
+ // block until all prefetch tasks are done, which are needed for witnesses even
+ // for unmodified state objects.
tr := s.getPrefetchedTrie()
if tr != nil {
// Prefetcher returned a live trie, swap it out for the current one
@@ -341,6 +347,10 @@ func (s *stateObject) updateTrie() (Trie, error) {
return nil, err
}
}
+ // Short circuit if nothing changed, don't bother with hashing anything
+ if len(s.uncommittedStorage) == 0 {
+ return s.trie, nil
+ }
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
// in circumstances similar to the following:
//
diff --git a/core/state/statedb.go b/core/state/statedb.go
index 4f84d93d63..ac82a8e3e3 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot"
+ "github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
@@ -105,7 +106,7 @@ type StateDB struct {
// resurrection. The account value is tracked as the original value
// before the transition. This map is populated at the transaction
// boundaries.
- stateObjectsDestruct map[common.Address]*types.StateAccount
+ stateObjectsDestruct map[common.Address]*stateObject
// This map tracks the account mutations that occurred during the
// transition. Uncommitted mutations belonging to the same account
@@ -146,6 +147,9 @@ type StateDB struct {
validRevisions []revision
nextRevisionId int
+ // State witness if cross validation is needed
+ witness *stateless.Witness
+
// Measurements gathered during execution for debugging purposes
AccountReads time.Duration
AccountHashes time.Duration
@@ -177,7 +181,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error)
originalRoot: root,
snaps: snaps,
stateObjects: make(map[common.Address]*stateObject),
- stateObjectsDestruct: make(map[common.Address]*types.StateAccount),
+ stateObjectsDestruct: make(map[common.Address]*stateObject),
mutations: make(map[common.Address]*mutation),
logs: make(map[common.Hash][]*types.Log),
preimages: make(map[common.Hash][]byte),
@@ -200,14 +204,19 @@ func (s *StateDB) SetLogger(l *tracing.Hooks) {
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the
// state trie concurrently while the state is mutated so that when we reach the
// commit phase, most of the needed data is already hot.
-func (s *StateDB) StartPrefetcher(namespace string, noreads bool) {
+func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
+ // Terminate any previously running prefetcher
if s.prefetcher != nil {
s.prefetcher.terminate(false)
s.prefetcher.report()
s.prefetcher = nil
}
+ // Enable witness collection if requested
+ s.witness = witness
+
+ // If snapshots are enabled, start prefethers explicitly
if s.snap != nil {
- s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace, noreads)
+ s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace, witness == nil)
// With the switch to the Proof-of-Stake consensus algorithm, block production
// rewards are now handled at the consensus layer. Consequently, a block may
@@ -582,7 +591,6 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
start := time.Now()
acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes()))
s.SnapshotAccountReads += time.Since(start)
-
if err == nil {
if acc == nil {
return nil
@@ -683,7 +691,7 @@ func (s *StateDB) Copy() *StateDB {
hasher: crypto.NewKeccakState(),
originalRoot: s.originalRoot,
stateObjects: make(map[common.Address]*stateObject, len(s.stateObjects)),
- stateObjectsDestruct: maps.Clone(s.stateObjectsDestruct),
+ stateObjectsDestruct: make(map[common.Address]*stateObject, len(s.stateObjectsDestruct)),
mutations: make(map[common.Address]*mutation, len(s.mutations)),
dbErr: s.dbErr,
refund: s.refund,
@@ -703,10 +711,17 @@ func (s *StateDB) Copy() *StateDB {
snaps: s.snaps,
snap: s.snap,
}
+ if s.witness != nil {
+ state.witness = s.witness.Copy()
+ }
// Deep copy cached state objects.
for addr, obj := range s.stateObjects {
state.stateObjects[addr] = obj.deepCopy(state)
}
+ // Deep copy destructed state objects.
+ for addr, obj := range s.stateObjectsDestruct {
+ state.stateObjectsDestruct[addr] = obj.deepCopy(state)
+ }
// Deep copy the object state markers.
for addr, op := range s.mutations {
state.mutations[addr] = op.copy()
@@ -788,7 +803,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
// set indefinitely). Note only the first occurred self-destruct
// event is tracked.
if _, ok := s.stateObjectsDestruct[obj.address]; !ok {
- s.stateObjectsDestruct[obj.address] = obj.origin
+ s.stateObjectsDestruct[obj.address] = obj
}
} else {
obj.finalise()
@@ -846,9 +861,46 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
obj := s.stateObjects[addr] // closure for the task runner below
workers.Go(func() error {
obj.updateRoot()
+
+ // If witness building is enabled and the state object has a trie,
+ // gather the witnesses for its specific storage trie
+ if s.witness != nil && obj.trie != nil {
+ s.witness.AddState(obj.trie.Witness())
+ }
return nil
})
}
+ // If witness building is enabled, gather all the read-only accesses
+ if s.witness != nil {
+ // Pull in anything that has been accessed before destruction
+ for _, obj := range s.stateObjectsDestruct {
+ // Skip any objects that haven't touched their storage
+ if len(obj.originStorage) == 0 {
+ continue
+ }
+ if trie := obj.getPrefetchedTrie(); trie != nil {
+ s.witness.AddState(trie.Witness())
+ } else if obj.trie != nil {
+ s.witness.AddState(obj.trie.Witness())
+ }
+ }
+ // Pull in only-read and non-destructed trie witnesses
+ for _, obj := range s.stateObjects {
+ // Skip any objects that have been updated
+ if _, ok := s.mutations[obj.address]; ok {
+ continue
+ }
+ // Skip any objects that haven't touched their storage
+ if len(obj.originStorage) == 0 {
+ continue
+ }
+ if trie := obj.getPrefetchedTrie(); trie != nil {
+ s.witness.AddState(trie.Witness())
+ } else if obj.trie != nil {
+ s.witness.AddState(obj.trie.Witness())
+ }
+ }
+ }
workers.Wait()
s.StorageUpdates += time.Since(start)
@@ -904,7 +956,13 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// Track the amount of time wasted on hashing the account trie
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
- return s.trie.Hash()
+ hash := s.trie.Hash()
+
+ // If witness building is enabled, gather the account trie witness
+ if s.witness != nil {
+ s.witness.AddState(s.trie.Witness())
+ }
+ return hash
}
// SetTxContext sets the current transaction hash and index which are
@@ -1060,7 +1118,9 @@ func (s *StateDB) handleDestruction() (map[common.Hash]*accountDelete, []*trieno
buf = crypto.NewKeccakState()
deletes = make(map[common.Hash]*accountDelete)
)
- for addr, prev := range s.stateObjectsDestruct {
+ for addr, prevObj := range s.stateObjectsDestruct {
+ prev := prevObj.origin
+
// The account was non-existent, and it's marked as destructed in the scope
// of block. It can be either case (a) or (b) and will be interpreted as
// null->null state transition.
@@ -1239,7 +1299,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool) (*stateUpdate, error) {
// Clear all internal flags and update state root at the end.
s.mutations = make(map[common.Address]*mutation)
- s.stateObjectsDestruct = make(map[common.Address]*types.StateAccount)
+ s.stateObjectsDestruct = make(map[common.Address]*stateObject)
origin := s.originalRoot
s.originalRoot = root
@@ -1412,3 +1472,8 @@ func (s *StateDB) markUpdate(addr common.Address) {
func (s *StateDB) PointCache() *utils.PointCache {
return s.db.PointCache()
}
+
+// Witness retrieves the current state witness being collected.
+func (s *StateDB) Witness() *stateless.Witness {
+ return s.witness
+}
diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go
index ff867309de..31405fa078 100644
--- a/core/state_prefetcher.go
+++ b/core/state_prefetcher.go
@@ -19,7 +19,6 @@ package core
import (
"sync/atomic"
- "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -31,16 +30,14 @@ import (
// data from disk before the main block processor start executing.
type statePrefetcher struct {
config *params.ChainConfig // Chain configuration options
- bc *BlockChain // Canonical block chain
- engine consensus.Engine // Consensus engine used for block rewards
+ chain *HeaderChain // Canonical block chain
}
// newStatePrefetcher initialises a new statePrefetcher.
-func newStatePrefetcher(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *statePrefetcher {
+func newStatePrefetcher(config *params.ChainConfig, chain *HeaderChain) *statePrefetcher {
return &statePrefetcher{
config: config,
- bc: bc,
- engine: engine,
+ chain: chain,
}
}
@@ -51,7 +48,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
var (
header = block.Header()
gaspool = new(GasPool).AddGas(block.GasLimit())
- blockContext = NewEVMBlockContext(header, p.bc, nil)
+ blockContext = NewEVMBlockContext(header, p.chain, nil)
evm = vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg)
signer = types.MakeSigner(p.config, header.Number, header.Time)
)
diff --git a/core/state_processor.go b/core/state_processor.go
index 7166ed8bd8..c21f644f98 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -22,7 +22,6 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
@@ -37,16 +36,14 @@ import (
// StateProcessor implements Processor.
type StateProcessor struct {
config *params.ChainConfig // Chain configuration options
- bc *BlockChain // Canonical block chain
- engine consensus.Engine // Consensus engine used for block rewards
+ chain *HeaderChain // Canonical header chain
}
// NewStateProcessor initialises a new StateProcessor.
-func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *StateProcessor {
+func NewStateProcessor(config *params.ChainConfig, chain *HeaderChain) *StateProcessor {
return &StateProcessor{
config: config,
- bc: bc,
- engine: engine,
+ chain: chain,
}
}
@@ -73,10 +70,11 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
misc.ApplyDAOHardFork(statedb)
}
var (
- context = NewEVMBlockContext(header, p.bc, nil)
- vmenv = vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg)
+ context vm.BlockContext
signer = types.MakeSigner(p.config, header.Number, header.Time)
)
+ context = NewEVMBlockContext(header, p.chain, nil)
+ vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg)
if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb)
}
@@ -101,7 +99,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
return nil, nil, 0, errors.New("withdrawals before shanghai")
}
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
- p.engine.Finalize(p.bc, header, statedb, block.Body())
+ p.chain.engine.Finalize(p.chain, header, statedb, block.Body())
return receipts, allLogs, *usedGas, nil
}
diff --git a/core/stateless.go b/core/stateless.go
new file mode 100644
index 0000000000..4c7e6f3102
--- /dev/null
+++ b/core/stateless.go
@@ -0,0 +1,73 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package core
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/lru"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/stateless"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/ethereum/go-ethereum/triedb"
+)
+
+// ExecuteStateless runs a stateless execution based on a witness, verifies
+// everything it can locally and returns the two computed fields that need the
+// other side to explicitly check.
+//
+// This method is a bit of a sore thumb here, but:
+// - It cannot be placed in core/stateless, because state.New prodces a circular dep
+// - It cannot be placed outside of core, because it needs to construct a dud headerchain
+//
+// TODO(karalabe): Would be nice to resolve both issues above somehow and move it.
+func ExecuteStateless(config *params.ChainConfig, witness *stateless.Witness) (common.Hash, common.Hash, error) {
+ // Create and populate the state database to serve as the stateless backend
+ memdb := witness.MakeHashDB()
+
+ db, err := state.New(witness.Root(), state.NewDatabaseWithConfig(memdb, triedb.HashDefaults), nil)
+ if err != nil {
+ return common.Hash{}, common.Hash{}, err
+ }
+ // Create a blockchain that is idle, but can be used to access headers through
+ chain := &HeaderChain{
+ config: config,
+ chainDb: memdb,
+ headerCache: lru.NewCache[common.Hash, *types.Header](256),
+ engine: beacon.New(ethash.NewFaker()),
+ }
+ processor := NewStateProcessor(config, chain)
+ validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block
+
+ // Run the stateless blocks processing and self-validate certain fields
+ receipts, _, usedGas, err := processor.Process(witness.Block, db, vm.Config{})
+ if err != nil {
+ return common.Hash{}, common.Hash{}, err
+ }
+ if err = validator.ValidateState(witness.Block, db, receipts, usedGas, true); err != nil {
+ return common.Hash{}, common.Hash{}, err
+ }
+ // Almost everything validated, but receipt and state root needs to be returned
+ receiptRoot := types.DeriveSha(receipts, trie.NewStackTrie(nil))
+ stateRoot := db.IntermediateRoot(config.IsEIP158(witness.Block.Number()))
+
+ return receiptRoot, stateRoot, nil
+}
diff --git a/core/stateless/database.go b/core/stateless/database.go
new file mode 100644
index 0000000000..135da62193
--- /dev/null
+++ b/core/stateless/database.go
@@ -0,0 +1,60 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package stateless
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/ethdb"
+)
+
+// MakeHashDB imports tries, codes and block hashes from a witness into a new
+// hash-based memory db. We could eventually rewrite this into a pathdb, but
+// simple is better for now.
+func (w *Witness) MakeHashDB() ethdb.Database {
+ var (
+ memdb = rawdb.NewMemoryDatabase()
+ hasher = crypto.NewKeccakState()
+ hash = make([]byte, 32)
+ )
+ // Inject all the "block hashes" (i.e. headers) into the ephemeral database
+ for _, header := range w.Headers {
+ rawdb.WriteHeader(memdb, header)
+ }
+ // Inject all the bytecodes into the ephemeral database
+ for code := range w.Codes {
+ blob := []byte(code)
+
+ hasher.Reset()
+ hasher.Write(blob)
+ hasher.Read(hash)
+
+ rawdb.WriteCode(memdb, common.BytesToHash(hash), blob)
+ }
+ // Inject all the MPT trie nodes into the ephemeral database
+ for node := range w.State {
+ blob := []byte(node)
+
+ hasher.Reset()
+ hasher.Write(blob)
+ hasher.Read(hash)
+
+ rawdb.WriteLegacyTrieNode(memdb, common.BytesToHash(hash), blob)
+ }
+ return memdb
+}
diff --git a/core/stateless/encoding.go b/core/stateless/encoding.go
new file mode 100644
index 0000000000..2b7245d377
--- /dev/null
+++ b/core/stateless/encoding.go
@@ -0,0 +1,129 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package stateless
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "slices"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+//go:generate go run github.com/fjl/gencodec -type extWitness -field-override extWitnessMarshalling -out gen_encoding_json.go
+
+// toExtWitness converts our internal witness representation to the consensus one.
+func (w *Witness) toExtWitness() *extWitness {
+ ext := &extWitness{
+ Block: w.Block,
+ Headers: w.Headers,
+ }
+ ext.Codes = make([][]byte, 0, len(w.Codes))
+ for code := range w.Codes {
+ ext.Codes = append(ext.Codes, []byte(code))
+ }
+ slices.SortFunc(ext.Codes, bytes.Compare)
+
+ ext.State = make([][]byte, 0, len(w.State))
+ for node := range w.State {
+ ext.State = append(ext.State, []byte(node))
+ }
+ slices.SortFunc(ext.State, bytes.Compare)
+ return ext
+}
+
+// fromExtWitness converts the consensus witness format into our internal one.
+func (w *Witness) fromExtWitness(ext *extWitness) error {
+ w.Block, w.Headers = ext.Block, ext.Headers
+
+ w.Codes = make(map[string]struct{}, len(ext.Codes))
+ for _, code := range ext.Codes {
+ w.Codes[string(code)] = struct{}{}
+ }
+ w.State = make(map[string]struct{}, len(ext.State))
+ for _, node := range ext.State {
+ w.State[string(node)] = struct{}{}
+ }
+ return w.sanitize()
+}
+
+// MarshalJSON marshals a witness as JSON.
+func (w *Witness) MarshalJSON() ([]byte, error) {
+ return w.toExtWitness().MarshalJSON()
+}
+
+// EncodeRLP serializes a witness as RLP.
+func (w *Witness) EncodeRLP(wr io.Writer) error {
+ return rlp.Encode(wr, w.toExtWitness())
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (w *Witness) UnmarshalJSON(input []byte) error {
+ var ext extWitness
+ if err := ext.UnmarshalJSON(input); err != nil {
+ return err
+ }
+ return w.fromExtWitness(&ext)
+}
+
+// DecodeRLP decodes a witness from RLP.
+func (w *Witness) DecodeRLP(s *rlp.Stream) error {
+ var ext extWitness
+ if err := s.Decode(&ext); err != nil {
+ return err
+ }
+ return w.fromExtWitness(&ext)
+}
+
+// sanitize checks for some mandatory fields in the witness after decoding so
+// the rest of the code can assume invariants and doesn't have to deal with
+// corrupted data.
+func (w *Witness) sanitize() error {
+ // Verify that the "parent" header (i.e. index 0) is available, and is the
+ // true parent of the block-to-be executed, since we use that to link the
+ // current block to the pre-state.
+ if len(w.Headers) == 0 {
+ return errors.New("parent header (for pre-root hash) missing")
+ }
+ for i, header := range w.Headers {
+ if header == nil {
+ return fmt.Errorf("witness header nil at position %d", i)
+ }
+ }
+ if w.Headers[0].Hash() != w.Block.ParentHash() {
+ return fmt.Errorf("parent hash different: witness %v, block parent %v", w.Headers[0].Hash(), w.Block.ParentHash())
+ }
+ return nil
+}
+
+// extWitness is a witness RLP encoding for transferring across clients.
+type extWitness struct {
+ Block *types.Block `json:"block" gencodec:"required"`
+ Headers []*types.Header `json:"headers" gencodec:"required"`
+ Codes [][]byte `json:"codes"`
+ State [][]byte `json:"state"`
+}
+
+// extWitnessMarshalling defines the hex marshalling types for a witness.
+type extWitnessMarshalling struct {
+ Codes []hexutil.Bytes
+ State []hexutil.Bytes
+}
diff --git a/core/stateless/gen_encoding_json.go b/core/stateless/gen_encoding_json.go
new file mode 100644
index 0000000000..1d0497976e
--- /dev/null
+++ b/core/stateless/gen_encoding_json.go
@@ -0,0 +1,74 @@
+// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
+
+package stateless
+
+import (
+ "encoding/json"
+ "errors"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+var _ = (*extWitnessMarshalling)(nil)
+
+// MarshalJSON marshals as JSON.
+func (e extWitness) MarshalJSON() ([]byte, error) {
+ type extWitness struct {
+ Block *types.Block `json:"block" gencodec:"required"`
+ Headers []*types.Header `json:"headers" gencodec:"required"`
+ Codes []hexutil.Bytes `json:"codes"`
+ State []hexutil.Bytes `json:"state"`
+ }
+ var enc extWitness
+ enc.Block = e.Block
+ enc.Headers = e.Headers
+ if e.Codes != nil {
+ enc.Codes = make([]hexutil.Bytes, len(e.Codes))
+ for k, v := range e.Codes {
+ enc.Codes[k] = v
+ }
+ }
+ if e.State != nil {
+ enc.State = make([]hexutil.Bytes, len(e.State))
+ for k, v := range e.State {
+ enc.State[k] = v
+ }
+ }
+ return json.Marshal(&enc)
+}
+
+// UnmarshalJSON unmarshals from JSON.
+func (e *extWitness) UnmarshalJSON(input []byte) error {
+ type extWitness struct {
+ Block *types.Block `json:"block" gencodec:"required"`
+ Headers []*types.Header `json:"headers" gencodec:"required"`
+ Codes []hexutil.Bytes `json:"codes"`
+ State []hexutil.Bytes `json:"state"`
+ }
+ var dec extWitness
+ if err := json.Unmarshal(input, &dec); err != nil {
+ return err
+ }
+ if dec.Block == nil {
+ return errors.New("missing required field 'block' for extWitness")
+ }
+ e.Block = dec.Block
+ if dec.Headers == nil {
+ return errors.New("missing required field 'headers' for extWitness")
+ }
+ e.Headers = dec.Headers
+ if dec.Codes != nil {
+ e.Codes = make([][]byte, len(dec.Codes))
+ for k, v := range dec.Codes {
+ e.Codes[k] = v
+ }
+ }
+ if dec.State != nil {
+ e.State = make([][]byte, len(dec.State))
+ for k, v := range dec.State {
+ e.State[k] = v
+ }
+ }
+ return nil
+}
diff --git a/core/stateless/witness.go b/core/stateless/witness.go
new file mode 100644
index 0000000000..7622c5eb61
--- /dev/null
+++ b/core/stateless/witness.go
@@ -0,0 +1,159 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package stateless
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "maps"
+ "slices"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// HeaderReader is an interface to pull in headers in place of block hashes for
+// the witness.
+type HeaderReader interface {
+ // GetHeader retrieves a block header from the database by hash and number,
+ GetHeader(hash common.Hash, number uint64) *types.Header
+}
+
+// Witness encompasses a block, state and any other chain data required to apply
+// a set of transactions and derive a post state/receipt root.
+type Witness struct {
+ Block *types.Block // Current block with rootHash and receiptHash zeroed out
+ Headers []*types.Header // Past headers in reverse order (0=parent, 1=parent's-parent, etc). First *must* be set.
+ Codes map[string]struct{} // Set of bytecodes ran or accessed
+ State map[string]struct{} // Set of MPT state trie nodes (account and storage together)
+
+ chain HeaderReader // Chain reader to convert block hash ops to header proofs
+ lock sync.Mutex // Lock to allow concurrent state insertions
+}
+
+// NewWitness creates an empty witness ready for population.
+func NewWitness(chain HeaderReader, block *types.Block) (*Witness, error) {
+ // Zero out the result fields to avoid accidentally sending them to the verifier
+ header := block.Header()
+ header.Root = common.Hash{}
+ header.ReceiptHash = common.Hash{}
+
+ // Retrieve the parent header, which will *always* be included to act as a
+ // trustless pre-root hash container
+ parent := chain.GetHeader(block.ParentHash(), block.NumberU64()-1)
+ if parent == nil {
+ return nil, errors.New("failed to retrieve parent header")
+ }
+ // Create the wtness with a reconstructed gutted out block
+ return &Witness{
+ Block: types.NewBlockWithHeader(header).WithBody(*block.Body()),
+ Codes: make(map[string]struct{}),
+ State: make(map[string]struct{}),
+ Headers: []*types.Header{parent},
+ chain: chain,
+ }, nil
+}
+
+// AddBlockHash adds a "blockhash" to the witness with the designated offset from
+// chain head. Under the hood, this method actually pulls in enough headers from
+// the chain to cover the block being added.
+func (w *Witness) AddBlockHash(number uint64) {
+ // Keep pulling in headers until this hash is populated
+ for int(w.Block.NumberU64()-number) > len(w.Headers) {
+ tail := w.Block.Header()
+ if len(w.Headers) > 0 {
+ tail = w.Headers[len(w.Headers)-1]
+ }
+ w.Headers = append(w.Headers, w.chain.GetHeader(tail.ParentHash, tail.Number.Uint64()-1))
+ }
+}
+
+// AddCode adds a bytecode blob to the witness.
+func (w *Witness) AddCode(code []byte) {
+ if len(code) == 0 {
+ return
+ }
+ w.Codes[string(code)] = struct{}{}
+}
+
+// AddState inserts a batch of MPT trie nodes into the witness.
+func (w *Witness) AddState(nodes map[string]struct{}) {
+ if len(nodes) == 0 {
+ return
+ }
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ for node := range nodes {
+ w.State[node] = struct{}{}
+ }
+}
+
+// Copy deep-copies the witness object. Witness.Block isn't deep-copied as it
+// is never mutated by Witness
+func (w *Witness) Copy() *Witness {
+ return &Witness{
+ Block: w.Block,
+ Headers: slices.Clone(w.Headers),
+ Codes: maps.Clone(w.Codes),
+ State: maps.Clone(w.State),
+ }
+}
+
+// String prints a human-readable summary containing the total size of the
+// witness and the sizes of the underlying components
+func (w *Witness) String() string {
+ blob, _ := rlp.EncodeToBytes(w)
+ bytesTotal := len(blob)
+
+ blob, _ = rlp.EncodeToBytes(w.Block)
+ bytesBlock := len(blob)
+
+ bytesHeaders := 0
+ for _, header := range w.Headers {
+ blob, _ = rlp.EncodeToBytes(header)
+ bytesHeaders += len(blob)
+ }
+ bytesCodes := 0
+ for code := range w.Codes {
+ bytesCodes += len(code)
+ }
+ bytesState := 0
+ for node := range w.State {
+ bytesState += len(node)
+ }
+ buf := new(bytes.Buffer)
+
+ fmt.Fprintf(buf, "Witness #%d: %v\n", w.Block.Number(), common.StorageSize(bytesTotal))
+ fmt.Fprintf(buf, " block (%4d txs): %10v\n", len(w.Block.Transactions()), common.StorageSize(bytesBlock))
+ fmt.Fprintf(buf, "%4d headers: %10v\n", len(w.Headers), common.StorageSize(bytesHeaders))
+ fmt.Fprintf(buf, "%4d trie nodes: %10v\n", len(w.State), common.StorageSize(bytesState))
+ fmt.Fprintf(buf, "%4d codes: %10v\n", len(w.Codes), common.StorageSize(bytesCodes))
+
+ return buf.String()
+}
+
+// Root returns the pre-state root from the first header.
+//
+// Note, this method will panic in case of a bad witness (but RLP decoding will
+// sanitize it and fail before that).
+func (w *Witness) Root() common.Hash {
+ return w.Headers[0].Root
+}
diff --git a/core/types.go b/core/types.go
index 36eb0d1ded..dc13de52ce 100644
--- a/core/types.go
+++ b/core/types.go
@@ -19,7 +19,9 @@ package core
import (
"sync/atomic"
+ "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
)
@@ -33,7 +35,10 @@ type Validator interface {
// ValidateState validates the given statedb and optionally the receipts and
// gas used.
- ValidateState(block *types.Block, state *state.StateDB, receipts types.Receipts, usedGas uint64) error
+ ValidateState(block *types.Block, state *state.StateDB, receipts types.Receipts, usedGas uint64, stateless bool) error
+
+ // ValidateWitness cross validates a block execution with stateless remote clients.
+ ValidateWitness(witness *stateless.Witness, receiptRoot common.Hash, stateRoot common.Hash) error
}
// Prefetcher is an interface for pre-caching transaction signatures and state.
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 26af0ea041..1944189b5d 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -231,6 +231,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
code := evm.StateDB.GetCode(addr)
+ if witness := evm.StateDB.Witness(); witness != nil {
+ witness.AddCode(code)
+ }
if len(code) == 0 {
ret, err = nil, nil // gas is unchanged
} else {
@@ -298,6 +301,9 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, AccountRef(caller.Address()), value, gas)
+ if witness := evm.StateDB.Witness(); witness != nil {
+ witness.AddCode(evm.StateDB.GetCode(addrCopy))
+ }
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
@@ -345,6 +351,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate()
+ if witness := evm.StateDB.Witness(); witness != nil {
+ witness.AddCode(evm.StateDB.GetCode(addrCopy))
+ }
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
@@ -400,6 +409,9 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, AccountRef(addrCopy), new(uint256.Int), gas)
+ if witness := evm.StateDB.Witness(); witness != nil {
+ witness.AddCode(evm.StateDB.GetCode(addrCopy))
+ }
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
diff --git a/core/vm/instructions.go b/core/vm/instructions.go
index 10cdd72e0c..9ec4544643 100644
--- a/core/vm/instructions.go
+++ b/core/vm/instructions.go
@@ -340,6 +340,10 @@ func opReturnDataCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeConte
func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
slot := scope.Stack.peek()
+ address := slot.Bytes20()
+ if witness := interpreter.evm.StateDB.Witness(); witness != nil {
+ witness.AddCode(interpreter.evm.StateDB.GetCode(address))
+ }
slot.SetUint64(uint64(interpreter.evm.StateDB.GetCodeSize(slot.Bytes20())))
return nil, nil
}
@@ -378,7 +382,11 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext)
uint64CodeOffset = math.MaxUint64
}
addr := common.Address(a.Bytes20())
- codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64())
+ code := interpreter.evm.StateDB.GetCode(addr)
+ if witness := interpreter.evm.StateDB.Witness(); witness != nil {
+ witness.AddCode(code)
+ }
+ codeCopy := getData(code, uint64CodeOffset, length.Uint64())
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)
return nil, nil
@@ -443,7 +451,11 @@ func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) (
lower = upper - 256
}
if num64 >= lower && num64 < upper {
- num.SetBytes(interpreter.evm.Context.GetHash(num64).Bytes())
+ res := interpreter.evm.Context.GetHash(num64)
+ if witness := interpreter.evm.StateDB.Witness(); witness != nil {
+ witness.AddBlockHash(num64)
+ }
+ num.SetBytes(res[:])
} else {
num.Clear()
}
diff --git a/core/vm/interface.go b/core/vm/interface.go
index 8b2c58898e..5f42643565 100644
--- a/core/vm/interface.go
+++ b/core/vm/interface.go
@@ -20,6 +20,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
@@ -87,6 +88,8 @@ type StateDB interface {
AddLog(*types.Log)
AddPreimage(common.Hash, []byte)
+
+ Witness() *stateless.Witness
}
// CallContext provides a basic interface for the EVM calling conventions. The EVM
diff --git a/tests/block_test.go b/tests/block_test.go
index 1ba84f5f24..52184eb274 100644
--- a/tests/block_test.go
+++ b/tests/block_test.go
@@ -26,11 +26,11 @@ import (
func TestBlockchain(t *testing.T) {
bt := new(testMatcher)
- // General state tests are 'exported' as blockchain tests, but we can run them natively.
- // For speedier CI-runs, the line below can be uncommented, so those are skipped.
- // For now, in hardfork-times (Berlin), we run the tests both as StateTests and
- // as blockchain tests, since the latter also covers things like receipt root
- bt.skipLoad(`^GeneralStateTests/`)
+
+ // We are running most of GeneralStatetests to tests witness support, even
+ // though they are ran as state tests too. Still, the performance tests are
+ // less about state andmore about EVM number crunching, so skip those.
+ bt.skipLoad(`^GeneralStateTests/VMTests/vmPerformance`)
// Skip random failures due to selfish mining test
bt.skipLoad(`.*bcForgedTest/bcForkUncle\.json`)
@@ -70,33 +70,25 @@ func TestExecutionSpecBlocktests(t *testing.T) {
}
func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) {
- // If -short flag is used, we don't execute all four permutations, only one.
- executionMask := 0xf
+ // Define all the different flag combinations we should run the tests with,
+ // picking only one for short tests.
+ //
+ // Note, witness building and self-testing is always enabled as it's a very
+ // good test to ensure that we don't break it.
+ var (
+ snapshotConf = []bool{false, true}
+ dbschemeConf = []string{rawdb.HashScheme, rawdb.PathScheme}
+ )
if testing.Short() {
- executionMask = (1 << (rand.Int63() & 4))
- }
- if executionMask&0x1 != 0 {
- if err := bt.checkFailure(t, test.Run(false, rawdb.HashScheme, nil, nil)); err != nil {
- t.Errorf("test in hash mode without snapshotter failed: %v", err)
- return
- }
- }
- if executionMask&0x2 != 0 {
- if err := bt.checkFailure(t, test.Run(true, rawdb.HashScheme, nil, nil)); err != nil {
- t.Errorf("test in hash mode with snapshotter failed: %v", err)
- return
- }
- }
- if executionMask&0x4 != 0 {
- if err := bt.checkFailure(t, test.Run(false, rawdb.PathScheme, nil, nil)); err != nil {
- t.Errorf("test in path mode without snapshotter failed: %v", err)
- return
- }
+ snapshotConf = []bool{snapshotConf[rand.Int()%2]}
+ dbschemeConf = []string{dbschemeConf[rand.Int()%2]}
}
- if executionMask&0x8 != 0 {
- if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil, nil)); err != nil {
- t.Errorf("test in path mode with snapshotter failed: %v", err)
- return
+ for _, snapshot := range snapshotConf {
+ for _, dbscheme := range dbschemeConf {
+ if err := bt.checkFailure(t, test.Run(snapshot, dbscheme, true, nil, nil)); err != nil {
+ t.Errorf("test with config {snapshotter:%v, scheme:%v} failed: %v", snapshot, dbscheme, err)
+ return
+ }
}
}
}
diff --git a/tests/block_test_util.go b/tests/block_test_util.go
index 04a04fdc28..62aa582c82 100644
--- a/tests/block_test_util.go
+++ b/tests/block_test_util.go
@@ -110,7 +110,7 @@ type btHeaderMarshaling struct {
ExcessBlobGas *math.HexOrDecimal64
}
-func (t *BlockTest) Run(snapshotter bool, scheme string, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) {
+func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) {
config, ok := Forks[t.json.Network]
if !ok {
return UnsupportedForkError{t.json.Network}
@@ -151,7 +151,8 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer *tracing.Hooks,
cache.SnapshotWait = true
}
chain, err := core.NewBlockChain(db, cache, gspec, nil, engine, vm.Config{
- Tracer: tracer,
+ Tracer: tracer,
+ EnableWitnessCollection: witness,
}, nil, nil)
if err != nil {
return err
diff --git a/trie/secure_trie.go b/trie/secure_trie.go
index cfa7f0bddb..3572117e03 100644
--- a/trie/secure_trie.go
+++ b/trie/secure_trie.go
@@ -214,6 +214,11 @@ func (t *StateTrie) GetKey(shaKey []byte) []byte {
return t.db.Preimage(common.BytesToHash(shaKey))
}
+// Witness returns a set containing all trie nodes that have been accessed.
+func (t *StateTrie) Witness() map[string]struct{} {
+ return t.trie.Witness()
+}
+
// Commit collects all dirty nodes in the trie and replaces them with the
// corresponding node hash. All collected nodes (including dirty leaves if
// collectLeaf is true) will be encapsulated into a nodeset for return.
diff --git a/trie/trie.go b/trie/trie.go
index e1a9201108..f44e10b918 100644
--- a/trie/trie.go
+++ b/trie/trie.go
@@ -661,6 +661,18 @@ func (t *Trie) hashRoot() (node, node) {
return hashed, cached
}
+// Witness returns a set containing all trie nodes that have been accessed.
+func (t *Trie) Witness() map[string]struct{} {
+ if len(t.tracer.accessList) == 0 {
+ return nil
+ }
+ witness := make(map[string]struct{})
+ for _, node := range t.tracer.accessList {
+ witness[string(node)] = struct{}{}
+ }
+ return witness
+}
+
// Reset drops the referenced root node and cleans all internal state.
func (t *Trie) Reset() {
t.root = nil
diff --git a/trie/verkle.go b/trie/verkle.go
index 1ea23186f9..94a5ca9a2c 100644
--- a/trie/verkle.go
+++ b/trie/verkle.go
@@ -369,3 +369,8 @@ func (t *VerkleTrie) ToDot() string {
func (t *VerkleTrie) nodeResolver(path []byte) ([]byte, error) {
return t.reader.node(path, common.Hash{})
}
+
+// Witness returns a set containing all trie nodes that have been accessed.
+func (t *VerkleTrie) Witness() map[string]struct{} {
+ panic("not implemented")
+}