Skip to content

Commit

Permalink
all: add stateless verifications
Browse files Browse the repository at this point in the history
  • Loading branch information
jwasinger authored and karalabe committed Jun 21, 2024
1 parent c10ac4f commit d86b650
Show file tree
Hide file tree
Showing 34 changed files with 876 additions and 70 deletions.
86 changes: 86 additions & 0 deletions cmd/utils/stateless/stateless.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package stateless

import (
"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/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb"
)

// StatelessExecute executes the block contained in the 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.NewBlockValidator(chainCfg, nil, engine)
processor := core.NewStateProcessor(chainCfg, nil, engine)

receipts, _, usedGas, err := processor.Process(witness.Block, db, vm.Config{}, witness)
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(blockHash common.Hash, bc *core.BlockChain) ([]byte, error) {
block := bc.GetBlockByHash(blockHash)
if block == nil {
return nil, fmt.Errorf("non-existent block %x", blockHash)
} else if block.NumberU64() == 0 {
return nil, fmt.Errorf("cannot build a stateless proof of the genesis block")
}
parentHash := block.ParentHash()
parent := bc.GetBlockByHash(parentHash)
if parent == nil {
return nil, fmt.Errorf("block %x parent not present", parentHash)
}

db, err := bc.StateAt(parent.Header().Root)
if err != nil {
return nil, err
}
db.EnableWitnessBuilding()
if bc.Snapshots() != nil {
db.StartPrefetcher("BuildStatelessProof", false)
defer db.StopPrefetcher()
}
stateProcessor := core.NewStateProcessor(bc.Config(), bc, bc.Engine())
_, _, _, err = stateProcessor.Process(block, db, vm.Config{}, nil)
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
}
22 changes: 13 additions & 9 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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 @@ -121,28 +122,31 @@ 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 {
// If validateRemoteRoot is false, the provided block header's root is not asserted to be equal to the one computed from
// execution.
func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64, checkRemoteRoot 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 checkRemoteRoot && 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
4 changes: 2 additions & 2 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1916,15 +1916,15 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s

// Process block using the parent state as reference point
pstart := time.Now()
receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig)
receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, nil)
if err != nil {
bc.reportBlock(block, receipts, err)
return nil, err
}
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
4 changes: 2 additions & 2 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error {
if err != nil {
return err
}
receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{})
receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}, nil)
if err != nil {
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
2 changes: 1 addition & 1 deletion core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (b *BlockGen) Difficulty() *big.Int {
func (b *BlockGen) SetParentBeaconRoot(root common.Hash) {
b.header.ParentBeaconRoot = &root
var (
blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase, nil)
vmenv = vm.NewEVM(blockContext, vm.TxContext{}, b.statedb, b.cm.config, vm.Config{})
)
ProcessBeaconBlockRoot(root, vmenv, b.statedb)
Expand Down
17 changes: 14 additions & 3 deletions 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 @@ -38,8 +40,9 @@ type ChainContext interface {
GetHeader(common.Hash, uint64) *types.Header
}

// NewEVMBlockContext creates a new context for use in the EVM.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
// NewEVMBlockContext creates a new context for use in the EVM. If witness is non-nil, the context sources block hashes
// for the BLOCKHASH opcode from the witness.
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address, witness *state.Witness) vm.BlockContext {
var (
beneficiary common.Address
baseFee *big.Int
Expand All @@ -62,10 +65,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
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)

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

// NodeIterator returns an iterator that returns nodes of the trie. Iteration
// starts at the key after the given start key. And error will be returned
// if fails to create node iterator.
Expand Down
26 changes: 19 additions & 7 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,6 @@ 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
if len(s.uncommittedStorage) == 0 {
return s.trie, nil
}
// Retrieve a pretecher populated trie, or fall back to the database
tr := s.getPrefetchedTrie()
if tr != nil {
Expand All @@ -341,6 +337,14 @@ func (s *stateObject) updateTrie() (Trie, error) {
return nil, err
}
}
// Short circuit if nothing changed, don't bother with hashing anything.
//
// We only quit after the prefetched trie is potentially resolved above
// because, when building a stateless witness we will need to collect
//storage access witnesses from the object's trie when we commit it.
if len(s.uncommittedStorage) == 0 {
return s.trie, nil
}
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
// in circumstances similar to the following:
//
Expand Down Expand Up @@ -446,7 +450,9 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
//
// Note, commit may run concurrently across all the state objects. Do not assume
// thread-safe access to the statedb.
func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, map[string][]byte, error) {
var al map[string][]byte

// commit the account metadata changes
op := &accountUpdate{
address: s.address,
Expand All @@ -468,12 +474,18 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
if len(op.storages) == 0 {
// nothing changed, don't bother to commit the trie
s.origin = s.data.Copy()
return op, nil, nil
if s.trie != nil && !s.trie.IsVerkle() {
al = s.trie.AccessList()
}
return op, nil, al, nil
}
root, nodes := s.trie.Commit(false)
s.data.Root = root
s.origin = s.data.Copy()
return op, nodes, nil
if !s.trie.IsVerkle() {
al = s.trie.AccessList()
}
return op, nodes, al, nil
}

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

0 comments on commit d86b650

Please sign in to comment.