stateless block proof generation w/ execution/verification.

Co-authored-by: Jared Wasinger <j-wasinger@hotmail.com>
Co-authored-by: Martin HS <martin@swende.se>
This commit is contained in:
Jared Wasinger 2024-01-21 17:22:23 -08:00
parent fe91d476ba
commit 1329dcf6fb
33 changed files with 1644 additions and 179 deletions

View file

@ -146,6 +146,8 @@ var (
configFileFlag, configFileFlag,
utils.LogDebugFlag, utils.LogDebugFlag,
utils.LogBacktraceAtFlag, utils.LogBacktraceAtFlag,
utils.CrossValidationEndpointFlag,
utils.WitnessRecordingPathFlag,
}, utils.NetworkFlags, utils.DatabaseFlags) }, utils.NetworkFlags, utils.DatabaseFlags)
rpcFlags = []cli.Flag{ rpcFlags = []cli.Flag{

230
cmd/stateless/main.go Normal file
View file

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

29
cmd/stateless/server.go Normal file
View file

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

View file

@ -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 ( var (
// TestnetFlags is the flag group of all built-in supported testnets. // TestnetFlags is the flag group of all built-in supported testnets.
TestnetFlags = []cli.Flag{ 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 { 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) 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 // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if

129
cmd/utils/stateless.go Normal file
View file

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

View file

@ -353,13 +353,7 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
beacon.ethone.Finalize(chain, header, state, txs, uncles, nil) beacon.ethone.Finalize(chain, header, state, txs, uncles, nil)
return return
} }
// Withdrawals processing. state.ApplyWithdrawals(withdrawals)
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)
}
// No block reward which is issued by consensus layer instead. // No block reward which is issued by consensus layer instead.
} }

View file

@ -19,6 +19,7 @@ package core
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
@ -47,6 +48,20 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engin
return validator 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 // ValidateBody validates the given block's uncles and verifies the block
// header's transaction and uncle roots. The headers are assumed to be already // header's transaction and uncle roots. The headers are assumed to be already
// validated at this point. // 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, // 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. // 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() header := block.Header()
if block.GasUsed() != usedGas { 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. // Validate the received block's bloom with the one derived from the generated receipts.
// For valid blocks this should always validate to true. // For valid blocks this should always validate to true.
rbloom := types.CreateBloom(receipts) rbloom := types.CreateBloom(receipts)
if rbloom != header.Bloom { 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]])) // Tre receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]]))
receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil)) receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil))
if receiptSha != header.ReceiptHash { 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 // Compute the state root and if enabled, check it against the
// an error if they don't match. // received state root and throw an error if they don't match.
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { root = statedb.IntermediateRoot(v.config.IsEIP158(header.Number))
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error()) 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 // CalcGasLimit computes the gas limit of the next block after parent. It aims

View file

@ -259,6 +259,17 @@ type BlockChain struct {
processor Processor // Block transaction processor interface processor Processor // Block transaction processor interface
forker *ForkChoice forker *ForkChoice
vmConfig vm.Config 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 // 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 // writeBlockWithState writes block, metadata and corresponding state data to the
// database. // 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 // Calculate the total difficulty of the block
ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1) ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1)
if ptd == nil { 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 // Make sure no inconsistent state is leaked during insertion
externTd := new(big.Int).Add(block.Difficulty(), ptd) 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. // 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) // 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.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd)
rawdb.WriteBlock(blockBatch, block) rawdb.WriteBlock(blockBatch, block)
rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts) rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts)
rawdb.WritePreimages(blockBatch, state.Preimages()) rawdb.WritePreimages(blockBatch, statedb.Preimages())
if err := blockBatch.Write(); err != nil { if err := blockBatch.Write(); err != nil {
log.Crit("Failed to write block into disk", "err", err) 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 // If node is running in path mode, skip explicit gc operation
// which is unnecessary in this mode. // which is unnecessary in this mode.
if bc.triedb.Scheme() == rawdb.PathScheme { 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. // writeBlockAndSetHead is the internal implementation of WriteBlockAndSetHead.
// This function expects the chain mutex to be held. // 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) { 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, state); err != nil { if err := bc.writeBlockWithState(block, receipts, db); err != nil {
return NonStatTy, err return NonStatTy, err
} }
currentBlock := bc.CurrentBlock() currentBlock := bc.CurrentBlock()
@ -1561,6 +1589,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
var ( var (
stats = insertStats{startTime: mclock.Now()} stats = insertStats{startTime: mclock.Now()}
lastCanon *types.Block lastCanon *types.Block
statedb *state.StateDB
err error
) )
// Fire a single chain head event if we've progressed the chain // Fire a single chain head event if we've progressed the chain
defer func() { defer func() {
@ -1736,7 +1766,12 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
if parent == nil { if parent == nil {
parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) 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 { if err != nil {
return it.index, err return it.index, err
} }
@ -1774,7 +1809,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
ptime := time.Since(pstart) ptime := time.Since(pstart)
vstart := time.Now() 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) bc.reportBlock(block, receipts, err)
followupInterrupt.Store(true) followupInterrupt.Store(true)
return it.index, err return it.index, err
@ -1809,6 +1844,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
} else { } else {
status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false) status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false)
} }
followupInterrupt.Store(true) followupInterrupt.Store(true)
if err != nil { if err != nil {
return it.index, err return it.index, err

66
core/cross_validator.go Normal file
View file

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

View file

@ -19,6 +19,8 @@ package core
import ( import (
"math/big" "math/big"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844" "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. // NewEVMBlockContext creates a new context for use in the EVM.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { 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 ( var (
beneficiary common.Address beneficiary common.Address
baseFee *big.Int baseFee *big.Int
@ -61,10 +73,19 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
if header.Difficulty.Cmp(common.Big0) == 0 { if header.Difficulty.Cmp(common.Big0) == 0 {
random = &header.MixDigest 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{ return vm.BlockContext{
CanTransfer: CanTransfer, CanTransfer: CanTransfer,
Transfer: Transfer, Transfer: Transfer,
GetHash: GetHashFn(header, chain), GetHash: getHash,
Coinbase: beneficiary, Coinbase: beneficiary,
BlockNumber: new(big.Int).Set(header.Number), BlockNumber: new(big.Int).Set(header.Number),
Time: header.Time, Time: header.Time,

View file

@ -126,6 +126,13 @@ type Trie interface {
// be created with new root and updated trie database for following usage // be created with new root and updated trie database for following usage
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) 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 // 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 // starts at the key after the given start key. And error will be returned
// if fails to create node iterator. // if fails to create node iterator.

View file

@ -194,6 +194,13 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
err error err error
value common.Hash 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 { if s.db.snap != nil {
start := time.Now() start := time.Now()
enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) 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{} return common.Hash{}
} }
val, err := tr.GetStorage(s.address, key.Bytes()) val, err := tr.GetStorage(s.address, key.Bytes())
//fmt.Printf("trie access list is %v\n", tr.AccessList())
if metrics.EnabledExpensive { if metrics.EnabledExpensive {
s.db.StorageReads += time.Since(start) 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. // 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 // The returned set can be nil if nothing to commit. This function assumes all
// storage mutations have already been flushed into trie by updateRoot. // 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 // Short circuit if trie is not even loaded, don't bother with committing anything
if s.trie == nil { if s.trie == nil {
s.origin = s.data.Copy() s.origin = s.data.Copy()
return nil, nil return nil, nil, nil
} }
// Track the amount of time wasted on committing the storage trie // Track the amount of time wasted on committing the storage trie
if metrics.EnabledExpensive { 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 // 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 // cached mutations. Call commit to acquire a set of nodes that have been
// modified, the set can be nil if nothing to commit. // 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 { if err != nil {
return nil, err return nil, nil, err
} }
s.data.Root = root s.data.Root = root
// Update original account data after commit // Update original account data after commit
s.origin = s.data.Copy() s.origin = s.data.Copy()
return nodes, nil return nodes, accessList, nil
} }
// AddBalance adds amount to s's balance. // AddBalance adds amount to s's balance.

411
core/state/state_witness.go Normal file
View file

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

View file

@ -138,6 +138,20 @@ type StateDB struct {
// Testing hooks // Testing hooks
onCommit func(states *triestate.Set) // Hook invoked when commit is performed 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. // 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. // commit phase, most of the needed data is already hot.
func (s *StateDB) StartPrefetcher(namespace string) { func (s *StateDB) StartPrefetcher(namespace string) {
if s.prefetcher != nil { if s.prefetcher != nil {
s.prefetcher.wait()
s.prefetcher.close() s.prefetcher.close()
s.prefetcher = nil s.prefetcher = nil
} }
if s.readPrefetcher != nil {
s.readPrefetcher.wait()
s.readPrefetcher.close()
s.readPrefetcher = nil
}
if s.snap != nil { if s.snap != nil {
s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace) 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. // from the gathered metrics.
func (s *StateDB) StopPrefetcher() { func (s *StateDB) StopPrefetcher() {
if s.prefetcher != nil { if s.prefetcher != nil {
s.prefetcher.wait()
s.prefetcher.close() s.prefetcher.close()
s.prefetcher = nil 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. // 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 { func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash {
stateObject := s.getStateObject(addr) stateObject := s.getStateObject(addr)
if stateObject != nil { if stateObject != nil {
return stateObject.GetCommittedState(hash) res := stateObject.GetCommittedState(hash)
return res
} }
return common.Hash{} return common.Hash{}
} }
@ -604,9 +634,15 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
return nil return nil
} }
} }
// Insert into the live set // Insert into the live set
obj := newObject(s, addr, data) obj := newObject(s, addr, data)
s.setStateObject(obj) 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 return obj
} }
@ -712,6 +748,9 @@ func (s *StateDB) Copy() *StateDB {
snaps: s.snaps, snaps: s.snaps,
snap: s.snap, snap: s.snap,
} }
if s.witness != nil {
state.witness = s.witness.Copy()
}
// Copy the dirty states, logs, and preimages // Copy the dirty states, logs, and preimages
for addr := range s.journal.dirties { for addr := range s.journal.dirties {
// As documented [here](https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527), // 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() 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. // IntermediateRoot computes the current root hash of the state trie.
// It is called in between transactions to get the root hash that // It is called in between transactions to get the root hash that
// goes into transaction receipts. // 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 // Finalise all the dirty storage states and write them into the tries
s.Finalise(deleteEmptyObjects) 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 prefetcher := s.prefetcher
if s.prefetcher != nil { if s.prefetcher != nil {
defer func() { defer func() {
// TODO: need to wait for read accounts to be resolved in prefetcher main trie?
s.prefetcher.wait()
s.prefetcher.close() s.prefetcher.close()
s.prefetcher = nil 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 // Although naively it makes sense to retrieve the account trie and then do
// the contract storage and account updates sequentially, that short circuits // the contract storage and account updates sequentially, that short circuits
// the account prefetcher. Instead, let's process all the storage updates // 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() obj.updateRoot()
} }
} }
// Now we're about to start to write changes to the trie. The trie is so far // 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 // _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. // 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)) 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 { for addr := range s.stateObjectsPending {
if obj := s.stateObjects[addr]; obj.deleted { if obj := s.stateObjects[addr]; !obj.deleted {
s.deleteStateObject(obj)
s.AccountDeleted += 1
} else {
s.updateStateObject(obj) s.updateStateObject(obj)
s.AccountUpdated += 1 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 { if prefetcher != nil {
prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs) prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
} }
@ -929,6 +1013,13 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
return s.trie.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 // SetTxContext sets the current transaction hash and index which are
// used when the EVM emits new state logs. It should be invoked before // used when the EVM emits new state logs. It should be invoked before
// transaction execution. // transaction execution.
@ -1153,6 +1244,28 @@ func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.A
return incomplete, nil 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. // Commit writes the state to the underlying in-memory trie database.
// Once the state is committed, tries cached in stateDB (including account // Once the state is committed, tries cached in stateDB (including account
// trie, storage tries) will no longer be functional. A new state instance // 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 storageTrieNodesDeleted int
nodes = trienode.NewMergedNodeSet() nodes = trienode.NewMergedNodeSet()
codeWriter = s.db.DiskDB().NewBatch() codeWriter = s.db.DiskDB().NewBatch()
root common.Hash
set *trienode.NodeSet
) )
// Handle all state deletions first // Handle all state deletions first
incomplete, err := s.handleDestruction(nodes) incomplete, err := s.handleDestruction(nodes)
@ -1184,6 +1299,31 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er
return common.Hash{}, err return common.Hash{}, err
} }
// Handle all state updates afterwards // 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 { for addr := range s.stateObjectsDirty {
obj := s.stateObjects[addr] obj := s.stateObjects[addr]
if obj.deleted { 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) rawdb.WriteCode(codeWriter, common.BytesToHash(obj.CodeHash()), obj.code)
obj.dirtyCode = false obj.dirtyCode = false
} }
// Write any storage changes in the state object to its storage trie // Write any storage changes in the state object to its storage trie
set, err := obj.commit() set, accessList, err := obj.commit()
if err != nil { if err != nil {
return common.Hash{}, err 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 // 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. // that the account was destructed and then resurrected in the same block.
// In this case, the node set is shared by both accounts. // 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 { if metrics.EnabledExpensive {
start = time.Now() 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 { if err != nil {
return common.Hash{}, err 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.storagesOrigin = make(map[common.Address]map[common.Hash][]byte)
s.stateObjectsDirty = make(map[common.Address]struct{}) s.stateObjectsDirty = make(map[common.Address]struct{})
s.stateObjectsDestruct = make(map[common.Address]*types.StateAccount) s.stateObjectsDestruct = make(map[common.Address]*types.StateAccount)
return root, nil return root, nil
} }

View file

@ -17,6 +17,7 @@
package state package state
import ( import (
"fmt"
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -70,13 +71,16 @@ func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePre
} }
return p 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 // close iterates over all the subfetchers, aborts any that were left spinning
// and reports the stats to the metrics subsystem. // and reports the stats to the metrics subsystem.
func (p *triePrefetcher) close() { func (p *triePrefetcher) close() {
for _, fetcher := range p.fetchers { for _, fetcher := range p.fetchers {
fetcher.abort() // safe to do multiple times
if metrics.Enabled { if metrics.Enabled {
if fetcher.root == p.root { if fetcher.root == p.root {
p.accountLoadMeter.Mark(int64(len(fetcher.seen))) p.accountLoadMeter.Mark(int64(len(fetcher.seen)))
@ -123,29 +127,22 @@ func (p *triePrefetcher) copy() *triePrefetcher {
storageSkipMeter: p.storageSkipMeter, storageSkipMeter: p.storageSkipMeter,
storageWasteMeter: p.storageWasteMeter, 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 return copy
} }
// prefetch schedules a batch of trie items to prefetch. // 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) { 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 the prefetcher is an inactive one, bail out
if p.fetches != nil { if p.fetches != nil {
return return
} }
// Active fetcher, schedule the retrievals // Active fetcher, schedule the retrievals
id := p.trieID(owner, root) id := p.trieID(owner, root)
fetcher := p.fetchers[id] fetcher := p.fetchers[id]
@ -175,16 +172,13 @@ func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie {
p.deliveryMissMeter.Mark(1) p.deliveryMissMeter.Mark(1)
return nil return nil
} }
// Interrupt the prefetcher if it's by any chance still running and return // Wait for the fetcher to finish
// a copy of any pre-loaded trie. fetcher.wait() // safe to do multiple times
fetcher.abort() // safe to do multiple times if fetcher.trie == nil {
trie := fetcher.peek()
if trie == nil {
p.deliveryMissMeter.Mark(1) p.deliveryMissMeter.Mark(1)
return nil return nil
} }
return trie return fetcher.db.CopyTrie(fetcher.trie)
} }
// used marks a batch of state items used to allow creating statistics as to // 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 addr common.Address // Address of the account that the trie belongs to
trie Trie // Trie being populated with nodes trie Trie // Trie being populated with nodes
tasks [][]byte // Items queued up for retrieval tasks [][]byte // Items queued up for retrieval
lock sync.Mutex // Lock protecting the task queue 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 wake chan bool // Wake channel if a new task is scheduled, true if the subfetcher should continue running when there are no pending tasks
stop chan struct{} // Channel to interrupt processing term chan struct{} // Channel to signal interruption
term chan struct{} // Channel to signal interruption
copy chan chan Trie // Channel to request a copy of the current trie
seen map[string]struct{} // Tracks the entries already loaded seen map[string]struct{} // Tracks the entries already loaded
dups int // Number of duplicate preload tasks 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, owner: owner,
root: root, root: root,
addr: addr, addr: addr,
wake: make(chan struct{}, 1), wake: make(chan bool, 1),
stop: make(chan struct{}),
term: make(chan struct{}), term: make(chan struct{}),
copy: make(chan chan Trie),
seen: make(map[string]struct{}), seen: make(map[string]struct{}),
} }
go sf.loop() 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) { func (sf *subfetcher) schedule(keys [][]byte) {
// Append the tasks to the current queue // Append the tasks to the current queue
sf.lock.Lock() sf.lock.Lock()
sf.tasks = append(sf.tasks, keys...) sf.tasks = append(sf.tasks, keys...)
sf.lock.Unlock() sf.lock.Unlock()
// Notify the prefetcher. The wake-chan is buffered, so this is async.
// Notify the prefetcher, it's fine if it's already terminated
select { select {
case sf.wake <- struct{}{}: case sf.wake <- true:
default: default:
} }
} }
// peek tries to retrieve a deep copy of the fetcher's trie in whatever form it // wait waits for the subfetcher to finish it's task. It is safe to call wait multiple
// 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
// times but it is not thread safe. // times but it is not thread safe.
func (sf *subfetcher) abort() { func (sf *subfetcher) wait() {
select { // Signal termination by nil tasks
case <-sf.stop: sf.lock.Lock()
default: if sf.closing {
close(sf.stop) 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 <-sf.term
} }
@ -316,50 +295,32 @@ func (sf *subfetcher) loop() {
} }
// Trie opened successfully, keep prefetching items // Trie opened successfully, keep prefetching items
for { for {
select { keepRunning := <-sf.wake
case <-sf.wake: if !keepRunning {
// Subfetcher was woken up, retrieve any tasks to avoid spinning the lock return
sf.lock.Lock() }
tasks := sf.tasks // Subfetcher was woken up, retrieve any tasks to avoid spinning the lock
sf.tasks = nil sf.lock.Lock()
sf.lock.Unlock() tasks := sf.tasks
sf.tasks = nil
sf.lock.Unlock()
// Prefetch any tasks until the loop is interrupted // Prefetch all tasks
for i, task := range tasks { for _, task := range tasks {
select { if _, ok := sf.seen[string(task)]; ok {
case <-sf.stop: sf.dups++
// If termination is requested, add any leftover back and return continue
sf.lock.Lock() }
sf.tasks = append(sf.tasks, tasks[i:]...) if len(task) == common.AddressLength {
sf.lock.Unlock() sf.trie.GetAccount(common.BytesToAddress(task))
return } else {
_, err := sf.trie.GetStorage(sf.addr, task)
case ch := <-sf.copy: if err != nil {
// Somebody wants a copy of the current trie, grant them // TODO: see what needs to be done in this case
ch <- sf.db.CopyTrie(sf.trie) fmt.Printf("prefetch storage failed: %+v\n", err)
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{}{}
}
} }
} }
sf.seen[string(task)] = struct{}{}
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
} }
} }
} }

View file

@ -21,6 +21,8 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/consensus/misc"
@ -36,9 +38,10 @@ import (
// //
// StateProcessor implements Processor. // StateProcessor implements Processor.
type StateProcessor struct { type StateProcessor struct {
config *params.ChainConfig // Chain configuration options config *params.ChainConfig // Chain configuration options
bc *BlockChain // Canonical block chain bc *BlockChain // Canonical block chain
engine consensus.Engine // Consensus engine used for block rewards engine consensus.Engine // Consensus engine used for block rewards
chainCtx *StatelessChainContext // chain context shim for stateless verification mode
} }
// NewStateProcessor initialises a new StateProcessor. // 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 // 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 transaction messages using the statedb and applying any rewards to both
// the processor (coinbase) and any included uncles. // 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 // 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 // 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. // 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 ( var (
receipts types.Receipts receipts types.Receipts
usedGas = new(uint64) usedGas = new(uint64)
@ -72,10 +94,16 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
misc.ApplyDAOHardFork(statedb) misc.ApplyDAOHardFork(statedb)
} }
var ( var (
context = NewEVMBlockContext(header, p.bc, nil) context vm.BlockContext
vmenv = vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg)
signer = types.MakeSigner(p.config, header.Number, header.Time) 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 { if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) 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()) { if len(withdrawals) > 0 && !p.config.IsShanghai(block.Number(), block.Time()) {
return nil, nil, 0, errors.New("withdrawals before shanghai") return nil, nil, 0, errors.New("withdrawals before shanghai")
} }
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards) // 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) 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) _, _, _ = vmenv.Call(vm.AccountRef(msg.From), *msg.To, msg.Data, 30_000_000, common.U2560)
statedb.Finalise(true) 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,
}
}

View file

@ -17,6 +17,7 @@
package core package core
import ( import (
"github.com/ethereum/go-ethereum/common"
"sync/atomic" "sync/atomic"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
@ -33,7 +34,7 @@ type Validator interface {
// ValidateState validates the given statedb and optionally the receipts and // ValidateState validates the given statedb and optionally the receipts and
// gas used. // 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. // Prefetcher is an interface for pre-caching transaction signatures and state.

View file

@ -319,6 +319,18 @@ func (b *Block) DecodeRLP(s *rlp.Stream) error {
return nil 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. // EncodeRLP serializes a block as RLP.
func (b *Block) EncodeRLP(w io.Writer) error { func (b *Block) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, &extblock{ return rlp.Encode(w, &extblock{

View file

@ -181,6 +181,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
if evm.depth > int(params.CallCreateDepth) { if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth return nil, gas, ErrDepth
} }
// Fail if we're trying to transfer more than the available balance // Fail if we're trying to transfer more than the available balance
if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance return nil, gas, ErrInsufficientBalance
@ -188,7 +189,6 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
snapshot := evm.StateDB.Snapshot() snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr) p, isPrecompile := evm.precompile(addr)
debug := evm.Config.Tracer != nil debug := evm.Config.Tracer != nil
if !evm.StateDB.Exist(addr) { if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {
// Calling a non existing account, don't do anything, but ping the tracer // 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. // 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. // The contract is a scoped environment for this execution context only.
code := evm.StateDB.GetCode(addr) 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 { if len(code) == 0 {
ret, err = nil, nil // gas is unchanged ret, err = nil, nil // gas is unchanged
} else { } 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. // 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. // The contract is a scoped environment for this execution context only.
contract := NewContract(caller, AccountRef(caller.Address()), value, gas) 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)) contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false) ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas gas = contract.Gas
@ -337,6 +345,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
addrCopy := addr addrCopy := addr
// Initialise a new contract and make initialise the delegate values // Initialise a new contract and make initialise the delegate values
contract := NewContract(caller, AccountRef(caller.Address()), nil, gas).AsDelegate() 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)) contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false) ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas 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. // 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. // The contract is a scoped environment for this execution context only.
contract := NewContract(caller, AccountRef(addrCopy), new(uint256.Int), gas) 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)) 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 // 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 // above we revert to the snapshot and consume any gas remaining. Additionally

View file

@ -342,7 +342,13 @@ func opReturnDataCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeConte
func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
slot := scope.Stack.peek() 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 return nil, nil
} }
@ -380,6 +386,10 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext)
uint64CodeOffset = 0xffffffffffffffff uint64CodeOffset = 0xffffffffffffffff
} }
addr := common.Address(a.Bytes20()) 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()) codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64())
scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) 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) { if interpreter.evm.StateDB.Empty(address) {
slot.Clear() slot.Clear()
} else { } 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()) slot.SetBytes(interpreter.evm.StateDB.GetCodeHash(address).Bytes())
} }
return nil, nil return nil, nil
@ -444,7 +458,13 @@ func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) (
lower = upper - 256 lower = upper - 256
} }
if num64 >= lower && num64 < upper { 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 { } else {
num.Clear() num.Clear()
} }

View file

@ -19,6 +19,8 @@ package vm
import ( import (
"math/big" "math/big"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
@ -79,6 +81,8 @@ type StateDB interface {
AddLog(*types.Log) AddLog(*types.Log)
AddPreimage(common.Hash, []byte) AddPreimage(common.Hash, []byte)
Witness() *state.Witness
} }
// CallContext provides a basic interface for the EVM calling conventions. The EVM // CallContext provides a basic interface for the EVM calling conventions. The EVM

View file

@ -20,8 +20,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"os"
"time" "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"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
@ -443,3 +448,47 @@ func (api *DebugAPI) GetTrieFlushInterval() (string, error) {
} }
return api.eth.blockchain.GetTrieFlushInterval().String(), nil 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)
}

View file

@ -213,7 +213,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
if config.OverrideVerkle != nil { if config.OverrideVerkle != nil {
overrides.OverrideVerkle = config.OverrideVerkle 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -159,6 +159,9 @@ type Config struct {
// OverrideVerkle (TODO: remove after the fork) // OverrideVerkle (TODO: remove after the fork)
OverrideVerkle *uint64 `toml:",omitempty"` OverrideVerkle *uint64 `toml:",omitempty"`
CrossValidationEndpoint string `toml:",omitempty"`
WitnessRecordingPath string `toml:",omitempty"`
} }
// CreateConsensusEngine creates a consensus engine for the given chain config. // CreateConsensusEngine creates a consensus engine for the given chain config.

View file

@ -501,6 +501,11 @@ web3._extend({
call: 'debug_getTrieFlushInterval', call: 'debug_getTrieFlushInterval',
params: 0 params: 0
}), }),
new web3._extend.Method({
name: 'buildProof',
call: 'debug_buildProof',
params: 1
}),
], ],
properties: [] properties: []
}); });

View file

@ -125,7 +125,8 @@ func (env *environment) discard() {
if env.state == nil { if env.state == nil {
return return
} }
env.state.StopPrefetcher() // TODO: re-enable prefetcher
//env.state.StopPrefetcher()
} }
// task contains all information for consensus engine sealing and result submitting. // 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 { if err != nil {
return nil, err return nil, err
} }
state.StartPrefetcher("miner") // TODO: re-enable prefetcher
//state.StartPrefetcher("miner")
// Note the passed coinbase may be different with header.Coinbase. // Note the passed coinbase may be different with header.Coinbase.
env := &environment{ env := &environment{

View file

@ -18,9 +18,14 @@ package tests
import ( import (
"math/rand" "math/rand"
"os"
"runtime" "runtime"
"testing" "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/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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. // 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. // TestExecutionSpec runs the test fixtures from execution-spec-tests.
func TestExecutionSpec(t *testing.T) { func TestExecutionSpec(t *testing.T) {
if !common.FileExist(executionSpecDir) { if !common.FileExist(executionSpecDir) {
@ -74,19 +163,52 @@ func TestExecutionSpec(t *testing.T) {
} }
func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { 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) t.Errorf("test in hash mode without snapshotter failed: %v", err)
return 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) t.Errorf("test in hash mode with snapshotter failed: %v", err)
return 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) t.Errorf("test in path mode without snapshotter failed: %v", err)
return 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) t.Errorf("test in path mode with snapshotter failed: %v", err)
return return
} }

View file

@ -22,10 +22,13 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/cmd/utils"
"math/big" "math/big"
"os" "os"
"reflect" "reflect"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
@ -109,7 +112,15 @@ type btHeaderMarshaling struct {
ExcessBlobGas *math.HexOrDecimal64 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] config, ok := Forks[t.json.Network]
if !ok { if !ok {
return UnsupportedForkError{t.json.Network} return UnsupportedForkError{t.json.Network}
@ -126,6 +137,14 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po
} else { } else {
tconf.HashDB = hashdb.Defaults tconf.HashDB = hashdb.Defaults
} }
closeCh, port, err := utils.RunLocalServer(config, 0)
if err != nil {
return err
}
defer func() {
closeCh <- struct{}{}
}()
// Commit genesis state // Commit genesis state
gspec := t.genesis(config) gspec := t.genesis(config)
triedb := triedb.NewDatabase(db, tconf) 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 // Wrap the original engine within the beacon-engine
engine := beacon.New(ethash.NewFaker()) 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 { if snapshotter {
cache.SnapshotLimit = 1 cache.SnapshotLimit = 1
cache.SnapshotWait = true cache.SnapshotWait = true
} }
chain, err := core.NewBlockChain(db, cache, gspec, nil, engine, vm.Config{ // TODO: create normal chain if not stateless mode
Tracer: tracer, chain, err := core.NewBlockchainWithCrossValidator(
}, nil, nil) fmt.Sprintf("http://localhost:%d", port),
"",
db,
cache,
gspec,
nil,
engine,
vm.Config{
//Tracer: tracer,
},
nil,
nil)
if err != nil { if err != nil {
return err return err
} }
@ -161,11 +191,6 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po
if err != nil { if err != nil {
return err 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() cmlast := chain.CurrentBlock().Hash()
if common.Hash(t.json.BestBlock) != cmlast { if common.Hash(t.json.BestBlock) != cmlast {
return fmt.Errorf("last block hash validation mismatch: want: %x, have: %x", 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 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) return t.validateImportedHeaders(chain, validBlocks)
} }

View file

@ -213,6 +213,25 @@ func (t *StateTrie) GetKey(shaKey []byte) []byte {
} }
return t.db.Preimage(common.BytesToHash(shaKey)) 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 // Commit collects all dirty nodes in the trie and replaces them with the
// corresponding node hash. All collected nodes (including dirty leaves if // corresponding node hash. All collected nodes (including dirty leaves if

View file

@ -107,7 +107,7 @@ func NewEmpty(db database.Database) *Trie {
} }
// MustNodeIterator is a wrapper of NodeIterator and will omit any encountered // 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 { func (t *Trie) MustNodeIterator(start []byte) NodeIterator {
it, err := t.NodeIterator(start) it, err := t.NodeIterator(start)
if err != nil { if err != nil {
@ -581,6 +581,10 @@ func (t *Trie) resolve(n node, prefix []byte) (node, error) {
return n, nil 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 // 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 // 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 // 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)) 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 // Commit collects all dirty nodes in the trie and replaces them with the
// corresponding node hash. All collected nodes (including dirty leaves if // corresponding node hash. All collected nodes (including dirty leaves if
// collectLeaf is true) will be encapsulated into a nodeset for return. // 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 // 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 // be created with new root and updated trie database for following usage
func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) { func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) {
defer t.tracer.reset()
defer func() { defer func() {
t.tracer.reset()
t.committed = true t.committed = true
}() }()
// Trie is empty and can be classified into two types of situations: // Trie is empty and can be classified into two types of situations:

View file

@ -216,6 +216,14 @@ func (t *VerkleTrie) Hash() common.Hash {
return t.root.Commit().Bytes() 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. // Commit writes all nodes to the tree's memory database.
func (t *VerkleTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet, error) { func (t *VerkleTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet, error) {
root, ok := t.root.(*verkle.InternalNode) root, ok := t.root.(*verkle.InternalNode)

View file

@ -45,6 +45,11 @@ var HashDefaults = &Config{
HashDB: hashdb.Defaults, HashDB: hashdb.Defaults,
} }
var PathDefaults = &Config{
Preimages: false,
PathDB: pathdb.Defaults,
}
// backend defines the methods needed to access/update trie nodes in different // backend defines the methods needed to access/update trie nodes in different
// state scheme. // state scheme.
type backend interface { type backend interface {

View file

@ -33,8 +33,9 @@ import (
// thread-safe to use. However, callers need to ensure the thread-safety // thread-safe to use. However, callers need to ensure the thread-safety
// of the referenced layer by themselves. // of the referenced layer by themselves.
type layerTree struct { type layerTree struct {
lock sync.RWMutex lock sync.RWMutex
layers map[common.Hash]layer layers map[common.Hash]layer
stateless bool
} }
// newLayerTree constructs the layerTree with the given head layer. // newLayerTree constructs the layerTree with the given head layer.