mirror of https://github.com/ethereum/go-ethereum
all: stateless witness builder and (self-)cross validator (#29719)
* all: add stateless verifications * all: simplify witness and integrate it into live geth --------- Co-authored-by: Péter Szilágyi <peterke@gmail.com>pull/30071/head
parent
73f7e7c087
commit
ed8fd0ac09
@ -0,0 +1,73 @@ |
||||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/lru" |
||||
"github.com/ethereum/go-ethereum/consensus/beacon" |
||||
"github.com/ethereum/go-ethereum/consensus/ethash" |
||||
"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/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, verifies
|
||||
// everything it can locally and returns the two computed fields 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(config *params.ChainConfig, witness *stateless.Witness) (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.NewDatabaseWithConfig(memdb, triedb.HashDefaults), nil) |
||||
if err != nil { |
||||
return common.Hash{}, common.Hash{}, err |
||||
} |
||||
// Create a blockchain that is idle, but can be used to access headers through
|
||||
chain := &HeaderChain{ |
||||
config: config, |
||||
chainDb: memdb, |
||||
headerCache: lru.NewCache[common.Hash, *types.Header](256), |
||||
engine: beacon.New(ethash.NewFaker()), |
||||
} |
||||
processor := NewStateProcessor(config, chain) |
||||
validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block
|
||||
|
||||
// Run the stateless blocks processing and self-validate certain fields
|
||||
receipts, _, usedGas, err := processor.Process(witness.Block, db, vm.Config{}) |
||||
if err != nil { |
||||
return common.Hash{}, common.Hash{}, err |
||||
} |
||||
if err = validator.ValidateState(witness.Block, db, receipts, usedGas, true); err != nil { |
||||
return common.Hash{}, common.Hash{}, err |
||||
} |
||||
// Almost everything validated, but receipt and state root needs to be returned
|
||||
receiptRoot := types.DeriveSha(receipts, trie.NewStackTrie(nil)) |
||||
stateRoot := db.IntermediateRoot(config.IsEIP158(witness.Block.Number())) |
||||
|
||||
return receiptRoot, stateRoot, nil |
||||
} |
@ -0,0 +1,60 @@ |
||||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package stateless |
||||
|
||||
import ( |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/rawdb" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/ethdb" |
||||
) |
||||
|
||||
// MakeHashDB imports tries, codes and block hashes from a witness into a new
|
||||
// hash-based memory db. We could eventually rewrite this into a pathdb, but
|
||||
// simple is better for now.
|
||||
func (w *Witness) MakeHashDB() ethdb.Database { |
||||
var ( |
||||
memdb = rawdb.NewMemoryDatabase() |
||||
hasher = crypto.NewKeccakState() |
||||
hash = make([]byte, 32) |
||||
) |
||||
// Inject all the "block hashes" (i.e. headers) into the ephemeral database
|
||||
for _, header := range w.Headers { |
||||
rawdb.WriteHeader(memdb, header) |
||||
} |
||||
// Inject all the bytecodes into the ephemeral database
|
||||
for code := range w.Codes { |
||||
blob := []byte(code) |
||||
|
||||
hasher.Reset() |
||||
hasher.Write(blob) |
||||
hasher.Read(hash) |
||||
|
||||
rawdb.WriteCode(memdb, common.BytesToHash(hash), blob) |
||||
} |
||||
// Inject all the MPT trie nodes into the ephemeral database
|
||||
for node := range w.State { |
||||
blob := []byte(node) |
||||
|
||||
hasher.Reset() |
||||
hasher.Write(blob) |
||||
hasher.Read(hash) |
||||
|
||||
rawdb.WriteLegacyTrieNode(memdb, common.BytesToHash(hash), blob) |
||||
} |
||||
return memdb |
||||
} |
@ -0,0 +1,129 @@ |
||||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package stateless |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"slices" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
//go:generate go run github.com/fjl/gencodec -type extWitness -field-override extWitnessMarshalling -out gen_encoding_json.go
|
||||
|
||||
// toExtWitness converts our internal witness representation to the consensus one.
|
||||
func (w *Witness) toExtWitness() *extWitness { |
||||
ext := &extWitness{ |
||||
Block: w.Block, |
||||
Headers: w.Headers, |
||||
} |
||||
ext.Codes = make([][]byte, 0, len(w.Codes)) |
||||
for code := range w.Codes { |
||||
ext.Codes = append(ext.Codes, []byte(code)) |
||||
} |
||||
slices.SortFunc(ext.Codes, bytes.Compare) |
||||
|
||||
ext.State = make([][]byte, 0, len(w.State)) |
||||
for node := range w.State { |
||||
ext.State = append(ext.State, []byte(node)) |
||||
} |
||||
slices.SortFunc(ext.State, bytes.Compare) |
||||
return ext |
||||
} |
||||
|
||||
// fromExtWitness converts the consensus witness format into our internal one.
|
||||
func (w *Witness) fromExtWitness(ext *extWitness) error { |
||||
w.Block, w.Headers = ext.Block, ext.Headers |
||||
|
||||
w.Codes = make(map[string]struct{}, len(ext.Codes)) |
||||
for _, code := range ext.Codes { |
||||
w.Codes[string(code)] = struct{}{} |
||||
} |
||||
w.State = make(map[string]struct{}, len(ext.State)) |
||||
for _, node := range ext.State { |
||||
w.State[string(node)] = struct{}{} |
||||
} |
||||
return w.sanitize() |
||||
} |
||||
|
||||
// MarshalJSON marshals a witness as JSON.
|
||||
func (w *Witness) MarshalJSON() ([]byte, error) { |
||||
return w.toExtWitness().MarshalJSON() |
||||
} |
||||
|
||||
// EncodeRLP serializes a witness as RLP.
|
||||
func (w *Witness) EncodeRLP(wr io.Writer) error { |
||||
return rlp.Encode(wr, w.toExtWitness()) |
||||
} |
||||
|
||||
// UnmarshalJSON unmarshals from JSON.
|
||||
func (w *Witness) UnmarshalJSON(input []byte) error { |
||||
var ext extWitness |
||||
if err := ext.UnmarshalJSON(input); err != nil { |
||||
return err |
||||
} |
||||
return w.fromExtWitness(&ext) |
||||
} |
||||
|
||||
// DecodeRLP decodes a witness from RLP.
|
||||
func (w *Witness) DecodeRLP(s *rlp.Stream) error { |
||||
var ext extWitness |
||||
if err := s.Decode(&ext); err != nil { |
||||
return err |
||||
} |
||||
return w.fromExtWitness(&ext) |
||||
} |
||||
|
||||
// sanitize checks for some mandatory fields in the witness after decoding so
|
||||
// the rest of the code can assume invariants and doesn't have to deal with
|
||||
// corrupted data.
|
||||
func (w *Witness) sanitize() error { |
||||
// Verify that the "parent" header (i.e. index 0) is available, and is the
|
||||
// true parent of the block-to-be executed, since we use that to link the
|
||||
// current block to the pre-state.
|
||||
if len(w.Headers) == 0 { |
||||
return errors.New("parent header (for pre-root hash) missing") |
||||
} |
||||
for i, header := range w.Headers { |
||||
if header == nil { |
||||
return fmt.Errorf("witness header nil at position %d", i) |
||||
} |
||||
} |
||||
if w.Headers[0].Hash() != w.Block.ParentHash() { |
||||
return fmt.Errorf("parent hash different: witness %v, block parent %v", w.Headers[0].Hash(), w.Block.ParentHash()) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// extWitness is a witness RLP encoding for transferring across clients.
|
||||
type extWitness struct { |
||||
Block *types.Block `json:"block" gencodec:"required"` |
||||
Headers []*types.Header `json:"headers" gencodec:"required"` |
||||
Codes [][]byte `json:"codes"` |
||||
State [][]byte `json:"state"` |
||||
} |
||||
|
||||
// extWitnessMarshalling defines the hex marshalling types for a witness.
|
||||
type extWitnessMarshalling struct { |
||||
Codes []hexutil.Bytes |
||||
State []hexutil.Bytes |
||||
} |
@ -0,0 +1,74 @@ |
||||
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
|
||||
|
||||
package stateless |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
) |
||||
|
||||
var _ = (*extWitnessMarshalling)(nil) |
||||
|
||||
// MarshalJSON marshals as JSON.
|
||||
func (e extWitness) MarshalJSON() ([]byte, error) { |
||||
type extWitness struct { |
||||
Block *types.Block `json:"block" gencodec:"required"` |
||||
Headers []*types.Header `json:"headers" gencodec:"required"` |
||||
Codes []hexutil.Bytes `json:"codes"` |
||||
State []hexutil.Bytes `json:"state"` |
||||
} |
||||
var enc extWitness |
||||
enc.Block = e.Block |
||||
enc.Headers = e.Headers |
||||
if e.Codes != nil { |
||||
enc.Codes = make([]hexutil.Bytes, len(e.Codes)) |
||||
for k, v := range e.Codes { |
||||
enc.Codes[k] = v |
||||
} |
||||
} |
||||
if e.State != nil { |
||||
enc.State = make([]hexutil.Bytes, len(e.State)) |
||||
for k, v := range e.State { |
||||
enc.State[k] = v |
||||
} |
||||
} |
||||
return json.Marshal(&enc) |
||||
} |
||||
|
||||
// UnmarshalJSON unmarshals from JSON.
|
||||
func (e *extWitness) UnmarshalJSON(input []byte) error { |
||||
type extWitness struct { |
||||
Block *types.Block `json:"block" gencodec:"required"` |
||||
Headers []*types.Header `json:"headers" gencodec:"required"` |
||||
Codes []hexutil.Bytes `json:"codes"` |
||||
State []hexutil.Bytes `json:"state"` |
||||
} |
||||
var dec extWitness |
||||
if err := json.Unmarshal(input, &dec); err != nil { |
||||
return err |
||||
} |
||||
if dec.Block == nil { |
||||
return errors.New("missing required field 'block' for extWitness") |
||||
} |
||||
e.Block = dec.Block |
||||
if dec.Headers == nil { |
||||
return errors.New("missing required field 'headers' for extWitness") |
||||
} |
||||
e.Headers = dec.Headers |
||||
if dec.Codes != nil { |
||||
e.Codes = make([][]byte, len(dec.Codes)) |
||||
for k, v := range dec.Codes { |
||||
e.Codes[k] = v |
||||
} |
||||
} |
||||
if dec.State != nil { |
||||
e.State = make([][]byte, len(dec.State)) |
||||
for k, v := range dec.State { |
||||
e.State[k] = v |
||||
} |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,159 @@ |
||||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package stateless |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"fmt" |
||||
"maps" |
||||
"slices" |
||||
"sync" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
// HeaderReader is an interface to pull in headers in place of block hashes for
|
||||
// the witness.
|
||||
type HeaderReader interface { |
||||
// GetHeader retrieves a block header from the database by hash and number,
|
||||
GetHeader(hash common.Hash, number uint64) *types.Header |
||||
} |
||||
|
||||
// Witness encompasses a block, state and any other chain data required to apply
|
||||
// a set of transactions and derive a post state/receipt root.
|
||||
type Witness struct { |
||||
Block *types.Block // Current block with rootHash and receiptHash zeroed out
|
||||
Headers []*types.Header // Past headers in reverse order (0=parent, 1=parent's-parent, etc). First *must* be set.
|
||||
Codes map[string]struct{} // Set of bytecodes ran or accessed
|
||||
State map[string]struct{} // Set of MPT state trie nodes (account and storage together)
|
||||
|
||||
chain HeaderReader // Chain reader to convert block hash ops to header proofs
|
||||
lock sync.Mutex // Lock to allow concurrent state insertions
|
||||
} |
||||
|
||||
// NewWitness creates an empty witness ready for population.
|
||||
func NewWitness(chain HeaderReader, block *types.Block) (*Witness, error) { |
||||
// Zero out the result fields to avoid accidentally sending them to the verifier
|
||||
header := block.Header() |
||||
header.Root = common.Hash{} |
||||
header.ReceiptHash = common.Hash{} |
||||
|
||||
// Retrieve the parent header, which will *always* be included to act as a
|
||||
// trustless pre-root hash container
|
||||
parent := chain.GetHeader(block.ParentHash(), block.NumberU64()-1) |
||||
if parent == nil { |
||||
return nil, errors.New("failed to retrieve parent header") |
||||
} |
||||
// Create the wtness with a reconstructed gutted out block
|
||||
return &Witness{ |
||||
Block: types.NewBlockWithHeader(header).WithBody(*block.Body()), |
||||
Codes: make(map[string]struct{}), |
||||
State: make(map[string]struct{}), |
||||
Headers: []*types.Header{parent}, |
||||
chain: chain, |
||||
}, nil |
||||
} |
||||
|
||||
// AddBlockHash adds a "blockhash" to the witness with the designated offset from
|
||||
// chain head. Under the hood, this method actually pulls in enough headers from
|
||||
// the chain to cover the block being added.
|
||||
func (w *Witness) AddBlockHash(number uint64) { |
||||
// Keep pulling in headers until this hash is populated
|
||||
for int(w.Block.NumberU64()-number) > len(w.Headers) { |
||||
tail := w.Block.Header() |
||||
if len(w.Headers) > 0 { |
||||
tail = w.Headers[len(w.Headers)-1] |
||||
} |
||||
w.Headers = append(w.Headers, w.chain.GetHeader(tail.ParentHash, tail.Number.Uint64()-1)) |
||||
} |
||||
} |
||||
|
||||
// AddCode adds a bytecode blob to the witness.
|
||||
func (w *Witness) AddCode(code []byte) { |
||||
if len(code) == 0 { |
||||
return |
||||
} |
||||
w.Codes[string(code)] = struct{}{} |
||||
} |
||||
|
||||
// AddState inserts a batch of MPT trie nodes into the witness.
|
||||
func (w *Witness) AddState(nodes map[string]struct{}) { |
||||
if len(nodes) == 0 { |
||||
return |
||||
} |
||||
w.lock.Lock() |
||||
defer w.lock.Unlock() |
||||
|
||||
for node := range nodes { |
||||
w.State[node] = struct{}{} |
||||
} |
||||
} |
||||
|
||||
// Copy deep-copies the witness object. Witness.Block isn't deep-copied as it
|
||||
// is never mutated by Witness
|
||||
func (w *Witness) Copy() *Witness { |
||||
return &Witness{ |
||||
Block: w.Block, |
||||
Headers: slices.Clone(w.Headers), |
||||
Codes: maps.Clone(w.Codes), |
||||
State: maps.Clone(w.State), |
||||
} |
||||
} |
||||
|
||||
// String prints a human-readable summary containing the total size of the
|
||||
// witness and the sizes of the underlying components
|
||||
func (w *Witness) String() string { |
||||
blob, _ := rlp.EncodeToBytes(w) |
||||
bytesTotal := len(blob) |
||||
|
||||
blob, _ = rlp.EncodeToBytes(w.Block) |
||||
bytesBlock := len(blob) |
||||
|
||||
bytesHeaders := 0 |
||||
for _, header := range w.Headers { |
||||
blob, _ = rlp.EncodeToBytes(header) |
||||
bytesHeaders += len(blob) |
||||
} |
||||
bytesCodes := 0 |
||||
for code := range w.Codes { |
||||
bytesCodes += len(code) |
||||
} |
||||
bytesState := 0 |
||||
for node := range w.State { |
||||
bytesState += len(node) |
||||
} |
||||
buf := new(bytes.Buffer) |
||||
|
||||
fmt.Fprintf(buf, "Witness #%d: %v\n", w.Block.Number(), common.StorageSize(bytesTotal)) |
||||
fmt.Fprintf(buf, " block (%4d txs): %10v\n", len(w.Block.Transactions()), common.StorageSize(bytesBlock)) |
||||
fmt.Fprintf(buf, "%4d headers: %10v\n", len(w.Headers), common.StorageSize(bytesHeaders)) |
||||
fmt.Fprintf(buf, "%4d trie nodes: %10v\n", len(w.State), common.StorageSize(bytesState)) |
||||
fmt.Fprintf(buf, "%4d codes: %10v\n", len(w.Codes), common.StorageSize(bytesCodes)) |
||||
|
||||
return buf.String() |
||||
} |
||||
|
||||
// Root returns the pre-state root from the first header.
|
||||
//
|
||||
// Note, this method will panic in case of a bad witness (but RLP decoding will
|
||||
// sanitize it and fail before that).
|
||||
func (w *Witness) Root() common.Hash { |
||||
return w.Headers[0].Root |
||||
} |
Loading…
Reference in new issue