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

Tests and misc updates for L2 withdrawals root #399

Merged
merged 20 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
22 changes: 11 additions & 11 deletions beacon/engine/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ type ExecutableData struct {
Deposits types.Deposits `json:"depositRequests"`
ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"`

// OP-Stack Holocene specific field:
// OP-Stack Isthmus specific field:
// instead of computing the root from a withdrawals list, set it directly.
// The "withdrawals" list attribute must be non-nil but empty.
WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"`
Expand Down Expand Up @@ -230,8 +230,8 @@ func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) {
// and that the blockhash of the constructed block matches the parameters. Nil
// Withdrawals value will propagate through the returned block. Empty
// Withdrawals value must be passed via non-nil, length 0 value in data.
func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (*types.Block, error) {
block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot)
func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, config *params.ChainConfig) (*types.Block, error) {
block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot, config)
if err != nil {
return nil, err
}
Expand All @@ -244,7 +244,7 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b
// ExecutableDataToBlockNoHash is analogous to ExecutableDataToBlock, but is used
// for stateless execution, so it skips checking if the executable data hashes to
// the requested hash (stateless has to *compute* the root hash, it's not given).
func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (*types.Block, error) {
func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, config *params.ChainConfig) (*types.Block, error) {
txs, err := decodeTransactions(data.Transactions)
if err != nil {
return nil, err
Expand Down Expand Up @@ -275,15 +275,15 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H
// ExecutableData before withdrawals are enabled by marshaling
// Withdrawals as the json null value.
var withdrawalsRoot *common.Hash
if data.WithdrawalsRoot != nil {
if data.Withdrawals == nil || len(data.Withdrawals) != 0 {
return nil, fmt.Errorf("attribute WithdrawalsRoot was set. Expecting non-nil empty withdrawals list, but got %v", data.Withdrawals)
}
h := *data.WithdrawalsRoot // copy, avoid any sharing of memory
withdrawalsRoot = &h
} else if data.Withdrawals != nil {
if config.IsOptimismIsthmus(data.Timestamp) && data.WithdrawalsRoot == nil {
return nil, fmt.Errorf("attribute WithdrawalsRoot is required for Isthmus blocks")
}
if data.Withdrawals != nil {
h := types.DeriveSha(types.Withdrawals(data.Withdrawals), trie.NewStackTrie(nil))
withdrawalsRoot = &h
} else if data.WithdrawalsRoot != nil {
h := *data.WithdrawalsRoot // copy, avoid any sharing of memory
withdrawalsRoot = &h
}
// Compute requestsHash if any requests are non-nil.
var (
Expand Down
62 changes: 62 additions & 0 deletions beacon/types/exec_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package types

import (
"bytes"
"encoding/json"
"fmt"
"testing"

"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/core/types"
)

func decodeEncodeJSON(input []byte, val interface{}) error {
if err := json.Unmarshal(input, &val); err != nil {
// not valid JSON, nothing to do
return nil
}
output, err := json.Marshal(val)
if err != nil {
return err
}
if !bytes.Equal(input, output) {
return fmt.Errorf("encode-decode is not equal, \ninput : %x\noutput: %x", input, output)
}
return nil
}

func FuzzJSON(f *testing.F) {
f.Fuzz(fuzzJSON)
}

func fuzzJSON(t *testing.T, input []byte) {
if len(input) == 0 {
return
}
{
var h types.Header
if err := decodeEncodeJSON(input, &h); err != nil {
t.Fatal(err)
}
var b types.Block
if err := decodeEncodeJSON(input, &b); err != nil {
t.Fatal(err)
}
var tx types.Transaction
if err := decodeEncodeJSON(input, &tx); err != nil {
t.Fatal(err)
}
var txs types.Transactions
if err := decodeEncodeJSON(input, &txs); err != nil {
t.Fatal(err)
}
var rs types.Receipts
if err := decodeEncodeJSON(input, &rs); err != nil {
t.Fatal(err)
}
var e engine.ExecutableData
if err := decodeEncodeJSON(input, &e); err != nil {
t.Fatal(err)
}
}
}
8 changes: 4 additions & 4 deletions consensus/beacon/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,17 +403,17 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
// Assign the final state root to header.
header.Root = state.IntermediateRoot(true)

if chain.Config().IsOptimismHolocene(header.Time) {
if body.Withdrawals == nil || len(body.Withdrawals) > 0 { // We verify nil/empty withdrawals in the CL pre-holocene
return nil, fmt.Errorf("expected non-nil empty withdrawals operation list in Holocene, but got: %v", body.Withdrawals)
if chain.Config().IsOptimismIsthmus(header.Time) {
if body.Withdrawals == nil || len(body.Withdrawals) > 0 { // We verify nil/empty withdrawals in the CL pre-Isthmus
return nil, fmt.Errorf("expected non-nil empty withdrawals operation list in Isthmus, but got: %v", body.Withdrawals)
}
// State-root has just been computed, we can get an accurate storage-root now.
h := state.GetStorageRoot(params.OptimismL2ToL1MessagePasser)
header.WithdrawalsHash = &h
}

// Assemble the final block.
block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil), chain.Config())

// Create the block witness and attach to block.
// This step needs to happen as late as possible to catch all access events.
Expand Down
2 changes: 1 addition & 1 deletion consensus/clique/clique.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))

// Assemble and return the final block for sealing.
return types.NewBlock(header, &types.Body{Transactions: body.Transactions}, receipts, trie.NewStackTrie(nil)), nil
return types.NewBlock(header, &types.Body{Transactions: body.Transactions}, receipts, trie.NewStackTrie(nil), chain.Config()), nil
}

// Authorize injects a private key into the consensus engine to mint new blocks
Expand Down
2 changes: 1 addition & 1 deletion consensus/ethash/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))

// Header seems complete, assemble into a block and return
return types.NewBlock(header, &types.Body{Transactions: body.Transactions, Uncles: body.Uncles}, receipts, trie.NewStackTrie(nil)), nil
return types.NewBlock(header, &types.Body{Transactions: body.Transactions, Uncles: body.Uncles, Withdrawals: body.Withdrawals}, receipts, trie.NewStackTrie(nil), chain.Config()), nil
}

// SealHash returns the hash of a block prior to it being sealed.
Expand Down
6 changes: 3 additions & 3 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
if block.Withdrawals() == nil {
return errors.New("missing withdrawals in block body")
}
if v.config.IsOptimismHolocene(header.Time) {
if v.config.IsOptimismIsthmus(header.Time) {
if len(block.Withdrawals()) > 0 {
return errors.New("no withdrawal block-operations allowed, withdrawalsRoot is set to storage root")
}
Expand Down Expand Up @@ -160,9 +160,9 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
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())
}
if v.config.IsOptimismHolocene(block.Time()) {
if v.config.IsOptimismIsthmus(block.Time()) {
if header.WithdrawalsHash == nil {
return errors.New("expected withdrawals root in OP-Stack post-Holocene block header")
return errors.New("expected withdrawals root in OP-Stack post-Isthmus block header")
}
// Validate the withdrawals root against the L2 withdrawals storage, similar to how the StateRoot is verified.
if root := statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser); *header.WithdrawalsHash != root {
Expand Down
4 changes: 4 additions & 0 deletions core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,10 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
b.header.Difficulty = big.NewInt(0)
}
}
if config.IsIsthmus(b.header.Time) {
b.withdrawals = make([]*types.Withdrawal, 0)
b.header.WithdrawalsHash = &types.EmptyWithdrawalsHash
}
// Mutate the state and block according to any hard-fork specs
if daoBlock := config.DAOForkBlock; daoBlock != nil {
limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange)
Expand Down
86 changes: 60 additions & 26 deletions core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ func ReadGenesis(db ethdb.Database) (*Genesis, error) {
return &genesis, nil
}

// hashAlloc computes the state root according to the genesis specification.
func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
// hashAlloc returns the following:
// * computed state root according to the genesis specification.
// * storage root of the L2ToL1MessagePasser contract.
// * error if any, when committing the genesis state (if so, state root and storage root will be empty).
func hashAlloc(ga *types.GenesisAlloc, isVerkle, isIsthmus bool) (common.Hash, common.Hash, error) {
// If a genesis-time verkle trie is requested, create a trie config
// with the verkle trie enabled so that the tree can be initialized
// as such.
Expand All @@ -143,7 +146,7 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
db := rawdb.NewMemoryDatabase()
statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(triedb.NewDatabase(db, config), nil))
if err != nil {
return common.Hash{}, err
return common.Hash{}, common.Hash{}, err
}
for addr, account := range *ga {
if account.Balance != nil {
Expand All @@ -155,15 +158,27 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
statedb.SetState(addr, key, value)
}
}
return statedb.Commit(0, false)

stateRoot, err := statedb.Commit(0, false)
if err != nil {
return common.Hash{}, common.Hash{}, err
}
// get the storage root of the L2ToL1MessagePasser contract
var storageRootMessagePasser common.Hash
if isIsthmus {
storageRootMessagePasser = statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser)
}

return stateRoot, storageRootMessagePasser, nil
}

// flushAlloc is very similar with hash, but the main difference is all the
// generated states will be persisted into the given database.
func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, error) {
// generated states will be persisted into the given database. Returns the
// same values as hashAlloc.
func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database, isIsthmus bool) (common.Hash, common.Hash, error) {
statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(triedb, nil))
if err != nil {
return common.Hash{}, err
return common.Hash{}, common.Hash{}, err
}
for addr, account := range *ga {
if account.Balance != nil {
Expand All @@ -177,17 +192,22 @@ func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, e
statedb.SetState(addr, key, value)
}
}
root, err := statedb.Commit(0, false)
stateRoot, err := statedb.Commit(0, false)
if err != nil {
return common.Hash{}, err
return common.Hash{}, common.Hash{}, err
}
// get the storage root of the L2ToL1MessagePasser contract
var storageRootMessagePasser common.Hash
if isIsthmus {
storageRootMessagePasser = statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser)
}
// Commit newly generated states into disk if it's not empty.
if root != types.EmptyRootHash {
if err := triedb.Commit(root, true); err != nil {
return common.Hash{}, err
if stateRoot != types.EmptyRootHash {
if err := triedb.Commit(stateRoot, true); err != nil {
return common.Hash{}, common.Hash{}, err
}
}
return root, nil
return stateRoot, storageRootMessagePasser, nil
}

func getGenesisState(db ethdb.Database, blockhash common.Hash) (alloc types.GenesisAlloc, err error) {
Expand Down Expand Up @@ -477,22 +497,27 @@ func (g *Genesis) IsVerkle() bool {

// ToBlock returns the genesis block according to genesis specification.
func (g *Genesis) ToBlock() *types.Block {
var root common.Hash
var stateRoot, storageRootMessagePasser common.Hash
var err error
if g.StateHash != nil {
if len(g.Alloc) > 0 {
panic(fmt.Errorf("cannot both have genesis hash %s "+
"and non-empty state-allocation", *g.StateHash))
}
root = *g.StateHash
} else if root, err = hashAlloc(&g.Alloc, g.IsVerkle()); err != nil {
// stateHash is only relevant for pre-bedrock (and hence pre-isthmus) chains.
// we bail here since this is not a valid usage of StateHash
if g.Config.IsIsthmus(g.Timestamp) {
panic(fmt.Errorf("stateHash usage disallowed in chain with isthmus active at genesis"))
}
stateRoot = *g.StateHash
} else if stateRoot, storageRootMessagePasser, err = hashAlloc(&g.Alloc, g.IsVerkle(), g.Config.IsIsthmus(g.Timestamp)); err != nil {
panic(err)
}
return g.toBlockWithRoot(root)
return g.toBlockWithRoot(stateRoot, storageRootMessagePasser)
}

// toBlockWithRoot constructs the genesis block with the given genesis state root.
func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
func (g *Genesis) toBlockWithRoot(stateRoot, storageRootMessagePasser common.Hash) *types.Block {
head := &types.Header{
Number: new(big.Int).SetUint64(g.Number),
Nonce: types.EncodeNonce(g.Nonce),
Expand All @@ -505,7 +530,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
Difficulty: g.Difficulty,
MixDigest: g.Mixhash,
Coinbase: g.Coinbase,
Root: root,
Root: stateRoot,
}
if g.GasLimit == 0 {
head.GasLimit = params.GenesisGasLimit
Expand Down Expand Up @@ -549,8 +574,17 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
head.RequestsHash = &types.EmptyRequestsHash
requests = make(types.Requests, 0)
}
// If Isthmus is active at genesis, set the WithdrawalRoot to the storage root of the L2ToL1MessagePasser contract.
if g.Config.IsIsthmus(g.Timestamp) {
if storageRootMessagePasser == (common.Hash{}) {
// if there was no MessagePasser contract storage, set the WithdrawalsHash to the empty hash
head.WithdrawalsHash = &types.EmptyWithdrawalsHash
} else {
head.WithdrawalsHash = &storageRootMessagePasser
}
}
}
return types.NewBlock(head, &types.Body{Withdrawals: withdrawals, Requests: requests}, nil, trie.NewStackTrie(nil))
return types.NewBlock(head, &types.Body{Withdrawals: withdrawals, Requests: requests}, nil, trie.NewStackTrie(nil), g.Config)
}

// Commit writes the block and state of a genesis specification to the database.
Expand All @@ -569,23 +603,23 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Blo
if config.Clique != nil && len(g.ExtraData) < 32+crypto.SignatureLength {
return nil, errors.New("can't start clique chain without signers")
}
var stateHash common.Hash
var stateRoot, storageRootMessagePasser common.Hash
var err error
if len(g.Alloc) == 0 {
if g.StateHash == nil {
log.Warn("Empty genesis alloc, and no 'stateHash' override was set")
stateHash = types.EmptyRootHash // default to the hash of the empty state. Some unit-tests rely on this.
stateRoot = types.EmptyRootHash // default to the hash of the empty state. Some unit-tests rely on this.
} else {
stateHash = *g.StateHash
stateRoot = *g.StateHash
}
} else {
// flush the data to disk and compute the state root
root, err := flushAlloc(&g.Alloc, triedb)
stateRoot, storageRootMessagePasser, err = flushAlloc(&g.Alloc, triedb, g.Config.IsIsthmus(g.Timestamp))
if err != nil {
return nil, err
}
stateHash = root
}
block := g.toBlockWithRoot(stateHash)
block := g.toBlockWithRoot(stateRoot, storageRootMessagePasser)

// Marshal the genesis state specification and persist.
blob, err := json.Marshal(g.Alloc)
Expand Down
9 changes: 6 additions & 3 deletions core/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,16 @@ func TestReadWriteGenesisAlloc(t *testing.T) {
{1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}},
{2}: {Balance: big.NewInt(2), Storage: map[common.Hash]common.Hash{{2}: {2}}},
}
hash, _ = hashAlloc(alloc, false)
stateRoot, storageRootMessagePasser, _ = hashAlloc(alloc, false, false)
)
if storageRootMessagePasser != (common.Hash{}) {
t.Fatalf("unexpected storage root")
}
blob, _ := json.Marshal(alloc)
rawdb.WriteGenesisStateSpec(db, hash, blob)
rawdb.WriteGenesisStateSpec(db, stateRoot, blob)

var reload types.GenesisAlloc
err := reload.UnmarshalJSON(rawdb.ReadGenesisStateSpec(db, hash))
err := reload.UnmarshalJSON(rawdb.ReadGenesisStateSpec(db, stateRoot))
if err != nil {
t.Fatalf("Failed to load genesis state %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/rawdb/accessors_indexes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestLookupStorage(t *testing.T) {
tx3 := types.NewTransaction(3, common.BytesToAddress([]byte{0x33}), big.NewInt(333), 3333, big.NewInt(33333), []byte{0x33, 0x33, 0x33})
txs := []*types.Transaction{tx1, tx2, tx3}

block := types.NewBlock(&types.Header{Number: big.NewInt(314)}, &types.Body{Transactions: txs}, nil, newTestHasher())
block := types.NewBlock(&types.Header{Number: big.NewInt(314)}, &types.Body{Transactions: txs}, nil, newTestHasher(), params.TestChainConfig)

// Check that no transactions entries are in a pristine database
for i, tx := range txs {
Expand Down
Loading