From 1329dcf6fbd2f3cd0c063ceefdb10b446d95f117 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Sun, 21 Jan 2024 17:22:23 -0800 Subject: [PATCH] stateless block proof generation w/ execution/verification. Co-authored-by: Jared Wasinger Co-authored-by: Martin HS --- cmd/geth/main.go | 2 + cmd/stateless/main.go | 230 +++++++++++++++++++ cmd/stateless/server.go | 29 +++ cmd/utils/flags.go | 14 ++ cmd/utils/stateless.go | 129 +++++++++++ consensus/beacon/consensus.go | 8 +- core/block_validator.go | 35 ++- core/blockchain.go | 58 ++++- core/cross_validator.go | 66 ++++++ core/evm.go | 23 +- core/state/database.go | 7 + core/state/state_object.go | 18 +- core/state/state_witness.go | 411 ++++++++++++++++++++++++++++++++++ core/state/statedb.go | 186 +++++++++++++-- core/state/trie_prefetcher.go | 161 +++++-------- core/state_processor.go | 64 +++++- core/types.go | 3 +- core/types/block.go | 12 + core/vm/evm.go | 16 +- core/vm/instructions.go | 24 +- core/vm/interface.go | 4 + eth/api_debug.go | 49 ++++ eth/backend.go | 2 +- eth/ethconfig/config.go | 3 + internal/web3ext/web3ext.go | 5 + miner/worker.go | 6 +- tests/block_test.go | 130 ++++++++++- tests/block_test_util.go | 67 +++++- trie/secure_trie.go | 19 ++ trie/trie.go | 24 +- trie/verkle.go | 8 + triedb/database.go | 5 + triedb/pathdb/layertree.go | 5 +- 33 files changed, 1644 insertions(+), 179 deletions(-) create mode 100644 cmd/stateless/main.go create mode 100644 cmd/stateless/server.go create mode 100644 cmd/utils/stateless.go create mode 100644 core/cross_validator.go create mode 100644 core/state/state_witness.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 2f7d37fdd7..caa5c89992 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -146,6 +146,8 @@ var ( configFileFlag, utils.LogDebugFlag, utils.LogBacktraceAtFlag, + utils.CrossValidationEndpointFlag, + utils.WitnessRecordingPathFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/cmd/stateless/main.go b/cmd/stateless/main.go new file mode 100644 index 0000000000..fd226bdf17 --- /dev/null +++ b/cmd/stateless/main.go @@ -0,0 +1,230 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "os" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/console/prompt" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/internal/debug" + "github.com/ethereum/go-ethereum/internal/flags" + "github.com/ethereum/go-ethereum/params" + "github.com/urfave/cli/v2" + "go.uber.org/automaxprocs/maxprocs" +) + +var ( + BlockWitnessFlag = &cli.StringFlag{ + Name: "block-witness", + Usage: "foo bar", + } + ChainConfigFlag = &cli.StringFlag{ + Name: "chain-config", + Usage: "path to a genesis file to source a chain configuration from", + } + + BlockWitness1Flag = &cli.StringFlag{ + Name: "witness1", + Usage: "path to a file containing an rlp-encoded block witness", + } + BlockWitness2Flag = &cli.StringFlag{ + Name: "witness2", + Usage: "path to a file containing an rlp-encoded block witness", + } + + LogFileFlag = &cli.StringFlag{ + Name: "logfile", + Usage: "if present, generate debug trace (just evm traces in the future). store trace to the file", + } + + WitnessDiffCommand = &cli.Command{ + Action: witnessCmp, + Name: "cmp", + Usage: "outputs whether two block witnesses are equal", + ArgsUsage: "cmp --witness1 /path/to/bw1.rlp --witness2 /path/to/bw2.rlp", + Flags: []cli.Flag{ + BlockWitness1Flag, + BlockWitness2Flag, + }, + Description: ``, + } + PPCommand = &cli.Command{ + Action: pp, + Name: "pp", + Usage: "", + ArgsUsage: "pp --block-witness /path/to/witness.rlp", + Flags: []cli.Flag{ + BlockWitnessFlag, + }, + Description: `pretty-print a block witness`, + } + ExecCommand = &cli.Command{ + Action: execCmd, + Name: "exec", + Usage: "", + ArgsUsage: "exec --block-witness /path/to/bw.rlp --chain-config /path/to/chainconfig.json" + + "--log-file /path/to/logfile.txt", + Flags: []cli.Flag{ + BlockWitnessFlag, + ChainConfigFlag, + LogFileFlag, + }, + Description: `statelessly execute and verify a block`, + } + ServerCommand = &cli.Command{ + Action: server, + Name: "server", + Usage: "", + ArgsUsage: "server --chain-config /path/to/chain-config.json", + Flags: []cli.Flag{ChainConfigFlag}, + Description: `Runs an HTTP server which provides an API endpoint for stateless block verification`, + } +) + +var app = flags.NewApp("stateless block execution/verification utilities") + +func init() { + app.Copyright = "Copyright 2013-2024 The go-ethereum Authors" + app.Commands = []*cli.Command{ + WitnessDiffCommand, + PPCommand, + ExecCommand, + ServerCommand, + } + + app.Flags = []cli.Flag{ + BlockWitnessFlag, + } + + app.Before = func(ctx *cli.Context) error { + maxprocs.Set() // Automatically set GOMAXPROCS to match Linux container CPU quota. + if err := debug.Setup(ctx); err != nil { + return err + } + return nil + } + app.After = func(ctx *cli.Context) error { + debug.Exit() + prompt.Stdin.Close() // Resets terminal mode. + return nil + } +} + +func loadChainConfig(chainConfigPath string) *params.ChainConfig { + var chainConfig *params.ChainConfig + + if chainConfigPath != "" { + configBytes, err := os.ReadFile(chainConfigPath) + if err != nil { + panic(err) + } + dec := json.NewDecoder(bytes.NewBuffer(configBytes)) + err = dec.Decode(&chainConfig) + if err != nil { + panic(err) + } + } else { + panic("chain config must be specified") + } + return chainConfig +} + +func execCmd(ctx *cli.Context) error { + var logWriter *bufio.Writer + blockWitnessPath := ctx.String(BlockWitnessFlag.Name) + if blockWitnessPath == "" { + panic("block witness required") + } + + logFile := ctx.String(LogFileFlag.Name) + if logFile != "" { + f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0744) + if err != nil { + return err + } + logWriter = bufio.NewWriter(f) + if err != nil { + panic(err) + } + defer logWriter.Flush() + } + + b, err := os.ReadFile(blockWitnessPath) + if err != nil { + panic(err) + } + + witness, err := state.DecodeWitnessRLP(b) + if err != nil { + panic(err) + } + + chainConfig := loadChainConfig(ctx.String(ChainConfigFlag.Name)) + + localRoot, err := utils.StatelessExecute(os.Stdout, chainConfig, witness) + if err != nil { + panic(err) + } + if localRoot != witness.Block.Root() { + return fmt.Errorf("state root mismatch (local: %x, remote: %x)", localRoot, witness.Block.Root()) + } + return nil +} + +func pp(ctx *cli.Context) error { + witnessPath := ctx.String(BlockWitnessFlag.Name) + b, err := os.ReadFile(witnessPath) + if err != nil { + return err + } + w, err := state.DecodeWitnessRLP(b) + if err != nil { + panic(err) + } + + fmt.Println(w.PrettyPrint()) + return nil +} + +func witnessCmp(ctx *cli.Context) error { + witness1Path := ctx.String(BlockWitness1Flag.Name) + witness2Path := ctx.String(BlockWitness2Flag.Name) + + b1, err := os.ReadFile(witness1Path) + if err != nil { + return err + } + + b2, err := os.ReadFile(witness2Path) + if err != nil { + return err + } + + w1, err := state.DecodeWitnessRLP(b1) + if err != nil { + panic(err) + } + + w2, err := state.DecodeWitnessRLP(b2) + if err != nil { + panic(err) + } + + w1Hash := w1.Hash() + w2Hash := w2.Hash() + if w1Hash != w2Hash { + fmt.Printf("witness 1 hash (%x) != witness 2 hash (%x)\n", w1Hash, w2Hash) + } + return nil +} + +func main() { + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/stateless/server.go b/cmd/stateless/server.go new file mode 100644 index 0000000000..079d358741 --- /dev/null +++ b/cmd/stateless/server.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/urfave/cli/v2" + "os" + "os/signal" + "syscall" +) + +func server(ctx *cli.Context) error { + chainConfig := loadChainConfig(ctx.String(ChainConfigFlag.Name)) + closeCh, _, err := utils.RunLocalServer(chainConfig, 8080) + if err != nil { + return err + } + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + select { + case <-sigc: + closeCh <- struct{}{} + } + + return nil +} diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index b813e52970..d4e2593830 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -910,6 +910,18 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. } ) +var CrossValidationEndpointFlag = &cli.StringFlag{ + Name: "crossvalidation.endpoint", + Usage: "http endpoint(s) to cross validate blocks against. Formatted as a comma-separated list of URLs", + Value: "", +} +var WitnessRecordingPathFlag = &cli.StringFlag{ + Name: "crossvalidation.badblockdir", + Usage: "location to store rlp-encoded block witnesses that do not pass cross validation", + // TODO: default to some directory under the datadir + Value: "", +} + var ( // TestnetFlags is the flag group of all built-in supported testnets. TestnetFlags = []cli.Flag{ @@ -1842,6 +1854,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if err := kzg4844.UseCKZG(ctx.String(CryptoKZGFlag.Name) == "ckzg"); err != nil { Fatalf("Failed to set KZG library implementation to %s: %v", ctx.String(CryptoKZGFlag.Name), err) } + cfg.CrossValidationEndpoint = ctx.String(CrossValidationEndpointFlag.Name) + cfg.WitnessRecordingPath = ctx.String(WitnessRecordingPathFlag.Name) } // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if diff --git a/cmd/utils/stateless.go b/cmd/utils/stateless.go new file mode 100644 index 0000000000..61a9ddafd4 --- /dev/null +++ b/cmd/utils/stateless.go @@ -0,0 +1,129 @@ +package utils + +import ( + "errors" + "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/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" + "io" + "net" + "net/http" +) + +func StatelessExecute(logOutput io.Writer, 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 + } + _, prestateRoot := rawdb.ReadAccountTrieNode(rawDb, nil) + + db, err := state.New(prestateRoot, state.NewDatabaseWithConfig(rawDb, trie.PathDefaults), nil) + if err != nil { + return common.Hash{}, err + } + engine := beacon.New(ethash.NewFaker()) + validator := core.NewStatelessBlockValidator(chainCfg, engine) + chainCtx := core.NewStatelessChainContext(rawDb, engine) + processor := core.NewStatelessStateProcessor(chainCfg, chainCtx, engine) + + receipts, _, usedGas, err := processor.ProcessStateless(witness, witness.Block, db, vm.Config{}) + if err != nil { + return common.Hash{}, err + } + // compute the state root. skip validation of computed root against + // the one provided in the block because this value is omitted from + // the witness. + if root, err = validator.ValidateState(witness.Block, db, receipts, usedGas, false); err != nil { + return common.Hash{}, err + } + // TODO: how to differentiate between errors that are definitely not consensus-failure caused, and ones + // that could be? + return root, nil +} + +// RunLocalServer runs an http server at the specified port (or 0 to use a random port). +// The server provides a POST endpoint /verify_block which takes input as an RLP-encoded +// block witness proof in the body, executes the block proof and returns the computed state root. +func RunLocalServer(chainConfig *params.ChainConfig, port int) (closeChan chan<- struct{}, actualPort int, err error) { + mux := http.NewServeMux() + mux.Handle("/verify_block", &verifyHandler{chainConfig}) + srv := http.Server{Handler: mux} + listener, err := net.Listen("tcp", ":"+fmt.Sprintf("%d", port)) + if err != nil { + return nil, 0, err + } + actualPort = listener.Addr().(*net.TCPAddr).Port + + go func() { + if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + }() + + closeCh := make(chan struct{}) + go func() { + select { + case <-closeCh: + if err := srv.Close(); err != nil { + panic(err) + } + } + }() + return closeCh, actualPort, nil +} + +type verifyHandler struct { + chainConfig *params.ChainConfig +} + +func (v *verifyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + respError := func(descr string, err error) { + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write([]byte(fmt.Sprintf("%s: %s", descr, err))); err != nil { + log.Error("write failed", "error", err) + } + + log.Error("responded with error", "descr", descr, "error", err) + } + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + respError("error reading body", err) + return + } + if len(body) == 0 { + respError("error", fmt.Errorf("empty body")) + return + } + witness, err := state.DecodeWitnessRLP(body) + if err != nil { + respError("error decoding body witness rlp", err) + return + } + defer func() { + if err := recover(); err != nil { + errr, _ := err.(error) + respError("execution error", errr) + return + } + }() + + root, err := StatelessExecute(nil, v.chainConfig, witness) + if err != nil { + respError("error verifying stateless proof", err) + return + } + + w.WriteHeader(http.StatusOK) + if _, err := w.Write(root[:]); err != nil { + log.Error("error writing response", "error", err) + } +} diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index a350e383a2..469d0b2ce4 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -353,13 +353,7 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. beacon.ethone.Finalize(chain, header, state, txs, uncles, nil) return } - // Withdrawals processing. - for _, w := range withdrawals { - // Convert amount from gwei to wei. - amount := new(uint256.Int).SetUint64(w.Amount) - amount = amount.Mul(amount, uint256.NewInt(params.GWei)) - state.AddBalance(w.Address, amount) - } + state.ApplyWithdrawals(withdrawals) // No block reward which is issued by consensus layer instead. } diff --git a/core/block_validator.go b/core/block_validator.go index f3d65cea25..cd461b5c9f 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -19,6 +19,7 @@ package core import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/state" @@ -47,6 +48,20 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engin return validator } +// NewBlockStatelessBlockValidator returns a BlockValidator which is configured to validate stateless block witnesses +// without the use of a full backing BlockChain +func NewStatelessBlockValidator(config *params.ChainConfig, engine consensus.Engine) *BlockValidator { + validator := &BlockValidator{ + config: config, + engine: engine, + bc: &BlockChain{ + chainConfig: config, + engine: engine, + }, + } + return validator +} + // ValidateBody validates the given block's uncles and verifies the block // header's transaction and uncle roots. The headers are assumed to be already // validated at this point. @@ -121,28 +136,30 @@ 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 { +// It returns the computed state root or an error. +func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64, rootCheck 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) } // Tre 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 rootCheck && 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 297a052409..a5e579afbd 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -259,6 +259,17 @@ type BlockChain struct { processor Processor // Block transaction processor interface forker *ForkChoice vmConfig vm.Config + + crossValidator *crossValidator +} + +// NewBlockchainWithCrossValidator returns a Blockchain configured to cross-validate imported blocks against a single +// remote stateless validator. Stateless witnesses are constructed for each imported block and validated against the +// remote endpoint. If validation fails, they are dumped to the folder specified at witnessRecordingPath +func NewBlockchainWithCrossValidator(endpoint string, witnessRecordingPath string, db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis, overrides *ChainOverrides, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(header *types.Header) bool, txLookupLimit *uint64) (*BlockChain, error) { + bc, err := NewBlockChain(db, cacheConfig, genesis, overrides, engine, vmConfig, shouldPreserve, txLookupLimit) + bc.crossValidator = &crossValidator{witnessRecordingPath, endpoint} + return bc, err } // NewBlockChain returns a fully initialised block chain using information @@ -1343,7 +1354,7 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error { // writeBlockWithState writes block, metadata and corresponding state data to the // database. -func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, state *state.StateDB) error { +func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, statedb *state.StateDB) error { // Calculate the total difficulty of the block ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1) if ptd == nil { @@ -1352,6 +1363,27 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. // Make sure no inconsistent state is leaked during insertion externTd := new(big.Int).Add(block.Difficulty(), ptd) + // Commit all cached state changes into underlying memory database. + root, err := statedb.Commit(block.NumberU64(), bc.chainConfig.IsEIP158(block.Number())) + if err != nil { + return err + } + + // TODO: Need to check that we want to cross-validate on all the paths this hits + if bc.crossValidator != nil { + witness := statedb.Witness() + witness.Block = block + err := bc.crossValidator.CrossValidateBlock(bc.chainConfig, witness) + if err != nil { + log.Error("failed to cross validate block", "error", err) + if innerErr := state.DumpBlockWitnessToFile(bc.Config(), statedb.Witness(), "block-dump"); innerErr != nil { + log.Error("failed to store witness to file", "error", innerErr) + } + // TODO: return the error and stop importing the current chain. + // I'm leaving it like this so I can watch validation errors as + // the client follows the chain. + } + } // Irrelevant of the canonical status, write the block itself to the database. // // Note all the components of block(td, hash->number map, header, body, receipts) @@ -1360,15 +1392,11 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd) rawdb.WriteBlock(blockBatch, block) rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts) - rawdb.WritePreimages(blockBatch, state.Preimages()) + rawdb.WritePreimages(blockBatch, statedb.Preimages()) if err := blockBatch.Write(); err != nil { log.Crit("Failed to write block into disk", "err", err) } - // Commit all cached state changes into underlying memory database. - root, err := state.Commit(block.NumberU64(), bc.chainConfig.IsEIP158(block.Number())) - if err != nil { - return err - } + // If node is running in path mode, skip explicit gc operation // which is unnecessary in this mode. if bc.triedb.Scheme() == rawdb.PathScheme { @@ -1442,8 +1470,8 @@ func (bc *BlockChain) WriteBlockAndSetHead(block *types.Block, receipts []*types // writeBlockAndSetHead is the internal implementation of WriteBlockAndSetHead. // This function expects the chain mutex to be held. -func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { - if err := bc.writeBlockWithState(block, receipts, state); err != nil { +func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, db *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { + if err := bc.writeBlockWithState(block, receipts, db); err != nil { return NonStatTy, err } currentBlock := bc.CurrentBlock() @@ -1561,6 +1589,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) var ( stats = insertStats{startTime: mclock.Now()} lastCanon *types.Block + statedb *state.StateDB + err error ) // Fire a single chain head event if we've progressed the chain defer func() { @@ -1736,7 +1766,12 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) if parent == nil { parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) } - statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) + + if bc.crossValidator != nil { + statedb, err = state.NewWithWitnessRecording(parent.Root, bc.stateCache, bc.snaps) + } else { + statedb, err = state.New(parent.Root, bc.stateCache, bc.snaps) + } if err != nil { return it.index, err } @@ -1774,7 +1809,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) 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) followupInterrupt.Store(true) return it.index, err @@ -1809,6 +1844,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) } else { status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false) } + followupInterrupt.Store(true) if err != nil { return it.index, err diff --git a/core/cross_validator.go b/core/cross_validator.go new file mode 100644 index 0000000000..36fb78ab88 --- /dev/null +++ b/core/cross_validator.go @@ -0,0 +1,66 @@ +package core + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "io" + "net/http" + "net/url" + + "github.com/ethereum/go-ethereum/core/state" +) + +// crossValidator holds configuration for stateless cross-validation of imported blocks. +type crossValidator struct { + // HTTP endpoint for stateless block validation (in the future this will be a list) + endpoint string + // path to dump witnesses to disk when cross-validation fails + witnessRecordingPath string +} + +// CrossValidateBlock verifies the given stateless witness using the configured cross validater endpoint. +// If cross-validation fails, it dumps the witness to a file on disk. +// +// TODO: differentiate between errors from witness verification (maybe consensus +// failure) and anything else. +func (c *crossValidator) CrossValidateBlock(chainConfig *params.ChainConfig, witness *state.Witness) error { + // encode the witness to RLP, zeroing-out the block state root before sending + // it for cross validation to make it impossible for a cross-validator to + // produce a correct validation result without computing it. + enc, _ := witness.EncodeRLP() + + // TODO: implement retry if endpoint can't be reached + p, err := url.JoinPath(c.endpoint, "verify_block") + if err != nil { + return fmt.Errorf("url.JoinPath failed: %v", err) + } + resp, err := http.Post(p, "application/octet-stream", bytes.NewBuffer(enc)) + if err != nil { + return fmt.Errorf("error accessing block verification endpoint: %v", err) + } + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body: %v", err) + } + return fmt.Errorf("cross-validator bad response code (%d): %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body: %v", err) + } + if bytes.Compare(body, witness.Block.Header().Root[:]) != 0 { + if errInner := state.DumpBlockWitnessToFile(chainConfig, witness, c.witnessRecordingPath); errInner != nil { + log.Error("failed to dump block to file", "error", errInner) + panic("should not happen") + } + return err + } + return nil +} + +// CrossValidate posts the provided witness to the URL at {endpoint}/verify_block and returns whether the remote +// verification was successful or not. diff --git a/core/evm.go b/core/evm.go index 73f6d7bc20..7b1a620b6c 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" @@ -39,6 +41,16 @@ type ChainContext interface { // NewEVMBlockContext creates a new context for use in the EVM. func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { + return newEVMBlockContext(nil, header, chain, author) +} + +// NewStatelessEVMBlockContext creates a new context for use in the EVM in stateless execution mode. The BLOCKHASH +// opcode sources block hashes from the provided witness in stateless execution mode. +func NewStatelessEVMBlockContext(witness *state.Witness, header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { + return newEVMBlockContext(witness, header, chain, author) +} + +func newEVMBlockContext(witness *state.Witness, header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { var ( beneficiary common.Address baseFee *big.Int @@ -61,10 +73,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common if header.Difficulty.Cmp(common.Big0) == 0 { random = &header.MixDigest } + var getHash vm.GetHashFunc + if witness != nil { + getHash = func(n uint64) common.Hash { + return witness.GetBlockHash(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 7520923eef..a2d01f168c 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -126,6 +126,13 @@ type Trie interface { // be created with new root and updated trie database for following usage Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) + // CommitAndObtainAccessList does the same thing as Commit and returns an + // access list map of trie nodes read from the database. + CommitAndObtainAccessList(collectLeaf bool) (common.Hash, *trienode.NodeSet, map[string][]byte, error) + + // AccessList returns a map of trie node read from the database. + 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 fc26af68db..e7c21e6881 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -194,6 +194,13 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { err error value common.Hash ) + + if s.db.witness != nil && s.db.snap != nil && s.origin != nil { + // when building a witness with snapshot enabled, prefetch all read slots to be collected + // and included in the witness when the block root hash is committed (intermediateroot/commit?) + s.db.readPrefetcher.prefetch(s.addrHash, s.origin.Root, s.address, [][]byte{key[:]}) + } + if s.db.snap != nil { start := time.Now() enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) @@ -217,6 +224,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return common.Hash{} } val, err := tr.GetStorage(s.address, key.Bytes()) + //fmt.Printf("trie access list is %v\n", tr.AccessList()) if metrics.EnabledExpensive { s.db.StorageReads += time.Since(start) } @@ -379,11 +387,11 @@ func (s *stateObject) updateRoot() { // commit obtains a set of dirty storage trie nodes and updates the account data. // The returned set can be nil if nothing to commit. This function assumes all // storage mutations have already been flushed into trie by updateRoot. -func (s *stateObject) commit() (*trienode.NodeSet, error) { +func (s *stateObject) commit() (*trienode.NodeSet, map[string][]byte, error) { // Short circuit if trie is not even loaded, don't bother with committing anything if s.trie == nil { s.origin = s.data.Copy() - return nil, nil + return nil, nil, nil } // Track the amount of time wasted on committing the storage trie if metrics.EnabledExpensive { @@ -392,15 +400,15 @@ func (s *stateObject) commit() (*trienode.NodeSet, error) { // The trie is currently in an open state and could potentially contain // cached mutations. Call commit to acquire a set of nodes that have been // modified, the set can be nil if nothing to commit. - root, nodes, err := s.trie.Commit(false) + root, nodes, accessList, err := s.trie.CommitAndObtainAccessList(false) if err != nil { - return nil, err + return nil, nil, err } s.data.Root = root // Update original account data after commit s.origin = s.data.Copy() - return nodes, nil + return nodes, accessList, 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..54fdf38182 --- /dev/null +++ b/core/state/state_witness.go @@ -0,0 +1,411 @@ +package state + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "sort" + + "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/params" + "github.com/ethereum/go-ethereum/rlp" +) + +type Witness struct { + Block *types.Block + blockHashes map[uint64]common.Hash + codes map[common.Hash]Code + root common.Hash + lists map[common.Hash]map[string][]byte +} + +func (w *Witness) GetBlockHash(num uint64) common.Hash { + return w.blockHashes[num] +} + +func (w *Witness) Root() common.Hash { + return w.root +} + +type rlpWitness struct { + EncBlock []byte + Root common.Hash + Owners []common.Hash + AllPaths [][]string + AllNodes [][][]byte + BlockNums []uint64 + BlockHashes []common.Hash + Codes []Code + CodeHashes []common.Hash +} + +func (e *rlpWitness) ToWitness() (*Witness, error) { + res := NewWitness(e.Root) + if err := rlp.DecodeBytes(e.EncBlock, &res.Block); err != nil { + return nil, err + } + for i := 0; i < len(e.Codes); i++ { + res.codes[e.CodeHashes[i]] = e.Codes[i] + } + for i, owner := range e.Owners { + pathMap := make(map[string][]byte) + for j := 0; j < len(e.AllPaths[i]); j++ { + pathMap[e.AllPaths[i][j]] = e.AllNodes[i][j] + } + res.lists[owner] = pathMap + } + for i, blockNum := range e.BlockNums { + res.blockHashes[blockNum] = e.BlockHashes[i] + } + return res, nil +} + +func DecodeWitnessRLP(b []byte) (*Witness, error) { + var res rlpWitness + if err := rlp.DecodeBytes(b, &res); err != nil { + return nil, err + } + if wit, err := res.ToWitness(); err != nil { + return nil, err + } else { + return wit, nil + } +} + +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, nodeMap := range w.lists { + encWit.Owners = append(encWit.Owners, owner) + var ownerPaths []string + var ownerNodes [][]byte + + for path, node := range nodeMap { + ownerPaths = append(ownerPaths, path) + ownerNodes = append(ownerNodes, node) + } + encWit.AllPaths = append(encWit.AllPaths, ownerPaths) + encWit.AllNodes = append(encWit.AllNodes, ownerNodes) + } + + for codeHash, code := range w.codes { + encWit.CodeHashes = append(encWit.CodeHashes, codeHash) + 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 raw trie nodes keyed by path to an owner +// in the witness. the witness takes ownership of the passed map. +func (w *Witness) addAccessList(owner common.Hash, list map[string][]byte) { + var stateNodes map[string][]byte + + if len(list) == 0 { + return + } + stateNodes, ok := w.lists[owner] + if !ok { + stateNodes = make(map[string][]byte) + w.lists[owner] = stateNodes + } + + for path, node := range list { + stateNodes[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 EVM bytecode in the witness. It does +// nothing if there is already a code associated with the given hash. +// The witness takes ownership over the passed code slice. +func (w *Witness) AddCode(hash common.Hash, code Code) { + if code, ok := w.codes[hash]; ok && len(code) > 0 { + return + } + w.codes[hash] = code +} + +// AddCodeHash adds a code hash to the witness +// TODO bug: adding a code hash before executing the same account later would result in the account's code +// not being added to the witness. this should be covered in state tests? +func (w *Witness) AddCodeHash(hash common.Hash) { + if _, ok := w.codes[hash]; ok { + return + } + w.codes[hash] = []byte{} +} + +// 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.lists { + 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.lists), common.StorageSize(len(w.lists)*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.lists { + res.lists[owner] = make(map[string][]byte) + for path, node := range owned { + cpy := make([]byte, len(node)) + copy(cpy, node) + res.lists[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 + 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 + var owners []common.Hash + for owner, _ := range w.lists { + owners = append(owners, owner) + } + sort.Slice(owners, func(i, j int) bool { + return bytes.Compare(owners[i][:], owners[j][:]) > 0 + }) + + var ownersPaths [][]string + var ownersNodes [][][]byte + + // sort the nodes of each owner by path + for _, owner := range owners { + nodes := w.lists[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) + } + + var blockNrs []uint64 + var blockHashes []common.Hash + for blockNr, blockHash := range w.blockHashes { + blockNrs = append(blockNrs, blockNr) + blockHashes = append(blockHashes, blockHash) + } + + var codeHashes []common.Hash + var codes []Code + 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, + AllPaths: ownersPaths, + AllNodes: ownersNodes, + BlockNums: blockNrs, + BlockHashes: blockHashes, + Codes: codes, + CodeHashes: codeHashes, + } +} + +// 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.AllPaths[i] + ownerNodes := sorted.AllNodes[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 i, codeHash := range sorted.CodeHashes { + code := sorted.Codes[i] + fmt.Fprintf(b, "\t%x:%x\n", codeHash, 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 common.Hash(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]Code), + root: root, + lists: make(map[common.Hash]map[string][]byte), + } +} + +// DumpBlockWitnessToFile serializes a witness object and writes it and the provided chain config to files on +// a given path. +func DumpBlockWitnessToFile(cfg *params.ChainConfig, w *Witness, path string) error { + enc, _ := w.EncodeRLP() + + blockHash := w.Block.Hash() + witnessOutputFName := fmt.Sprintf("%d-%x.rlp", w.Block.NumberU64(), blockHash[0:8]) + witnessPath := filepath.Join(path, witnessOutputFName) + err := os.WriteFile(witnessPath, enc, 0644) + if err != nil { + return err + } + + cfgOutputFName := fmt.Sprintf("%d-%x-chaincfg.json", w.Block.NumberU64(), blockHash[0:8]) + cfgPath := filepath.Join(path, cfgOutputFName) + f, err := os.OpenFile(cfgPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer f.Close() + + cfgWriter := json.NewEncoder(f) + cfgWriter.Encode(cfg) + return nil +} + +// PopulateDB imports trie nodes from the witness +// into the specified backing database. +func (w *Witness) PopulateDB(db ethdb.Database) error { + batch := db.NewBatch() + for owner, nodes := range w.lists { + for path, node := range nodes { + if owner == (common.Hash{}) { + rawdb.WriteAccountTrieNode(batch, []byte(path), node) + } else { + rawdb.WriteStorageTrieNode(batch, owner, []byte(path), node) + } + } + } + + for blockNum, blockHash := range w.blockHashes { + fakeHeader := types.Header{} + fakeHeader.ParentHash = blockHash + fakeHeader.Number = new(big.Int).SetUint64(blockNum) + rawdb.WriteHeader(batch, &fakeHeader) + } + + 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 a4b8cf93e2..49399280f3 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -138,6 +138,20 @@ type StateDB struct { // Testing hooks onCommit func(states *triestate.Set) // Hook invoked when commit is performed + + witness *Witness + readPrefetcher *triePrefetcher +} + +// NewWithWitnessRecording creates a new state from a given trie. The state is configured to construct a stateless +// block witness which is completed after Commit is called. +func NewWithWitnessRecording(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { + sdb, err := New(root, db, snaps) + if err != nil { + return nil, err + } + sdb.witness = NewWitness(root) + return sdb, nil } // New creates a new state from a given trie. @@ -177,11 +191,20 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) // commit phase, most of the needed data is already hot. func (s *StateDB) StartPrefetcher(namespace string) { if s.prefetcher != nil { + s.prefetcher.wait() s.prefetcher.close() s.prefetcher = nil } + if s.readPrefetcher != nil { + s.readPrefetcher.wait() + s.readPrefetcher.close() + s.readPrefetcher = nil + } if s.snap != nil { s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace) + if s.witness != nil { + s.readPrefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace) + } } } @@ -189,9 +212,15 @@ func (s *StateDB) StartPrefetcher(namespace string) { // from the gathered metrics. func (s *StateDB) StopPrefetcher() { if s.prefetcher != nil { + s.prefetcher.wait() s.prefetcher.close() s.prefetcher = nil } + if s.readPrefetcher != nil { + s.readPrefetcher.wait() + s.readPrefetcher.close() + s.readPrefetcher = nil + } } // setError remembers the first non-nil error it is called with. @@ -350,7 +379,8 @@ func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { - return stateObject.GetCommittedState(hash) + res := stateObject.GetCommittedState(hash) + return res } return common.Hash{} } @@ -604,9 +634,15 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { return nil } } + // Insert into the live set obj := newObject(s, addr, data) s.setStateObject(obj) + if s.witness != nil && s.snap != nil { + // when building witness with snap enabled, prefetch all read accounts to later be collected and + // included in the witness + s.readPrefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, [][]byte{addr[:]}) + } return obj } @@ -712,6 +748,9 @@ func (s *StateDB) Copy() *StateDB { snaps: s.snaps, snap: s.snap, } + if s.witness != nil { + state.witness = s.witness.Copy() + } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { // As documented [here](https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527), @@ -866,6 +905,33 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { s.clearJournalAndRefund() } +func (s *StateDB) collectReadStorageAccessLists() { + for _, obj := range s.stateObjects { + // load read storage slots from the finished trie in the prefetcher as these continue to be prefetched + // until commit. + tr := s.readPrefetcher.trie(obj.addrHash, obj.data.Root) + if tr == nil { + continue + } + accessList := tr.AccessList() + if len(accessList) > 0 { + s.witness.addAccessList(obj.addrHash, accessList) + } + } +} + +func (s *StateDB) collectReadAccountsAccessLists() { + tr := s.readPrefetcher.trie(common.Hash{}, s.originalRoot) + if tr == nil { + // TODO: ensure this case is b/c of empty block + return + } + accessList := tr.AccessList() + if len(accessList) > 0 { + s.witness.addAccessList(common.Hash{}, accessList) + } +} + // 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. @@ -873,20 +939,24 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // Finalise all the dirty storage states and write them into the tries s.Finalise(deleteEmptyObjects) - // If there was a trie prefetcher operating, it gets aborted and irrevocably - // modified after we start retrieving tries. Remove it from the statedb after - // this round of use. - // - // This is weird pre-byzantium since the first tx runs with a prefetcher and - // the remainder without, but pre-byzantium even the initial prefetcher is - // useless, so no sleep lost. prefetcher := s.prefetcher if s.prefetcher != nil { defer func() { + // TODO: need to wait for read accounts to be resolved in prefetcher main trie? + s.prefetcher.wait() s.prefetcher.close() s.prefetcher = nil }() } + if s.readPrefetcher != nil { + // TODO: move read prefetcher logic into Commit? + s.readPrefetcher.wait() + s.collectReadStorageAccessLists() + s.collectReadAccountsAccessLists() + s.readPrefetcher.close() + s.readPrefetcher = nil + } + // Although naively it makes sense to retrieve the account trie and then do // the contract storage and account updates sequentially, that short circuits // the account prefetcher. Instead, let's process all the storage updates @@ -897,6 +967,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { obj.updateRoot() } } + // Now we're about to start to write changes to the trie. The trie is so far // _untouched_. We can check with the prefetcher, if it can give us a trie // which has the same root, but also has some content loaded into it. @@ -906,16 +977,29 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { } } usedAddrs := make([][]byte, 0, len(s.stateObjectsPending)) + + // perform updates before deletions. In the case where a full node + // has two children, one of them selfdestructs and makes the recipient + // another non-existing sibling, applying deletion before update would + // result in the unecessary premature collapse of the full node into a short node + // for the untouched third sibling. + var deletedObjects []*stateObject for addr := range s.stateObjectsPending { - if obj := s.stateObjects[addr]; obj.deleted { - s.deleteStateObject(obj) - s.AccountDeleted += 1 - } else { + if obj := s.stateObjects[addr]; !obj.deleted { s.updateStateObject(obj) s.AccountUpdated += 1 + usedAddrs = append(usedAddrs, common.CopyBytes(addr[:])) // Copy needed for closure + } else { + deletedObjects = append(deletedObjects, obj) } - usedAddrs = append(usedAddrs, common.CopyBytes(addr[:])) // Copy needed for closure + usedAddrs = append(usedAddrs, common.CopyBytes(addr[:])) } + for _, deletedObj := range deletedObjects { + s.deleteStateObject(deletedObj) + s.AccountDeleted += 1 + usedAddrs = append(usedAddrs, common.CopyBytes(deletedObj.address[:])) // Copy needed for closure + } + if prefetcher != nil { prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs) } @@ -929,6 +1013,13 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { return s.trie.Hash() } +// makes the statedb configure to build a stateless block witness +// this must be called after initializing a statedb and before +// starting prefetchers or applying state changes +func (s *StateDB) EnableWitnessRecording() { + s.witness = NewWitness(s.originalRoot) +} + // SetTxContext sets the current transaction hash and index which are // used when the EVM emits new state logs. It should be invoked before // transaction execution. @@ -1153,6 +1244,28 @@ func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.A return incomplete, nil } +// 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 +} + +// ApplyWithdrawals credits the balance of each account that is the recipient +// of a withdrawal. +func (s *StateDB) ApplyWithdrawals(withdrawals types.Withdrawals) { + for _, w := range withdrawals { + // Convert amount from gwei to wei. + amount := new(big.Int).SetUint64(w.Amount) + amount = amount.Mul(amount, big.NewInt(params.GWei)) + s.AddBalance(w.Address, amount) + } + + if s.witness != nil { + al := s.trie.AccessList() + s.witness.addAccessList(common.Hash{}, al) + } +} + // Commit writes the state to the underlying in-memory trie database. // Once the state is committed, tries cached in stateDB (including account // trie, storage tries) will no longer be functional. A new state instance @@ -1177,6 +1290,8 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er storageTrieNodesDeleted int nodes = trienode.NewMergedNodeSet() codeWriter = s.db.DiskDB().NewBatch() + root common.Hash + set *trienode.NodeSet ) // Handle all state deletions first incomplete, err := s.handleDestruction(nodes) @@ -1184,6 +1299,31 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er return common.Hash{}, err } // Handle all state updates afterwards + if s.witness != nil { + if s.snap == nil { + // if the snapshot is not in use, all read state values and their intermediate + // nodes are resolved in the statedb/object tries. + // + // we collect access lists after commit because there are circumstances where + // computation of a new trie root hash where a value has been deleted could + // collapse a parent branch node + sibling into a full node for the sibling. + // In this case, the sibling would only be read during root hash computation. + // TODO: verify the above assertion is the case. + for addr := range s.stateObjects { + obj := s.stateObjects[addr] + if _, ok := s.stateObjectsDirty[addr]; ok && !obj.deleted { + // collect dirty object access witness if/when we commit them + continue + } + if obj.trie != nil { + al := obj.trie.AccessList() + s.witness.addAccessList(obj.addrHash, al) + } + } + accessList := s.trie.AccessList() + s.witness.addAccessList(common.Hash{}, accessList) + } + } for addr := range s.stateObjectsDirty { obj := s.stateObjects[addr] if obj.deleted { @@ -1194,11 +1334,19 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er rawdb.WriteCode(codeWriter, common.BytesToHash(obj.CodeHash()), obj.code) obj.dirtyCode = false } + // Write any storage changes in the state object to its storage trie - set, err := obj.commit() + set, accessList, err := obj.commit() if err != nil { return common.Hash{}, err } + + if s.witness != nil { + // storage trie nodes for writes accrue in the state object's trie instance + // storage trie nodes from read slots are accrued in the prefetcher and + // retrieved in IntermediateRoot + s.witness.addAccessList(obj.addrHash, accessList) + } // Merge the dirty nodes of storage trie into global set. It is possible // that the account was destructed and then resurrected in the same block. // In this case, the node set is shared by both accounts. @@ -1221,7 +1369,14 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er if metrics.EnabledExpensive { start = time.Now() } - root, set, err := s.trie.Commit(true) + + if s.witness != nil { + var accessList map[string][]byte + root, set, accessList, err = s.trie.CommitAndObtainAccessList(true) + s.witness.addAccessList(common.Hash{}, accessList) + } else { + root, set, err = s.trie.Commit(true) + } if err != nil { return common.Hash{}, err } @@ -1295,6 +1450,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er s.storagesOrigin = make(map[common.Address]map[common.Hash][]byte) s.stateObjectsDirty = make(map[common.Address]struct{}) s.stateObjectsDestruct = make(map[common.Address]*types.StateAccount) + return root, nil } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index c2a49417d4..e624e65162 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -17,6 +17,7 @@ package state import ( + "fmt" "sync" "github.com/ethereum/go-ethereum/common" @@ -70,13 +71,16 @@ func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePre } return p } +func (p *triePrefetcher) wait() { + for _, fetcher := range p.fetchers { + fetcher.wait() + } +} // close iterates over all the subfetchers, aborts any that were left spinning // and reports the stats to the metrics subsystem. func (p *triePrefetcher) close() { for _, fetcher := range p.fetchers { - fetcher.abort() // safe to do multiple times - if metrics.Enabled { if fetcher.root == p.root { p.accountLoadMeter.Mark(int64(len(fetcher.seen))) @@ -123,29 +127,22 @@ func (p *triePrefetcher) copy() *triePrefetcher { storageSkipMeter: p.storageSkipMeter, storageWasteMeter: p.storageWasteMeter, } - // If the prefetcher is already a copy, duplicate the data - if p.fetches != nil { - for root, fetch := range p.fetches { - if fetch == nil { - continue - } - copy.fetches[root] = p.db.CopyTrie(fetch) - } - return copy - } - // Otherwise we're copying an active fetcher, retrieve the current states - for id, fetcher := range p.fetchers { - copy.fetches[id] = fetcher.peek() - } return copy } // prefetch schedules a batch of trie items to prefetch. +// prefetch is called from two locations: +// 1. Finalize of the state-objects storage roots. This happens at the end +// of every transaction, meaning that if several transactions touches +// upon the same contract, the parameters invoking this method may be +// repeated. +// 2. Finalize of the main account trie. This happens only once per block. func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, keys [][]byte) { // If the prefetcher is an inactive one, bail out if p.fetches != nil { return } + // Active fetcher, schedule the retrievals id := p.trieID(owner, root) fetcher := p.fetchers[id] @@ -175,16 +172,13 @@ func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie { p.deliveryMissMeter.Mark(1) return nil } - // Interrupt the prefetcher if it's by any chance still running and return - // a copy of any pre-loaded trie. - fetcher.abort() // safe to do multiple times - - trie := fetcher.peek() - if trie == nil { + // Wait for the fetcher to finish + fetcher.wait() // safe to do multiple times + if fetcher.trie == nil { p.deliveryMissMeter.Mark(1) return nil } - return trie + return fetcher.db.CopyTrie(fetcher.trie) } // used marks a batch of state items used to allow creating statistics as to @@ -215,13 +209,12 @@ type subfetcher struct { addr common.Address // Address of the account that the trie belongs to trie Trie // Trie being populated with nodes - tasks [][]byte // Items queued up for retrieval - lock sync.Mutex // Lock protecting the task queue + tasks [][]byte // Items queued up for retrieval + lock sync.Mutex // Lock protecting the task queue + closing bool // set to true if the subfetcher is closing - wake chan struct{} // Wake channel if a new task is scheduled - stop chan struct{} // Channel to interrupt processing - term chan struct{} // Channel to signal interruption - copy chan chan Trie // Channel to request a copy of the current trie + wake chan bool // Wake channel if a new task is scheduled, true if the subfetcher should continue running when there are no pending tasks + term chan struct{} // Channel to signal interruption seen map[string]struct{} // Tracks the entries already loaded dups int // Number of duplicate preload tasks @@ -237,10 +230,8 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo owner: owner, root: root, addr: addr, - wake: make(chan struct{}, 1), - stop: make(chan struct{}), + wake: make(chan bool, 1), term: make(chan struct{}), - copy: make(chan chan Trie), seen: make(map[string]struct{}), } go sf.loop() @@ -251,42 +242,30 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo func (sf *subfetcher) schedule(keys [][]byte) { // Append the tasks to the current queue sf.lock.Lock() + sf.tasks = append(sf.tasks, keys...) sf.lock.Unlock() - - // Notify the prefetcher, it's fine if it's already terminated + // Notify the prefetcher. The wake-chan is buffered, so this is async. select { - case sf.wake <- struct{}{}: + case sf.wake <- true: default: } } -// peek tries to retrieve a deep copy of the fetcher's trie in whatever form it -// is currently. -func (sf *subfetcher) peek() Trie { - ch := make(chan Trie) - select { - case sf.copy <- ch: - // Subfetcher still alive, return copy from it - return <-ch - - case <-sf.term: - // Subfetcher already terminated, return a copy directly - if sf.trie == nil { - return nil - } - return sf.db.CopyTrie(sf.trie) - } -} - -// abort interrupts the subfetcher immediately. It is safe to call abort multiple +// wait waits for the subfetcher to finish it's task. It is safe to call wait multiple // times but it is not thread safe. -func (sf *subfetcher) abort() { - select { - case <-sf.stop: - default: - close(sf.stop) +func (sf *subfetcher) wait() { + // Signal termination by nil tasks + sf.lock.Lock() + if sf.closing { + sf.lock.Unlock() + return // already exiting } + sf.closing = true + sf.lock.Unlock() + // Notify the prefetcher. The wake-chan is buffered, so this is async. + sf.wake <- false + // Wait for it to terminate <-sf.term } @@ -316,50 +295,32 @@ func (sf *subfetcher) loop() { } // Trie opened successfully, keep prefetching items for { - select { - case <-sf.wake: - // Subfetcher was woken up, retrieve any tasks to avoid spinning the lock - sf.lock.Lock() - tasks := sf.tasks - sf.tasks = nil - sf.lock.Unlock() + keepRunning := <-sf.wake + if !keepRunning { + return + } + // Subfetcher was woken up, retrieve any tasks to avoid spinning the lock + sf.lock.Lock() + tasks := sf.tasks + sf.tasks = nil + sf.lock.Unlock() - // Prefetch any tasks until the loop is interrupted - for i, task := range tasks { - select { - case <-sf.stop: - // If termination is requested, add any leftover back and return - sf.lock.Lock() - sf.tasks = append(sf.tasks, tasks[i:]...) - sf.lock.Unlock() - return - - case ch := <-sf.copy: - // Somebody wants a copy of the current trie, grant them - ch <- sf.db.CopyTrie(sf.trie) - - default: - // No termination request yet, prefetch the next entry - if _, ok := sf.seen[string(task)]; ok { - sf.dups++ - } else { - if len(task) == common.AddressLength { - sf.trie.GetAccount(common.BytesToAddress(task)) - } else { - sf.trie.GetStorage(sf.addr, task) - } - sf.seen[string(task)] = struct{}{} - } + // Prefetch all tasks + for _, task := range tasks { + if _, ok := sf.seen[string(task)]; ok { + sf.dups++ + continue + } + if len(task) == common.AddressLength { + sf.trie.GetAccount(common.BytesToAddress(task)) + } else { + _, err := sf.trie.GetStorage(sf.addr, task) + if err != nil { + // TODO: see what needs to be done in this case + fmt.Printf("prefetch storage failed: %+v\n", err) } } - - case ch := <-sf.copy: - // Somebody wants a copy of the current trie, grant them - ch <- sf.db.CopyTrie(sf.trie) - - case <-sf.stop: - // Termination is requested, abort and leave remaining tasks - return + sf.seen[string(task)] = struct{}{} } } } diff --git a/core/state_processor.go b/core/state_processor.go index 9e32ab4e56..5e0c5f3dc7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -21,6 +21,8 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc" @@ -36,9 +38,10 @@ 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 + chainCtx *StatelessChainContext // chain context shim for stateless verification mode } // NewStateProcessor initialises a new StateProcessor. @@ -50,6 +53,25 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen } } +func NewStatelessStateProcessor(config *params.ChainConfig, chainCtx *StatelessChainContext, engine consensus.Engine) *StateProcessor { + return &StateProcessor{ + config: config, + chainCtx: chainCtx, + engine: engine, + bc: &BlockChain{ + chainConfig: config, + engine: engine, + }, + } +} +func (p *StateProcessor) ProcessStateless(witness *state.Witness, block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { + return p.process(witness, block, statedb, cfg) +} + +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { + return p.process(nil, block, statedb, cfg) +} + // 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 +79,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(witness *state.Witness, block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -72,10 +94,16 @@ 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) ) + if witness != nil { + context = NewStatelessEVMBlockContext(witness, header, p.chainCtx, nil) + } else { + context = NewEVMBlockContext(header, p.bc, nil) + } + vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg) + if beaconRoot := block.BeaconRoot(); beaconRoot != nil { ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } @@ -98,6 +126,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg if len(withdrawals) > 0 && !p.config.IsShanghai(block.Number(), block.Time()) { 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.Transactions(), block.Uncles(), withdrawals) @@ -189,3 +218,26 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, vmenv *vm.EVM, statedb *stat _, _, _ = vmenv.Call(vm.AccountRef(msg.From), *msg.To, msg.Data, 30_000_000, common.U2560) statedb.Finalise(true) } + +// StatelessChainContext implements a stateless chain context stub +// which is used in place of a backing blockchain instance. +type StatelessChainContext struct { + chaindb ethdb.Database + engine consensus.Engine +} + +func (s *StatelessChainContext) GetHeader(hash common.Hash, number uint64) *types.Header { + //return rawdb.ReadHeader(s.chaindb, hash, number) + panic("not implemented") +} + +func (s *StatelessChainContext) Engine() consensus.Engine { + return s.engine +} + +func NewStatelessChainContext(chaindb ethdb.Database, engine consensus.Engine) *StatelessChainContext { + return &StatelessChainContext{ + chaindb, + engine, + } +} diff --git a/core/types.go b/core/types.go index 36eb0d1ded..c6572e8dcd 100644 --- a/core/types.go +++ b/core/types.go @@ -17,6 +17,7 @@ package core import ( + "github.com/ethereum/go-ethereum/common" "sync/atomic" "github.com/ethereum/go-ethereum/core/state" @@ -33,7 +34,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. diff --git a/core/types/block.go b/core/types/block.go index 1a357baa3a..71dec45a26 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -319,6 +319,18 @@ func (b *Block) DecodeRLP(s *rlp.Stream) error { return nil } +// 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 +} + // EncodeRLP serializes a block as RLP. func (b *Block) EncodeRLP(w io.Writer) error { return rlp.Encode(w, &extblock{ diff --git a/core/vm/evm.go b/core/vm/evm.go index 16cc854908..7221008f76 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -181,6 +181,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } + // Fail if we're trying to transfer more than the available balance if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { return nil, gas, ErrInsufficientBalance @@ -188,7 +189,6 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) debug := evm.Config.Tracer != nil - if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non existing account, don't do anything, but ping the tracer @@ -229,6 +229,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 { @@ -293,6 +298,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 @@ -337,6 +345,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 @@ -390,6 +401,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 023aa0af00..95c18f3ff3 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -342,7 +342,13 @@ func opReturnDataCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeConte func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() - slot.SetUint64(uint64(interpreter.evm.StateDB.GetCodeSize(slot.Bytes20()))) + address := slot.Bytes20() + slot.SetUint64(uint64(interpreter.evm.StateDB.GetCodeSize(address))) + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + code := interpreter.evm.StateDB.GetCode(address) + codeHash := interpreter.evm.StateDB.GetCodeHash(address) + witness.AddCode(codeHash, code) + } return nil, nil } @@ -380,6 +386,10 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) uint64CodeOffset = 0xffffffffffffffff } addr := common.Address(a.Bytes20()) + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + witness.AddCode(interpreter.evm.StateDB.GetCodeHash(addr), interpreter.evm.StateDB.GetCode(addr)) + } + codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64()) scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) @@ -418,6 +428,10 @@ func opExtCodeHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) if interpreter.evm.StateDB.Empty(address) { slot.Clear() } else { + _ = interpreter.evm.StateDB.GetCode(address) // ensure the account leaf is fetched and included in the witness + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + witness.AddCodeHash(interpreter.evm.StateDB.GetCodeHash(address)) + } slot.SetBytes(interpreter.evm.StateDB.GetCodeHash(address).Bytes()) } return nil, nil @@ -444,7 +458,13 @@ 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).Bytes() + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + var bh common.Hash + copy(bh[:], res[:]) + witness.AddBlockHash(bh, num64) + } + num.SetBytes(res[:]) } else { num.Clear() } diff --git a/core/vm/interface.go b/core/vm/interface.go index 25bfa06720..f546c63312 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/types" "github.com/ethereum/go-ethereum/params" @@ -79,6 +81,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_debug.go b/eth/api_debug.go index 05010a3969..97701d4214 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -20,8 +20,13 @@ import ( "context" "errors" "fmt" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "os" "time" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/rawdb" @@ -443,3 +448,47 @@ func (api *DebugAPI) GetTrieFlushInterval() (string, error) { } return api.eth.blockchain.GetTrieFlushInterval().String(), nil } + +func BuildProof(number uint64, bc *core.BlockChain) ([]byte, error) { + if number == 0 { + panic("cannot build genesis block proof") + } + parent := bc.GetBlockByNumber(number - 1) + db, err := bc.StateAt(parent.Header().Root) + if err != nil { + return nil, err + } + db.EnableWitnessRecording() + db.StartPrefetcher("apidebug") + block := bc.GetBlockByNumber(number) + + logconfig := &logger.Config{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: true, + Debug: true, + } + tracer := logger.NewJSONLogger(logconfig, os.Stdout) + _ = tracer + + stateProcessor := core.NewStateProcessor(bc.Config(), bc, bc.Engine()) + _, _, _, err = stateProcessor.Process(block, db, vm.Config{}) + 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 +} + +func (api *DebugAPI) BuildProof(num rpc.BlockNumber) ([]byte, error) { + return BuildProof(uint64(num), api.eth.blockchain) +} diff --git a/eth/backend.go b/eth/backend.go index 0a0813aafa..ad195361b0 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -213,7 +213,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.OverrideVerkle != nil { overrides.OverrideVerkle = config.OverrideVerkle } - eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, eth.engine, vmConfig, eth.shouldPreserve, &config.TransactionHistory) + eth.blockchain, err = core.NewBlockchainWithCrossValidator(config.CrossValidationEndpoint, config.WitnessRecordingPath, chainDb, cacheConfig, config.Genesis, &overrides, eth.engine, vmConfig, eth.shouldPreserve, &config.TransactionHistory) if err != nil { return nil, err } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index ad664afb5b..5841eac400 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -159,6 +159,9 @@ type Config struct { // OverrideVerkle (TODO: remove after the fork) OverrideVerkle *uint64 `toml:",omitempty"` + + CrossValidationEndpoint string `toml:",omitempty"` + WitnessRecordingPath string `toml:",omitempty"` } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index b86b5909d2..b51577a819 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -501,6 +501,11 @@ web3._extend({ call: 'debug_getTrieFlushInterval', params: 0 }), + new web3._extend.Method({ + name: 'buildProof', + call: 'debug_buildProof', + params: 1 + }), ], properties: [] }); diff --git a/miner/worker.go b/miner/worker.go index 052f34ff11..f7aa44e4d1 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -125,7 +125,8 @@ func (env *environment) discard() { if env.state == nil { return } - env.state.StopPrefetcher() + // TODO: re-enable prefetcher + //env.state.StopPrefetcher() } // task contains all information for consensus engine sealing and result submitting. @@ -720,7 +721,8 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co if err != nil { return nil, err } - state.StartPrefetcher("miner") + // TODO: re-enable prefetcher + //state.StartPrefetcher("miner") // Note the passed coinbase may be different with header.Coinbase. env := &environment{ diff --git a/tests/block_test.go b/tests/block_test.go index aa6f27b8f3..138fabfd47 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -18,9 +18,14 @@ package tests import ( "math/rand" + "os" "runtime" "testing" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/log" + "golang.org/x/exp/slog" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" ) @@ -61,6 +66,90 @@ func TestBlockchain(t *testing.T) { // which run natively, so there's no reason to run them here. } +func networkPostMerge(network string) bool { + switch network { + case "Frontier": + return false + case "EIP150": + return false + case "EIP158": + return false + case "Byzantium": + return false + case "Constantinople": + return false + case "ConstantinopleFix": + return false + case "Istanbul": + return false + case "MuirGlacier": + return false + case "Berlin": + return false + case "London": + return false + case "ArrowGlacier": + return false + case "GreyGlacier": + return false + case "Merge": + return true + case "Shanghai": + return true + case "Cancun": + return true + } + return false +} + +func TestStatelessBlockchain(t *testing.T) { + bt := new(testMatcher) + + // 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. +} + // TestExecutionSpec runs the test fixtures from execution-spec-tests. func TestExecutionSpec(t *testing.T) { if !common.FileExist(executionSpecDir) { @@ -74,19 +163,52 @@ func TestExecutionSpec(t *testing.T) { } func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { - if err := bt.checkFailure(t, test.Run(false, rawdb.HashScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(false, rawdb.HashScheme, nil)); err != nil { t.Errorf("test in hash mode without snapshotter failed: %v", err) return } - if err := bt.checkFailure(t, test.Run(true, rawdb.HashScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(true, rawdb.HashScheme, nil)); err != nil { t.Errorf("test in hash mode with snapshotter failed: %v", err) return } - if err := bt.checkFailure(t, test.Run(false, rawdb.PathScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(false, rawdb.PathScheme, nil)); err != nil { t.Errorf("test in path mode without snapshotter failed: %v", err) return } - if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil)); err != nil { + t.Errorf("test in path mode with snapshotter failed: %v", err) + return + } +} + +func execBlockTestStateless(t *testing.T, bt *testMatcher, test *BlockTest) { + handler := log.NewTerminalHandlerWithLevel(os.Stdout, slog.Level(667), false) + log.SetDefault(log.NewLogger(handler)) + logconfig := &logger.Config{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: true, + Debug: true, + } + tracer := logger.NewJSONLogger(logconfig, os.Stdout) + _ = tracer + + if err := bt.checkFailure(t, test.RunStateless(false, rawdb.HashScheme, 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)); 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)); 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)); err != nil { t.Errorf("test in path mode with snapshotter failed: %v", err) return } diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 6d3c4e5331..8e222df02f 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -22,10 +22,13 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/cmd/utils" "math/big" "os" "reflect" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -109,7 +112,15 @@ type btHeaderMarshaling struct { ExcessBlobGas *math.HexOrDecimal64 } -func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, postCheck func(error, *core.BlockChain)) (result error) { +func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger) error { + return t.run(false, snapshotter, scheme, tracer) +} + +func (t *BlockTest) RunStateless(snapshotter bool, scheme string, tracer vm.EVMLogger) error { + return t.run(true, snapshotter, scheme, tracer) +} + +func (t *BlockTest) run(stateless bool, snapshotter bool, scheme string, tracer vm.EVMLogger) error { config, ok := Forks[t.json.Network] if !ok { return UnsupportedForkError{t.json.Network} @@ -126,6 +137,14 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po } else { tconf.HashDB = hashdb.Defaults } + closeCh, port, err := utils.RunLocalServer(config, 0) + if err != nil { + return err + } + defer func() { + closeCh <- struct{}{} + }() + // Commit genesis state gspec := t.genesis(config) triedb := triedb.NewDatabase(db, tconf) @@ -144,14 +163,25 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po // Wrap the original engine within the beacon-engine engine := beacon.New(ethash.NewFaker()) - cache := &core.CacheConfig{TrieCleanLimit: 0, StateScheme: scheme, Preimages: true} + cache := &core.CacheConfig{TrieCleanLimit: 0, StateScheme: scheme, Preimages: true, TrieDirtyDisabled: true} if snapshotter { cache.SnapshotLimit = 1 cache.SnapshotWait = true } - chain, err := core.NewBlockChain(db, cache, gspec, nil, engine, vm.Config{ - Tracer: tracer, - }, nil, nil) + // TODO: create normal chain if not stateless mode + chain, err := core.NewBlockchainWithCrossValidator( + fmt.Sprintf("http://localhost:%d", port), + "", + db, + cache, + gspec, + nil, + engine, + vm.Config{ + //Tracer: tracer, + }, + nil, + nil) if err != nil { return err } @@ -161,11 +191,6 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po if err != nil { return err } - // Import succeeded: regardless of whether the _test_ succeeds or not, schedule - // the post-check to run - if postCheck != nil { - defer postCheck(result, chain) - } cmlast := chain.CurrentBlock().Hash() if common.Hash(t.json.BestBlock) != cmlast { return fmt.Errorf("last block hash validation mismatch: want: %x, have: %x", t.json.BestBlock, cmlast) @@ -183,6 +208,28 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po return err } } + + if stateless { + for _, blk := range validBlocks { + // TODO: set up test chassis to do verification on block import (in BlockChain) in addition to BuildProof + // (used by standalone witness execution path) + enc, err := eth.BuildProof(blk.BlockHeader.Number.Uint64(), chain) + if err != nil { + return fmt.Errorf("failed to build proof: %v", err) + } + witness, err := state.DecodeWitnessRLP(enc) + if err != nil { + return fmt.Errorf("error decoding witness: %v", err) + } + root, err := utils.StatelessExecute(os.Stdout, 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 t.validateImportedHeaders(chain, validBlocks) } diff --git a/trie/secure_trie.go b/trie/secure_trie.go index efd4dfb5d3..d7bac514d5 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -213,6 +213,25 @@ 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() +} + +func (t *StateTrie) CommitAndObtainAccessList(collectLeaf bool) (common.Hash, *trienode.NodeSet, map[string][]byte, error) { + // Write all the pre-images to the actual disk database + if len(t.getSecKeyCache()) > 0 { + if t.preimages != nil { + preimages := make(map[common.Hash][]byte) + for hk, key := range t.secKeyCache { + preimages[common.BytesToHash([]byte(hk))] = key + } + t.preimages.insertPreimage(preimages) + } + t.secKeyCache = make(map[string][]byte) + } + // Commit the trie and return its modified nodeset. + return t.trie.CommitAndObtainAccessList(collectLeaf) +} // Commit collects all dirty nodes in the trie and replaces them with the // corresponding node hash. All collected nodes (including dirty leaves if diff --git a/trie/trie.go b/trie/trie.go index 12764e18d1..70a66a5b94 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -107,7 +107,7 @@ func NewEmpty(db database.Database) *Trie { } // MustNodeIterator is a wrapper of NodeIterator and will omit any encountered -// error but just print out an error message. +// error but just printg out an error message. func (t *Trie) MustNodeIterator(start []byte) NodeIterator { it, err := t.NodeIterator(start) if err != nil { @@ -581,6 +581,10 @@ func (t *Trie) resolve(n node, prefix []byte) (node, error) { return n, nil } +// TODO: for resolveAndTrack, differentiate between hash node resolve failure in stateless +// vs normal execution. In normal mode, it represents an error with database +// consistency. In stateless execution, it means that the witness is incomplete. + // resolveAndTrack loads node from the underlying store with the given node hash // and path prefix and also tracks the loaded node blob in tracer treated as the // node's original value. The rlp-encoded blob is preferred to be loaded from @@ -602,6 +606,22 @@ func (t *Trie) Hash() common.Hash { return common.BytesToHash(hash.(hashNode)) } +func (t *Trie) AccessList() map[string][]byte { + return t.tracer.accessList +} + +func (t *Trie) CommitAndObtainAccessList(collectLeaf bool) (common.Hash, *trienode.NodeSet, map[string][]byte, error) { + accessList := t.tracer.accessList + // Commit will reset the tracer accessList, so after this + // operation, we have full ownership of the map (hence: no need to + // deep-copy or even copy). + rootHash, nodes, err := t.Commit(collectLeaf) + if err != nil { + return rootHash, nodes, nil, err + } + return rootHash, nodes, accessList, err +} + // 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. @@ -609,8 +629,8 @@ func (t *Trie) Hash() common.Hash { // Once the trie is committed, it's not usable anymore. A new trie must // be created with new root and updated trie database for following usage func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) { - defer t.tracer.reset() defer func() { + t.tracer.reset() t.committed = true }() // Trie is empty and can be classified into two types of situations: diff --git a/trie/verkle.go b/trie/verkle.go index 01d813d9ec..b2a6332d7b 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -216,6 +216,14 @@ func (t *VerkleTrie) Hash() common.Hash { return t.root.Commit().Bytes() } +func (t *VerkleTrie) AccessList() map[string][]byte { + panic("not implemented") +} + +func (t *VerkleTrie) CommitAndObtainAccessList(collectLeaf bool) (common.Hash, *trienode.NodeSet, map[string][]byte, error) { + panic("not implemented") +} + // Commit writes all nodes to the tree's memory database. func (t *VerkleTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet, error) { root, ok := t.root.(*verkle.InternalNode) diff --git a/triedb/database.go b/triedb/database.go index 939a21f147..fdfd82d255 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -45,6 +45,11 @@ var HashDefaults = &Config{ HashDB: hashdb.Defaults, } +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 { diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index d314779910..cbdb70ec03 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -33,8 +33,9 @@ import ( // thread-safe to use. However, callers need to ensure the thread-safety // of the referenced layer by themselves. type layerTree struct { - lock sync.RWMutex - layers map[common.Hash]layer + lock sync.RWMutex + layers map[common.Hash]layer + stateless bool } // newLayerTree constructs the layerTree with the given head layer.