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

Isthmus: Tests and misc updates for L2 withdrawals root #399

Open
wants to merge 13 commits into
base: l2-withdrawals-root
Choose a base branch
from
Open
10 changes: 5 additions & 5 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,7 +275,7 @@ 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 config.IsOptimismIsthmus(data.Timestamp) && 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)
protolambda marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down
47 changes: 47 additions & 0 deletions beacon/types/exec_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package types

import (
"testing"

"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
)

var (
withdrawalsHash1 = common.HexToHash("dead")

isthumusExecData = engine.ExecutableData{
ParentHash: common.HexToHash("parent"),
FeeRecipient: common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
StateRoot: common.HexToHash("sRoot"),
ReceiptsRoot: common.HexToHash("rRoot"),
LogsBloom: common.Hex2Bytes("0x376c47978271565f56DEB45495afa69E59c16Ab2"),
Random: common.HexToHash("randao"),
BaseFeePerGas: hexutil.MustDecodeBig("0x2000000"),
Transactions: [][]byte{},
Withdrawals: []*types.Withdrawal{},
WithdrawalsRoot: &withdrawalsHash1,
}

executableDataSamples = []engine.ExecutableData{
isthumusExecData,
}
)

func TestExecutableDataJSONEncodeDecode(t *testing.T) {
vdamle marked this conversation as resolved.
Show resolved Hide resolved
for i := range executableDataSamples {
b, err := executableDataSamples[i].MarshalJSON()
if err != nil {
t.Fatal("error marshaling executable data to json:", err)
}
r := engine.ExecutableData{}
err = r.UnmarshalJSON(b)
if err != nil {
t.Fatal("error unmarshalling executable data from json:", err)
}
assert.Equal(t, withdrawalsHash1, *r.WithdrawalsRoot)
}
}
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
71 changes: 45 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 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,24 @@ 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
storageRootMessagePasser := statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser)
vdamle marked this conversation as resolved.
Show resolved Hide resolved

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) (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 +189,19 @@ 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
storageRootMessagePasser := statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser)
vdamle marked this conversation as resolved.
Show resolved Hide resolved
// 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 +491,23 @@ 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 {
// TODO - need to get the storage root of the L2ToL1MessagePasser contract?
stateRoot = *g.StateHash
vdamle marked this conversation as resolved.
Show resolved Hide resolved
} else if stateRoot, storageRootMessagePasser, err = hashAlloc(&g.Alloc, g.IsVerkle()); 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 +520,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 +564,12 @@ 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 conf.IsIsthmus(g.Timestamp) {
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 +588,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)
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)
)
if storageRootMessagePasser.Cmp(common.Hash{}) != 0 {
vdamle marked this conversation as resolved.
Show resolved Hide resolved
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
9 changes: 5 additions & 4 deletions core/rawdb/chain_iterator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
)

func TestChainIterator(t *testing.T) {
Expand All @@ -34,7 +35,7 @@ func TestChainIterator(t *testing.T) {
var block *types.Block
var txs []*types.Transaction
to := common.BytesToAddress([]byte{0x11})
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, newTestHasher()) // Empty genesis block
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, newTestHasher(), params.TestChainConfig) // Empty genesis block
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
for i := uint64(1); i <= 10; i++ {
Expand All @@ -60,7 +61,7 @@ func TestChainIterator(t *testing.T) {
})
}
txs = append(txs, tx)
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, &types.Body{Transactions: types.Transactions{tx}}, nil, newTestHasher())
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, &types.Body{Transactions: types.Transactions{tx}}, nil, newTestHasher(), params.TestChainConfig)
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
}
Expand Down Expand Up @@ -111,7 +112,7 @@ func TestIndexTransactions(t *testing.T) {
to := common.BytesToAddress([]byte{0x11})

// Write empty genesis block
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, newTestHasher())
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, newTestHasher(), params.TestChainConfig)
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())

Expand All @@ -138,7 +139,7 @@ func TestIndexTransactions(t *testing.T) {
})
}
txs = append(txs, tx)
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, &types.Body{Transactions: types.Transactions{tx}}, nil, newTestHasher())
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, &types.Body{Transactions: types.Transactions{tx}}, nil, newTestHasher(), params.TestChainConfig)
WriteBlock(chainDb, block)
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
}
Expand Down
Loading