diff --git a/cmd/utils/stateless/stateless.go b/cmd/utils/stateless/stateless.go new file mode 100644 index 0000000000..27ed0bc197 --- /dev/null +++ b/cmd/utils/stateless/stateless.go @@ -0,0 +1,86 @@ +package stateless + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" +) + +// StatelessExecute executes the block contained in the Witness returning the post state root or an error +func StatelessExecute(chainCfg *params.ChainConfig, witness *state.Witness) (root common.Hash, err error) { + rawDb := rawdb.NewMemoryDatabase() + if err := witness.PopulateDB(rawDb); err != nil { + return common.Hash{}, err + } + blob := rawdb.ReadAccountTrieNode(rawDb, nil) + prestateRoot := crypto.Keccak256Hash(blob) + + db, err := state.New(prestateRoot, state.NewDatabaseWithConfig(rawDb, triedb.PathDefaults), nil) + if err != nil { + return common.Hash{}, err + } + engine := beacon.New(ethash.NewFaker()) + validator := core.NewBlockValidator(chainCfg, nil, engine) + processor := core.NewStateProcessor(chainCfg, nil, engine) + + receipts, _, usedGas, err := processor.Process(witness.Block, db, vm.Config{}, witness) + if err != nil { + return common.Hash{}, err + } + + // compute the state root. + if root, err = validator.ValidateState(witness.Block, db, receipts, usedGas, false); err != nil { + return common.Hash{}, err + } + return root, nil +} + +// BuildStatelessProof executes a block, collecting the accessed pre-state into +// a Witness. The RLP-encoded witness is returned. +func BuildStatelessProof(blockHash common.Hash, bc *core.BlockChain) ([]byte, error) { + block := bc.GetBlockByHash(blockHash) + if block == nil { + return nil, fmt.Errorf("non-existent block %x", blockHash) + } else if block.NumberU64() == 0 { + return nil, fmt.Errorf("cannot build a stateless proof of the genesis block") + } + parentHash := block.ParentHash() + parent := bc.GetBlockByHash(parentHash) + if parent == nil { + return nil, fmt.Errorf("block %x parent not present", parentHash) + } + + db, err := bc.StateAt(parent.Header().Root) + if err != nil { + return nil, err + } + db.EnableWitnessBuilding() + if bc.Snapshots() != nil { + db.StartPrefetcher("BuildStatelessProof", false) + defer db.StopPrefetcher() + } + stateProcessor := core.NewStateProcessor(bc.Config(), bc, bc.Engine()) + _, _, _, err = stateProcessor.Process(block, db, vm.Config{}, nil) + if err != nil { + return nil, err + } + if _, err = db.Commit(block.NumberU64(), true); err != nil { + return nil, err + } + proof := db.Witness() + proof.Block = block + enc, err := proof.EncodeRLP() + if err != nil { + return nil, err + } + return enc, nil +} diff --git a/core/block_validator.go b/core/block_validator.go index 3d49f4e6a3..f5cbe5b185 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -20,6 +20,7 @@ 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/types" @@ -121,28 +122,31 @@ 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 { +// If validateRemoteRoot is false, the provided block header's root is not asserted to be equal to the one computed from +// execution. +func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64, checkRemoteRoot bool) (root common.Hash, err error) { header := block.Header() if block.GasUsed() != usedGas { - return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas) + return root, fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas) } // Validate the received block's bloom with the one derived from the generated receipts. // For valid blocks this should always validate to true. rbloom := types.CreateBloom(receipts) if rbloom != header.Bloom { - return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom) + return root, fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom) } // The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]])) receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil)) if receiptSha != header.ReceiptHash { - return fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha) + return root, fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha) } - // Validate the state root against the received state root and throw - // an error if they don't match. - if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { - return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error()) + // Compute the state root and if enabled, check it against the + // received state root and throw an error if they don't match. + root = statedb.IntermediateRoot(v.config.IsEIP158(header.Number)) + if checkRemoteRoot && header.Root != root { + return root, fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error()) } - return nil + return root, nil } // CalcGasLimit computes the gas limit of the next block after parent. It aims diff --git a/core/blockchain.go b/core/blockchain.go index ac4eb1c47e..1305efbb9a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1916,7 +1916,7 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s // Process block using the parent state as reference point pstart := time.Now() - receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) + receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, nil) if err != nil { bc.reportBlock(block, receipts, err) return nil, err @@ -1924,7 +1924,7 @@ 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, true); err != nil { bc.reportBlock(block, receipts, err) return nil, err } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index e4bc3e09a6..4af5be7515 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -163,12 +163,12 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { if err != nil { return err } - receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}) + receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}, nil) if err != nil { blockchain.reportBlock(block, receipts, err) return err } - err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas) + _, err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas, true) if err != nil { blockchain.reportBlock(block, receipts, err) return err diff --git a/core/chain_makers.go b/core/chain_makers.go index 58985347bb..d7996b0350 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -99,7 +99,7 @@ func (b *BlockGen) Difficulty() *big.Int { func (b *BlockGen) SetParentBeaconRoot(root common.Hash) { b.header.ParentBeaconRoot = &root var ( - blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) + blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase, nil) vmenv = vm.NewEVM(blockContext, vm.TxContext{}, b.statedb, b.cm.config, vm.Config{}) ) ProcessBeaconBlockRoot(root, vmenv, b.statedb) diff --git a/core/evm.go b/core/evm.go index 5d3c454d7c..aee619b047 100644 --- a/core/evm.go +++ b/core/evm.go @@ -19,6 +19,8 @@ package core import ( "math/big" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" @@ -38,8 +40,9 @@ type ChainContext interface { GetHeader(common.Hash, uint64) *types.Header } -// NewEVMBlockContext creates a new context for use in the EVM. -func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { +// NewEVMBlockContext creates a new context for use in the EVM. If witness is non-nil, the context sources block hashes +// for the BLOCKHASH opcode from the witness. +func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address, witness *state.Witness) vm.BlockContext { var ( beneficiary common.Address baseFee *big.Int @@ -62,10 +65,18 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common if header.Difficulty.Sign() == 0 { random = &header.MixDigest } + var getHash vm.GetHashFunc + if witness != nil { + getHash = func(n uint64) common.Hash { + return witness.BlockHash(n) + } + } else { + getHash = GetHashFn(header, chain) + } return vm.BlockContext{ CanTransfer: CanTransfer, Transfer: Transfer, - GetHash: GetHashFn(header, chain), + GetHash: getHash, Coinbase: beneficiary, BlockNumber: new(big.Int).Set(header.Number), Time: header.Time, diff --git a/core/state/database.go b/core/state/database.go index d71f8f34b6..eebfb1c7a7 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) + // AccessList returns a map of path->blob containing all trie nodes that have + // been accessed. + AccessList() map[string][]byte + // 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..8060d0b86e 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -323,10 +323,6 @@ 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 - if len(s.uncommittedStorage) == 0 { - return s.trie, nil - } // Retrieve a pretecher populated trie, or fall back to the database tr := s.getPrefetchedTrie() if tr != nil { @@ -341,6 +337,14 @@ func (s *stateObject) updateTrie() (Trie, error) { return nil, err } } + // Short circuit if nothing changed, don't bother with hashing anything. + // + // We only quit after the prefetched trie is potentially resolved above + // because, when building a stateless witness we will need to collect + //storage access witnesses from the object's trie when we commit it. + 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: // @@ -446,7 +450,9 @@ func (s *stateObject) commitStorage(op *accountUpdate) { // // Note, commit may run concurrently across all the state objects. Do not assume // thread-safe access to the statedb. -func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) { +func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, map[string][]byte, error) { + var al map[string][]byte + // commit the account metadata changes op := &accountUpdate{ address: s.address, @@ -468,12 +474,18 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) { if len(op.storages) == 0 { // nothing changed, don't bother to commit the trie s.origin = s.data.Copy() - return op, nil, nil + if s.trie != nil && !s.trie.IsVerkle() { + al = s.trie.AccessList() + } + return op, nil, al, nil } root, nodes := s.trie.Commit(false) s.data.Root = root s.origin = s.data.Copy() - return op, nodes, nil + if !s.trie.IsVerkle() { + al = s.trie.AccessList() + } + return op, nodes, al, nil } // AddBalance adds amount to s's balance. diff --git a/core/state/state_witness.go b/core/state/state_witness.go new file mode 100644 index 0000000000..2223952bc1 --- /dev/null +++ b/core/state/state_witness.go @@ -0,0 +1,374 @@ +package state + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "sort" + "sync" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/rlp" +) + +// A Witness encompasses a block and all state necessary to compute the +// post-state root. +type Witness struct { + Block *types.Block + blockHashes map[uint64]common.Hash + codes map[common.Hash][]byte + root common.Hash + tries map[common.Hash]map[string][]byte + triesLock sync.Mutex +} + +// BlockHash returns the block hash corresponding to an ancestor block between 1-256 blocks old. +func (w *Witness) BlockHash(num uint64) common.Hash { + return w.blockHashes[num] +} + +// Root returns the post-state root of the witness if it has been computed or 0x00..0 if not. +func (w *Witness) Root() common.Hash { + return w.root +} + +// rlpWitness is the encoding structure for a Witness +type rlpWitness struct { + EncBlock []byte + Root common.Hash + Owners []common.Hash + TriesPaths [][]string + TriesNodes [][][]byte + BlockNums []uint64 + BlockHashes []common.Hash + Codes [][]byte +} + +func (e *rlpWitness) toWitness() (*Witness, error) { + res := NewWitness(e.Root) + if err := rlp.DecodeBytes(e.EncBlock, &res.Block); err != nil { + return nil, err + } + for _, code := range e.Codes { + codeHash := crypto.Keccak256Hash(code) + if _, ok := res.codes[codeHash]; ok { + return nil, errors.New("duplicate code in witness") + } + res.codes[codeHash] = code + } + for i, owner := range e.Owners { + trieNodes := make(map[string][]byte) + for j := 0; j < len(e.TriesPaths[i]); j++ { + trieNodes[e.TriesPaths[i][j]] = e.TriesNodes[i][j] + } + res.tries[owner] = trieNodes + } + for i, blockNum := range e.BlockNums { + res.blockHashes[blockNum] = e.BlockHashes[i] + } + return res, nil +} + +// DecodeWitnessRLP decodes a byte slice into a witness object. +func DecodeWitnessRLP(b []byte) (*Witness, error) { + var res rlpWitness + if err := rlp.DecodeBytes(b, &res); err != nil { + return nil, err + } + return res.toWitness() +} + +// EncodeRLP encodes a witness object into bytes. The Witness' state root is +// zeroed before the encoding. The encoding is not deterministic (the result +// can differ for the same Witness) +func (w *Witness) EncodeRLP() ([]byte, error) { + var encWit rlpWitness + var encBlock bytes.Buffer + if err := w.Block.EncodeRLPWithZeroRoot(&encBlock); err != nil { + return nil, err + } + encWit.EncBlock = encBlock.Bytes() + + for owner, trie := range w.tries { + encWit.Owners = append(encWit.Owners, owner) + var ownerPaths []string + var ownerNodes [][]byte + + for path, node := range trie { + ownerPaths = append(ownerPaths, path) + ownerNodes = append(ownerNodes, node) + } + encWit.TriesPaths = append(encWit.TriesPaths, ownerPaths) + encWit.TriesNodes = append(encWit.TriesNodes, ownerNodes) + } + + for _, code := range w.codes { + encWit.Codes = append(encWit.Codes, code) + } + + for blockNum, blockHash := range w.blockHashes { + encWit.BlockNums = append(encWit.BlockNums, blockNum) + encWit.BlockHashes = append(encWit.BlockHashes, blockHash) + } + res, err := rlp.EncodeToBytes(&encWit) + if err != nil { + return nil, err + } + return res, nil +} + +// addAccessList associates a map of RLP-encoded trie nodes keyed by path to +// an owner in the witness. the witness takes ownership of the passed map. It +// is safe to call this method concurrently. +func (w *Witness) addAccessList(owner common.Hash, newTrieNodes map[string][]byte) { + var trie map[string][]byte + + if len(newTrieNodes) == 0 { + return + } + w.triesLock.Lock() + defer w.triesLock.Unlock() + + trie, ok := w.tries[owner] + if !ok { + trie = make(map[string][]byte) + w.tries[owner] = trie + } + for path, node := range newTrieNodes { + trie[path] = node + } +} + +// AddBlockHash adds a block hash/number to the witness +func (w *Witness) AddBlockHash(hash common.Hash, num uint64) { + w.blockHashes[num] = hash +} + +// AddCode associates a hash with code in the Witness. +// The Witness takes ownership over the passed code slice. +func (w *Witness) AddCode(hash common.Hash, code []byte) { + if hash == types.EmptyCodeHash || hash == (common.Hash{}) || len(code) == 0 { + return + } + w.codes[hash] = code +} + +// Summary prints a human-readable summary containing the total size of the +// witness and the sizes of the underlying components +func (w *Witness) Summary() string { + b := new(bytes.Buffer) + xx, err := rlp.EncodeToBytes(w.Block) + if err != nil { + panic(err) + } + totBlock := len(xx) + + yy, _ := w.EncodeRLP() + + totWit := len(yy) + totCode := 0 + for _, c := range w.codes { + totCode += len(c) + } + totNodes := 0 + totPaths := 0 + nodePathCount := 0 + for _, ownerPaths := range w.tries { + for path, node := range ownerPaths { + nodePathCount++ + totNodes += len(node) + totPaths += len(path) + } + } + + fmt.Fprintf(b, "%4d hashes: %v\n", len(w.blockHashes), common.StorageSize(len(w.blockHashes)*32)) + fmt.Fprintf(b, "%4d owners: %v\n", len(w.tries), common.StorageSize(len(w.tries)*32)) + fmt.Fprintf(b, "%4d nodes: %v\n", nodePathCount, common.StorageSize(totNodes)) + fmt.Fprintf(b, "%4d paths: %v\n", nodePathCount, common.StorageSize(totPaths)) + fmt.Fprintf(b, "%4d codes: %v\n", len(w.codes), common.StorageSize(totCode)) + fmt.Fprintf(b, "%4d codeHashes: %v\n", len(w.codes), common.StorageSize(len(w.codes)*32)) + fmt.Fprintf(b, "block (%4d txs): %v\n", len(w.Block.Transactions()), common.StorageSize(totBlock)) + fmt.Fprintf(b, "Total size: %v\n ", common.StorageSize(totWit)) + return b.String() +} + +// Copy deep-copies the witness object. Witness.Block isn't deep-copied as it +// is never mutated by Witness +func (w *Witness) Copy() *Witness { + var res Witness + res.Block = w.Block + + for blockNr, blockHash := range w.blockHashes { + res.blockHashes[blockNr] = blockHash + } + for codeHash, code := range w.codes { + cpy := make([]byte, len(code)) + copy(cpy, code) + res.codes[codeHash] = cpy + } + res.root = w.root + for owner, owned := range w.tries { + res.tries[owner] = make(map[string][]byte) + for path, node := range owned { + cpy := make([]byte, len(node)) + copy(cpy, node) + res.tries[owner][path] = cpy + } + } + return &res +} + +// sortedWitness encodes returns an rlpWitness where hash-map items are sorted lexicographically by key +// in the encoder object to ensure that the encoded bytes are always the same for a given witness. +func (w *Witness) sortedWitness() *rlpWitness { + var ( + sortedCodeHashes []common.Hash + owners []common.Hash + ownersPaths [][]string + ownersNodes [][][]byte + blockNrs []uint64 + blockHashes []common.Hash + codeHashes []common.Hash + codes [][]byte + ) + for key := range w.codes { + sortedCodeHashes = append(sortedCodeHashes, key) + } + sort.Slice(sortedCodeHashes, func(i, j int) bool { + return bytes.Compare(sortedCodeHashes[i][:], sortedCodeHashes[j][:]) > 0 + }) + + // sort the list of owners + for owner := range w.tries { + owners = append(owners, owner) + } + sort.Slice(owners, func(i, j int) bool { + return bytes.Compare(owners[i][:], owners[j][:]) > 0 + }) + + // sort the trie nodes of each trie by path + for _, owner := range owners { + nodes := w.tries[owner] + var ownerPaths []string + for path := range nodes { + ownerPaths = append(ownerPaths, path) + } + sort.Strings(ownerPaths) + + var ownerNodes [][]byte + for _, path := range ownerPaths { + ownerNodes = append(ownerNodes, nodes[path]) + } + ownersPaths = append(ownersPaths, ownerPaths) + ownersNodes = append(ownersNodes, ownerNodes) + } + + for blockNr, blockHash := range w.blockHashes { + blockNrs = append(blockNrs, blockNr) + blockHashes = append(blockHashes, blockHash) + } + + for codeHash := range w.codes { + codeHashes = append(codeHashes, codeHash) + } + sort.Slice(codeHashes, func(i, j int) bool { + return bytes.Compare(codeHashes[i][:], codeHashes[j][:]) > 0 + }) + + for _, codeHash := range codeHashes { + codes = append(codes, w.codes[codeHash]) + } + + encBlock, _ := rlp.EncodeToBytes(w.Block) + return &rlpWitness{ + EncBlock: encBlock, + Root: common.Hash{}, + Owners: owners, + TriesPaths: ownersPaths, + TriesNodes: ownersNodes, + BlockNums: blockNrs, + BlockHashes: blockHashes, + Codes: codes, + } +} + +// PrettyPrint displays the contents of a witness object in a human-readable format to standard output. +func (w *Witness) PrettyPrint() string { + sorted := w.sortedWitness() + b := new(bytes.Buffer) + fmt.Fprintf(b, "block: %+v\n", w.Block) + fmt.Fprintf(b, "root: %x\n", sorted.Root) + fmt.Fprint(b, "owners:\n") + for i, owner := range sorted.Owners { + if owner == (common.Hash{}) { + fmt.Fprintf(b, "\troot:\n") + } else { + fmt.Fprintf(b, "\t%x:\n", owner) + } + ownerPaths := sorted.TriesPaths[i] + ownerNodes := sorted.TriesNodes[i] + for j, path := range ownerPaths { + fmt.Fprintf(b, "\t\t%x:%x\n", []byte(path), ownerNodes[j]) + } + } + fmt.Fprintf(b, "block hashes:\n") + for i, blockNum := range sorted.BlockNums { + blockHash := sorted.BlockHashes[i] + fmt.Fprintf(b, "\t%d:%x\n", blockNum, blockHash) + } + fmt.Fprintf(b, "codes:\n") + for _, code := range sorted.Codes { + hash := crypto.Keccak256Hash(code) + fmt.Fprintf(b, "\t%x:%x\n", hash, code) + } + return b.String() +} + +// Hash returns the sha256 hash of a Witness +func (w *Witness) Hash() common.Hash { + res, err := rlp.EncodeToBytes(w.sortedWitness()) + if err != nil { + panic(err) + } + + return sha256.Sum256(res[:]) +} + +// NewWitness returns a new Witness object. +func NewWitness(root common.Hash) *Witness { + return &Witness{ + Block: nil, + blockHashes: make(map[uint64]common.Hash), + codes: make(map[common.Hash][]byte), + root: root, + tries: make(map[common.Hash]map[string][]byte), + } +} + +// PopulateDB imports tries,codes and block hashes from the witness +// into the specified path-based backing db. +func (w *Witness) PopulateDB(db ethdb.Database) error { + batch := db.NewBatch() + for owner, nodes := range w.tries { + for path, node := range nodes { + if owner == (common.Hash{}) { + rawdb.WriteAccountTrieNode(batch, []byte(path), node) + } else { + rawdb.WriteStorageTrieNode(batch, owner, []byte(path), node) + } + } + } + for codeHash, code := range w.codes { + rawdb.WriteCode(batch, codeHash, code) + } + if err := batch.Write(); err != nil { + return err + } + return nil +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 4f84d93d63..dbb5b30b76 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -105,7 +105,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 @@ -163,6 +163,7 @@ type StateDB struct { StorageUpdated atomic.Int64 AccountDeleted int StorageDeleted atomic.Int64 + witness *Witness } // New creates a new state from a given trie. @@ -177,7 +178,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), @@ -582,7 +583,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 @@ -703,6 +703,9 @@ 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) @@ -755,6 +758,12 @@ func (s *StateDB) RevertToSnapshot(revid int) { s.validRevisions = s.validRevisions[:idx] } +// EnableWitnessRecording configures the StateDB to build a stateless block +// witness this must becalled before starting prefetchers or applying state changes +func (s *StateDB) EnableWitnessBuilding() { + s.witness = NewWitness(s.originalRoot) +} + // GetRefund returns the current value of the refund counter. func (s *StateDB) GetRefund() uint64 { return s.refund @@ -788,7 +797,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() @@ -808,6 +817,46 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { s.clearJournalAndRefund() } +// collectNonCommittedTrieAccessLists is called if witness building is enabled. +// It collects witness access lists for accounts that read from storage, and +// whose tries are not going to be hashed/committed (accounts with non-mutated +// storage and self-destructed accounts). +func (s *StateDB) collectNonCommittedTrieAccessLists() { + collectAccount := func(obj *stateObject) { + if obj.Root() == types.EmptyRootHash { + // TODO: unsure if this explicit check is needed + return + } + tr := obj.getPrefetchedTrie() + if tr == nil { + if obj.trie == nil { + // object storage was never read from + return + } + // either the snapshot is not enabled or the object was not present + tr = obj.trie + } + + al := tr.AccessList() + if al == nil { + panic("impossible case: storage trie is non-empty and known to have been read from but a nil access list was returned") + } + if len(al) == 0 { + panic("impossible case: storage trie is non-empty and known to have been read from but a zero-length access list was returned") + } + s.witness.addAccessList(obj.addrHash, al) + } + for _, obj := range s.stateObjectsDestruct { + collectAccount(obj) + } + for _, obj := range s.stateObjects { + if _, ok := s.mutations[obj.address]; ok { + continue + } + collectAccount(obj) + } +} + // IntermediateRoot computes the current root hash of the state trie. // It is called in between transactions to get the root hash that // goes into transaction receipts. @@ -824,6 +873,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { s.prefetcher = nil // Pre-byzantium, unset any used up prefetcher }() } + // Process all storage updates concurrently. The state object update root // method will internally call a blocking trie fetch from the prefetcher, // so there's no need to explicitly wait for the prefetchers to finish. @@ -849,6 +899,12 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { return nil }) } + if s.witness != nil { + // if witness building is enabled, collect storage access lists from + // tries that will not be committed: accounts with non-mutated storage + // and self-destructed accounts. + s.collectNonCommittedTrieAccessLists() + } workers.Wait() s.StorageUpdates += time.Since(start) @@ -1060,7 +1116,8 @@ 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, obj := range s.stateObjectsDestruct { + prev := obj.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. @@ -1168,6 +1225,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool) (*stateUpdate, error) { root common.Hash workers errgroup.Group ) + // Schedule the account trie first since that will be the biggest, so give // it the most time to crunch. // @@ -1178,8 +1236,25 @@ func (s *StateDB) commit(deleteEmptyObjects bool) (*stateUpdate, error) { // Obviously it's not an end of the world issue, just something the original // code didn't anticipate for. workers.Go(func() error { + var ( + newroot common.Hash + set *trienode.NodeSet + al map[string][]byte + ) // Write the account trie changes, measuring the amount of wasted time - newroot, set := s.trie.Commit(true) + if s.witness != nil { + newroot, set = s.trie.Commit(true) + al = s.trie.AccessList() + if al == nil { + panic("this should only happen if starting with completely empty state") + } + if len(al) == 0 { + panic("blocks without state changes not possible") + } + s.witness.addAccessList(common.Hash{}, al) + } else { + newroot, set = s.trie.Commit(true) + } root = newroot if err := merge(set); err != nil { @@ -1207,7 +1282,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool) (*stateUpdate, error) { // Run the storage updates concurrently to one another workers.Go(func() error { // Write any storage changes in the state object to its storage trie - update, set, err := obj.commit() + update, set, al, err := obj.commit() if err != nil { return err } @@ -1215,9 +1290,12 @@ func (s *StateDB) commit(deleteEmptyObjects bool) (*stateUpdate, error) { return err } lock.Lock() + defer lock.Unlock() updates[obj.addrHash] = update s.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime - lock.Unlock() + if s.witness != nil && len(al) > 0 { + s.witness.addAccessList(obj.addrHash, al) + } return nil }) } @@ -1239,7 +1317,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 @@ -1294,6 +1372,12 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool) (*stateU return ret, err } +// Witness returns a block witness object being constructed or nil if the +// StateDB instance is not configured to record stateless witnesses. +func (s *StateDB) Witness() *Witness { + return s.witness +} + // Commit writes the state mutations into the configured data stores. // // Once the state is committed, tries cached in stateDB (including account diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index ff867309de..4441d8a567 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -51,7 +51,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.bc, nil, 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..db6937b3a2 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -36,20 +36,45 @@ 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 + config *params.ChainConfig // Chain configuration options + bc *BlockChain // Canonical block chain + engine consensus.Engine // Consensus engine used for block rewards + statelessChainCtx ChainContext } -// NewStateProcessor initialises a new StateProcessor. +// NewStateProcessor initialises a new StateProcessor. If the provided +// Blockchain is nil, stateless execution mode is enabled. func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *StateProcessor { - return &StateProcessor{ - config: config, - bc: bc, - engine: engine, + if bc != nil { + return &StateProcessor{ + config: config, + bc: bc, + engine: engine, + } + } else { + return &StateProcessor{ + config: config, + engine: engine, + statelessChainCtx: &statelessChainContext{engine}, + bc: &BlockChain{ + chainConfig: config, + engine: engine, + }, + } } } +type statelessChainContext struct { + engine consensus.Engine +} + +func (s *statelessChainContext) Engine() consensus.Engine { + return s.engine +} +func (s *statelessChainContext) GetHeader(hash common.Hash, number uint64) *types.Header { + panic("not implemented") +} + // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. @@ -57,7 +82,7 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, witness *state.Witness) (types.Receipts, []*types.Log, uint64, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -73,10 +98,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.bc, nil, witness) + vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg) if beaconRoot := block.BeaconRoot(); beaconRoot != nil { ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } @@ -177,7 +203,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo return nil, err } // Create a new context to be used in the EVM environment - blockContext := NewEVMBlockContext(header, bc, author) + blockContext := NewEVMBlockContext(header, bc, author, statedb.Witness()) txContext := NewEVMTxContext(msg) vmenv := vm.NewEVM(blockContext, txContext, statedb, config, cfg) return ApplyTransactionWithEVM(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) diff --git a/core/types.go b/core/types.go index 36eb0d1ded..0eaeb34529 100644 --- a/core/types.go +++ b/core/types.go @@ -19,6 +19,8 @@ package core import ( "sync/atomic" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -33,7 +35,7 @@ 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, rootCheck bool) (common.Hash, error) } // Prefetcher is an interface for pre-caching transaction signatures and state. @@ -49,5 +51,5 @@ type Processor interface { // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. - Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) + Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, witness *state.Witness) (types.Receipts, []*types.Log, uint64, error) } diff --git a/core/types/block.go b/core/types/block.go index 4857cd6e50..31931f124d 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -174,6 +174,18 @@ type Body struct { Withdrawals []*Withdrawal `rlp:"optional"` } +// EncodeRLPWithZeroRoot encodes a block (with header state root set to 0x00...0) to RLP +func (b *Block) EncodeRLPWithZeroRoot(w io.Writer) error { + old := b.header.Root + b.header.Root = common.Hash{} + err := b.EncodeRLP(w) + b.header.Root = old + if err != nil { + return err + } + return nil +} + // Block represents an Ethereum block. // // Note the Block type tries to be 'immutable', and contains certain caches that rely diff --git a/core/vm/evm.go b/core/vm/evm.go index 26af0ea041..ee91d8026a 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -231,6 +231,11 @@ 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) + codeCopy := make([]byte, len(code)) + copy(codeCopy[:], code[:]) + if witness := evm.StateDB.Witness(); witness != nil { + witness.AddCode(evm.StateDB.GetCodeHash(addr), codeCopy) + } if len(code) == 0 { ret, err = nil, nil // gas is unchanged } else { @@ -298,6 +303,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.GetCodeHash(addrCopy), 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 +353,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.GetCodeHash(addrCopy), 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 +411,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.GetCodeHash(addrCopy), 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..f1e8222144 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -340,6 +340,12 @@ 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 { + code := interpreter.evm.StateDB.GetCode(address) + codeHash := interpreter.evm.StateDB.GetCodeHash(address) + witness.AddCode(codeHash, code) + } slot.SetUint64(uint64(interpreter.evm.StateDB.GetCodeSize(slot.Bytes20()))) return nil, nil } @@ -378,6 +384,11 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) uint64CodeOffset = math.MaxUint64 } addr := common.Address(a.Bytes20()) + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + hash := interpreter.evm.StateDB.GetCodeHash(addr) + code := interpreter.evm.StateDB.GetCode(addr) + witness.AddCode(hash, code) + } codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64()) scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) @@ -416,6 +427,11 @@ func opExtCodeHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) if interpreter.evm.StateDB.Empty(address) { slot.Clear() } else { + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + hash := interpreter.evm.StateDB.GetCodeHash(address) + code := interpreter.evm.StateDB.GetCode(address) + witness.AddCode(hash, code) + } slot.SetBytes(interpreter.evm.StateDB.GetCodeHash(address).Bytes()) } return nil, nil @@ -443,7 +459,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(res, num64) + } + num.SetBytes(res[:]) } else { num.Clear() } diff --git a/core/vm/interface.go b/core/vm/interface.go index 8b2c58898e..8e840355df 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -19,6 +19,8 @@ package vm import ( "math/big" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" @@ -87,6 +89,8 @@ type StateDB interface { AddLog(*types.Log) AddPreimage(common.Hash, []byte) + + Witness() *state.Witness } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/eth/api_backend.go b/eth/api_backend.go index 8a9898b956..f949d7bb8f 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -258,7 +258,7 @@ func (b *EthAPIBackend) GetEVM(ctx context.Context, msg *core.Message, state *st if blockCtx != nil { context = *blockCtx } else { - context = core.NewEVMBlockContext(header, b.eth.BlockChain(), nil) + context = core.NewEVMBlockContext(header, b.eth.BlockChain(), nil, nil) } return vm.NewEVM(context, txContext, state, b.ChainConfig(), *vmConfig) } diff --git a/eth/api_debug.go b/eth/api_debug.go index d5e4dda140..b95c218926 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -22,6 +22,8 @@ import ( "fmt" "time" + "github.com/ethereum/go-ethereum/cmd/utils/stateless" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/rawdb" @@ -443,3 +445,17 @@ func (api *DebugAPI) GetTrieFlushInterval() (string, error) { } return api.eth.blockchain.GetTrieFlushInterval().String(), nil } + +// BuildStatelessProof executes a block, collecting the accessed pre-state into +// a Witness. The RLP-encoded witness is returned. +func (api *DebugAPI) BuildStatelessProof(numOrHash rpc.BlockNumberOrHash) ([]byte, error) { + var blockHash common.Hash + if numOrHash.BlockNumber != nil { + number := numOrHash.BlockNumber.Int64() + block := api.eth.blockchain.GetBlockByNumber(uint64(number)) + blockHash = block.Hash() + } else { + blockHash = *numOrHash.BlockHash + } + return stateless.BuildStatelessProof(blockHash, api.eth.blockchain) +} diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index ac3b59e97e..6836ef2b3a 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -218,7 +218,7 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio // Assemble the call and the call context var ( msgContext = core.NewEVMTxContext(call) - evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil) + evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil, nil) dirtyState = opts.State.Copy() evm = vm.NewEVM(evmContext, msgContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true}) diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 372c76f496..4cc9a044c0 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -146,7 +146,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, nil, fmt.Errorf("block #%d not found", next) } - _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) + _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}, nil) if err != nil { return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } @@ -235,7 +235,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, } // Insert parent beacon block root in the state as per EIP-4788. if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) + context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil, nil) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, eth.blockchain.Config(), vm.Config{}) core.ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } @@ -248,7 +248,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, // Assemble the transaction call message and return if the requested offset msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) + context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil, nil) if idx == txIndex { return tx, context, statedb, release, nil } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 51b55ffdbb..3360363299 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -266,7 +266,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed for task := range taskCh { var ( signer = types.MakeSigner(api.backend.ChainConfig(), task.block.Number(), task.block.Time()) - blockCtx = core.NewEVMBlockContext(task.block.Header(), api.chainContext(ctx), nil) + blockCtx = core.NewEVMBlockContext(task.block.Header(), api.chainContext(ctx), nil, nil) ) // Trace all the transactions contained within for i, tx := range task.block.Transactions() { @@ -378,7 +378,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed // Insert block's parent beacon block root in the state // as per EIP-4788. if beaconRoot := next.BeaconRoot(); beaconRoot != nil { - context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) + context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil, nil) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, api.backend.ChainConfig(), vm.Config{}) core.ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } @@ -527,7 +527,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config roots []common.Hash signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) chainConfig = api.backend.ChainConfig() - vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, nil) deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) ) if beaconRoot := block.BeaconRoot(); beaconRoot != nil { @@ -605,7 +605,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac var ( txs = block.Transactions() blockHash = block.Hash() - blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, nil) signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) results = make([]*txTraceResult, len(txs)) ) @@ -665,7 +665,7 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat // as the GetHash function of BlockContext is not safe for // concurrent use. // See: https://github.com/ethereum/go-ethereum/issues/29114 - blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, nil) res, err := api.traceTx(ctx, txs[task.index], msg, txctx, blockCtx, task.statedb, config) if err != nil { results[task.index] = &txTraceResult{TxHash: txs[task.index].Hash(), Error: err.Error()} @@ -678,7 +678,7 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat // Feed the transactions into the tracers and return var failed error - blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, nil) txloop: for i, tx := range txs { // Send the trace task over for execution @@ -755,7 +755,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block dumps []string signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) chainConfig = api.backend.ChainConfig() - vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, nil) canon = true ) // Check if there are any overrides: the caller may wish to enable a future @@ -935,7 +935,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc } defer release() - vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, nil) // Apply the customization rules if required. if config != nil { if err := config.StateOverrides.Apply(statedb); err != nil { diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 6fbb50848d..319fe5e786 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -173,7 +173,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block for idx, tx := range block.Transactions() { msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), b.chain, nil) + context := core.NewEVMBlockContext(block.Header(), b.chain, nil, nil) if idx == txIndex { return tx, context, statedb, release, nil } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 0ecedf1130..9a6b70a528 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1110,7 +1110,7 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S defer cancel() // Get a new instance of the EVM. - blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil) + blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil, nil) if blockOverrides != nil { blockOverrides.Apply(&blockCtx) } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index cf5160caf7..56509ee6bd 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -569,7 +569,7 @@ func (b testBackend) GetEVM(ctx context.Context, msg *core.Message, state *state vmConfig = b.chain.GetVMConfig() } txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(header, b.chain, nil) + context := core.NewEVMBlockContext(header, b.chain, nil, nil) if blockContext != nil { context = *blockContext } diff --git a/miner/worker.go b/miner/worker.go index 5dc3e2056b..2652cea1a4 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -196,7 +196,7 @@ func (miner *Miner) prepareWork(genParams *generateParams) (*environment, error) return nil, err } if header.ParentBeaconRoot != nil { - context := core.NewEVMBlockContext(header, miner.chain, nil) + context := core.NewEVMBlockContext(header, miner.chain, nil, nil) vmenv := vm.NewEVM(context, vm.TxContext{}, env.state, miner.chainConfig, vm.Config{}) core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, vmenv, env.state) } diff --git a/tests/block_test.go b/tests/block_test.go index 1ba84f5f24..bd88d611bc 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -18,12 +18,94 @@ package tests import ( "math/rand" + "runtime" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" ) +func TestStatelessBlockchain(t *testing.T) { + bt := new(testMatcher) + + // These tests fail as of https://github.com/ethereum/go-ethereum/pull/28666, since we + // no longer delete "leftover storage" when deploying a contract. + bt.skipLoad(`^GeneralStateTests/stSStoreTest/InitCollision\.json`) + bt.skipLoad(`^GeneralStateTests/stRevertTest/RevertInCreateInInit\.json`) + bt.skipLoad(`^GeneralStateTests/stExtCodeHash/dynamicAccountOverwriteEmpty\.json`) + bt.skipLoad(`^GeneralStateTests/stCreate2/create2collisionStorage\.json`) + bt.skipLoad(`^GeneralStateTests/stCreate2/RevertInCreateInInitCreate2\.json`) + + // this test imports a forked chain. The witness builder API receives a block by number + // loading it from the chain. So it fails to properly source the forked chain block, + // erroneously using the one from the main chain (hence the state root mismatch). + bt.skipLoad(`^InvalidBlocks/bcMultiChainTest/UncleFromSideChain\.json`) + // Skip random failures due to selfish mining test + bt.skipLoad(`.*bcForgedTest/bcForkUncle\.json`) + // Skip random failures due to selfish mining test + bt.skipLoad(`.*bcForgedTest/bcForkUncle\.json`) + + // Slow tests + bt.slow(`.*bcExploitTest/DelegateCallSpam.json`) + bt.slow(`.*bcExploitTest/ShanghaiLove.json`) + bt.slow(`.*bcExploitTest/SuicideIssue.json`) + bt.slow(`.*/bcForkStressTest/`) + bt.slow(`.*/bcGasPricerTest/RPC_API_Test.json`) + bt.slow(`.*/bcWalletTest/`) + + // Very slow test + bt.skipLoad(`.*/stTimeConsuming/.*`) + // test takes a lot for time and goes easily OOM because of sha3 calculation on a huge range, + // using 4.6 TGas + bt.skipLoad(`.*randomStatetest94.json.*`) + + // skip uncle tests for stateless + bt.skipLoad(`.*/UnclePopulation.json`) + // skip this test in stateless because it uses 5000 blocks and the + // historical state of older blocks is unavailable for stateless + // test verification after importing the test set. + bt.skipLoad(`.*/bcWalletTest/walletReorganizeOwners.json`) + + bt.walk(t, blockTestDir, func(t *testing.T, name string, test *BlockTest) { + if runtime.GOARCH == "386" && runtime.GOOS == "windows" && rand.Int63()%2 == 0 { + t.Skip("test (randomly) skipped on 32-bit windows") + } + + config, ok := Forks[test.json.Network] + if !ok { + t.Fatalf("test malformed: doesn't have chain config embedded") + } + isMerged := config.TerminalTotalDifficulty != nil && config.TerminalTotalDifficulty.BitLen() == 0 + if isMerged { + execBlockTestStateless(t, bt, test) + } else { + t.Skip("skipping pre-merge test") + } + }) + // There is also a LegacyTests folder, containing blockchain tests generated + // prior to Istanbul. However, they are all derived from GeneralStateTests, + // which run natively, so there's no reason to run them here. +} + +func execBlockTestStateless(t *testing.T, bt *testMatcher, test *BlockTest) { + if err := bt.checkFailure(t, test.RunStateless(false, rawdb.HashScheme, nil, nil)); err != nil { + t.Errorf("test in hash mode without snapshotter failed: %v", err) + return + } + if err := bt.checkFailure(t, test.RunStateless(true, rawdb.HashScheme, nil, nil)); err != nil { + t.Errorf("test in hash mode with snapshotter failed: %v", err) + return + } + if err := bt.checkFailure(t, test.RunStateless(false, rawdb.PathScheme, nil, nil)); err != nil { + t.Errorf("test in path mode without snapshotter failed: %v", err) + return + } + if err := bt.checkFailure(t, test.RunStateless(true, rawdb.PathScheme, nil, nil)); err != nil { + t.Errorf("test in path mode with snapshotter failed: %v", err) + return + } +} + func TestBlockchain(t *testing.T) { bt := new(testMatcher) // General state tests are 'exported' as blockchain tests, but we can run them natively. diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 04a04fdc28..3439a79b7b 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -26,6 +26,9 @@ import ( "os" "reflect" + "github.com/ethereum/go-ethereum/cmd/utils/stateless" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -34,7 +37,6 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" @@ -111,6 +113,14 @@ type btHeaderMarshaling struct { } func (t *BlockTest) Run(snapshotter bool, scheme string, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) { + return t.run(false, snapshotter, scheme, tracer, postCheck) +} + +func (t *BlockTest) RunStateless(snapshotter bool, scheme string, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) { + return t.run(true, snapshotter, scheme, tracer, postCheck) +} + +func (t *BlockTest) run(isStateless bool, snapshotter bool, scheme string, tracer *tracing.Hooks, postCheck func(error, *core.BlockChain)) (result error) { config, ok := Forks[t.json.Network] if !ok { return UnsupportedForkError{t.json.Network} @@ -184,7 +194,29 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer *tracing.Hooks, return err } } - return t.validateImportedHeaders(chain, validBlocks) + if err := t.validateImportedHeaders(chain, validBlocks); err != nil { + return err + } + if isStateless { + for _, blk := range validBlocks { + proof, err := stateless.BuildStatelessProof(blk.BlockHeader.Hash, chain) + if err != nil { + return fmt.Errorf("failed to build proof: %v", err) + } + witness, err := state.DecodeWitnessRLP(proof) + if err != nil { + return fmt.Errorf("failed to decode witness RLP: %v", err) + } + root, err := stateless.StatelessExecute(config, witness) + if err != nil { + return fmt.Errorf("verification execution error: %v", err) + } + if root != blk.BlockHeader.StateRoot { + return fmt.Errorf("state root mismatch (wanted: %x, got: %x)", blk.BlockHeader.StateRoot, root) + } + } + } + return nil } func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { diff --git a/tests/state_test.go b/tests/state_test.go index 76fec97de0..837decfa86 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -302,7 +302,7 @@ func runBenchmark(b *testing.B, t *StateTest) { // Prepare the EVM. txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), nil, &t.json.Env.Coinbase) + context := core.NewEVMBlockContext(block.Header(), nil, &t.json.Env.Coinbase, nil) context.GetHash = vmTestBlockHash context.BaseFee = baseFee evm := vm.NewEVM(context, txContext, state.StateDB, config, vmconfig) diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 416bab9472..5958bfeecf 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -278,7 +278,7 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh // Prepare the EVM. txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), nil, &t.json.Env.Coinbase) + context := core.NewEVMBlockContext(block.Header(), nil, &t.json.Env.Coinbase, nil) context.GetHash = vmTestBlockHash context.BaseFee = baseFee context.Random = nil diff --git a/trie/secure_trie.go b/trie/secure_trie.go index cfa7f0bddb..bb0c891e79 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -214,6 +214,10 @@ func (t *StateTrie) GetKey(shaKey []byte) []byte { return t.db.Preimage(common.BytesToHash(shaKey)) } +func (t *StateTrie) AccessList() map[string][]byte { + return t.trie.AccessList() +} + // 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..b50f55639b 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -601,6 +601,12 @@ func (t *Trie) Hash() common.Hash { return common.BytesToHash(hash.(hashNode)) } +// AccessList returns a map of path->blob containing all trie nodes that have +// been accessed. +func (t *Trie) AccessList() map[string][]byte { + return t.tracer.accessList +} + // 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/verkle.go b/trie/verkle.go index 1ea23186f9..f9d40ac421 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -369,3 +369,9 @@ func (t *VerkleTrie) ToDot() string { func (t *VerkleTrie) nodeResolver(path []byte) ([]byte, error) { return t.reader.node(path, common.Hash{}) } + +// AccessList returns a map of path->blob containing all trie nodes that have +// been accessed. +func (t *VerkleTrie) AccessList() map[string][]byte { + panic("not implemented") +} diff --git a/triedb/database.go b/triedb/database.go index ef757e7f5b..1c800db1b6 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -46,6 +46,13 @@ var HashDefaults = &Config{ HashDB: hashdb.Defaults, } +// PathDefaults represents a config for using path-based scheme with +// default settings. +var PathDefaults = &Config{ + Preimages: false, + PathDB: pathdb.Defaults, +} + // backend defines the methods needed to access/update trie nodes in different // state scheme. type backend interface {