Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/evm/blockrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func blockTestCmd(ctx *cli.Context) error {
continue
}
test := tests[name]
if err := test.Run(false, rawdb.HashScheme, tracer, func(res error, chain *core.BlockChain) {
if err := test.Run(false, rawdb.HashScheme, false, tracer, func(res error, chain *core.BlockChain) {
if ctx.Bool(DumpFlag.Name) {
if state, _ := chain.State(); state != nil {
fmt.Println(string(state.Dump(nil)))
Expand Down
37 changes: 32 additions & 5 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
Expand All @@ -34,14 +36,12 @@ import (
type BlockValidator struct {
config *params.ChainConfig // Chain configuration options
bc *BlockChain // Canonical block chain
engine consensus.Engine // Consensus engine used for validating
}

// NewBlockValidator returns a new block validator which is safe for re-use
func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engine consensus.Engine) *BlockValidator {
func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain) *BlockValidator {
validator := &BlockValidator{
config: config,
engine: engine,
bc: blockchain,
}
return validator
Expand All @@ -59,7 +59,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
// Header validity is known at this point. Here we verify that uncles, transactions
// and withdrawals given in the block body match the header.
header := block.Header()
if err := v.engine.VerifyUncles(v.bc, block); err != nil {
if err := v.bc.engine.VerifyUncles(v.bc, block); err != nil {
return err
}
if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash {
Expand Down Expand Up @@ -121,7 +121,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {

// ValidateState validates the various changes that happen after a state transition,
// such as amount of used gas, the receipt roots and the state root itself.
func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64) error {
func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64, stateless bool) error {
header := block.Header()
if block.GasUsed() != usedGas {
return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas)
Expand All @@ -132,6 +132,11 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
if rbloom != header.Bloom {
return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom)
}
// In stateless mode, return early because the receipt and state root are not
// provided through the witness, rather the cross validator needs to return it.
if stateless {
return nil
}
// The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]]))
receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil))
if receiptSha != header.ReceiptHash {
Expand All @@ -145,6 +150,28 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
return nil
}

// ValidateWitness cross validates a block execution with stateless remote clients.
//
// Normally we'd distribute the block witness to remote cross validators, wait
// for them to respond and then merge the results. For now, however, it's only
// Geth, so do an internal stateless run.
func (v *BlockValidator) ValidateWitness(witness *stateless.Witness, receiptRoot common.Hash, stateRoot common.Hash) error {
// Run the cross client stateless execution
// TODO(karalabe): Self-stateless for now, swap with other clients
crossReceiptRoot, crossStateRoot, err := ExecuteStateless(v.config, witness)
if err != nil {
return fmt.Errorf("stateless execution failed: %v", err)
}
// Stateless cross execution suceeeded, validate the withheld computed fields
if crossReceiptRoot != receiptRoot {
return fmt.Errorf("cross validator receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, receiptRoot)
}
if crossStateRoot != stateRoot {
return fmt.Errorf("cross validator state root mismatch (cross: %x local: %x)", crossStateRoot, stateRoot)
}
return nil
}

// CalcGasLimit computes the gas limit of the next block after parent. It aims
// to keep the baseline gas close to the provided target, and increase it towards
// the target if the baseline gas is lower.
Expand Down
33 changes: 24 additions & 9 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
Expand Down Expand Up @@ -302,18 +303,18 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
vmConfig: vmConfig,
logger: vmConfig.Tracer,
}
bc.flushInterval.Store(int64(cacheConfig.TrieTimeLimit))
bc.forker = NewForkChoice(bc, shouldPreserve)
bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb)
bc.validator = NewBlockValidator(chainConfig, bc, engine)
bc.prefetcher = newStatePrefetcher(chainConfig, bc, engine)
bc.processor = NewStateProcessor(chainConfig, bc, engine)

var err error
bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped)
if err != nil {
return nil, err
}
bc.flushInterval.Store(int64(cacheConfig.TrieTimeLimit))
bc.forker = NewForkChoice(bc, shouldPreserve)
bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb)
bc.validator = NewBlockValidator(chainConfig, bc)
bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
bc.processor = NewStateProcessor(chainConfig, bc.hc)

bc.genesisBlock = bc.GetBlockByNumber(0)
if bc.genesisBlock == nil {
return nil, ErrNoGenesis
Expand Down Expand Up @@ -1809,7 +1810,14 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
// while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction.
if bc.chainConfig.IsByzantium(block.Number()) {
statedb.StartPrefetcher("chain", !bc.vmConfig.EnableWitnessCollection)
var witness *stateless.Witness
if bc.vmConfig.EnableWitnessCollection {
witness, err = stateless.NewWitness(bc, block)
if err != nil {
return it.index, err
}
}
statedb.StartPrefetcher("chain", witness)
}
activeState = statedb

Expand Down Expand Up @@ -1924,11 +1932,18 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s
ptime := time.Since(pstart)

vstart := time.Now()
if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil {
if err := bc.validator.ValidateState(block, statedb, receipts, usedGas, false); err != nil {
bc.reportBlock(block, receipts, err)
return nil, err
}
vtime := time.Since(vstart)

if witness := statedb.Witness(); witness != nil {
if err = bc.validator.ValidateWitness(witness, block.ReceiptHash(), block.Root()); err != nil {
bc.reportBlock(block, receipts, err)
karalabe marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("cross verification failed: %v", err)
}
}
proctime := time.Since(start) // processing + validation

// Update the metrics touched during block processing and validation
Expand Down
3 changes: 1 addition & 2 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error {
blockchain.reportBlock(block, receipts, err)
return err
}
err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas)
if err != nil {
if err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas, false); err != nil {
blockchain.reportBlock(block, receipts, err)
return err
}
Expand Down
4 changes: 4 additions & 0 deletions core/state/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ type Trie interface {
// be created with new root and updated trie database for following usage
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet)

// Witness returns a set containing all trie nodes that have been accessed.
karalabe marked this conversation as resolved.
Show resolved Hide resolved
// The returned map could be nil if the witness is empty.
Witness() map[string]struct{}

// NodeIterator returns an iterator that returns nodes of the trie. Iteration
// starts at the key after the given start key. And error will be returned
// if fails to create node iterator.
Expand Down
16 changes: 13 additions & 3 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,17 @@ func (s *stateObject) finalise() {
//
// It assumes all the dirty storage slots have been finalized before.
func (s *stateObject) updateTrie() (Trie, error) {
// Short circuit if nothing changed, don't bother with hashing anything
// Short circuit if nothing was accessed, don't trigger a prefetcher warning
if len(s.uncommittedStorage) == 0 {
return s.trie, nil
// Nothing was written, so we could stop early. Unless we have both reads
// and witness collection enabled, in which case we need to fetch the trie.
if s.db.witness == nil || len(s.originStorage) == 0 {
return s.trie, nil
}
}
// Retrieve a pretecher populated trie, or fall back to the database
// Retrieve a pretecher populated trie, or fall back to the database. This will
// block until all prefetch tasks are done, which are needed for witnesses even
// for unmodified state objects.
tr := s.getPrefetchedTrie()
if tr != nil {
// Prefetcher returned a live trie, swap it out for the current one
Expand All @@ -341,6 +347,10 @@ func (s *stateObject) updateTrie() (Trie, error) {
return nil, err
}
}
// Short circuit if nothing changed, don't bother with hashing anything
if len(s.uncommittedStorage) == 0 {
return s.trie, nil
}
jwasinger marked this conversation as resolved.
Show resolved Hide resolved
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
// in circumstances similar to the following:
//
Expand Down
Loading