Skip to content

refactor(aggsender): unify block range adjustment#1573

Merged
arnaubennassar merged 10 commits intodevelopfrom
feat/aggsender-adjust-block-range
Apr 15, 2026
Merged

refactor(aggsender): unify block range adjustment#1573
arnaubennassar merged 10 commits intodevelopfrom
feat/aggsender-adjust-block-range

Conversation

@arnaubennassar
Copy link
Copy Markdown
Collaborator

🔄 Changes Summary

  • Unify aggsender certificate block-range adjustment under a single AdjustBlockRange flow path.
  • Move max-L2 limiting, finalized-root claim provability checks, certificate-size trimming, and invalid-claim posterior-unclaim validation into the same ordered pipeline.
  • Remove the old standalone max-L2 limiter and the CertificateBuildParams range mutator helpers, then update PP and aggchain prover flows and mocks to use the unified adjuster.
  • Rewrite and extend aggsender tests to cover the new adjustment flow and root-based behavior.

⚠️ Breaking Changes

  • 🛠️ Config: None.
  • 🔌 API/CLI: Internal aggsender flow interfaces now use AdjustBlockRange; LimitCertSize, Range, AdjustToBlock, and the standalone max-L2 limiter path were removed.
  • 🗑️ Deprecated Features: Removed MaxL2BlockNumberLimiter and its dedicated tests.

📋 Config Updates

  • 🧾 Diff/Config snippet: None.

✅ Testing

  • 🤖 Automatic: go test ./aggsender/...
  • 🖱️ Manual: Not run.

🐞 Issues

  • None.

🔗 Related PRs

  • None.

📝 Notes

  • Claim filtering for finalized data is now driven by L1InfoTreeRootFromWhichToProve: proposer paths select the finalized root up front, while validator paths assert that the provided root is finalized before accepting the range.
  • Invalid-claim/unclaim validation remains the last adjustment step so earlier truncations cannot silently invalidate the final certificate range.

@claude
Copy link
Copy Markdown

claude bot commented Apr 8, 2026

Claude finished @arnaubennassar's task in 2m 40s —— View job


Code Review Complete

🚀 What's New

This PR successfully unifies the aggsender certificate block-range adjustment logic under a single AdjustBlockRange method. The refactoring consolidates four previously separate adjustment flows (max-L2 limiting, finalized-root claim provability checks, certificate-size trimming, and invalid-claim posterior-unclaim validation) into one ordered pipeline in aggsender/flows/adjust_block_range.go.

🐛 Bug Fixes

None.

📋 Config Updates

None.

⚠️ Breaking Changes

  • Internal API changes: The AggsenderFlowBaser interface now exposes AdjustBlockRange with a unified BlockRangeAdjustmentOptions struct. The following methods/types were removed:
    • LimitCertSize method
    • CertificateBuildParams.Range() method
    • CertificateBuildParams.AdjustToBlock() method
    • MaxL2BlockNumberLimiter class and its dedicated tests
  • Flow behavior: Claim filtering based on finalized data is now driven by L1InfoTreeRootFromWhichToProve, with proposer paths selecting the finalized root upfront and validator paths asserting that the provided root is finalized.

📊 Review Summary

✅ Strengths

  1. 🟢 Excellent test coverage: The new adjust_block_range_test.go file contains 690 lines of comprehensive tests covering edge cases, error conditions, and normal flows. Tests are well-structured and use table-driven patterns.

  2. 🟢 Clear separation of concerns: The unified AdjustBlockRange method has a clean pipeline structure with well-defined steps:

    • adjustMaxL2BlockRange
    • validateRootToProveIsFinalized
    • adjustClaimsNotProvableAgainstRoot
    • limitCertSize
    • adjustInvalidClaimsAreNotUnclaimed
  3. 🟢 Better code organization: Removing 258 lines from flow_base.go and consolidating logic reduces cognitive load and makes the codebase easier to maintain.

  4. 🟢 Consistent error handling: Error messages are descriptive and provide context about what went wrong.

  5. 🟢 Performance optimization: The GER validation cache (gerValidationCache) prevents redundant queries for the same Global Exit Root across multiple claims.

🟡 Suggested Improvements

  1. 🟡 Documentation (aggsender/flows/adjust_block_range.go:33-72):

    • The AdjustBlockRange function lacks a doc comment explaining the adjustment pipeline flow and what each option does
    • Consider adding inline comments explaining the ordering rationale (e.g., why invalid-claim validation happens last)
  2. 🟡 Magic numbers (aggsender/flows/adjust_block_range.go:362-408):

    • The filter functions use capacity estimation via aggkitcommon.EstimateSliceCapacity but the logic could be more readable with a comment explaining the span/fullSpan calculation
  3. 🟡 Potential nil pointer (aggsender/flows/adjust_block_range.go:269-296):

    • claimHasPosteriorUnclaim modifies the usedUnclaims slice but doesn't validate that the slice length matches the unclaims length. While the current usage is safe, adding a defensive check would improve robustness.
  4. 🟡 Test clarity (aggsender/flows/adjust_block_range_test.go:382-435):

    • Some test names like "handles nil claim global index" could be more specific about the expected behavior (e.g., "returns false when claim has nil global index")

🔴 Issues Found

  1. 🔴 Security: Missing validation for nil GlobalIndex (aggsender/types/certificate_build_params.go:153-179):

    func (c *CertificateBuildParams) GetClaimsFilteringUnclaims() []claimsynctypes.Claim {
        // ...
        for i, unclaim := range c.Unclaims {
            if claim.GlobalIndex.Cmp(unclaim.GlobalIndex) == 0 && !usedUnclaims[i] {  // Line 167

    Issue: If unclaim.GlobalIndex is nil, calling .Cmp() will cause a nil pointer dereference panic.

    Fix: Add nil checks before comparison:

    if claim.GlobalIndex != nil && unclaim.GlobalIndex != nil && 
       claim.GlobalIndex.Cmp(unclaim.GlobalIndex) == 0 && !usedUnclaims[i] {

    Fix this →

  2. 🔴 Logic: Inconsistent nil handling (aggsender/flows/adjust_block_range.go:269-296):

    func claimHasPosteriorUnclaim(...) bool {
        if claim.GlobalIndex == nil {
            return false  // Line 275
        }
        for idx, unclaim := range unclaims {
            if usedUnclaims[idx] || unclaim.GlobalIndex == nil {
                continue  // Line 280
            }

    Issue: The function returns false when claim.GlobalIndex is nil (line 275) but silently skips unclaims with nil GlobalIndex (line 280). This inconsistency could lead to unexpected behavior. The function should document why nil claims return false but nil unclaims are skipped.

    Recommendation: Add a comment explaining the rationale or ensure consistent handling.

  3. 🟡 Error message clarity (aggsender/flows/adjust_block_range.go:86-88):

    return nil, fmt.Errorf("adjustBlockRange can't adapt the retry certificate, "+
        "the ToBlock %d is greater than the maxL2BlockNumber %d. Err: %w",
        buildParams.ToBlock, options.MaxL2BlockNumber, ErrMaxL2BlockNumberExceededInARetryCert)

    The error prefix "adjustBlockRange" is lowercase and inconsistent with Go conventions. Consider "AdjustBlockRange" or removing the prefix since the function name is already in the stack trace.

  4. 🟡 Missing edge case test: The cloneCertificateBuildParamsWithRange function correctly preserves the ExtraData field (line 358), but there's no explicit test verifying that an empty string ExtraData is preserved vs. a nil/missing field.

  5. 🟡 Code duplication: The filter functions filterBridgesInRange, filterClaimsInRange, and filterUnclaimsInRange (lines 362-408) have nearly identical logic. Consider using Go generics or a helper function to reduce duplication.

🔍 Additional Observations

  1. CI Failure: The "Go Vulnerability Check" workflow is failing due to two vulnerabilities in dependencies:

    • google.golang.org/grpc@v1.79.1 (Authorization bypass) - Fixed in v1.79.3
    • github.com/pion/dtls/v2@v2.2.12 (AES GCM nonce issue) - No fix available yet

    Note: These are dependency issues unrelated to this PR's changes. They should be addressed separately.

  2. Migration quality: The removal of MaxL2BlockNumberLimiter (127 lines) and the old adjustment logic (258 lines from flow_base.go) is clean. All error constants were properly migrated to the new file.

  3. Interface changes: The AggsenderFlowBaser interface is cleaner with the unified AdjustBlockRange method replacing multiple adjustment methods.

  4. Backward compatibility: While this is a breaking change for internal APIs, external interfaces remain unchanged, which is good for API consumers.


🎯 Recommendations

Priority: 🔴 Required

  1. Fix the nil pointer dereference risk in GetClaimsFilteringUnclaims (certificate_build_params.go:167)

Priority: 🟡 Suggested

  1. Add doc comments to AdjustBlockRange explaining the pipeline flow and ordering
  2. Add test case for ExtraData preservation in cloneCertificateBuildParamsWithRange
  3. Consider refactoring the three filter functions to reduce code duplication
  4. Improve error message consistency (capitalization)
  5. Document the nil handling rationale in claimHasPosteriorUnclaim

Priority: 🟢 Optional

  1. Update dependency versions to fix the security vulnerabilities (separate PR)
  2. Add inline comments explaining the adjustment step ordering rationale
  3. Consider adding benchmark tests for the new unified flow to ensure no performance regression

✅ Verdict

This is a well-executed refactoring that significantly improves code organization and maintainability. The unified adjustment flow is clearer and more testable than the previous scattered approach. However, there is one critical nil pointer dereference bug that must be fixed before merging.

Once the required fix is applied, this PR will be ready to merge. The suggested improvements are non-blocking but would further enhance code quality.

Approval Status: ⚠️ Changes Requested (fix nil pointer dereference bug)


Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors aggsender’s certificate block-range adjustment logic into a unified AdjustBlockRange pipeline, removing the previous standalone max-L2 limiter and range-mutator helpers, and updating builder flows and tests accordingly.

Changes:

  • Introduces a unified AdjustBlockRange flow that applies max-L2 limiting, root-provability checks, certificate-size trimming, and invalid-claim/unclaim validation in one ordered pipeline.
  • Updates PP and aggchain prover flows (and mocks) to use AdjustBlockRange, deleting the old max-L2 limiter implementation and tests.
  • Expands/rewrites tests around the new adjustment behavior, and tweaks E2E Docker compose UID/GID handling for rootless Docker.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test/e2e/envs/loader.go Detects rootless Docker for compose UID/GID injection; makes aggkit data dir permissions /tmp-like.
aggsender/types/interfaces.go Adds AdjustBlockRange to flow base interface; removes LimitCertSize and max-L2 limiter interface.
aggsender/types/certificate_build_params.go Removes Range/AdjustToBlock; adds GetClaimsFilteringUnclaims.
aggsender/types/certificate_build_params_test.go Replaces range/adjust tests with GetClaimsFilteringUnclaims tests.
aggsender/types/certificate_build_params_additional_test.go Adds additional helper tests (nil handling, retry semantics, filtering edge cases).
aggsender/types/block_range_adjustment.go Adds BlockRangeAdjustmentOptions struct for flow-specific constraints.
aggsender/optimistic/optimistichash/calculate_hash_commit_imported_bridges.go Preallocates hash input buffer for imported-bridge commit hashing.
aggsender/mocks/mock_aggsender_flow_baser.go Updates mock to include AdjustBlockRange; removes LimitCertSize.
aggsender/flows/max_l2blocknumber_limiter.go Deleted (superseded by unified adjuster).
aggsender/flows/max_l2blocknumber_limiter_test.go Deleted (superseded by unified adjuster tests).
aggsender/flows/flow_base.go Removes old non-finalized-claim adjustment and exported size limiter usage from build-param generation.
aggsender/flows/flow_base_test.go Updates tests to target new adjustment entrypoint and unexported size limiter.
aggsender/flows/builder_flow_pp.go Routes PP flow through AdjustBlockRange (generate + get build params).
aggsender/flows/builder_flow_pp_test.go Updates PP flow tests for root-proof-based behavior and unified adjuster calls.
aggsender/flows/builder_flow_pp_additional_test.go Adds PP flow tests covering GenerateBuildParams / BuildCertificate / Signer wiring with adjuster.
aggsender/flows/builder_flow_aggchain_prover.go Routes aggchain prover flow through AdjustBlockRange, adjusts range cloning behavior, and adds post-prover adjust call.
aggsender/flows/builder_flow_aggchain_prover_test.go Updates aggchain prover flow tests for proof-based provability checks and adjuster usage.
aggsender/flows/builder_flow_aggchain_prover_optimistic_test.go Updates optimistic aggchain prover test scaffolding to mock AdjustBlockRange.
aggsender/flows/adjust_block_range.go New unified adjuster implementation (max-L2, root finalization validation, provability trimming, size limiting, invalid-claim/unclaim validation, cloning helpers).
aggsender/flows/adjust_block_range_test.go New test suite covering unified adjuster components and edge cases.

Comment on lines +153 to 161
// GetClaimsFilteringUnclaims returns a list of claims that contains all the claims of the CertificateBuildParams
// except the ones that have been unclaimed
func (c *CertificateBuildParams) GetClaimsFilteringUnclaims() []claimsynctypes.Claim {
filteredClaims := make([]claimsynctypes.Claim, 0, len(c.Claims))
if len(c.Unclaims) == 0 {
// 99.9% of the times c.Unclaims is going to be empty
filteredClaims = append(filteredClaims, c.Claims...)
return filteredClaims
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

GetClaimsFilteringUnclaims does not handle a nil receiver (it immediately reads c.Claims/c.Unclaims), while the other CertificateBuildParams helpers in this file explicitly return safe defaults when c is nil. Consider adding an early if c == nil { return nil } (or an empty slice) to avoid potential panics and keep the API consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
ErrMaxL2BlockNumberExceededInARetryCert = errors.New("maxL2BlockNumberLimiter. " +
"Max L2 block number exceeded in a retry certificate")
ErrComplete = errors.New("maxL2BlockNumberLimiter. " +
"All certs send, no more certificates can be sent")
ErrBuildParamsIsNil = errors.New("maxL2BlockNumberLimiter. BuildParams is nil")
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The exported sentinel errors still use the old "maxL2BlockNumberLimiter" prefix even though this logic now lives in AdjustBlockRange. Updating the error messages to match the new component name would make logs and wrapped errors less confusing to operators and tests that assert on strings.

Suggested change
ErrMaxL2BlockNumberExceededInARetryCert = errors.New("maxL2BlockNumberLimiter. " +
"Max L2 block number exceeded in a retry certificate")
ErrComplete = errors.New("maxL2BlockNumberLimiter. " +
"All certs send, no more certificates can be sent")
ErrBuildParamsIsNil = errors.New("maxL2BlockNumberLimiter. BuildParams is nil")
ErrMaxL2BlockNumberExceededInARetryCert = errors.New("AdjustBlockRange. " +
"Max L2 block number exceeded in a retry certificate")
ErrComplete = errors.New("AdjustBlockRange. " +
"All certs send, no more certificates can be sent")
ErrBuildParamsIsNil = errors.New("AdjustBlockRange. BuildParams is nil")

Copilot uses AI. Check for mistakes.

if rootForLeafCount.Index > latestFinalizedRoot.Index {
return fmt.Errorf("L1 info tree root %s at leaf count %d is not finalized yet. latest finalized index: %d",
buildParams.L1InfoTreeRootFromWhichToProve.Hex(), buildParams.L1InfoTreeLeafCount, latestFinalizedRoot.Index+1)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

In validateRootToProveIsFinalized the error says "latest finalized index" but formats latestFinalizedRoot.Index+1. Either report the actual latest finalized index (latestFinalizedRoot.Index) or rename the label to "leaf count" to avoid off-by-one confusion when debugging finalization mismatches.

Suggested change
buildParams.L1InfoTreeRootFromWhichToProve.Hex(), buildParams.L1InfoTreeLeafCount, latestFinalizedRoot.Index+1)
buildParams.L1InfoTreeRootFromWhichToProve.Hex(), buildParams.L1InfoTreeLeafCount, latestFinalizedRoot.Index)

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, fmt.Errorf("ppFlow - error adjusting block range: %w", err)
}

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

AdjustBlockRange now performs range truncation (size limiting / root provability trimming). If forceOneBridgeExit is true, the current early-return check happens before AdjustBlockRange, so the adjusted range could lose its only bridge exits and still proceed to build a claims-only certificate. Consider re-checking forceOneBridgeExit (and/or IsEmpty) after AdjustBlockRange, or have AdjustBlockRange enforce RequireOneBridgeInCertificate as a final post-condition after all truncation steps.

Suggested change
if p.forceOneBridgeExit && buildParams.NumberOfBridges() == 0 {
// AdjustBlockRange may truncate the range and remove bridge exits, so re-check the final range.
p.log.Infof("PPFlow - forceOneBridgeExit is true, but adjusted range has no bridges, "+
"so no certificate will be built for range: %d - %d",
buildParams.FromBlock, buildParams.ToBlock)
return nil, nil
}
if buildParams.IsEmpty() {
// AdjustBlockRange may truncate the range and remove all bridges/claims, so re-check the final range.
p.log.Infof("PPFlow - no bridges or claims found for adjusted range: %d - %d, so no certificate will be built",
buildParams.FromBlock, buildParams.ToBlock)
return nil, nil
}

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +73

return current, nil
}

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

RequireOneBridgeInCertificate is only enforced inside adjustMaxL2BlockRange. Later steps (adjustClaimsNotProvableAgainstRoot, limitCertSize, adjustInvalidClaimsAreNotUnclaimed) can also shrink the range and potentially remove all bridges while leaving claims. If flows rely on this option (e.g. PP with forceOneBridgeExit), consider enforcing it as a final check at the end of AdjustBlockRange (after all truncations).

Suggested change
return current, nil
}
if err := validateRequireOneBridgeInCertificate(current, options); err != nil {
return nil, err
}
return current, nil
}
func validateRequireOneBridgeInCertificate(
buildParams *types.CertificateBuildParams,
options types.BlockRangeAdjustmentOptions,
) error {
if !options.RequireOneBridgeInCertificate || buildParams == nil {
return nil
}
if len(buildParams.Claims) > 0 && len(buildParams.Bridges) == 0 {
return fmt.Errorf(
"require one bridge in certificate: adjusted certificate for block range [%d, %d] contains claims but no bridges",
buildParams.FromBlock,
buildParams.ToBlock,
)
}
return nil
}

Copilot uses AI. Check for mistakes.
Comment on lines +305 to +308
buildParams, err = a.baseFlow.AdjustBlockRange(ctx, buildParams, a.adjustmentOptions(false))
if err != nil {
return nil, fmt.Errorf("aggchainProverFlow - error adjusting block range after prover result: %w", err)
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

AdjustBlockRange is called after the proof is generated and the params are sliced to aggchainProof.EndBlock. If any adjustment step further trims the range (e.g. invalid-claim/unclaim validation after an unclaim got truncated out), buildParams.ToBlock can become < AggchainProof.EndBlock, leaving the proof inconsistent with the final certificate range. Consider making this post-prover AdjustBlockRange validate-only (no range changes), or detect range changes and regenerate the proof for the final range.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@joanestebanr joanestebanr left a comment

Choose a reason for hiding this comment

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

After discussing the solution, from a functional point of view it seems ok to me

@arnaubennassar
Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Collaborator

@joanestebanr joanestebanr left a comment

Choose a reason for hiding this comment

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

Checking code

@arnaubennassar arnaubennassar merged commit 2e557e1 into develop Apr 15, 2026
24 of 25 checks passed
@arnaubennassar arnaubennassar deleted the feat/aggsender-adjust-block-range branch April 15, 2026 08:33
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.

3 participants