diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index b2eb1dc982..90a02c395e 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -36,6 +36,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` Deposits types.Deposits `json:"depositRequests"` ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -62,6 +63,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) enc.Deposits = e.Deposits enc.ExecutionWitness = e.ExecutionWitness + enc.WithdrawalsRoot = e.WithdrawalsRoot return json.Marshal(&enc) } @@ -87,6 +89,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` Deposits *types.Deposits `json:"depositRequests"` ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -166,5 +169,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.ExecutionWitness != nil { e.ExecutionWitness = dec.ExecutionWitness } + if dec.WithdrawalsRoot != nil { + e.WithdrawalsRoot = dec.WithdrawalsRoot + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 41c475ed57..f956c799f0 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -94,6 +94,11 @@ type ExecutableData struct { ExcessBlobGas *uint64 `json:"excessBlobGas"` Deposits types.Deposits `json:"depositRequests"` ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + + // 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. @@ -270,7 +275,13 @@ 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.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 } @@ -337,6 +348,8 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. BlobGasUsed: block.BlobGasUsed(), ExcessBlobGas: block.ExcessBlobGas(), ExecutionWitness: block.ExecutionWitness(), + // 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 d03d01e7d4..f79505f3d3 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 the final block. block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) diff --git a/core/block_validator.go b/core/block_validator.go index 4f51f5dc17..4b75fd276a 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -73,7 +73,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 { @@ -155,6 +160,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 621891e4b4..1881d0025f 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -429,6 +429,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 5200dae2fc..e1a09a4021 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -881,6 +881,7 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), "len(params.Deposits)", len(params.Deposits), + "params.WithdrawalsRoot", params.WithdrawalsRoot, "beaconRoot", beaconRoot, "error", err) return api.invalid(err, nil), nil @@ -996,6 +997,7 @@ func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, v "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), "len(params.Deposits)", len(params.Deposits), + "params.WithdrawalsRoot", params.WithdrawalsRoot, "beaconRoot", beaconRoot, "error", err) errorMsg := err.Error() 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 adad450200..d732bc9f70 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<