Skip to content

Commit

Permalink
all: withdrawals root in header is reused to commit to OP-Stack L2 wi…
Browse files Browse the repository at this point in the history
…thdrawals storage root
  • Loading branch information
protolambda committed Sep 21, 2024
1 parent 5f7ebba commit 8d2f6ac
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 42 deletions.
74 changes: 40 additions & 34 deletions beacon/engine/gen_ed.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion beacon/engine/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
Expand Down
9 changes: 9 additions & 0 deletions consensus/beacon/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 15 additions & 1 deletion core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions eth/catalyst/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion eth/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
20 changes: 18 additions & 2 deletions eth/downloader/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ func (f *fetchResult) Done(kind uint) bool {
return v&(1<<kind) == 0
}

type OPStackChainConfig interface {
IsOptimismHolocene(time uint64) bool
}

// queue represents hashes that are either need fetching or are being fetched
type queue struct {
mode SyncMode // Synchronisation mode to decide on the block parts to schedule for fetching
Expand Down Expand Up @@ -156,10 +160,15 @@ type queue struct {
closed bool

logTime time.Time // Time instance when status was last reported

// opConfig is used for OP-Stack chain configuration checks.
// This may be nil if not an OP-Stack chain.
opConfig OPStackChainConfig
}

// newQueue creates a new download queue for scheduling block retrieval.
func newQueue(blockCacheLimit int, thresholdInitialSize int) *queue {
// The
func newQueue(opConfig OPStackChainConfig, blockCacheLimit int, thresholdInitialSize int) *queue {
lock := new(sync.RWMutex)
q := &queue{
headerContCh: make(chan bool, 1),
Expand All @@ -169,6 +178,7 @@ func newQueue(blockCacheLimit int, thresholdInitialSize int) *queue {
receiptWakeCh: make(chan bool, 1),
active: sync.NewCond(lock),
lock: lock,
opConfig: opConfig,
}
q.Reset(blockCacheLimit, thresholdInitialSize)
return q
Expand Down Expand Up @@ -804,7 +814,13 @@ func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, txListH
if withdrawalLists[index] == nil {
return errInvalidBody
}
if withdrawalListHashes[index] != *header.WithdrawalsHash {
if q.opConfig != nil && q.opConfig.IsOptimismHolocene(header.Time) {
// If Holocene, we expect an empty list of withdrawal operations,
// but the WithdrawalsHash in the header is used for the withdrawals state storage-root.
if withdrawalListHashes[index] != types.EmptyWithdrawalsHash {
return errInvalidBody
}
} else if withdrawalListHashes[index] != *header.WithdrawalsHash {
return errInvalidBody
}
}
Expand Down
6 changes: 3 additions & 3 deletions eth/downloader/queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestBasics(t *testing.T) {
numOfBlocks := len(emptyChain.blocks)
numOfReceipts := len(emptyChain.blocks) / 2

q := newQueue(10, 10)
q := newQueue(nil, 10, 10)
if !q.Idle() {
t.Errorf("new queue should be idle")
}
Expand Down Expand Up @@ -196,7 +196,7 @@ func TestBasics(t *testing.T) {
func TestEmptyBlocks(t *testing.T) {
numOfBlocks := len(emptyChain.blocks)

q := newQueue(10, 10)
q := newQueue(nil, 10, 10)

q.Prepare(1, SnapSync)

Expand Down Expand Up @@ -275,7 +275,7 @@ func XTestDelivery(t *testing.T) {
if false {
log.SetDefault(log.NewLogger(slog.NewTextHandler(os.Stdout, nil)))
}
q := newQueue(10, 10)
q := newQueue(nil, 10, 10)
var wg sync.WaitGroup
q.Prepare(1, SnapSync)
wg.Add(1)
Expand Down
2 changes: 2 additions & 0 deletions params/protocol_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ var (
OptimismBaseFeeRecipient = common.HexToAddress("0x4200000000000000000000000000000000000019")
// The L1 portion of the transaction fee accumulates at this predeploy
OptimismL1FeeRecipient = common.HexToAddress("0x420000000000000000000000000000000000001A")
// The L2 withdrawals contract predeploy address
OptimismL2ToL1MessagePasser = common.HexToAddress("0x4200000000000000000000000000000000000016")
)

const (
Expand Down

0 comments on commit 8d2f6ac

Please sign in to comment.