Skip to content

Feat: pbft to rollup sequencer, include P2P & block produce#274

Merged
FletcherMan merged 6 commits intomainfrom
feat/pbft_to_singleSeq
Mar 13, 2026
Merged

Feat: pbft to rollup sequencer, include P2P & block produce#274
FletcherMan merged 6 commits intomainfrom
feat/pbft_to_singleSeq

Conversation

@tomatoishealthy
Copy link
Copy Markdown
Contributor

@tomatoishealthy tomatoishealthy commented Dec 30, 2025

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

  • New Features
    • Parent-hash-based L2 block assembly for more flexible block construction.
    • Client-side API to assemble L2 blocks from a parent hash using provided transactions.
    • Improved assembly diagnostics, timing metrics, and execution-result recording for better reliability and error reporting.

@tomatoishealthy tomatoishealthy requested a review from a team as a code owner December 30, 2025 03:14
@tomatoishealthy tomatoishealthy requested review from panos-xyz and removed request for a team December 30, 2025 03:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 30, 2025

Warning

Rate limit exceeded

@tomatoishealthy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5d3097f-5851-48c3-a7b0-13d94cab0958

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4cd3a and 280bfb9.

📒 Files selected for processing (2)
  • eth/catalyst/l2_api.go
  • ethclient/authclient/engine.go
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Consensus API Layer
eth/catalyst/l2_api.go
Adds l2ConsensusAPI.AssembleL2BlockV2(parentHash common.Hash, timestamp *uint64, txs [][]byte). Resolves parent by hash, decodes/validates raw txs, locks around build, calls Miner.BuildBlock, collects receipts/state/withdraw trie root/logs bloom, records timing, and returns ExecutableL2Data or errors.
Client Engine Layer
ethclient/authclient/engine.go
Adds Client.AssembleL2BlockV2(ctx, parentHash, timestamp, transactions) which marshals each tx to bytes, calls engine_assembleL2BlockV2 with parent hash, timestamp and serialized txs, and returns the ExecutableL2Data or marshaling errors.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I stitched a block from hash and little hops of code,
Decoded each byte down a winding road,
Miner hummed, roots and receipts in a row,
Timing noted, logs all aglow,
Hooray — V2 hops onward, steady and bold! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions 'pbft to rollup sequencer, include P2P & block produce' but the actual changes only introduce AssembleL2BlockV2 API methods for L2 block assembly, which is a specific technical addition unrelated to the broader PBFT migration objectives. Update the title to reflect the actual changes, such as 'Add AssembleL2BlockV2 API for L2 block assembly' or be more specific about what block production capability is being added in this commit.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pbft_to_singleSeq

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) and ExecutableL2Data construction (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

📥 Commits

Reviewing files that changed from the base of the PR and between d108c19 and 51859dc.

📒 Files selected for processing (2)
  • eth/catalyst/l2_api.go
  • ethclient/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)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 with AssembleL2Block.

The new AssembleL2BlockV2 method shares substantial logic with AssembleL2Block:

  • Transaction decoding (lines 406-413 vs 75-82)
  • ExecutableL2Data construction (lines 424-441 vs 96-113)

Consider extracting shared logic into helper functions to improve maintainability. For example:

  1. decodeTransactions already exists but a helper for building the ExecutableL2Data response could be added.
  2. 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 AssembleL2Block and AssembleL2BlockV2.

🤖 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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51859dc and 88d3d86.

📒 Files selected for processing (2)
  • eth/catalyst/l2_api.go
  • ethclient/authclient/engine.go

Copy link
Copy Markdown
Collaborator

@FletcherMan FletcherMan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Missing Jade fork check — V1 has a critical safety check (IsJadeFork vs UseZktrie) that prevents assembling blocks with the wrong state storage format during the Jade fork transition. V2 omits this entirely.
  2. No concurrency protection — with arbitrary parent hashes and future reorg support, concurrent calls could race on BuildBlock and writeVerified.

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.

Comment thread eth/catalyst/l2_api.go Outdated
// 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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment thread eth/catalyst/l2_api.go Outdated
// 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())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment thread eth/catalyst/l2_api.go Outdated
log.Debug("AssembleL2BlockV2", "parentHash", parentHash.Hex())

// Get parent block by hash
parent := api.eth.BlockChain().GetHeaderByHash(parentHash)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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())
}

Comment thread eth/catalyst/l2_api.go Outdated
}

start := time.Now()
newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, time.Now(), transactions)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment thread eth/catalyst/l2_api.go Outdated
}

start := time.Now()
newBlockResult, err := api.eth.Miner().BuildBlock(parentHash, time.Now(), transactions)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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, err

This 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]>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
eth/catalyst/l2_api.go (1)

420-424: ⚠️ Potential issue | 🟠 Major

Use 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 while BuildBlock seals 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

📥 Commits

Reviewing files that changed from the base of the PR and between 88d3d86 and f46be77.

📒 Files selected for processing (2)
  • eth/catalyst/l2_api.go
  • ethclient/authclient/engine.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • ethclient/authclient/engine.go

allen.wu and others added 3 commits March 12, 2026 20:48
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]>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
eth/catalyst/l2_api.go (1)

396-449: Extract the shared assembly pipeline into one helper.

AssembleL2BlockV2 now duplicates most of AssembleL2Block'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

📥 Commits

Reviewing files that changed from the base of the PR and between f46be77 and 4a4cd3a.

📒 Files selected for processing (2)
  • eth/catalyst/l2_api.go
  • ethclient/authclient/engine.go

@FletcherMan FletcherMan merged commit fa69f4d into main Mar 13, 2026
8 checks passed
@FletcherMan FletcherMan deleted the feat/pbft_to_singleSeq branch March 13, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate the existing PBFT consensus to a Rollup-style sequencer architecture

2 participants