all: stateless witness builder and (self-)cross validator (#29719)

* all: add stateless verifications

* all: simplify witness and integrate it into live geth

---------

Co-authored-by: Péter Szilágyi <peterke@gmail.com>
pull/30071/head
jwasinger 5 months ago committed by GitHub
parent 73f7e7c087
commit ed8fd0ac09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      cmd/evm/blockrunner.go
  2. 37
      core/block_validator.go
  3. 33
      core/blockchain.go
  4. 3
      core/blockchain_test.go
  5. 4
      core/state/database.go
  6. 14
      core/state/state_object.go
  7. 85
      core/state/statedb.go
  8. 11
      core/state_prefetcher.go
  9. 16
      core/state_processor.go
  10. 73
      core/stateless.go
  11. 60
      core/stateless/database.go
  12. 129
      core/stateless/encoding.go
  13. 74
      core/stateless/gen_encoding_json.go
  14. 159
      core/stateless/witness.go
  15. 7
      core/types.go
  16. 12
      core/vm/evm.go
  17. 16
      core/vm/instructions.go
  18. 3
      core/vm/interface.go
  19. 48
      tests/block_test.go
  20. 3
      tests/block_test_util.go
  21. 5
      trie/secure_trie.go
  22. 12
      trie/trie.go
  23. 5
      trie/verkle.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)))

@ -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.

@ -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

@ -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
}

@ -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.

@ -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 {
// 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:
//

@ -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
}

@ -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)
)

@ -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
}

@ -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 <http://www.gnu.org/licenses/>.
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
}

@ -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 <http://www.gnu.org/licenses/>.
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
}

@ -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 <http://www.gnu.org/licenses/>.
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
}

@ -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
}

@ -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 <http://www.gnu.org/licenses/>.
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
}

@ -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.

@ -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

@ -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()
}

@ -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

@ -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
}
snapshotConf = []bool{snapshotConf[rand.Int()%2]}
dbschemeConf = []string{dbschemeConf[rand.Int()%2]}
}
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)
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
}
}
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
}
}
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
}
}
}

@ -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}
@ -152,6 +152,7 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer *tracing.Hooks,
}
chain, err := core.NewBlockChain(db, cache, gspec, nil, engine, vm.Config{
Tracer: tracer,
EnableWitnessCollection: witness,
}, nil, nil)
if err != nil {
return err

@ -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.

@ -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

@ -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")
}

Loading…
Cancel
Save