diff --git a/cmd/keeper/main.go b/cmd/keeper/main.go index 14316e6659..15fea02243 100644 --- a/cmd/keeper/main.go +++ b/cmd/keeper/main.go @@ -53,7 +53,8 @@ func main() { } vmConfig := vm.Config{} - if err := core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness); err != nil { + _, _, err = core.ExecuteStateless(context.Background(), chainConfig, vmConfig, payload.Block, payload.Witness, true) + if err != nil { fmt.Fprintf(os.Stderr, "stateless self-validation failed: %v\n", err) os.Exit(10) } diff --git a/core/block_validator.go b/core/block_validator.go index 0b2d48c2dc..008444fbbc 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" @@ -32,11 +33,16 @@ import ( // BlockValidator implements Validator. type BlockValidator struct { config *params.ChainConfig // Chain configuration options + bc *BlockChain // Canonical block chain } // NewBlockValidator returns a new block validator which is safe for re-use -func NewBlockValidator(config *params.ChainConfig) *BlockValidator { - return &BlockValidator{config: config} +func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain) *BlockValidator { + validator := &BlockValidator{ + config: config, + bc: blockchain, + } + return validator } // ValidateBody validates the given block's uncles and verifies the block @@ -47,9 +53,17 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { if v.config.IsOsaka(block.Number(), block.Time()) && block.Size() > params.MaxBlockSize { return ErrBlockOversized } + // Check whether the block is already imported. + if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { + return ErrKnownBlock + } + // 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.bc.engine.VerifyUncles(v.bc, block); err != nil { + return err + } if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash { return fmt.Errorf("uncle root hash mismatch (header value %x, calculated %x)", header.UncleHash, hash) } @@ -96,12 +110,20 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { return errors.New("data blobs present in block body") } } + + // Ancestor block must be known. + if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { + if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { + return consensus.ErrUnknownAncestor + } + return consensus.ErrPrunedAncestor + } return nil } // 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, res *ProcessResult) error { +func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error { if res == nil { return errors.New("nil ProcessResult value") } @@ -119,6 +141,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(res.Receipts, trie.NewStackTrie(nil)) if receiptSha != header.ReceiptHash { diff --git a/core/blockchain.go b/core/blockchain.go index c4ca7fea15..3664da8316 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -423,7 +423,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, return nil, err } bc.flushInterval.Store(int64(cfg.TrieTimeLimit)) - bc.validator = NewBlockValidator(chainConfig) + bc.validator = NewBlockValidator(chainConfig, bc) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc) @@ -1908,7 +1908,7 @@ func (bc *BlockChain) insertChain(ctx context.Context, chain types.Blocks, setHe defer close(abort) // Peek the error for the first block to decide the directing import logic - it := newInsertIterator(chain, results, bc.validator, bc) + it := newInsertIterator(chain, results, bc.validator) block, err := it.next() // Left-trim all the known blocks that don't need to build snapshot @@ -2259,7 +2259,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, vstart := time.Now() _, _, spanEnd = telemetry.StartSpan(ctx, "bc.validator.ValidateState") - err = bc.validator.ValidateState(block, statedb, res) + err = bc.validator.ValidateState(block, statedb, res, false) spanEnd(&err) if err != nil { bc.reportBadBlock(block, res, err) @@ -2276,10 +2276,24 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, if witness := statedb.Witness(); witness != nil && config.StatelessSelfValidation { log.Warn("Running stateless self-validation", "block", block.Number(), "hash", block.Hash()) + // Remove critical computed fields from the block to force true recalculation + context := block.Header() + context.Root = common.Hash{} + context.ReceiptHash = common.Hash{} + + task := types.NewBlockWithHeader(context).WithBody(*block.Body()) + // Run the stateless self-cross-validation - if err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, block, witness); err != nil { + crossStateRoot, crossReceiptRoot, err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, task, witness, false) + if err != nil { return nil, fmt.Errorf("stateless self-validation failed: %v", err) } + if crossStateRoot != block.Root() { + return nil, fmt.Errorf("stateless self-validation root mismatch (cross: %x local: %x)", crossStateRoot, block.Root()) + } + if crossReceiptRoot != block.ReceiptHash() { + return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash()) + } } var ( diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index 1601c72a00..07a250a1bb 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -21,7 +21,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -96,21 +95,19 @@ type insertIterator struct { results <-chan error // Verification result sink from the consensus engine errors []error // Header verification errors for the blocks - index int // Current offset of the iterator - validator Validator // Validator to run if verification succeeds - bc *BlockChain // Blockchain for admission checks + index int // Current offset of the iterator + validator Validator // Validator to run if verification succeeds } // newInsertIterator creates a new iterator based on the given blocks, which are // assumed to be a contiguous chain. -func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator, bc *BlockChain) *insertIterator { +func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator) *insertIterator { return &insertIterator{ chain: chain, results: results, errors: make([]error, 0, len(chain)), index: -1, validator: validator, - bc: bc, } } @@ -130,26 +127,8 @@ func (it *insertIterator) next() (*types.Block, error) { if it.errors[it.index] != nil { return it.chain[it.index], it.errors[it.index] } - - block := it.chain[it.index] - - // Skip blocks we've already imported and fully processed. - if it.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { - return block, ErrKnownBlock - } - // Verify uncle blocks against chain history (pre-merge only) - if err := it.bc.engine.VerifyUncles(it.bc, block); err != nil { - return block, err - } - // Ensure the parent block is known and its state is available. - if !it.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { - if !it.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { - return block, consensus.ErrUnknownAncestor - } - return block, consensus.ErrPrunedAncestor - } - // Validate the block body against header - return block, it.validator.ValidateBody(block) + // Block header valid, run body validation and return + return it.chain[it.index], it.validator.ValidateBody(it.chain[it.index]) } // previous returns the previous header that was being processed, or nil. diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 0d90c62bcf..ce592f0267 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -166,7 +166,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { blockchain.reportBadBlock(block, res, err) return err } - err = blockchain.validator.ValidateState(block, statedb, res) + err = blockchain.validator.ValidateState(block, statedb, res, false) if err != nil { blockchain.reportBadBlock(block, res, err) return err diff --git a/core/stateless.go b/core/stateless.go index 1002bcc12f..7cca42b01c 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -18,6 +18,7 @@ package core import ( "context" + "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" @@ -28,53 +29,58 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" ) -// ExecuteStateless runs a stateless execution based on a witness, fully -// validating the block including header, body, state root and receipt root. +// ExecuteStateless runs a stateless execution based on a witness, verifies +// everything it can locally and returns the state root and receipt root, that +// need the other side to explicitly check. // // This method is a bit of a sore thumb here, but: // - It cannot be placed in core/stateless, because state.New prodces a circular dep // - It cannot be placed outside of core, because it needs to construct a dud headerchain // // TODO(karalabe): Would be nice to resolve both issues above somehow and move it. -func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig vm.Config, block *types.Block, witness *stateless.Witness) error { +func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig vm.Config, block *types.Block, witness *stateless.Witness, validateHeader bool) (common.Hash, common.Hash, error) { // Create and populate the state database to serve as the stateless backend memdb := witness.MakeHashDB() db, err := state.New(witness.Root(), state.NewDatabase(triedb.NewDatabase(memdb, triedb.HashDefaults), nil)) if err != nil { - return err + return common.Hash{}, common.Hash{}, err } - // Create a blockchain that is idle, but can be used to access headers through - engine := beacon.New(ethash.NewFaker()) chain := &HeaderChain{ config: config, chainDb: memdb, headerCache: lru.NewCache[common.Hash, *types.Header](256), - engine: engine, + engine: beacon.New(ethash.NewFaker()), } processor := NewStateProcessor(chain) - validator := NewBlockValidator(config) + validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block - // Pre-execution: Verify the block header against the parent header - if err := engine.VerifyHeader(chain, block.Header()); err != nil { - return err + if validateHeader { + if err := beacon.New(ethash.NewFaker()).VerifyHeader(chain, block.Header()); err != nil { + return common.Hash{}, common.Hash{}, fmt.Errorf("error verifying header in stateless validation: %w", err) + } + + validator := NewBlockValidator(config, nil) + if err := validator.ValidateBody(block); err != nil { + return common.Hash{}, common.Hash{}, fmt.Errorf("error validating body in stateless validation: %w", err) + } } - // Pre-execution: Verify the block body against the header - if err := validator.ValidateBody(block); err != nil { - return err - } - - // Process the block by executing all transactions + // Run the stateless blocks processing and self-validate certain fields res, err := processor.Process(ctx, block, db, vmconfig) if err != nil { - return err + return common.Hash{}, common.Hash{}, err } + if err = validator.ValidateState(block, db, res, validateHeader); err != nil { + return common.Hash{}, common.Hash{}, err + } + // Almost everything validated, but receipt and state root needs to be returned + receiptRoot := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) + stateRoot := db.IntermediateRoot(config.IsEIP158(block.Number())) - // Post-execution: Validate gas, bloom, receipts, state root and - // other post execution artifacts - return validator.ValidateState(block, db, res) + return stateRoot, receiptRoot, nil } diff --git a/core/types.go b/core/types.go index a7681ce6d2..87bbfcff58 100644 --- a/core/types.go +++ b/core/types.go @@ -33,7 +33,7 @@ type Validator interface { ValidateBody(block *types.Block) error // ValidateState validates the given statedb and optionally the process result. - ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult) error + ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult, stateless bool) error } // Prefetcher is an interface for pre-caching transaction signatures and state. diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index f6a51bfd77..aaaf69a0cd 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -284,10 +284,11 @@ func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, v api.lastNewPayloadUpdate.Store(time.Now().Unix()) log.Trace("Executing block statelessly", "number", block.Number(), "hash", params.BlockHash) - if err := core.ExecuteStateless(context.Background(), api.config(), vm.Config{}, block, witness); err != nil { + stateRoot, receiptRoot, err := core.ExecuteStateless(context.Background(), api.config(), vm.Config{}, block, witness, false) + if err != nil { log.Warn("ExecuteStatelessPayload: execution failed", "err", err) errorMsg := err.Error() return engine.StatelessPayloadStatusV1{Status: engine.INVALID, ValidationError: &errorMsg}, nil } - return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: block.Root(), ReceiptsRoot: block.ReceiptHash()}, nil + return engine.StatelessPayloadStatusV1{Status: engine.VALID, StateRoot: stateRoot, ReceiptsRoot: receiptRoot}, nil }