Skip to content

Conversation

@tomatoishealthy
Copy link

@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
    • Introduced parent hash-based L2 block assembly, replacing block number dependencies for more flexible construction.
    • Added new engine API endpoint supporting parent hash-referenced block building, improving L2 block generation with enhanced transaction encoding and state management capabilities.

✏️ Tip: You can customize this high-level summary in your review settings.

@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
Contributor

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

The changes introduce a new V2 API method for L2 block assembly (AssembleL2BlockV2) across the consensus layer and client library. This method performs parent hash-based block assembly instead of block number-based assembly, handling transaction decoding, parent header fetching, block building, and comprehensive metadata population including receipts and withdraw trie root.

Changes

Cohort / File(s) Summary
Consensus API Layer
eth/catalyst/l2_api.go
Adds AssembleL2BlockV2 method to l2ConsensusAPI that builds L2 blocks from parent hash. Includes parent header validation, transaction decoding, miner integration, state root computation, receipt/log aggregation, execution result storage, and performance logging. Populates all block fields (parent hash, number, miner, timestamp, gas, base fee, transactions, state data, receipts, next L1 message index, withdraw trie root, block hash).
Client Engine Layer
ethclient/authclient/engine.go
Adds AssembleL2BlockV2 method to Client that marshals transactions to binary and invokes the engine_assembleL2BlockV2 RPC endpoint with parent hash and serialized transactions. Includes per-transaction marshaling error handling and response conversion.

Sequence Diagram

sequenceDiagram
    participant Client as Client App
    participant EngineAPI as Engine API<br/>(ethclient)
    participant ConsensusAPI as Consensus API<br/>(l2_api)
    participant Miner as Miner
    participant StateDB as State Manager
    
    Client->>EngineAPI: AssembleL2BlockV2(parentHash, txs)
    EngineAPI->>EngineAPI: Marshal transactions to binary
    EngineAPI->>ConsensusAPI: engine_assembleL2BlockV2 RPC call
    
    ConsensusAPI->>ConsensusAPI: Log operation start
    ConsensusAPI->>StateDB: Fetch parent header by hash
    alt Parent not found
        ConsensusAPI-->>EngineAPI: Error: parent not found
    else Parent found
        ConsensusAPI->>ConsensusAPI: Decode transactions
        alt Decode error
            ConsensusAPI-->>EngineAPI: Error: decode failed
        else Decode success
            ConsensusAPI->>Miner: Build block with parent & txs
            Miner->>StateDB: Execute transactions & compute state root
            Miner-->>ConsensusAPI: Built block with metadata
            ConsensusAPI->>ConsensusAPI: Populate block fields<br/>(receipts, logs, gas, fees, roots)
            ConsensusAPI->>ConsensusAPI: Compute withdraw trie root
            ConsensusAPI->>ConsensusAPI: Store execution results
            ConsensusAPI->>ConsensusAPI: Log completion & metrics
            ConsensusAPI-->>EngineAPI: ExecutableL2Data payload
        end
    end
    
    EngineAPI-->>Client: ExecutableL2Data or error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A block assembled from a parent's hash so fine,
V2 arrives to mark the paradigm design,
No block numbers here, just cryptographic kin,
The miner shapes the future from within,
With withdraw roots and receipts all aligned! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'pbft to rollup sequencer' and 'P2P & block produce', but the actual changes only add AssembleL2BlockV2 methods - no PBFT migration, sequencer refactoring, or P2P changes are present in the provided file changes. Update the title to accurately reflect the actual changes, such as 'Add AssembleL2BlockV2 methods for L2 block assembly' or similar, to match the parent-hash-based block assembly implementation shown in the code changes.
✅ 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.
✨ Finishing touches
  • 📝 Generate docstrings

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
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)

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.

2 participants