From 8d2f6ac23024b008e6e8c350146449a21cccca93 Mon Sep 17 00:00:00 2001 From: protolambda Date: Sat, 21 Sep 2024 16:21:09 -0600 Subject: [PATCH] all: withdrawals root in header is reused to commit to OP-Stack L2 withdrawals storage root --- beacon/engine/gen_ed.go | 74 +++++++++++++++++++---------------- beacon/engine/types.go | 15 ++++++- consensus/beacon/consensus.go | 9 +++++ core/block_validator.go | 16 +++++++- core/types/block.go | 8 ++++ eth/catalyst/api.go | 1 + eth/downloader/downloader.go | 7 +++- eth/downloader/queue.go | 20 +++++++++- eth/downloader/queue_test.go | 6 +-- params/protocol_params.go | 2 + 10 files changed, 116 insertions(+), 42 deletions(-) diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index 6893d64a16..806328892c 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -17,23 +17,24 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -58,29 +59,31 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.Withdrawals = e.Withdrawals enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) + enc.WithdrawalsRoot = e.WithdrawalsRoot return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -154,5 +157,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.ExcessBlobGas != nil { e.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } + if dec.WithdrawalsRoot != nil { + e.WithdrawalsRoot = dec.WithdrawalsRoot + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 7e905a23c6..d20c60ac94 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -87,6 +87,11 @@ type ExecutableData struct { Withdrawals []*types.Withdrawal `json:"withdrawals"` BlobGasUsed *uint64 `json:"blobGasUsed"` ExcessBlobGas *uint64 `json:"excessBlobGas"` + + // OP-Stack Holocene 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"` } // JSON type overrides for executableData. @@ -240,7 +245,13 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b // ExecutableData before withdrawals are enabled by marshaling // Withdrawals as the json null value. var withdrawalsRoot *common.Hash - if data.Withdrawals != nil { + 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 { h := types.DeriveSha(types.Withdrawals(data.Withdrawals), trie.NewStackTrie(nil)) withdrawalsRoot = &h } @@ -293,6 +304,8 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. Withdrawals: block.Withdrawals(), BlobGasUsed: block.BlobGasUsed(), ExcessBlobGas: block.ExcessBlobGas(), + // OP-Stack addition: withdrawals list alone does not express the withdrawals storage-root. + WithdrawalsRoot: block.WithdrawalsRoot(), } bundle := BlobsBundleV1{ Commitments: make([]hexutil.Bytes, 0), diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 60be9152a0..661f5d75b8 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -403,6 +403,15 @@ 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) + } + // State-root has just been computed, we can get an accurate storage-root now. + h := state.GetStorageRoot(params.OptimismL2ToL1MessagePasser) + header.WithdrawalsHash = &h + } + // Assemble and return the final block. return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)), nil } diff --git a/core/block_validator.go b/core/block_validator.go index 75f7f8a94b..97a5ba011e 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -75,7 +75,12 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { if block.Withdrawals() == nil { return errors.New("missing withdrawals in block body") } - if hash := types.DeriveSha(block.Withdrawals(), trie.NewStackTrie(nil)); hash != *header.WithdrawalsHash { + if v.config.IsOptimismHolocene(header.Time) { + if len(block.Withdrawals()) > 0 { + return errors.New("no withdrawal block-operations allowed, withdrawalsRoot is set to storage root") + } + // The withdrawalsHash is verified in ValidateState, like the state root, as verification requires state merkleization. + } else if hash := types.DeriveSha(block.Withdrawals(), trie.NewStackTrie(nil)); hash != *header.WithdrawalsHash { return fmt.Errorf("withdrawals root hash mismatch (header value %x, calculated %x)", *header.WithdrawalsHash, hash) } } else if block.Withdrawals() != nil { @@ -147,6 +152,15 @@ 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 header.WithdrawalsHash == nil { + return errors.New("expected withdrawals root in OP-Stack post-Holocene 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 { + return fmt.Errorf("invalid withdrawals hash (remote: %s local: %s) dberr: %w", *header.WithdrawalsHash, root, statedb.Error()) + } + } return nil } diff --git a/core/types/block.go b/core/types/block.go index 3773a12853..b1bdb68157 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -391,6 +391,14 @@ func (b *Block) ReceiptHash() common.Hash { return b.header.ReceiptHash } func (b *Block) UncleHash() common.Hash { return b.header.UncleHash } func (b *Block) Extra() []byte { return common.CopyBytes(b.header.Extra) } +func (b *Block) WithdrawalsRoot() *common.Hash { + if b.header.WithdrawalsHash == nil { + return nil + } + h := *b.header.WithdrawalsHash + return &h +} + func (b *Block) BaseFee() *big.Int { if b.header.BaseFee == nil { return nil diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 77dbd9a417..39c3bac57a 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -580,6 +580,7 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe "params.ExcessBlobGas", ebg, "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), + "params.WithdrawalsRoot", params.WithdrawalsRoot, "beaconRoot", beaconRoot, "error", err) return api.invalid(err, nil), nil diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index f5313715b6..fc79401dc1 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -146,6 +146,11 @@ type Downloader struct { // BlockChain encapsulates functions required to sync a (full or snap) blockchain. type BlockChain interface { + // Config returns the chain configuration. + // OP-Stack diff, to adjust withdrawal-hash verification. + // Usage of ths in the Downloader is discouraged. + Config() *params.ChainConfig + // HasHeader verifies a header's presence in the local chain. HasHeader(common.Hash, uint64) bool @@ -201,7 +206,7 @@ func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, dropPeer dl := &Downloader{ stateDB: stateDb, mux: mux, - queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), + queue: newQueue(chain.Config(), blockCacheMaxItems, blockCacheInitialItems), peers: newPeerSet(), blockchain: chain, dropPeer: dropPeer, diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index 5441ad1187..b5b0f4f587 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -121,6 +121,10 @@ func (f *fetchResult) Done(kind uint) bool { return v&(1<