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

Stateless witness builder #29719

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 77 additions & 0 deletions cmd/utils/stateless/stateless.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package stateless

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb"
)

// StatelessExecute executes a witness returning the post state root or an error
func StatelessExecute(chainCfg *params.ChainConfig, witness *state.Witness) (root common.Hash, err error) {
rawDb := rawdb.NewMemoryDatabase()
if err := witness.PopulateDB(rawDb); err != nil {
return common.Hash{}, err
}
blob := rawdb.ReadAccountTrieNode(rawDb, nil)
prestateRoot := crypto.Keccak256Hash(blob)

db, err := state.New(prestateRoot, state.NewDatabaseWithConfig(rawDb, triedb.PathDefaults), nil)
if err != nil {
return common.Hash{}, err
}
engine := beacon.New(ethash.NewFaker())
validator := core.NewStatelessBlockValidator(chainCfg, engine)
processor := core.NewStatelessStateProcessor(chainCfg, engine)

receipts, _, usedGas, err := processor.ProcessStateless(witness, witness.Block, db, vm.Config{})
if err != nil {
return common.Hash{}, err
}

// compute the state root.
if root, err = validator.ValidateState(witness.Block, db, receipts, usedGas, false); err != nil {
return common.Hash{}, err
}
return root, nil
}

// BuildStatelessProof executes a block, collecting the accessed pre-state into
// a Witness. The RLP-encoded witness is returned.
func BuildStatelessProof(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()
if bc.Snapshots() != nil {
db.StartPrefetcher("apidebug")
defer db.StopPrefetcher()
}
block := bc.GetBlockByNumber(number)
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
}
35 changes: 26 additions & 9 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"

"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -47,6 +49,20 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engin
return validator
}

// NewBlockStatelessBlockValidator returns a BlockValidator which is configured to validate stateless block witnesses
// without the use of a full backing BlockChain
func NewStatelessBlockValidator(config *params.ChainConfig, engine consensus.Engine) *BlockValidator {
validator := &BlockValidator{
config: config,
engine: engine,
bc: &BlockChain{
chainConfig: config,
engine: engine,
},
}
return validator
}

// ValidateBody validates the given block's uncles and verifies the block
// header's transaction and uncle roots. The headers are assumed to be already
// validated at this point.
Expand Down Expand Up @@ -121,28 +137,29 @@ 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, checkRoot bool) (root common.Hash, err error) {
header := block.Header()
if block.GasUsed() != usedGas {
return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas)
return root, fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas)
}
// Validate the received block's bloom with the one derived from the generated receipts.
// For valid blocks this should always validate to true.
rbloom := types.CreateBloom(receipts)
if rbloom != header.Bloom {
return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom)
return root, fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom)
}
// The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]]))
receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil))
if receiptSha != header.ReceiptHash {
return fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha)
return root, fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha)
}
// Validate the state root against the received state root and throw
// an error if they don't match.
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error())
// Compute the state root and if enabled, check it against the
// received state root and throw an error if they don't match.
root = statedb.IntermediateRoot(v.config.IsEIP158(header.Number))
if checkRoot && 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
Expand Down
2 changes: 1 addition & 1 deletion core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1924,7 +1924,7 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s
ptime := time.Since(pstart)

vstart := time.Now()
if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil {
if _, err := bc.validator.ValidateState(block, statedb, receipts, usedGas, true); err != nil {
bc.reportBlock(block, receipts, err)
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +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)
_, err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas, true)
if err != nil {
blockchain.reportBlock(block, receipts, err)
return err
Expand Down
22 changes: 21 additions & 1 deletion core/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package core
import (
"math/big"

"github.com/ethereum/go-ethereum/core/state"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
Expand All @@ -40,6 +42,16 @@ type ChainContext interface {

// NewEVMBlockContext creates a new context for use in the EVM.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
return newEVMBlockContext(nil, header, chain, author)
}

// NewStatelessEVMBlockContext creates a new context for use in the EVM in stateless execution mode. The BLOCKHASH
// opcode sources block hashes from the provided witness in stateless execution mode.
func NewStatelessEVMBlockContext(witness *state.Witness, header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
return newEVMBlockContext(witness, header, chain, author)
}

func newEVMBlockContext(witness *state.Witness, header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
var (
beneficiary common.Address
baseFee *big.Int
Expand All @@ -62,10 +74,18 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common
if header.Difficulty.Sign() == 0 {
random = &header.MixDigest
}
var getHash vm.GetHashFunc
if witness != nil {
getHash = func(n uint64) common.Hash {
return witness.BlockHash(n)
}
} else {
getHash = GetHashFn(header, chain)
}
return vm.BlockContext{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: GetHashFn(header, chain),
GetHash: getHash,
Coinbase: beneficiary,
BlockNumber: new(big.Int).Set(header.Number),
Time: header.Time,
Expand Down
8 changes: 8 additions & 0 deletions core/state/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ type Trie interface {
// be created with new root and updated trie database for following usage
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error)

// AccessList returns a map of path->blob containing all trie nodes that have
// been accessed.
AccessList() map[string][]byte

// CommitAndObtainAccessList does the same thing as Commit, but also returns
// the access list of the trie.
CommitAndObtainAccessList(collectLeaf bool) (common.Hash, *trienode.NodeSet, map[string][]byte, error)

// 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
18 changes: 11 additions & 7 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
}
value.SetBytes(content)
}
// If witness building is enabled, prefetch any trie paths loaded directly
// via the snapshots
if s.db.prefetcher != nil && err == nil && s.db.witness != nil && s.data.Root != types.EmptyRootHash {
s.db.prefetcher.prefetch(s.addrHash, s.origin.Root, s.address, [][]byte{key[:]})
}
}
// If the snapshot is unavailable or reading from it fails, load from the database.
if s.db.snap == nil || err != nil {
Expand Down Expand Up @@ -446,24 +451,23 @@ func (s *stateObject) updateRoot() {
//
// Note, commit may run concurrently across all the state objects. Do not assume
// thread-safe access to the statedb.
func (s *stateObject) commit() (*trienode.NodeSet, error) {
func (s *stateObject) commit() (*trienode.NodeSet, map[string][]byte, error) {
// Short circuit if trie is not even loaded, don't bother with committing anything
if s.trie == nil {
s.origin = s.data.Copy()
return nil, nil
return nil, nil, nil
}
// The trie is currently in an open state and could potentially contain
// cached mutations. Call commit to acquire a set of nodes that have been
// modified, the set can be nil if nothing to commit.
root, nodes, err := s.trie.Commit(false)
if err != nil {
return nil, err
}
//
// note: The error is ignored here. MPT trie Commit will never return an error.
root, nodes, al, _ := s.trie.CommitAndObtainAccessList(false)
s.data.Root = root

// Update original account data after commit
s.origin = s.data.Copy()
return nodes, nil
return nodes, al, nil
}

// AddBalance adds amount to s's balance.
Expand Down