Feat: pbft to rollup sequencer, include P2P & block produce#274
Feat: pbft to rollup sequencer, include P2P & block produce#274FletcherMan merged 6 commits intomainfrom
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds AssembleL2BlockV2 to the consensus API and client: accepts a parent block hash, optional timestamp, and raw tx bytes; verifies parent by hash, decodes and validates transactions, invokes Miner.BuildBlock, records execution results and roots, and returns an ExecutableL2Data or an error. Changes
Sequence DiagramsequenceDiagram
participant Client as Client
participant EngineRPC as Engine RPC
participant ConsensusAPI as Consensus API
participant Miner as Miner
participant StateDB as State Manager
Client->>EngineRPC: AssembleL2BlockV2(parentHash, timestamp, txs)
EngineRPC->>EngineRPC: Marshal transactions -> bytes
EngineRPC->>ConsensusAPI: engine_assembleL2BlockV2(parentHash, timestamp, txBytes)
ConsensusAPI->>StateDB: Fetch parent header by hash
alt Parent not found
ConsensusAPI-->>EngineRPC: Error (parent not found)
else
ConsensusAPI->>ConsensusAPI: Decode and validate tx bytes
alt Decode/validation error
ConsensusAPI-->>EngineRPC: Error (invalid tx)
else
ConsensusAPI->>Miner: BuildBlock(parent, timestamp, txs)
Miner->>StateDB: Execute txs, update state, compute withdraw trie root
Miner-->>ConsensusAPI: Block, receipts, logs, roots
ConsensusAPI->>ConsensusAPI: Assemble ExecutableL2Data (metadata, txs, receipts, roots, timing)
ConsensusAPI-->>EngineRPC: ExecutableL2Data
end
end
EngineRPC-->>Client: ExecutableL2Data or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
ethclient/authclient/engine.go (1)
60-77: LGTM! Implementation is correct.The method correctly implements the parent-hash-based assembly flow, properly marshaling transactions and calling the corresponding RPC endpoint.
The transaction marshaling logic (lines 65-72) is duplicated from
AssembleL2Block(lines 16-23). Consider extracting this to a helper function to reduce duplication:🔎 Optional refactor to extract transaction marshaling
+// marshalTransactions converts a slice of transactions to binary format +func marshalTransactions(transactions types.Transactions) ([][]byte, error) { + txs := make([][]byte, 0, len(transactions)) + for i, tx := range transactions { + bz, err := tx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal tx, index: %d, error: %v", i, err) + } + txs = append(txs, bz) + } + return txs, nil +} + // AssembleL2Block assembles L2 Block used for L2 sequencer to propose a block in L2 consensus progress func (ec *Client) AssembleL2Block(ctx context.Context, number *big.Int, transactions types.Transactions) (*catalyst.ExecutableL2Data, error) { - txs := make([][]byte, 0, len(transactions)) - for i, tx := range transactions { - bz, err := tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal tx, index: %d, error: %v", i, err) - } - txs = append(txs, bz) - } + txs, err := marshalTransactions(transactions) + if err != nil { + return nil, err + } var result catalyst.ExecutableL2Data - err := ec.c.CallContext(ctx, &result, "engine_assembleL2Block", &catalyst.AssembleL2BlockParams{ + err = ec.c.CallContext(ctx, &result, "engine_assembleL2Block", &catalyst.AssembleL2BlockParams{ Number: number.Uint64(), Transactions: txs, }) return &result, err }Then apply the same helper to
AssembleL2BlockV2:func (ec *Client) AssembleL2BlockV2(ctx context.Context, parentHash common.Hash, transactions types.Transactions) (*catalyst.ExecutableL2Data, error) { - txs := make([][]byte, 0, len(transactions)) - for i, tx := range transactions { - bz, err := tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal tx, index: %d, error: %v", i, err) - } - txs = append(txs, bz) - } + txs, err := marshalTransactions(transactions) + if err != nil { + return nil, err + } var result catalyst.ExecutableL2Data - err := ec.c.CallContext(ctx, &result, "engine_assembleL2BlockV2", parentHash, txs) + err = ec.c.CallContext(ctx, &result, "engine_assembleL2BlockV2", parentHash, txs) return &result, err }eth/catalyst/l2_api.go (1)
364-413: LGTM! Implementation correctly enables parent-hash-based block assembly.The method properly handles parent lookup, transaction decoding, block building, and result caching. The nil check for the parent header (lines 372-374) prevents potential panics.
The implementation duplicates significant logic from
AssembleL2Block(lines 67-114), including transaction decoding (lines 377-384 vs 75-82) andExecutableL2Dataconstruction (lines 395-412 vs 96-113). Consider extracting shared logic to reduce maintenance burden:🔎 Recommended refactor to extract common logic
Add helper methods to reduce duplication:
// decodeTransactionsList decodes a slice of binary-encoded transactions func decodeTransactionsList(encodedTxs [][]byte) (types.Transactions, error) { transactions := make(types.Transactions, 0, len(encodedTxs)) for i, otx := range encodedTxs { var tx types.Transaction if err := tx.UnmarshalBinary(otx); err != nil { return nil, fmt.Errorf("transaction %d is not valid: %v", i, err) } transactions = append(transactions, &tx) } return transactions, nil } // buildExecutableL2Data constructs ExecutableL2Data from a block build result func (api *l2ConsensusAPI) buildExecutableL2Data(newBlockResult *miner.BlockResult, procTime time.Duration) *ExecutableL2Data { withdrawTrieRoot := api.writeVerified(newBlockResult.State, newBlockResult.Block, newBlockResult.Receipts, procTime) return &ExecutableL2Data{ ParentHash: newBlockResult.Block.ParentHash(), Number: newBlockResult.Block.NumberU64(), Miner: newBlockResult.Block.Coinbase(), Timestamp: newBlockResult.Block.Time(), GasLimit: newBlockResult.Block.GasLimit(), BaseFee: newBlockResult.Block.BaseFee(), Transactions: encodeTransactions(newBlockResult.Block.Transactions()), StateRoot: newBlockResult.Block.Root(), GasUsed: newBlockResult.Block.GasUsed(), ReceiptRoot: newBlockResult.Block.ReceiptHash(), LogsBloom: newBlockResult.Block.Bloom().Bytes(), NextL1MessageIndex: newBlockResult.Block.Header().NextL1MsgIndex, WithdrawTrieRoot: withdrawTrieRoot, Hash: newBlockResult.Block.Hash(), } }Then refactor both methods:
func (api *l2ConsensusAPI) AssembleL2Block(params AssembleL2BlockParams) (*ExecutableL2Data, error) { log.Info("Assembling block", "block number", params.Number) parent := api.eth.BlockChain().CurrentHeader() expectedBlockNumber := parent.Number.Uint64() + 1 if params.Number != expectedBlockNumber { log.Warn("Cannot assemble block with discontinuous block number", "expected number", expectedBlockNumber, "actual number", params.Number) return nil, fmt.Errorf("cannot assemble block with discontinuous block number %d, expected number is %d", params.Number, expectedBlockNumber) } - transactions := make(types.Transactions, 0, len(params.Transactions)) - for i, otx := range params.Transactions { - var tx types.Transaction - if err := tx.UnmarshalBinary(otx); err != nil { - return nil, fmt.Errorf("transaction %d is not valid: %v", i, err) - } - transactions = append(transactions, &tx) - } + transactions, err := decodeTransactionsList(params.Transactions) + if err != nil { + return nil, err + } start := time.Now() newBlockResult, err := api.eth.Miner().BuildBlock(parent.Hash(), time.Now(), transactions) if err != nil { return nil, err } procTime := time.Since(start) - withdrawTrieRoot := api.writeVerified(newBlockResult.State, newBlockResult.Block, newBlockResult.Receipts, procTime) - return &ExecutableL2Data{ - ParentHash: newBlockResult.Block.ParentHash(), - Number: newBlockResult.Block.NumberU64(), - Miner: newBlockResult.Block.Coinbase(), - Timestamp: newBlockResult.Block.Time(), - GasLimit: newBlockResult.Block.GasLimit(), - BaseFee: newBlockResult.Block.BaseFee(), - Transactions: encodeTransactions(newBlockResult.Block.Transactions()), - - StateRoot: newBlockResult.Block.Root(), - GasUsed: newBlockResult.Block.GasUsed(), - ReceiptRoot: newBlockResult.Block.ReceiptHash(), - LogsBloom: newBlockResult.Block.Bloom().Bytes(), - NextL1MessageIndex: newBlockResult.Block.Header().NextL1MsgIndex, - WithdrawTrieRoot: withdrawTrieRoot, - - Hash: newBlockResult.Block.Hash(), - }, nil + return api.buildExecutableL2Data(newBlockResult, procTime), nil }func (api *l2ConsensusAPI) AssembleL2BlockV2(parentHash common.Hash, txs [][]byte) (*ExecutableL2Data, error) { log.Debug("AssembleL2BlockV2", "parentHash", parentHash.Hex()) parent := api.eth.BlockChain().GetHeaderByHash(parentHash) if parent == nil { return nil, fmt.Errorf("parent block not found: %s", parentHash.Hex()) } - transactions := make(types.Transactions, 0, len(txs)) - for i, otx := range txs { - var tx types.Transaction - if err := tx.UnmarshalBinary(otx); err != nil { - return nil, fmt.Errorf("transaction %d is not valid: %v", i, err) - } - transactions = append(transactions, &tx) - } + transactions, err := decodeTransactionsList(txs) + if err != nil { + return nil, err + } start := time.Now() newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, time.Now(), transactions) if err != nil { return nil, err } procTime := time.Since(start) - withdrawTrieRoot := api.writeVerified(newBlockResult.State, newBlockResult.Block, newBlockResult.Receipts, procTime) - - return &ExecutableL2Data{ - ParentHash: newBlockResult.Block.ParentHash(), - Number: newBlockResult.Block.NumberU64(), - Miner: newBlockResult.Block.Coinbase(), - Timestamp: newBlockResult.Block.Time(), - GasLimit: newBlockResult.Block.GasLimit(), - BaseFee: newBlockResult.Block.BaseFee(), - Transactions: encodeTransactions(newBlockResult.Block.Transactions()), - - StateRoot: newBlockResult.Block.Root(), - GasUsed: newBlockResult.Block.GasUsed(), - ReceiptRoot: newBlockResult.Block.ReceiptHash(), - LogsBloom: newBlockResult.Block.Bloom().Bytes(), - NextL1MessageIndex: newBlockResult.Block.Header().NextL1MsgIndex, - WithdrawTrieRoot: withdrawTrieRoot, - - Hash: newBlockResult.Block.Hash(), - }, nil + return api.buildExecutableL2Data(newBlockResult, procTime), nil }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
eth/catalyst/l2_api.goethclient/authclient/engine.go
🧰 Additional context used
🧬 Code graph analysis (1)
ethclient/authclient/engine.go (4)
ethclient/authclient/client.go (1)
Client(10-12)core/types/transaction.go (1)
Transactions(615-615)eth/catalyst/api_types.go (1)
ExecutableL2Data(89-111)eth/catalyst/gen_l2_ed.go (2)
ExecutableL2Data(18-33)ExecutableL2Data(59-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (go)
There was a problem hiding this comment.
🧹 Nitpick comments (2)
ethclient/authclient/engine.go (1)
69-81: Consider extracting shared transaction marshaling logic.The transaction marshaling loop (lines 70-77) is identical to the one in
AssembleL2Block(lines 16-23). Consider extracting this into a helper function to reduce duplication and simplify future maintenance.♻️ Suggested helper extraction
+func marshalTransactions(transactions types.Transactions) ([][]byte, error) { + txs := make([][]byte, 0, len(transactions)) + for i, tx := range transactions { + bz, err := tx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal tx, index: %d, error: %v", i, err) + } + txs = append(txs, bz) + } + return txs, nil +} + func (ec *Client) AssembleL2BlockV2(ctx context.Context, parentHash common.Hash, transactions types.Transactions) (*catalyst.ExecutableL2Data, error) { - txs := make([][]byte, 0, len(transactions)) - for i, tx := range transactions { - bz, err := tx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal tx, index: %d, error: %v", i, err) - } - txs = append(txs, bz) + txs, err := marshalTransactions(transactions) + if err != nil { + return nil, err } var result catalyst.ExecutableL2Data - err := ec.c.CallContext(ctx, &result, "engine_assembleL2BlockV2", parentHash, txs) + err = ec.c.CallContext(ctx, &result, "engine_assembleL2BlockV2", parentHash, txs) return &result, err }The same helper could also be used by
AssembleL2Block.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ethclient/authclient/engine.go` around lines 69 - 81, The transaction marshaling loop in AssembleL2BlockV2 is duplicated with AssembleL2Block; extract it into a shared helper (e.g., marshalTransactions or encodeTransactions) that accepts transactions types.Transactions and returns ([][]byte, error), performing the MarshalBinary per tx and returning a descriptive error on failure, then replace the loop in both AssembleL2BlockV2 and AssembleL2Block to call this helper and pass its result into ec.c.CallContext.eth/catalyst/l2_api.go (1)
396-442: Significant code duplication withAssembleL2Block.The new
AssembleL2BlockV2method shares substantial logic withAssembleL2Block:
- Transaction decoding (lines 406-413 vs 75-82)
ExecutableL2Dataconstruction (lines 424-441 vs 96-113)Consider extracting shared logic into helper functions to improve maintainability. For example:
decodeTransactionsalready exists but a helper for building theExecutableL2Dataresponse could be added.- The common post-build flow (writeVerified + constructing return value) could be unified.
♻️ Suggested refactor for shared ExecutableL2Data construction
+func buildExecutableL2Data(block *types.Block, withdrawTrieRoot common.Hash) *ExecutableL2Data { + return &ExecutableL2Data{ + ParentHash: block.ParentHash(), + Number: block.NumberU64(), + Miner: block.Coinbase(), + Timestamp: block.Time(), + GasLimit: block.GasLimit(), + BaseFee: block.BaseFee(), + Transactions: encodeTransactions(block.Transactions()), + StateRoot: block.Root(), + GasUsed: block.GasUsed(), + ReceiptRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + NextL1MessageIndex: block.Header().NextL1MsgIndex, + WithdrawTrieRoot: withdrawTrieRoot, + Hash: block.Hash(), + } +}This helper can then be used by both
AssembleL2BlockandAssembleL2BlockV2.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@eth/catalyst/l2_api.go` around lines 396 - 442, AssembleL2BlockV2 duplicates transaction decoding and ExecutableL2Data assembly from AssembleL2Block; extract a shared helper(s) such as decodeTransactions (reuse existing decodeTransactions) and a new buildExecutableL2Data(parentHash, newBlockResult, procTime) that calls writeVerified(newBlockResult.State, newBlockResult.Block, newBlockResult.Receipts, procTime) and constructs/returns the ExecutableL2Data (setting ParentHash, Number, Miner, Timestamp, GasLimit, BaseFee, Transactions via encodeTransactions, StateRoot, GasUsed, ReceiptRoot, LogsBloom, NextL1MessageIndex, WithdrawTrieRoot and Hash). Replace the duplicated blocks in both AssembleL2Block and AssembleL2BlockV2 to call decodeTransactions for tx decoding and buildExecutableL2Data for the post-build flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@eth/catalyst/l2_api.go`:
- Around line 396-442: AssembleL2BlockV2 duplicates transaction decoding and
ExecutableL2Data assembly from AssembleL2Block; extract a shared helper(s) such
as decodeTransactions (reuse existing decodeTransactions) and a new
buildExecutableL2Data(parentHash, newBlockResult, procTime) that calls
writeVerified(newBlockResult.State, newBlockResult.Block,
newBlockResult.Receipts, procTime) and constructs/returns the ExecutableL2Data
(setting ParentHash, Number, Miner, Timestamp, GasLimit, BaseFee, Transactions
via encodeTransactions, StateRoot, GasUsed, ReceiptRoot, LogsBloom,
NextL1MessageIndex, WithdrawTrieRoot and Hash). Replace the duplicated blocks in
both AssembleL2Block and AssembleL2BlockV2 to call decodeTransactions for tx
decoding and buildExecutableL2Data for the post-build flow.
In `@ethclient/authclient/engine.go`:
- Around line 69-81: The transaction marshaling loop in AssembleL2BlockV2 is
duplicated with AssembleL2Block; extract it into a shared helper (e.g.,
marshalTransactions or encodeTransactions) that accepts transactions
types.Transactions and returns ([][]byte, error), performing the MarshalBinary
per tx and returning a descriptive error on failure, then replace the loop in
both AssembleL2BlockV2 and AssembleL2Block to call this helper and pass its
result into ec.c.CallContext.
FletcherMan
left a comment
There was a problem hiding this comment.
Review Summary
The approach of using parent hash instead of block number is a good direction toward reorg support. However, there are several issues that need to be addressed before merging.
High severity:
- Missing Jade fork check — V1 has a critical safety check (
IsJadeForkvsUseZktrie) that prevents assembling blocks with the wrong state storage format during the Jade fork transition. V2 omits this entirely. - No concurrency protection — with arbitrary parent hashes and future reorg support, concurrent calls could race on
BuildBlockandwriteVerified.
Medium severity:
3. Hardcoded time.Now() as the block timestamp — V1 allows callers to specify timestamps. For deterministic block production in a sequencer, this should be a parameter.
4. No unit tests for the new endpoints.
Low severity:
5. parent variable is fetched but unused after the nil check.
6. PR title mentions "P2P" changes but the diff only contains block assembly APIs.
See inline comments for details.
| // AssembleL2BlockV2 assembles a new L2 block based on parent hash. | ||
| // This differs from AssembleL2Block which uses block number. | ||
| // Using parent hash allows building on any parent block, enabling future reorg support. | ||
| func (api *l2ConsensusAPI) AssembleL2BlockV2(parentHash common.Hash, txs [][]byte) (*ExecutableL2Data, error) { |
There was a problem hiding this comment.
[High] No concurrency protection
The existing newBlockLock mutex is not acquired here. Since V2 is designed for "future reorg support" with arbitrary parent hashes, concurrent callers could race on BuildBlock and writeVerified, potentially corrupting the verified map or producing conflicting blocks. Please use the same locking pattern as other block-producing methods.
| // This differs from AssembleL2Block which uses block number. | ||
| // Using parent hash allows building on any parent block, enabling future reorg support. | ||
| func (api *l2ConsensusAPI) AssembleL2BlockV2(parentHash common.Hash, txs [][]byte) (*ExecutableL2Data, error) { | ||
| log.Debug("AssembleL2BlockV2", "parentHash", parentHash.Hex()) |
There was a problem hiding this comment.
[Nit] V1 (AssembleL2Block) uses log.Info for the assembly entry log. V2 uses log.Debug, which reduces operational visibility. Consider using log.Info for consistency.
| log.Debug("AssembleL2BlockV2", "parentHash", parentHash.Hex()) | ||
|
|
||
| // Get parent block by hash | ||
| parent := api.eth.BlockChain().GetHeaderByHash(parentHash) |
There was a problem hiding this comment.
[Low] parent is fetched and nil-checked but never used after line 402. Since BuildBlock will also fail if the parent is missing, this is effectively a fast-fail pre-check. Either add a comment explaining that intent, or simplify:
if api.eth.BlockChain().GetHeaderByHash(parentHash) == nil {
return nil, fmt.Errorf("parent block not found: %s", parentHash.Hex())
}| } | ||
|
|
||
| start := time.Now() | ||
| newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, time.Now(), transactions) |
There was a problem hiding this comment.
[High] Missing Jade fork check
AssembleL2Block (V1) has this safety check at lines 89–91:
if api.eth.BlockChain().Config().IsJadeFork(uint64(timestamp.Unix())) == api.eth.BlockChain().Config().Morph.UseZktrie {
return nil, fmt.Errorf("cannot assemble block for fork, useZKtrie: %v, please switch geth", api.eth.BlockChain().Config().Morph.UseZktrie)
}This prevents producing blocks with the wrong state storage format (MPT vs zkTrie) when crossing the Jade fork boundary. V2 omits it entirely, which could silently produce invalid blocks. Please add the equivalent check here.
| } | ||
|
|
||
| start := time.Now() | ||
| newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, time.Now(), transactions) |
There was a problem hiding this comment.
[Medium] Hardcoded time.Now() as block timestamp
time.Now() is called twice: once for start (the perf metric) and once as the block timestamp passed to BuildBlock. V1 allows the caller to specify an explicit timestamp via params.Timestamp. For a deterministic sequencer, the block timestamp should be caller-controlled to ensure consistency. Consider adding a timestamp parameter to AssembleL2BlockV2.
| err := ec.c.CallContext(ctx, &result, "engine_assembleL2BlockV2", parentHash, txs) | ||
| return &result, err | ||
| } | ||
|
|
There was a problem hiding this comment.
[Minor] Currently this returns a non-nil &result even when err != nil, since result is declared on the stack and always has an address. Other methods in this file (AssembleL2Block) use *catalyst.ExecutableL2Data as the out-param to avoid this. Consider:
var result *catalyst.ExecutableL2Data
err := ec.c.CallContext(ctx, &result, "engine_assembleL2BlockV2", parentHash, txs)
return result, errThis way a nil pointer is returned on error, consistent with the rest of the client.
…leL2BlockV2 - Add newBlockLock mutex for concurrency protection - Add Jade fork check to prevent wrong storage format - Use log.Info instead of log.Debug for consistency - Simplify parent nil check - Fix client return type to pointer for nil on error Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
eth/catalyst/l2_api.go (1)
420-424:⚠️ Potential issue | 🟠 MajorUse one explicit block timestamp for both the fork check and
BuildBlock.These two independent
time.Now()reads can disagree at the Jade boundary: the guard can evaluate one second whileBuildBlockseals the next. That makes fork-format selection non-deterministic and also means identical(parentHash, txs)inputs can produce different payloads depending on local clock time. Please thread a caller-supplied timestamp through V2 and use that same value in both places.Suggested fix
-func (api *l2ConsensusAPI) AssembleL2BlockV2(parentHash common.Hash, txs [][]byte) (*ExecutableL2Data, error) { +func (api *l2ConsensusAPI) AssembleL2BlockV2(parentHash common.Hash, timestamp uint64, txs [][]byte) (*ExecutableL2Data, error) { api.newBlockLock.Lock() defer api.newBlockLock.Unlock() log.Info("AssembleL2BlockV2", "parentHash", parentHash.Hex()) + blockTime := time.Unix(int64(timestamp), 0) // Get parent block by hash if api.eth.BlockChain().GetHeaderByHash(parentHash) == nil { return nil, fmt.Errorf("parent block not found: %s", parentHash.Hex()) } @@ - if api.eth.BlockChain().Config().IsJadeFork(uint64(time.Now().Unix())) == api.eth.BlockChain().Config().Morph.UseZktrie { + if api.eth.BlockChain().Config().IsJadeFork(timestamp) == api.eth.BlockChain().Config().Morph.UseZktrie { return nil, fmt.Errorf("cannot assemble block for fork, useZKtrie: %v, please switch geth", api.eth.BlockChain().Config().Morph.UseZktrie) } - newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, time.Now(), transactions) + newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, blockTime, transactions)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@eth/catalyst/l2_api.go` around lines 420 - 424, The code uses two independent time.Now() calls for the Jade fork check and for BuildBlock causing nondeterminism; modify the API to accept a caller-supplied timestamp (e.g., ts or blockTime) and thread that value into both the fork check and the call to api.eth.Miner().BuildBlock(parentHash, ..., transactions) so you call api.eth.BlockChain().Config().IsJadeFork(uint64(ts.Unix())) and pass the same ts to BuildBlock instead of calling time.Now() twice; update the V2 entrypoint to require this timestamp and propagate it through to where parentHash and transactions are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@eth/catalyst/l2_api.go`:
- Around line 420-424: The code uses two independent time.Now() calls for the
Jade fork check and for BuildBlock causing nondeterminism; modify the API to
accept a caller-supplied timestamp (e.g., ts or blockTime) and thread that value
into both the fork check and the call to api.eth.Miner().BuildBlock(parentHash,
..., transactions) so you call
api.eth.BlockChain().Config().IsJadeFork(uint64(ts.Unix())) and pass the same ts
to BuildBlock instead of calling time.Now() twice; update the V2 entrypoint to
require this timestamp and propagate it through to where parentHash and
transactions are used.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b64e9765-da7c-495b-be2a-e7a1dac75ca5
📒 Files selected for processing (2)
eth/catalyst/l2_api.goethclient/authclient/engine.go
🚧 Files skipped from review as they are similar to previous changes (1)
- ethclient/authclient/engine.go
Allow caller to specify block timestamp for deterministic sequencing, consistent with V1 behavior. Falls back to time.Now() when nil. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add the same Jade fork safety check that V1 has, preventing block production with wrong storage format (MPT vs zkTrie) at fork boundary. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
eth/catalyst/l2_api.go (1)
396-449: Extract the shared assembly pipeline into one helper.
AssembleL2BlockV2now duplicates most ofAssembleL2Block's decode/build/cache/response flow, and the two entry points already diverged on locking. Pulling the common path behind a private helper would make future fixes land in one place instead of drifting again.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@eth/catalyst/l2_api.go` around lines 396 - 449, AssembleL2BlockV2 duplicates most of AssembleL2Block; extract the shared decode/build/cache/response pipeline into a private helper (e.g. func (api *l2ConsensusAPI) assembleL2BlockCommon(parentHash common.Hash, timestamp *uint64, txs [][]byte) (*ExecutableL2Data, error)) that performs: parent header check, transaction unmarshalling into types.Transactions, timestamp resolution, call to api.eth.Miner().BuildBlock, call to writeVerified(newBlockResult.State,...), and construction of the ExecutableL2Data (use encodeTransactions, NextL1MessageIndex, WithdrawTrieRoot, etc. as in current code). Keep locking semantics in the two public entry points (AssembleL2Block and AssembleL2BlockV2) by leaving api.newBlockLock.Lock()/Unlock() where they currently are and simply call the new helper from each; ensure error paths and returned values are preserved exactly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@eth/catalyst/l2_api.go`:
- Around line 396-449: AssembleL2BlockV2 duplicates most of AssembleL2Block;
extract the shared decode/build/cache/response pipeline into a private helper
(e.g. func (api *l2ConsensusAPI) assembleL2BlockCommon(parentHash common.Hash,
timestamp *uint64, txs [][]byte) (*ExecutableL2Data, error)) that performs:
parent header check, transaction unmarshalling into types.Transactions,
timestamp resolution, call to api.eth.Miner().BuildBlock, call to
writeVerified(newBlockResult.State,...), and construction of the
ExecutableL2Data (use encodeTransactions, NextL1MessageIndex, WithdrawTrieRoot,
etc. as in current code). Keep locking semantics in the two public entry points
(AssembleL2Block and AssembleL2BlockV2) by leaving
api.newBlockLock.Lock()/Unlock() where they currently are and simply call the
new helper from each; ensure error paths and returned values are preserved
exactly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7df6da4b-32b4-447f-ba78-d4efdf6c5f9c
📒 Files selected for processing (2)
eth/catalyst/l2_api.goethclient/authclient/engine.go
1. Purpose or design rationale of this PR
Migrate PBFT consensus to a single sequencer and add new block-handling APIs in geth.
Summary by CodeRabbit