Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome to Polygon Aggkit Tech Docs

Welcome to the official documentation for the Polygon Aggkit. This guide will help you get started with building and deploying rollups to the Agglayer.

Setup environment to local debug on VSCode

Requirements

  • Working and running kurtosis-cdk environment setup.
  • In test/scripts/env.sh setup KURTOSIS_FOLDER pointing to your setup.

Tip

Use your WIP branch in Kurtosis CDK as needed

1. Create configuration for this kurtosis environment

scripts/local_config

2. Stop the aggkit node started by Kurtosis CDK

kurtosis service stop aggkit cdk-node-001

3. Add to vscode launch.json

After execution of scripts/local_config, it suggests an entry for launch.json configurations

AggOracle Component

Overview

The AggOracle component ensures the Global Exit Root (GER) is propagated from L1 to the L2 sovereign chain smart contract. This is critical for enabling asset and message bridging between chains.

The GER is indexed from the L2 smart contract by L2GERSyncer component and persisted in local storage.

The AggOracle supports two operational modes:

  1. Direct Injection Mode: Direct injection of GERs into the L2 GER Manager contract
  2. AggOracle Committee Mode: Consensus-based GER injection through a committee of oracle members

Key Components:

  • ChainSender: Interface for submitting GERs to the smart contract.
  • EVMChainGERSender: An implementation of ChainSender interface supporting both operational modes.

Workflow

What is Global Exit Root (GER)?

The Global Exit Root consolidates:

  • Mainnet Exit Root (MER): Updated during bridge transactions from L1.

  • Rollup Exit Root (RER): Updated when verified rollup batches are submitted via ZKP.

      GER = hash(MER, RER)
    

Operational Modes

1. Direct Injection Mode

In this mode, the AggOracle directly injects GERs into the L2 GER Manager contract:

  1. Fetch Finalized GER: AggOracle retrieves the latest GER finalized on L1.
  2. Check GER Injection: Confirms whether the GER is already stored in the smart contract.
  3. Direct Injection: If missing, AggOracle directly submits the GER via the insertGlobalExitRoot function.
  4. Sync Locally: L2GERSyncer fetches and stores the GER locally for downstream use.

2. AggOracle Committee Mode

In this mode, GER injection requires consensus from a committee of oracle members:

  1. Fetch Finalized GER: AggOracle retrieves the latest GER finalized on L1.
  2. Check GER Status: Confirms whether the GER is already injected or proposed.
  3. Propose GER: If not yet proposed, committee member submits the GER via proposeGlobalExitRoot function to the AggOracleCommittee contract.
  4. Committee Proposals: Other committee members submit the same GER via proposeGlobalExitRoot to signal agreement.
  5. Automatic Injection: Once quorum is reached, the GER is automatically injected into the L2 GER Manager contract.
  6. Sync Locally: L2GERSyncer fetches and stores the GER locally for downstream use.

Committee Consensus Mechanism

  • Committee Members: A predefined set of authorized oracle addresses
  • Quorum: Minimum number of votes required for GER injection
  • Proposal Tracking: Each member’s latest proposed GER is tracked; members cannot re-propose the same GER
  • Proposal Counting: The committee contract track proposals for each GER
  • Automatic Execution: GER injection happens automatically when quorum is reached

The sequence diagrams below depict the interactions in both operational modes.

Direct Injection Mode:

sequenceDiagram
    participant AggOracle
    participant ChainSender
    participant L1InfoTreeSyncer
    participant L2GERManager

    AggOracle->>AggOracle: start (Direct Injection Mode)
    AggOracle->>AggOracle: process latest GER
    loop trigger on preconfigured frequency
        AggOracle->>L1InfoTreeSyncer: get latest finalized GER
        L1InfoTreeSyncer-->>AggOracle: return GER from L1 info tree
        AggOracle->>ChainSender: ProcessGER
        ChainSender->>L2GERManager: check if GER injected
        L2GERManager-->>ChainSender: GER injection status
        alt GER already injected
            ChainSender->>ChainSender: log GER already injected
        else GER not injected
            ChainSender->>L2GERManager: insertGlobalExitRoot(GER)
            L2GERManager-->>ChainSender: transaction result
        end
    end
    AggOracle->>AggOracle: handle GER processing error

AggOracle Committee Mode:

sequenceDiagram
    participant AggOracle
    participant ChainSender
    participant L1InfoTreeSyncer
    participant AggOracleCommittee
    participant L2GERManager
    participant OtherCommitteeMembers

    AggOracle->>AggOracle: start (Committee Mode)
    AggOracle->>AggOracle: process latest GER
    loop trigger on preconfigured frequency
        AggOracle->>L1InfoTreeSyncer: get latest finalized GER
        L1InfoTreeSyncer-->>AggOracle: return GER from L1 info tree
        AggOracle->>ChainSender: ProcessGER
        ChainSender->>L2GERManager: check if GER injected
        L2GERManager-->>ChainSender: GER injection status
        alt GER already injected
            ChainSender->>ChainSender: log GER already injected
        else GER not injected
            ChainSender->>AggOracleCommittee: check if GER proposed
            AggOracleCommittee-->>ChainSender: proposal status
            alt GER already proposed
                ChainSender->>ChainSender: log GER already proposed
            else GER not yet proposed
                ChainSender->>AggOracleCommittee: proposeGlobalExitRoot(GER)
                AggOracleCommittee->>AggOracleCommittee: record proposal
                OtherCommitteeMembers->>AggOracleCommittee: other members proposeGlobalExitRoot(GER)
                AggOracleCommittee->>AggOracleCommittee: record additional proposals
                alt quorum reached
                    AggOracleCommittee->>L2GERManager: insertGlobalExitRoot(GER)
                    L2GERManager-->>AggOracleCommittee: injection result
                else quorum not reached
                    AggOracleCommittee->>AggOracleCommittee: wait for more votes
                end
            end
        end
    end
    AggOracle->>AggOracle: handle GER processing error

Key Components

1. AggOracle

The AggOracle fetches the finalized GER and ensures its injection into the L2 smart contract using the configured operational mode.

Functions:

  • Start: Periodically processes GER updates using a ticker.
  • processLatestGER: Fetches the latest GER and delegates processing to the ChainSender.

2. ChainSender Interface

Defines the unified interface for submitting GERs in both operational modes.

// Common methods for both modes
IsGERInjected(ger common.Hash) (bool, error)
ProcessGER(ctx context.Context, ger common.Hash) error

// Direct injection mode
InjectGER(ctx context.Context, ger common.Hash) error

// Committee mode specific
ProposeGER(ctx context.Context, ger common.Hash) error
IsGERProposed(ger common.Hash) (bool, error)

3. EVMChainGERSender

Implements ChainSender using Ethereum clients and transaction management, supporting both operational modes.

Mode Selection:

  • Direct Injection Mode: When EnableAggOracleCommittee = false
  • Committee Mode: When EnableAggOracleCommittee = true and AggOracleCommitteeAddr is configured

Functions:

Common Functions:

  • IsGERInjected: Verifies GER presence in the L2 GER Manager contract.
  • ProcessGER: Routes to either InjectGER or ProposeGER based on operational mode.

Direct Injection Mode:

  • InjectGER: Directly submits the GER using insertGlobalExitRoot and monitors transaction status.

Committee Mode:

  • ProposeGER: Proposes the GER to the AggOracleCommittee using proposeGlobalExitRoot.
  • IsGERProposed: Checks if the current committee member has already proposed the GER.

Validation:

  • Direct Mode: Validates that the sender address is authorized as GlobalExitRootUpdater
  • Committee Mode: Validates that the sender is a registered committee member

Smart Contract Integration

1. L2 GER Manager Contract

Used in both operational modes for final GER storage and status checking.

  • Contract: GlobalExitRootManagerL2SovereignChain.sol
  • Key Functions:
    • insertGlobalExitRoot: Final GER injection (called directly in Direct Mode, or by committee contract in Committee Mode)
    • GlobalExitRootMap: Check if a GER is already injected
    • GlobalExitRootUpdater: Get authorized updater address (for Direct Mode validation)
  • Source Code: zkevm-contracts
  • Bindings: Available in cdk-contracts-tooling

2. AggOracleCommittee Contract

Used exclusively in Committee Mode for consensus-based GER proposals.

⚠️ Implementation Note: The client-side code only handles proposal submission via proposeGlobalExitRoot. The consensus mechanism, quorum handling, and automatic GER injection are handled at the smart contract level. Refer to the actual AggOracleCommittee.sol contract for complete implementation details.

  • Contract: AggOracleCommittee.sol

  • Key Functions (as defined in current interface):

    • proposeGlobalExitRoot: Submit GER proposal (called via transaction)
    • GetAggOracleMemberIndex: Validate committee membership
    • AddressToLastProposedGER: Track last proposal by each member
  • Additional Functions (used in implementation but not in interface):

    • AggOracleMembers: Get committee member information (used in validation)
    • ProposedGERToReport: Get proposal status for a specific GER
  • Initialization Parameters:

    • Committee Members: Array of authorized oracle addresses
    • Quorum: Minimum proposals/votes required for consensus (contract-level implementation)
  • Bindings: Available in cdk-contracts-tooling


Configuration

Direct Injection Mode Configuration

AggOracle:
  TargetChainType: "EVM"
  URLRPCL1: "https://eth-mainnet.g.alchemy.com/v2/your-api-key"
  WaitPeriodNextGER: "5s"
  EnableAggOracleCommittee: false
  EVMSender:
    GlobalExitRootL2: "0x123...abc"  # L2 GER Manager contract address
    AggOracleCommitteeAddr: "0x000...000"  # Not used in direct mode
    GasOffset: 80000
    WaitPeriodMonitorTx: "1s"
    EthTxManager:
      FrequencyToMonitorTxs: "1s"
      WaitTxToBeMined: "2m"
      # ... other EthTxManager config

Committee Mode Configuration

AggOracle:
  TargetChainType: "EVM"
  URLRPCL1: "https://eth-mainnet.g.alchemy.com/v2/your-api-key"
  WaitPeriodNextGER: "5s"
  EnableAggOracleCommittee: true
  EVMSender:
    GlobalExitRootL2: "0x123...abc"  # L2 GER Manager contract address
    AggOracleCommitteeAddr: "0x456...def"  # AggOracleCommittee contract address
    GasOffset: 80000
    WaitPeriodMonitorTx: "1s"
    EthTxManager:
      FrequencyToMonitorTxs: "1s"
      WaitTxToBeMined: "2m"
      # Ensure the From address is a committee member
      # ... other EthTxManager config

Key Configuration Differences

ConfigurationDirect Injection ModeCommittee Mode
EnableAggOracleCommitteefalsetrue
AggOracleCommitteeAddrNot requiredRequired (valid contract address)
EthTxManager.FromMust be authorized as GlobalExitRootUpdaterMust be a registered committee member
GER SubmissionDirect via insertGlobalExitRootProposal via proposeGlobalExitRoot

📊 Aggoracle Metrics

The Aggoracle service exposes Prometheus metrics to track Global Exit Root (GER) processing activity, latency, and error rates. All metrics are registered under the namespace: aggoracle

MetricTypeDescriptionUnit
aggoracle_ger_processing_trigger_totalCounterTotal number of GER processing triggers.count
aggoracle_ger_processing_errors_totalCounterTotal number of GER processing errors.count
aggoracle_ger_processing_duration_secondsHistogramTime taken to process a single Global Exit Root from start to finish.seconds

Summary

The AggOracle component automates the propagation of GERs from L1 to L2, enabling bridging across networks. It supports two operational modes:

  • Direct Injection Mode: Simple, single-authority GER injection
  • Committee Mode: Consensus-based GER injection providing enhanced security through multiple oracle validation

Refer to the EVM implementation in evm.go for guidance on building chain senders for non-EVM chains.

AggSender Component

AggSender is responsible for building and packing the information required to prove a target chain’s bridge state into a certificate. This certificate provides the inputs needed to build a proof that is eventually going to be settled on L1 via the agglayer.

The AggSender consists of a multisig committee, where one participant acts as the proposer, and the remaining members act as validators. The proposer is responsible for building and signing the certificate, and propagating it to the validators for verification via gRPC. Each validator independently validates the proposed certificate and returns a signature to the proposer if the validation is successful. Proposer will pack each signature (including its own) in the certificate, and send it to agglayer for settlement.

The multisig committee is registered on the rollup contract on L1. It contains a list of signers, each represented by an Ethereum address and a URL. It is important that when initializing the rollup contract:

  • the first signer in the list corresponds to the AggSender proposer. For the proposer, the url parameter may be omitted (as it is not used for validation requests).
  • the remaining signers represent AggSender validators, and their url fields must be properly set, as these endpoints are used to send certificate validation requests via gRPC.

Component Diagram

The image below depicts the Aggsender components (the editable link of the diagram is found here).

Flow

Starting the AggSender

Aggsender gets the epoch configuration from the Agglayer. It checks the last certificate in DB (if exists) against the Agglayer, to be sure that both are on the same page:

  • If the DB is empty then get, as starting point, the last certificate Agglayer has.
  • If it is a fresh start, and there are no certificates before this, it will set its starting block to 1 and start polling bridges and claims from the syncer from that block.
  • If Aggsender is not on the same page as Agglayer it will log error and not proceed with the process of building new certificates, because this case means that there was another player involved that sent a certificate in place of the Aggsender which is an invalid case since Aggsender is a single instance per L2 network. It can also happen if we put a different Aggsender db (from a different network).
  • If both Aggsender and Agglayer have the same certificate, then Aggsender will start the certificate monitoring and build process since this is a valid use case.
sequenceDiagram
    participant Agglayer
    participant Aggsender Proposer
    participant Aggsender Validator 1
    participant Aggsender Validator N

    Aggsender Proposer->>Agglayer: Read epoch configuration
    Aggsender Proposer->>Agglayer: Read latest known certificate
    Aggsender Proposer-->>Aggsender Proposer: Wait for an epoch
    Aggsender Proposer-->>Aggsender Proposer: Build certificate
    Aggsender Proposer->>Aggsender Validator 1: Validate certificate
    Aggsender Proposer->>Aggsender Validator N: Validate certificate
    Aggsender Validator 1-->>Aggsender Proposer: Return signature if valid
    Aggsender Validator N-->>Aggsender Proposer: Return signature if valid
    Aggsender Proposer->>Agglayer: Send certificate

PessimisticProof Mode

Aggsender will wait until the epoch event is triggered and ask the L2BridgeSyncer if there are new bridges and claims to be sent to Agglayer. Once we reach the moment in epoch when we need to send a certificate, the Aggsender will poll all the bridges and claims from the bridge syncer, based on the last sent L2 block to the Agglayer, until the block that the syncer has.

It is important to mention that no certificate will be sent to the Agglayer if the syncer has no bridges, since bridges change the Local Exit Root (LER).

If we have bridges, certificate will be built, signed, and sent to the Agglayer using the provided Agglayer RPC URL.

Currently, Agglayer only supports one certificate per L1 epoch, per network, so we can not send more than one certificate. After the certificate is sent, we wait until the next epoch, either to resend it if its status is InError, or to build a new one if its status Settled. Also, we have no limit yet in how many bridges and claims can be sent in a single certificate. This might be something to test and check, because certificates carry a lot of data through RPC, so we might hit the rpc layer limit at some point. For this reason, we introduced the MaxCertSize configuration parameter on the Aggsender, where the user can define the maximum size of the certificate (based on the rpc communication layer limit) in bytes, and the Aggsender will limit the number of bridges and claims it will send to the Agglayer based on this parameter. Since both bridges and claims carry fixed size of data (each field is a fixed size field), we can we great precision calculate the size of a certificate.

InError status on a certificate can mean a number of things. It can be an error that happened on the Agglayer. It can be an error in the data Aggsender sent, or the certificate was sent in between two epochs, which Agglayer considers invalid. Either way, the given certificate needs to be re-sent in the next epoch (or immediately after we notice its status change based on the RetryCertAfterInError config parameter), with all the previously sent bridges and claims, plus the new ones that happened after them, that the syncer saw and saved.

It is important to mention that, in the case of resending the certificate, the certificate height must be reused. If we are sending a new certificate, its height must be incremented based on the previously sent certificate.

Suppose the previously sent certificate was not marked as InError, or Settled on the Agglayer. In that case, we can not send/resend the certificate, even though a new epoch event is handled since it was not processed yet by the Agglayer (neither Settled nor marked as InError).

The image below depicts the interaction between different components when building and sending a certificate to the Agglayer in the PessimisticProof mode.

sequenceDiagram
    participant User
    participant L1RPC as L1 Network
    participant L2RPC as L2 Network
    participant Bridge as Bridge Smart Contract
    participant AggLayer
    participant L2BridgeSyncer
    participant L1InfoTreeSync
    participant AggSender

    User->>L1RPC: bridge (L1->L2)
    L1RPC->>Bridge: bridgeAsset
    Bridge->>AggLayer: updateL1InfoTree
    Bridge->>Bridge: auto claim

    User->>L2RPC: bridge (L2->L1)
    L2RPC->>L2BridgeSyncer: bridgeAsset emits bridgeEvent

    User->>L2RPC: claimAsset emits claimEvent
    L2RPC->>L1InfoTreeSync: index claimEvent

    AggSender->>AggSender: wait for epoch to elapse
    AggSender->>L1InfoTreeSync: check latest sent certificate
    AggSender->>L2BridgeSyncer: get published bridges
    AggSender->>L2BridgeSyncer: get imported bridge exits
    Note right of AggSender: generate a Merkle proof for each imported bridge exit
    AggSender->>L1InfoTreeSync: get l1 info tree merkle proof for imported bridge exits
    AggSender->>AggLayer: send certificate

AggchainProof Mode

In essence, the AggchainProof mode follows the same logic and flow as PessimisticProof mode. Only difference is in two points:

  • Calling the aggchain prover to generate an aggchain proof that will be sent in the certfiicate to the Agglayer.
  • Resending an InError certficate does not expand it with new bridges and events that the syncer might have gotten in the meantime. This is done because aggchain prover already generated a proof for a given block range, and since proof generation can be a long process, this is a small optimization.
  • Note that this might change in the future.

Calling the aggchain prover is done right before signing and sending the certificate to the Agglayer. To generate an aggchain proof prover needs couple of things:

  • Block range on L2 for which we are trying to generate a certificate.
  • Finalized L1 info tree root, leaf, and proof on the L1 info tree. Basically, this is the latest finalized l1 info tree root needed by the prover to generate the proof. This root is also use to generate merkle proof for every imported bridge exit (claim) in certificate.
  • Injected GlobalExitRoot’s on L2 and their leaves and proofs. Merkle proofs of the injected GERs are calculated based on the finalized L1 info tree root.
  • Imported bridge exits (claims) we intend to include in the certificate for the given block range.

The image below depicts the interaction between different components when building and sending a certificate to the Agglayer in the AggchainProof mode.

sequenceDiagram
    participant User
    participant L1RPC as L1 Network
    participant L2RPC as L2 Network
    participant Bridge as Bridge Smart Contract
    participant AggLayer
    participant L2BridgeSyncer
    participant L1InfoTreeSync
    participant AggSender
    participant AggchainProver

    User->>L1RPC: bridge (L1->L2)
    L1RPC->>Bridge: bridgeAsset
    Bridge->>AggLayer: updateL1InfoTree
    Bridge->>Bridge: auto claim

    User->>L2RPC: bridge (L2->L1)
    L2RPC->>L2BridgeSyncer: bridgeAsset emits bridgeEvent

    User->>L2RPC: claimAsset
    L2RPC->>L1InfoTreeSync: claimEvent

    AggSender->>AggSender: wait for epoch to elapse
    AggSender->>L1InfoTreeSync: check latest sent certificate
    AggSender->>L2BridgeSyncer: get published bridges
    AggSender->>L2BridgeSyncer: get imported bridge exits
    AggSender->>L1InfoTreeSync: get finalized l1 info tree root
    AggSender->>L2RPC: get injected GERs
    Note right of AggSender: generate a Merkle proof for each injected GER
    AggSender->>L1InfoTreeSync: get l1 info tree merkle proof for injected GERs
    AggSender->>AggchainProver: generate aggchain proof
    Note right of AggSender: generate a Merkle proof for each imported bridge exit
    AggSender->>L1InfoTreeSync: get l1 info tree merkle proof for imported bridge exits
    AggSender->>AggLayer: send certificate

Certificate Data

The certificate is the data submitted to Agglayer. Must be signed to be accepted by Agglayer. Agglayer responds with a certificateID (hash)

Field NameDescription
network_idThis is the id of the rollup (>0)
heightOrder of certificates. First one is 0
prev_local_exit_rootThe first one must be the one in smart contract (currently is a 0x000…00)
new_local_exit_rootIt’s the root after bridge_exits
bridge_exitsThese are the leaves of the LER tree included in this certificate. (bridgeAssert calls)
imported_bridge_exitsThese are the claims done in this network
aggchain_paramsAggchain params returned by the aggchain prover
aggchain_proofAggchain proof generated by the aggchain prover
custom_chain_dataCustom chain data returned by the aggchain prover

Configuration

NameTypeDescription
StoragePathstringFull file path (with file name) where to store Aggsender DB
AgglayerClient*aggkitgrpc.ClientConfigAgglayer gRPC client configuration.
AggsenderPrivateKeySignerConfigConfiguration of the signer used to sign the certificate on the Aggsender before sending it to the Agglayer. It can be a local private key, or an external one.
URLRPCL2stringL2 RPC
BlockFinalitystringIndicates which finality the AggLayer follows (FinalizedBlock, SafeBlock, LatestBlock, PendingBlock) you can add an offset e.g: “FinalizedBlock/20” or “FinalizedBlock/-20”
TriggerCertModestringMode used to trigger certificate sending. Options: “EpochBased”, “NewBridge”, “ASAP”, “Auto” (default: “Auto”)
TriggerEpochBasedTriggerEpochBasedConfigConfiguration for EpochBased trigger mode (used when TriggerCertMode is “EpochBased”)
TriggerASAPTriggerASAPConfigConfiguration for ASAP trigger mode (used when TriggerCertMode is “ASAP”)
MaxRetriesStoreCertificateintNumber of retries if Aggsender fails to store certificates on DB. 0 = infinite retries
DelayBetweenRetriesDurationDelay between retries for storing certificate and initial status check
MaxCertSizeuintThe maximum size of the certificate. 0 means infinite size
DryRunboolIf true, AggSender will not send certificates to Agglayer (for debugging)
EnableRPCboolEnable the Aggsender’s RPC layer
AggkitProverClient*aggkitgrpc.ClientConfigConfiguration for the AggkitProver gRPC client
ModestringDefines the mode of the AggSender (PessimisticProof or AggchainProof)
CheckStatusCertificateIntervalDurationInterval at which the AggSender will check the certificate status in Agglayer
RetryCertAfterInErrorboolIf true, Aggsender will re-send InError certificates immediately after status change
MaxSubmitCertificateRateRateLimitConfigMaximum allowed rate of submission of certificates in a given time.
GlobalExitRootL2AddrAddressAddress of the GlobalExitRootManager contract on L2 sovereign chain (needed for AggchainProof mode)
SovereignRollupAddrAddressAddress of the sovereign rollup contract on L1
RequireStorageContentCompatibilityboolIf true, data stored in the database must be compatible with the running environment
RequireNoFEPBlockGapboolIf true, AggSender should not accept a gap between lastBlock from lastCertificate and first block of FEP
OptimisticModeConfigoptimistic.ConfigConfiguration for optimistic mode (required by FEP mode).
RequireOneBridgeInPPCertificateboolIf true, AggSender requires at least one bridge exit for Pessimistic Proof certificates
MaxL2BlockNumberuint64Set the last block to be included in a certificate (0 = disabled)
MaxL2BlockRangeuint64Maximum L2 block range allowed in a certificate, computed as ToBlock - FromBlock (0 = disabled)
StopOnFinishedSendingAllCertificatesboolStop when there are no more certificates to send due to MaxL2BlockNumber
StorageRetainCertificatesPolicyStorageRetainCertificatesPolicyConfigure the certificate retain policy
UnsetClaimsMaxLogBlockRangeuint64Proactive max block range for eth_getLogs queries when fetching unset claims. 0 means disabled (fallback to reactive chunking on error)

StorageRetainCertificatesPolicy

The StorageRetainCertificatesPolicy structure configures the certificate retain policy

Field NameTypeDescription
RetainCertificatesCountuint32If it is 0, all certificates are stored. If it is greater than 0, it is the number of certificates stored in the DB. The last certificate sent is always saved because it is necessary for proper operation.
KeepCertificatesHistoryboolIf true, discarded certificates are moved to the certificate_info_history table instead of being deleted

TriggerEpochBasedConfig

The TriggerEpochBasedConfig structure configures the epoch-based trigger mode for certificate sending. This configuration is used when TriggerCertMode is set to “EpochBased” (or when “Auto” mode resolves to epoch-based triggering).

Field NameTypeDescription
EpochNotificationPercentageuintIndicates the percentage of the epoch at which the AggSender should send the certificate. 0 = begin, 50 = middle, 100 = end

Example:

[AggSender]
    TriggerCertMode = "EpochBased"
    [AggSender.TriggerEpochBased]
        EpochNotificationPercentage = 50

The epoch-based trigger waits for a specific percentage of the epoch to elapse before sending certificates to the Agglayer. This allows for coordinated certificate submission aligned with L1 epoch boundaries.

TriggerASAPConfig

The TriggerASAPConfig structure configures the ASAP (As Soon As Possible) trigger mode for certificate sending. This configuration is used when TriggerCertMode is set to “ASAP”.

Field NameTypeDescription
DelayBetweenCertificatesDurationThe delay to wait before sending a new certificate after the previous one is settled
MinimumNewCertificateIntervalDurationThe minimum interval between two new certificate triggers (0 = no minimum interval)

Example:

[AggSender]
    TriggerCertMode = "ASAP"
    [AggSender.TriggerASAP]
        DelayBetweenCertificates = "1s"
        MinimumNewCertificateInterval = "1h"

The ASAP trigger sends certificates as soon as possible after the last certificate reaches a final state (settled or in error). The DelayBetweenCertificates parameter adds a configurable delay before sending, while MinimumNewCertificateInterval ensures a minimum time gap between certificate submissions to prevent excessive certificate generation.

Trigger Modes

The TriggerCertMode field supports the following modes:

  • EpochBased: Triggers certificate sending based on epoch progression. Uses the TriggerEpochBased configuration to determine when in the epoch to send certificates.
  • NewBridge: Triggers certificate sending immediately when new bridge events are detected on L2.
  • ASAP: Triggers certificate sending as soon as possible after the last certificate reaches a final state (settled or in error).
  • Auto: Automatically selects the appropriate trigger mode based on the AggSender mode:
    • PreconfPP mode → NewBridge trigger
    • PessimisticProof and AggchainProof modes → EpochBased trigger

OptimisticConfig

The OptimisticConfig structure configures the optimistic mode for the AggSender. This configuration is required when running in FEP (Fast Exit Protocol) mode.

Field NameTypeDescription
SovereignRollupAddrAddressThe L1 address of the AggchainFEP contract
TrustedSequencerKeySignerConfigThe private key used to sign optimistic proofs. Must be the trusted sequencer’s key.
OpNodeURLstringThe URL of the OpNode service used to fetch aggregation proof public values
RequireKeyMatchTrustedSequencerboolIf true, enables a sanity check that the signer’s public key matches the trusted sequencer address. This ensures the signer is the trusted sequencer and not a random signer.

Example:

[AggSender]
    [AggSender.OptimisticModeConfig]
        SovereignRollupAddr = "0x1234..."
        TrustedSequencerKey = { Method="local", Path="/opt/private_key.keystore", Password="password" }
        OpNodeURL = "http://localhost:8080"
        RequireKeyMatchTrustedSequencer = true

The optimistic mode is used in FEP (Fast Exit Protocol) to enable faster exit processing by allowing optimistic proofs to be submitted before full verification. The trusted sequencer is responsible for signing these proofs, and this configuration ensures that only the authorized trusted sequencer can submit proofs.

Use Cases

This paragraph explains different use cases with outcomes:

  • No bridges from L2 -> L1 means no certificate will be built.
  • Having bridges without claims, means a certificate will be built and sent.
  • Having bridges and claims, means a certificate will be built and sent.
  • If the previous certificate we sent is InError, we need to resend that certificate with all the previous sent data, plus new bridges and claims we saw after that.
  • If the previously sent certificate is not InError or Settled, no new certificate will be sent/resent. The AggSender waits for one of these two statuses on the Agglayer.

Debugging in Local with Bats E2E Tests

Preconditions:

  • Make sure you have the up to date aggkit:local Docker image built. In order to build one, run make build-docker-ci command.
  • Run the bridge_spammer in background (namely make sure that the additional_services has bridge_spammer provided).
  1. Start kurtosis with pessimistic proof (OP stack): ./test/run-local-e2e.sh single-l2-network-op-pessimistic path_to_kurtosis_cdk_repo - Note that the fourth argument corresponds to the e2e repo path. In case you would like to run the set of e2e tests immediately after the kurtosis environment is up and running, you should provide a real path.
  2. After kurtosis is started, stop the aggkit-001 service (kurtosis service stop aggkit aggkit-001).
  3. Open the repo in an IDE (like Visual Studio), and run ./scripts/local_config_pp from the main repo folder. This will generate a ./tmp folder in which Aggsender storage will be saved, and other aggkit node data, and will print a launch.json:
{
   "version": "0.2.0",
   "configurations": [
       {
           "name": "Debug aggsender",
           "type": "go",
           "request": "launch",
           "mode": "auto",
           "program": "cmd/",
           "cwd": "${workspaceFolder}",
           "args":[
               "run",
               "-cfg", "tmp/aggkit/local_config/test.kurtosis.toml",
               "-components", "aggsender",
           ]
       }
   ]
}
  1. Copy this to your launch.json and start debugging.
  2. This will start the aggkit with the aggsender running.
  3. Wait for some time, until bridge_spammer deposits are indexed by the aggsender. As a result of bridge activity, there should be a certificate, and you can debug the whole process.
  4. Optionally you can run the E2E tests as well, by running the following command and providing the real e2e repo path: ./test/run-local-e2e.sh single-l2-network-op-pessimistic - path_to_e2e_repo

Prometheus Metrics

If enabled in the configuration, Aggsender exposes the following Prometheus metrics:

Metric NameTypeDescription
aggsender_number_of_certificates_sentCounterNumber of certificates sent
aggsender_number_of_certificates_in_errorCounterNumber of certificates in error
aggsender_number_of_sending_retriesCounterNumber of sending retries
aggsender_number_of_certificates_settledCounterNumber of certificates settled
aggsender_number_of_prover_errorsCounterNumber of prover errors
aggsender_multisig_threshold_not_reachedCounterNumber of times multisig threshold was not reached
aggsender_validator_errors_totalCounter (labeled by aggsender_validator)Total number of errors returned by a validator over time
aggsender_validator_invalid_signature_totalCounter (labeled by aggsender_validator)Number of times a validator returned an invalid signature
aggsender_validate_timeHistogramTime taken to validate a certificate (seconds)
aggsender_prover_timeHistogramTime taken by the prover (seconds)
aggsender_certificate_settlement_timeHistogramTime taken to settle a certificate (seconds)
aggsender_certificate_build_timeHistogramTime taken to build a certificate (seconds)

Configuration Example

To enable Prometheus metrics, configure Aggsender as follows:

[Prometheus]
Enabled = true
Host = "localhost"
Port = 9091

With this configuration, the metrics will be available at: http://localhost:9091/metrics

Additional Documentation

  1. (https://potential-couscous-4gw6qyo.pages.github.io/protocol/workflow_centralized.html)
  2. Initial PR
  3. (https://agglayer.github.io/agglayer/pessimistic_proof/index.html)

AggSender Validator Component

The AggsenderValidator is a critical component of the AggKit framework that provides certificate validation services for the AggSender. It ensures that certificates built by the AggSender are correct and valid before they are submitted to the AggLayer.

Conceptual Overview

Purpose

The Aggsender Validator serves as an independent validation layer that verifies the correctness of certificates generated by the AggSender Proposer. It acts as a security gate, ensuring that only properly constructed certificates are submitted to the AggLayer, preventing invalid submissions that could cause issues in the chain . A given validator is part of a committee whose signature acts as a vote for correctness of a new certificate. When a threshold of signatures for a new certificate is reached, a certificate can be accepted by the Agglayer.

Key Functions

  1. Certificate Validation: Validates the structure, content, and integrity of certificates
  2. Certificate Signing: Signs valid certificates using a configured signer
  3. Health Monitoring: Provides health check endpoints for monitoring service status
  4. gRPC Service: Exposes validation functionality through a gRPC interface

Validation Process

The validator performs comprehensive checks on incoming certificates:

  • Certificate Continuity: Verifies certificates are contiguous (no gaps in height)
  • Previous Certificate Status: Checks that previous certificates are properly settled
  • Certificate Reconstruction: Rebuilds the certificate using data indexed from the L1 and L2 RPCs and compares with incoming certificate. Validators and proposers should use independent RPCs
  • Content Verification: Validates that all certificate fields match expected values

Architecture Overview

Components

The AggsenderValidator consists of several key components:

┌─────────────────────────────────────────────────────────────┐
│                    AggsenderValidator                       │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────┐                   │
│  │  gRPC Service   │  │ Validation Logic│                   │
│  │                 │  │                 │                   │
│  │ - HealthCheck   │  │ - CertValidator │                   │
│  │ - ValidateCert  │  │ - FlowInterface │                   │
│  └─────────────────┘  └─────────────────┘                   │
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐                   │
│  │   Data Access   │  │     Signing     │                   │
│  │                 │  │                 │                   │
│  │ - L1InfoTree    │  │ - Signer        │                   │
│  │ - CertQuerier   │  │ - KeyManagement │                   │
│  │ - LERQuerier    │  │                 │                   │
│  └─────────────────┘  └─────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

Aggsender Validator as a part of a committee

The Aggsender Validator operates as part of a distributed validator committee that provides security through consensus. This multi-signature approach ensures that certificates are validated by multiple independent parties before being accepted by the AggLayer.

sequenceDiagram
    participant AP as AggSender Proposer
    participant AV1 as Validator 1
    participant AV2 as Validator 2
    participant AV3 as Validator 3
    participant AVN as Validator N
    participant AL as AggLayer

    Note over AP: Certificate ready for validation
    
    par Parallel Validation Requests
        AP->>AV1: ValidateCertificate(cert, prevCertID)
        AP->>AV2: ValidateCertificate(cert, prevCertID)
        AP->>AV3: ValidateCertificate(cert, prevCertID)
        AP->>AVN: ValidateCertificate(cert, prevCertID)
    end

    par Independent Validation & Signing
        AV1->>AV1: Validate & Sign
        AV2->>AV2: Validate & Sign
        AV3->>AV3: Validate & Sign
        AVN->>AVN: Validate & Sign
    end

    par Signature Responses
        AV1-->>AP: Signature 1 ✓
        AV2-->>AP: Signature 2 ✓
        AV3-->>AP: Validation Error ✗
        AVN-->>AP: Signature N ✓
    end

    Note over AP: Check if threshold met<br/>(e.g., 3 out of 4 signatures)
    
    alt Threshold Met
        AP->>AL: SubmitCertificate(cert + signatures)
        AL-->>AP: Certificate ID
    else Threshold Not Met
        AP->>AP: Reject certificate<br/>Log validation failures
    end

Committee Consensus Process

  1. Certificate Proposal: The AggSender Proposer builds a certificate and submits it to all committee members
  2. Parallel Validation: Each validator independently validates the certificate using the same validation logic
  3. Independent Signing: Valid certificates are signed by each validator using their unique private keys
  4. Signature Collection: The proposer collects signatures from all validators
  5. Threshold Check: The proposer verifies that enough validators have signed (e.g., 3 out of 4, or 67% majority)
  6. Certificate Submission: If threshold is met, the certificate with collected signatures is submitted to AggLayer
  7. Rejection Handling: If threshold is not met, the certificate is rejected and the process logs validation failures

This committee-based approach provides several security benefits:

  • Decentralization: No single point of failure in validation
  • Consensus: Multiple independent validators must agree on certificate validity
  • Fault Tolerance: System continues to operate even if some validators are unavailable
  • Security: Malicious or compromised validators cannot unilaterally approve invalid certificates

Core Interfaces

CertificateValidator

The main validation engine that implements the core validation logic:

  • ValidateCertificate(ctx, params): Main validation method
  • Checks certificate continuity, and content verification, verifies proof for each claim (imported bridge exit)

ValidatorService (gRPC)

Exposes validation functionality via gRPC:

  • HealthCheck(): Returns service status and version
  • ValidateCertificate(): Validates and signs certificates

Local vs Remote Validation

The system supports two validation modes:

  1. LocalValidator: Validates certificates locally without signing
  2. RemoteValidator: Connects to a remote validation service via gRPC

Validation Modes

Local Validator

  • Runs validation logic in the same process as AggSender
  • Does not sign certificates (validation only)
  • Useful for development and testing
  • Direct access to storage and other components

Remote Validator

  • Connects to a separate AggsenderValidator service
  • Provides full validation and signing capabilities
  • Production-ready with proper isolation
  • Communicates via gRPC protocol

Requirements & Configuration

Prerequisites

  1. Go Environment: Go 1.24+
  2. Database Access: SQLite databases for L1InfoTreeSync and BridgeL2Sync
  3. L1/L2 Connectivity: Access to L1 and L2 RPC endpoints
  4. Signer Configuration: Private key for certificate signing
  5. AggLayer Client: gRPC connection to AggLayer
  6. Expose gRPC service: Needs to be accessible to Aggsender Proposer

Configuration Parameters

The validator is configured using a .toml file. Check the default values in this file.

NameTypeDescription
MaxL2BlockRangeuint64Maximum L2 block range allowed in a certificate, computed as ToBlock - FromBlock. 0 means disabled
UnsetClaimsMaxLogBlockRangeuint64Proactive max block range for eth_getLogs queries when fetching unset claims. 0 means disabled (fallback to reactive chunking on error)

Running the Validator

As a Standalone Component

# Run only the validator component
./aggkit run --components aggsender-validator --cfg config.toml

As Part of AggSender

The validator can be integrated into the AggSender flow:

# Run AggSender with validator in PessimisticProof mode
./aggkit run --components aggsender --cfg config.toml

Configuration in AggSender:

[AggSender]
Mode = "PessimisticProof"
RequireValidatorCall = true  # Use remote validator

[AggSender.ValidatorClient]
URL = "localhost:50051"

Development Setup

For local development and testing:

  1. Setup Database: Ensure SQLite databases are accessible
  2. Configure Components: Set up L1InfoTreeSync and BridgeSync
  3. Start Dependencies: Run required L1/L2 networks
  4. Launch Validator: Start the validator service

Integration with AggSender

The validator integrates with AggSender in two ways:

  1. Local Integration: Embedded validation within AggSender process (used only for development purposes, to validate the certificate using the same logic as Aggsender Validator, but in the Aggsender Proposer itself)
  2. Remote Integration: Separate validator service accessed via gRPC

Remote Integration Flow

sequenceDiagram
    participant AP as AggSender Proposer
    participant AV as AggSender Validator
    participant AL as AggLayer
    
    AP->>AV: ValidateCertificate(cert, prevCertID)
    AV->>AV: Reconstruct certificate
    AV->>AV: Compare certificates
    AV->>AV: Sign certificate
    AV->>AP: Return signature
    AP->>AL: Submit signed certificate

gRPC API

The validator exposes a gRPC API defined in proto/v1/validator.proto:

Service Definition

service AggsenderValidator {
    rpc HealthCheck(google.protobuf.Empty) returns (HealthCheckResponse);
    rpc ValidateCertificate(ValidateCertificateRequest) returns (ValidateCertificateResponse);
}

Methods

HealthCheck

Returns the status and version of the validator service.

Response:

message HealthCheckResponse {
  string version = 1;  // Version of the validator
  string status = 2;   // Status (OK, ERROR, etc.)
  string reason = 3;   // Additional status information
}

ValidateCertificate

Validates a certificate and returns a signature if valid. If not, it returns an error.

Request:

message ValidateCertificateRequest {
  agglayer.node.types.v1.CertificateId previous_certificate_id = 1;
  agglayer.node.types.v1.Certificate certificate = 2;
  uint64 last_l2_block_in_cert = 3;
}

Response:

message ValidateCertificateResponse {
  agglayer.interop.types.v1.FixedBytes65 signature = 1;
}

Validation Logic

Certificate Validation Steps

  1. Null Check: Ensure proposed certificate is not null
  2. Certificate Continuity: Check height progression and LocalExitRoot continuity
  3. Previous Certificate Status: Ensure previous certificate is settled
  4. Certificate Reconstruction: Rebuild certificate using the same building logic the proposer used
  5. Content Comparison: Compare reconstructed vs. incoming certificate
  6. Proof Verification: Verifying proofs for each claim (iimported bridge exit)
  7. Signing: Sign the certificate if validation passes

Key Validation Rules

  • Height Continuity: Each certificate height must be previous + 1
  • LER Consistency: PrevLocalExitRoot must match previous certificate’s NewLocalExitRoot
  • First Certificate: Height 0 must have correct starting LocalExitRoot defined in the rollup contract
  • Block Range: Must be contiguous with no gaps

Error Handling

The validator returns specific errors for different validation failures:

  • ErrNilCertificate: Certificate is null
  • Certificate height mismatch errors
  • LocalExitRoot continuity errors
  • Certificate comparison differences

Monitoring & Debugging

Health Checks

The validator provides health check endpoints for monitoring:

# Check validator health via gRPC
grpcurl -plaintext localhost:50051 aggkit.aggsender.validator.v1.AggsenderValidator/HealthCheck

Logging

The validator provides detailed logging for debugging:

  • Certificate validation steps
  • Comparison differences between certificates
  • Error details and stack traces
  • Performance metrics

Common Issues

  1. Certificate Height Gaps: Ensure continuous certificate submission
  2. LocalExitRoot Mismatches: Verify bridge synchronization is correct
  3. Signing Failures: Verify signer configuration and key access
  4. gRPC Connection Issues: Check network connectivity and service status

Best Practices

  1. Use Remote Validation: For production environments, use remote validator service
  2. Monitor Health: Implement regular health checks and alerting
  3. Secure Keys: Use proper key management for signing certificates
  4. Backup Storage: Ensure L2 bridge and L1InfoTree syncers storage is backed up
  5. Performance Monitoring: Monitor validation times and resource usage
  6. Error Handling: Implement proper retry logic for transient failures

Examples

Basic Validation Call

// Create validator
validator := validator.NewAggsenderValidator(
    logger, flowPP, l1InfoTreeQuerier, certQuerier, lerQuerier)

// Validate certificate
params := types.VerifyIncomingRequest{
    Certificate:         cert,
    PreviousCertificate: prevCert,
    LastL2BlockInCert:   blockNumber,
}

err := validator.ValidateCertificate(ctx, params)
if err != nil {
    log.Errorf("Validation failed: %v", err)
    return err
}

Remote Validator Client

// Create remote validator client
remoteValidator, err := validator.NewRemoteValidator(
    grpcConfig, storage)
if err != nil {
    return err
}

// Validate and sign certificate
signature, err := remoteValidator.ValidateAndSignCertificate(
    ctx, certificate, lastL2Block)
if err != nil {
    log.Errorf("Remote validation failed: %v", err)
    return err
}

Auto Claim Service

The Auto Claim service automates bridge claims for configured destination networks, in both directions:

  • L1 to L2: bridge exits initiated on L1, discovered from l1bridgesync.
  • L2 to Lx (L2 to L1 and L2 to L2): bridge exits initiated on a rollup, discovered by watching each source rollup’s local exit root (LER) advance in l1infotreesync and fetching the corresponding bridges and Merkle proofs from that rollup’s own bridge service through bridgeservicefinder.

For every discovered bridge exit, Auto Claim stores it as a request in a local SQLite database, evaluates a configurable policy, prepares the claim proof in-process, submits the destination-chain claim transaction through EthTxManager, and tracks the request through confirmation or failure.

Auto Claim is disabled by default. origin_network on a bridge exit is the origin network of the bridged token (used in the claim calldata), which is distinct from source_network, the network the bridge exit was initiated on. For an L1-to-L2 request source_network is always 0; for an L2-to-Lx request it is the source rollup’s network ID. source_network, together with destination_network and deposit_count, is the request’s real claim identity — it is also what the claim global index encodes.

Architecture

Auto Claim runs inside the Aggkit process and reuses the existing syncers. Two bridge detectors discover bridge exits — one per direction — and feed the same per-destination claimers. Each claimer (with its own policy, sender, and EthTxManager) owns one destination network. Readiness for an L2-destination claimer is no longer tracked by a dedicated per-claimer l2gersync instance; instead, during proof preparation the claimer gates on the destination network’s own aggkit bridge service, calling its GET /bridge/v1/injected-l1-info-leaf endpoint (resolved through the shared bridgeservicefinder.Finder — the same finder the L2-to-Lx detector uses to resolve source bridge services). This applies uniformly to both directions, including L1-to-L2: an L1-to-L2 claimer with an L2 destination gates the same way. A claimer whose destination is L1 (NetworkID = 0) has no such gate: it is ready as soon as l1infotreesync has the relevant leaf, since the GER already exists in the L1 GER manager by construction. All Auto Claim request/cursor state lives in a single Auto Claim SQLite database; there is no per-claimer isolated SQLite database or L2 reorg detector anymore.

flowchart LR
    subgraph Syncers
        L1BS[l1bridgesync]
        L1IT[l1infotreesync]
    end

    subgraph Finder["bridgeservicefinder"]
        BSF["Finder<br/>(networkID -> bridge service URL)"]
    end

    subgraph AutoClaim["Auto Claim runtime"]
        WD1["L1-to-L2 bridge detector"]
        WD2["L2-to-Lx bridge detector"]
        DB[("SQLite storage")]
        API["REST API (optional)<br/>/autoclaim/v1"]
        subgraph Claimer["Claimer (one per destination network)"]
            CL["Claim engine"]
            POL["Policy"]
            PP["Proof preparer<br/>(L1-origin or rollup-origin)"]
            SND["Sender"]
        end
    end

    SRCBS["Source rollup's bridge service<br/>(remote, /bridge/v1/claim-candidates + /claim-proof)"]
    DSTBS["Destination network's own bridge service<br/>(remote, /bridge/v1/injected-l1-info-leaf)"]

    ETM["EthTxManager<br/>(one per claimer)"]
    DST["Destination bridge contract"]

    L1BS -->|bridge exits| WD1
    L1IT -->|verified-batches LER updates| WD2
    BSF -->|"GetURL(source)"| WD2
    WD2 -->|claim candidates, no proofs| SRCBS
    L1IT -->|inclusion index, proofs| PP
    BSF -->|"GetURL(destination)"| PP
    PP -->|"GET /injected-l1-info-leaf, L2 destination only"| DSTBS
    PP -->|fetch leaf proof at claim time| SRCBS
    WD1 -->|enqueue immediately| DB
    WD2 -->|enqueue immediately| DB
    CL <--> DB
    CL --> POL
    CL --> PP
    CL --> SND
    SND -->|claimAsset / claimMessage| ETM
    ETM --> DST
    SND -->|isClaimed check| DST
    API <--> DB

    Operator((Operator)) -->|inspect / approve / reject| API

Package layout (for contributors):

PackageResponsibility
autoclaim/runtimeWires storage, both bridge detectors, the bridge service finder, claimers, senders, transaction managers, and the API at startup.
autoclaim/bridgedetectorL1-to-L2 (bridgedetector.L1ToL2) and L2-to-Lx (bridgedetector.L2ToLx) bridge discovery, durable cursors, idempotent enqueue.
autoclaim/claimerPer-destination engine: policy evaluation, proof preparation, send orchestration, recovery.
bridgeservicefinder (reused)Resolves each network’s bridge service base URL from the rollup manager and health-gates it — as a source (the L2-to-Lx detector’s claim-candidate discovery, and the rollup-origin proof preparer’s claim-time leaf-proof fetch) and as a destination (every L2-destination claimer’s GER-injection readiness gate, in either direction).
autoclaim/policyNamed policy registry and the allow-all, api-approve, no-message, basic-filter implementations.
autoclaim/proofClaim proof construction: Preparer for L1-origin requests (from l1infotreesync and l1bridgesync), RollupPreparer for rollup-origin requests (from l1infotreesync and the source rollup’s bridge service), and SourceAwarePreparer, which dispatches between them per request.
autoclaim/senderClaim submission through EthTxManager, transaction attempt tracking, status mapping, retries.
autoclaim/claimtxABI packing of claimAsset and claimMessage calldata (byte-identical for L1- and L2-destination bridges).
autoclaim/simulatoreth_estimateGas claim simulation on the target chain, used by basic-filter.
autoclaim/storageSQLite repository and migrations for requests, attempts, and cursors (including the per-source LER cursor).
autoclaim/apiOptional standalone admin REST handlers for manual approve/reject decisions, plus generated swagger docs.
autoclaim/apitypesShared REST DTOs and query parsing used by the admin API and the bridge-service public endpoints.
autoclaim/typesRequest lifecycle state machine, domain records, and shared interfaces.
autoclaim/configConfiguration structs, defaults, and validation.

How a claim is processed

L1 to L2

sequenceDiagram
    participant L1BS as l1bridgesync
    participant WD as L1-to-L2 detector
    participant DB as Storage
    participant CL as Claimer
    participant PP as Proof preparer
    participant BSF as bridgeservicefinder
    participant DSTBS as Destination bridge service
    participant L1IT as l1infotreesync
    participant SND as Sender
    participant ETM as EthTxManager
    participant L2 as Destination bridge

    loop Every PollInterval
        WD->>L1BS: Get L1-initiated bridge exits (any token origin_network)
        WD->>L2: Already claimed (isClaimed)? Skip if so
        WD->>DB: Enqueue request as `detected` (idempotent, no GER precondition)
    end

    loop Every WaitPeriod (per claimer)
        CL->>DB: Load pending requests for its network
        CL->>CL: Evaluate policy (approve / reject / manual)
        CL->>PP: Build claim proof
        PP->>BSF: GetURL(destination)
        PP->>DSTBS: GET /injected-l1-info-leaf, network_id=destination, leaf_index=bridge index
        alt 404, no injected GER covers the bridge yet
            DSTBS-->>PP: 404 Not Found
            PP-->>CL: not ready (nil proof)
            CL->>DB: Stay `detected` / return to `queued`, retry next cycle
        else 200, GER covers the bridge
            DSTBS-->>PP: covering L1 info tree index
            PP->>L1IT: Build proof from resolved leaf index
            PP-->>CL: ClaimProof (with L1InfoTreeIndex)
            CL->>DB: Persist l1_info_tree_index
            CL->>SND: Send approved request
            SND->>L2: Already claimed (isClaimed)?
            alt Already claimed
                SND->>DB: Mark `confirmed`
            else Not claimed
                SND->>ETM: Add claimAsset / claimMessage tx
                ETM->>L2: Submit claim transaction
                SND->>DB: Record attempt, track tx status
            end
        end
    end

The L1-to-L2 detector enqueues detected bridge exits immediately as detected requests — it imposes no GER precondition and does not hold its cursor waiting for GER injection. The only detector-side filter is an already-claimed pre-check: before enqueueing, it asks the destination claimer whether the target bridge already reports the global index as claimed (isClaimed), and skips such bridges without storing a request. GER readiness is checked per-claimer during proof preparation, by calling the destination network’s own aggkit bridge service rather than running a dedicated per-claimer GER syncer: the preparer resolves the destination’s bridge service base URL through bridgeservicefinder.Finder.GetURL(destination) and calls its GET /bridge/v1/injected-l1-info-leaf with network_id=<destination> and leaf_index=<bridge inclusion index>. A 404 response means no injected GER covers the bridge yet — the preparer returns “not ready” and the claimer retries on the next cycle without consuming retry budget. A 200 response returns the covering L1 info tree leaf index, which the preparer then uses to build the proof from l1infotreesync. This removes the need for a per-claimer l2gersync instance (and its own isolated SQLite database and L2 reorg detector): the destination network’s own aggkit node already runs the GER syncer that backs its bridge service (supporting both legacy GlobalExitRootMap polling and sovereign UpdateHashChainValue-event tracking), so Auto Claim reuses that state over the network instead of duplicating it locally. This is a breaking operational requirement: any claimer whose destination is an L2 network (NetworkID != 0), in either direction, now requires [AutoClaim.BridgeServiceFinder].RollupManagerAddr to be configured and that destination network’s bridge service to be reachable — even for a pure L1-to-L2 setup with [AutoClaim.L2ToLxBridgeDetector].Enabled = false.

L2 to Lx (L2 to L1 and L2 to L2)

sequenceDiagram
    participant L1IT as l1infotreesync
    participant BSF as bridgeservicefinder
    participant WD as L2-to-Lx detector
    participant SRC as Source rollup's bridge service
    participant DB as Storage
    participant CL as Claimer
    participant PP as RollupPreparer
    participant DSTBS as Destination bridge service, L2 dest only
    participant SND as Sender
    participant ETM as EthTxManager
    participant DST as Destination bridge

    loop Every PollInterval
        WD->>L1IT: GetVerifiedBatchesInBlockRange(from, to)
        Note over WD,L1IT: rows come from VerifyBatchesTrustedAggregator, which the rollup manager<br/>emits for both zkEVM and pessimistic/aggchain verifications
        WD->>WD: Keep newest LER per source rollup in the window
        alt Source has a new LER since its cursor
            WD->>BSF: GetURL(source)
            alt URL not resolved / unhealthy
                WD->>WD: Skip source this round, do not advance its LER cursor
            else URL resolved
                WD->>SRC: GET /bridge/v1/claim-candidates?destination_network_ids=...&from_ler=cursor&to_ler=newLER
                SRC-->>WD: bridges only, no proofs (paginated)
                WD->>DST: Already claimed (isClaimed by source+deposit)? Skip if so
                WD->>DB: Enqueue request (source_network, ler, verify_block_num)
                WD->>WD: Advance source's LER cursor to newLER
            end
        end
    end

    loop Every WaitPeriod (per claimer)
        CL->>DB: Load pending requests for its network
        CL->>CL: Evaluate policy (approve / reject / manual)
        CL->>PP: Build claim proof
        PP->>L1IT: Find L1 info tree leaf covering the source's LER
        alt Destination is L2
            PP->>BSF: GetURL(destination)
            PP->>DSTBS: GET /injected-l1-info-leaf, network_id=destination, leaf_index=covering leaf
            Note over PP,DSTBS: 404 means not ready until an injected GER covers it
        else Destination is L1, network 0
            Note over PP: no gate, ready as soon as l1infotreesync has the leaf
        end
        PP->>SRC: GET /bridge/v1/claim-proof?network_id=source&leaf_index=...&deposit_count=... (always, fetched fresh)
        PP->>L1IT: GetRollupExitTreeMerkleProof(source, leaf.RollupExitRoot)
        PP->>PP: Verify leaf-to-LER and LER-to-RER proofs locally
        PP-->>CL: ClaimProof
        CL->>SND: Send approved request
        SND->>DST: Already claimed (isClaimed)?
        alt Already claimed
            SND->>DB: Mark `confirmed`
        else Not claimed
            SND->>ETM: Add claimAsset / claimMessage tx
            ETM->>DST: Submit claim transaction
            SND->>DB: Record attempt, track tx status
        end
    end

The L2-to-Lx detector (bridgedetector.L2ToLx) does not sync any source L2 locally. It polls l1infotreesync.GetVerifiedBatchesInBlockRange over an L1 block window (same window/overlap mechanism as the L1-to-L2 detector, durable cursor name l2-to-lx) for verified-batches rows — populated by l1infotreesync from VerifyBatchesTrustedAggregator, which the rollup manager emits for both zkEVM/state-transition and pessimistic/aggchain verifications — and keeps the newest local exit root (LER) per source rollup network observed in the window.

For each source network whose newest LER differs from its stored LER cursor (autoclaim_ler_cursor, keyed by source_network):

  1. It resolves the source’s bridge service base URL through bridgeservicefinder.Finder.GetURL(source). A finder miss (unresolved or unhealthy URL) skips the source for this round without advancing its LER cursor — no LERs are lost, they are simply retried on the next poll once the URL becomes available.
  2. It fetches every page of GET /bridge/v1/claim-candidates from that source’s bridge service, requesting destination_network_ids = every enabled claimer’s destination network except the source itself, from_ler = the source’s previous LER cursor (or the value derived below the first time the source is seen), and to_ler = the newly observed LER. A 404 response (the source has not synced the requested LER yet) is treated as “not synced yet, retry later” and also skips the source without advancing its cursor.
  3. Each returned candidate is routed to the claimer owning its destination network. The detector asks that claimer whether the bridge is already claimed (keyed by source_network + deposit_count, not by token origin); already claimed candidates are skipped without being stored. The remaining candidates are enqueued as detected requests carrying source_network, the observed ler, and the L1 block the LER was verified at (verify_block_num). claim-candidates no longer returns a per-bridge Merkle proof, and the detector does not fetch or store one: the leaf-to-LER proof is always fetched fresh from the source’s bridge service at claim time (see below).
  4. Only once every page for a source has been enqueued does the detector advance that source’s LER cursor to the new LER. Sources are processed independently — a finder miss or sync delay on one source never blocks others.

Source rollup networks are auto-discovered: any rollup ID that appears in a verified-batches row is a source, and bridgeservicefinder resolves its URL from the on-chain rollup manager (or from a static override — see Configuration) without any per-source configuration list.

Initial LER cursor. The first time a source network is seen (no LER cursor row yet), the detector derives the initial from_ler: if AutoClaim.L2ToLxBridgeDetector.StartL1Block is 0, from_ler is omitted (the full bridge history is requested). Otherwise it resolves l1infotreesync.GetLatestL1InfoLeafUntilBlock(StartL1Block), then that leaf’s GetLocalExitRoot(source, leaf.RollupExitRoot); a zero LER (the source had not yet been verified at that block) also falls back to omitting from_ler.

Proof preparation for a rollup-origin request (autoclaim/proof.RollupPreparer) mirrors the L1-to-L2 preparer but adds a source-network dimension:

  1. It selects the first L1 info tree leaf, at or after the request’s verify_block_num, whose rollup exit root contains a LER of the source network that covers the bridge (the stored LER or a later one — the rollup exit tree is append-only, so any later LER still covers it).
  2. Destination readiness: for an L2 destination, it calls that destination network’s own aggkit bridge service (GET /bridge/v1/injected-l1-info-leaf, resolved through bridgeservicefinder.Finder.GetURL(destination)) for the first injected GER at or after the chosen leaf, exactly like the L1-to-L2 path — a 404 means not ready yet. For an L1 destination (NetworkID = 0), there is no such gate — the request is ready as soon as l1infotreesync has the leaf, since the GER already exists in the L1 GER manager by construction.
  3. It always fetches a fresh leaf-to-LER Merkle proof from the source network’s bridge service, via GET /bridge/v1/claim-proof?network_id=<source>&leaf_index=<chosen leaf>&deposit_count=<dc> (resolved through the same bridgeservicefinder.Finder). The proof is never fetched or stored at detection time (see above), so this claim-time fetch is the only place it ever exists, and it always reflects the source’s current state at the chosen leaf — there is no separate “staleness” case to special-case. A transient fetch failure (source not synced yet, network error) yields “not ready” and is retried next cycle without burning the claim retry budget.
  4. It builds the LER-to-rollup-exit-root proof locally from l1infotreesync.GetRollupExitTreeMerkleProof(source, leaf.RollupExitRoot) — non-empty for a rollup source, unlike the always-empty L1-origin case.
  5. Both proofs are verified locally (tree.VerifyProof) before the claim proof is used; a verification failure is a hard error, not a retry.

Claim submission for a rollup-origin request uses the same claimAsset/claimMessage ABI packing as the L1-to-L2 path (all v2 bridge contracts — L1 and L2 — share an identical claim ABI), with the claim global index and isClaimed check keyed by source_network (bridgesync.GenerateGlobalIndexForNetworkID(source, depositCount)) instead of always assuming L1 origin.

RollupPreparer and the L1-origin Preparer are combined behind a single proof.SourceAwarePreparer, which every claimer uses: it dispatches each request to the L1-origin preparer when Bridge.SourceNetwork == 0, and to the rollup-origin preparer otherwise. A claimer’s routing therefore depends on each request’s source, not on the claimer’s own destination network.

Request lifecycle

Requests are uniquely keyed by source_network:destination_network:deposit_count (for example 0:1:42 for an L1-to-L2 request, or 1:0:7 for an L2-to-L1 request from rollup 1); this key is also the request ID used by the API. source_network is the network the bridge exit was initiated on — always 0 for L1-to-L2 requests, the source rollup’s network ID for L2-to-Lx requests — and is distinct from origin_network, the bridged token’s origin network, which can be non-zero for either direction (for example an L2-origin token bridged from L1, or a wrapped token bridged from one rollup to another).

stateDiagram-v2
    [*] --> detected: bridge detector enqueues bridge

    detected --> policy_approved: policy approves
    detected --> policy_rejected: policy rejects
    detected --> manual_approval_required: policy defers to operator

    manual_approval_required --> policy_approved: API approve
    manual_approval_required --> policy_rejected: API reject

    policy_approved --> queued
    queued --> sending: sender picks up request
    sending --> queued: proof not ready / retryable error
    sending --> sent: tx handed to EthTxManager
    sending --> confirmed: already claimed on target
    sent --> confirmed: tx Mined / Safe / Finalized
    sent --> queued: tx Failed / Evicted, retry budget left
    sent --> failed: retry budget exhausted

    policy_rejected --> [*]
    confirmed --> [*]
    failed --> [*]

    note right of failed
        Any non-terminal status can
        also move to failed on
        unrecoverable errors.
    end note

Status values: detected, policy-approved, policy-rejected, manual-approval-required, queued, sending, sent, confirmed, failed, dry-run (the diagram uses underscores because hyphens are not valid in mermaid state names). Terminal statuses are policy-rejected, confirmed, failed, and dry-run. Policy results are approved, rejected, and manual. Both directions share the same state machine, policies, and claimer/sender code; only bridge discovery and proof preparation differ.

Step by step:

  1. A bridge detector (L1-to-L2 or L2-to-Lx) discovers a bridge exit whose destination matches an enabled claimer. Bridges the target bridge contract already reports as claimed (isClaimed, keyed by source_network and deposit_count) are skipped without being stored. Each remaining matched bridge exit is enqueued immediately as detected with no GER precondition. Enqueue is idempotent and deduplicated by the request key.
  2. The claimer evaluates the configured policy and moves the request to policy-approved, policy-rejected, or manual-approval-required. For basic-filter, the claimer prepares and stores the exact claim proof before policy evaluation so simulation uses the same calldata as the later send path; if proof data is not ready, the request stays detected and is retried next claimer cycle without burning retry budget.
  3. During proof preparation the claimer gates on destination readiness: for an L2 destination, the destination network’s own bridge service must report (via GET /bridge/v1/injected-l1-info-leaf) an injected GER whose L1 info tree leaf index is at or after the bridge’s inclusion index; for an L1 destination, l1infotreesync having the relevant leaf is sufficient. If not ready, preparation returns “not ready” and the claimer retries.
  4. Once ready, the proof is built — from l1infotreesync and l1bridgesync for an L1-origin request, or from l1infotreesync and a fresh leaf-to-LER proof fetched from the source rollup’s bridge service for a rollup-origin request (fetched fresh at claim time on every attempt, never stored). The l1_info_tree_index is written to the stored request at this point.
  5. Approved requests move to queued and then sending. If proof data is no longer available the request returns to queued.
  6. The sender first checks whether the target bridge already reports the global index as claimed; if so the request is confirmed without submitting a duplicate transaction.
  7. Otherwise the sender packs claimAsset (asset leaves) or claimMessage (message leaves) — identical ABI for L1- and L2-destination claims — submits through EthTxManager, and records each transaction attempt.
  8. Transaction-manager statuses Created and Sent keep the request in flight; Mined, Safe, or Finalized mark it confirmed; Failed and Evicted send it back to queued while retry budget remains (retry_count < MaxRetries), otherwise it becomes failed.

Running Auto Claim

Run Aggkit with the autoclaim component selected; that alone enables Auto Claim (there is no separate enable flag). Set [AutoClaim].DryRun = true to run the full pipeline (discovery, policy evaluation, proof preparation) while skipping claim transaction submission — matching requests end in the terminal dry-run status. Startup also requires:

  • l1bridgesync and l1infotreesync, always: the L1-to-L2 detector reads L1 bridge exits, and the claimer prepares L1 info tree proofs in-process for every request regardless of direction.
  • [AutoClaim.BridgeServiceFinder].RollupManagerAddr, whenever [AutoClaim.L2ToLxBridgeDetector].Enabled = true or any enabled claimer has an L2 destination (NetworkID != 0), in either direction: required for the finder to resolve bridge service URLs — as a source (the L2-to-Lx detector’s discovery and the rollup-origin proof preparer’s claim-time leaf-proof fetch) and/or as a destination (every L2-destination claimer’s GER-injection readiness gate). This is a breaking operational requirement: even a pure L1-to-L2 deployment now needs a configured, reachable [AutoClaim.BridgeServiceFinder] and a reachable destination bridge service whenever it targets an L2 destination, regardless of whether [AutoClaim.L2ToLxBridgeDetector] is enabled.
  • At least one claimer with NetworkID = 0 (an L1 destination) requires [AutoClaim.L2ToLxBridgeDetector].Enabled = true, since only that detector can discover requests destined for L1.

Auto Claim no longer runs a per-claimer GER syncer: there is no per-claimer l2gersync instance, no isolated per-claimer SQLite database, and no per-claimer L2 reorg detector or dedicated L2 RPC client for GER tracking. An L1-destination claimer (NetworkID = 0) has no GER-injection gate at all — it is ready as soon as l1infotreesync has the relevant leaf. Every L2-destination claimer instead gates readiness with an HTTP call to that network’s own aggkit bridge service. The shared [L2GERSync] and [ReorgDetectorL2] sections are unrelated to Auto Claim now — they only configure the node-global l2gersync instance that backs Aggoracle and the bridge service’s own /bridge/v1/injected-l1-info-leaf handler, not anything Auto Claim consumes directly.

Public request inspection (will / will not claim) is served by the bridge service when the autoclaim component runs (see API); the standalone Auto Claim admin API only needs to be enabled for the manual approve / reject endpoints used by the api-approve policy, so operators can keep admin controls off the public surface.

Configuration

Minimal configuration enabling both directions:

[AutoClaim]
# DryRun = true   # optional: prepare claims but do not submit them (requests end as "dry-run")
StoragePath = "/var/lib/aggkit/autoclaim.sqlite"

# Optional admin API for manual approve / reject (api-approve policy). Public request inspection is
# served by the bridge service instead — see the API section.
[AutoClaim.API]
Enabled = true
Host = "0.0.0.0"
Port = 5579

[AutoClaim.L1ToL2BridgeDetector]
Enabled = true
StartBlock = 0
PollInterval = "3s"
EtrogL1UpgradeBlock = 0

[AutoClaim.L2ToLxBridgeDetector]
Enabled = true
StartL1Block = 0
PollInterval = "3s"

[AutoClaim.BridgeServiceFinder]
RollupManagerAddr = "0x0000000000000000000000000000000000000000"
PollInterval = "30s"
# BlockFinality, BlockChunkSize, HealthCheckPath, HealthCheckTimeout, RequireAllHealthyOnStart default to
# FinalizedBlock, 10000, "/health", "5s", and false respectively when left unset (see the table below).

[AutoClaim.BridgeServiceFinder.BridgeURLs]
# Static override map from source network ID to bridge service base URL. Required to reach network 0 (L1),
# which is never enumerated on-chain:
# 0 = "http://static-override-l1:5577"

[[AutoClaim.Claimers]]
Enabled = true
ID = "l2-primary"
NetworkType = "EVM"
NetworkID = 1
URLRPC = "http://l2-rpc:8545"
BridgeAddr = "0x0000000000000000000000000000000000000000"
PolicyName = "api-approve"
GasOffset = 100000
WaitPeriod = "1s"
RetryAfter = "1s"
MaxRetries = 30

[AutoClaim.Claimers.Policy]
AllowMessageClaims = false
AllowedOrigins = [0]
AllowedTokens = []
ManualFallback = false
MaxGas = 500000

[AutoClaim.Claimers.EthTxManager]
FrequencyToMonitorTxs = "1s"
WaitTxToBeMined = "2s"
WaitReceiptMaxTime = "250ms"
WaitReceiptCheckInterval = "1s"
PrivateKeys = [
    { Method = "local", Path = "/etc/aggkit/autoclaim.keystore", Password = "change-me" },
]
ForcedGas = 0
GasPriceMarginFactor = 1
MaxGasPriceLimit = 0
StoragePath = "/var/lib/aggkit/ethtxmanager-autoclaim-l2-primary.sqlite"
ReadPendingL1Txs = false
SafeStatusL1NumberOfBlocks = 0
FinalizedStatusL1NumberOfBlocks = 0
EstimateGasMaxRetries = 1

[AutoClaim.Claimers.EthTxManager.Etherman]
URL = "http://l2-rpc:8545"
MultiGasProvider = false
L1ChainID = 2151908
HTTPHeaders = {}

Replace BridgeAddr, NetworkID, URLRPC, L1ChainID, RollupManagerAddr, storage paths, and signer settings with values for the target networks. Use the existing EthTxManager configuration style for private keys; do not put secrets in logs or checked-in configuration. An L1-destination claimer uses the same [[AutoClaim.Claimers]] shape with NetworkID = 0, BridgeAddr set to the L1 bridge contract, and URLRPC pointing at an L1 RPC endpoint; it has no GER-injection gate at all, since the GER already exists on L1 by construction.

Top-level keys

KeyDefaultRequired when enabledDescription
AutoClaim.DryRunfalseNoRuns the full pipeline but skips submitting claim transactions; matching requests end in the terminal dry-run status. Auto Claim is enabled by selecting the autoclaim component (there is no separate enable flag).
AutoClaim.StoragePath{{PathRWData}}/autoclaim.sqliteYesSQLite database for requests, cursors, decisions, proofs, and transaction attempts.
AutoClaim.API.EnabledfalseNoEnables the admin routes (approve/reject) on the shared admin API server ([AdminREST]).
AutoClaim.L1ToL2BridgeDetector.EnabledtrueNoEnables L1 bridge discovery for configured L2 claimers.
AutoClaim.L1ToL2BridgeDetector.StartBlock0NoFirst L1 block used when a destination-network cursor does not exist. New claimers backfill from this block.
AutoClaim.L1ToL2BridgeDetector.PollInterval3sYesHow often the bridge detector polls l1bridgesync. Must be greater than zero.
AutoClaim.L1ToL2BridgeDetector.EtrogL1UpgradeBlock0NoL1 block where Etrog global-index encoding becomes active for legacy zkEVM destination network 1; 0 treats bridges as post-Etrog.
AutoClaim.L2ToLxBridgeDetector.EnabledfalseNoEnables rollup-origin (L2-to-L1, L2-to-L2) bridge discovery. Requires AutoClaim.BridgeServiceFinder.RollupManagerAddr to be set, and is itself required by any claimer with NetworkID = 0.
AutoClaim.L2ToLxBridgeDetector.StartL1Block0NoL1 block used to derive a newly discovered source network’s initial LER cursor (via the GER at that block); 0 means full history (from_ler omitted on first fetch).
AutoClaim.L2ToLxBridgeDetector.PollInterval3sYes, when the detector is enabledHow often the detector polls l1infotreesync for new verified-batches rows. Must be greater than zero.
AutoClaim.BridgeServiceFinder.RollupManagerAddr{{L1NetworkConfig.RollupManagerAddr}}Yes, when L2ToLxBridgeDetector.Enabled = true or any enabled claimer has an L2 destination (NetworkID != 0)Address of the rollup manager / agglayer manager contract on L1 used to enumerate attached rollups and resolve their bridge service URLs — both as claim-candidate/claim-proof sources and as GER-injection-gate destinations — and their bridge contracts.
AutoClaim.BridgeServiceFinder.BridgeURLs{}NoStatic override map from source network ID to bridge service base URL (e.g. 1 = "http://bridge-svc-1:5577"). Highest-priority source; never overridden by on-chain events. The only way to resolve network 0 (L1), which is not enumerated on-chain.
AutoClaim.BridgeServiceFinder.PollInterval30sNoPeriod between finder event-scan iterations that keep cached URLs fresh from on-chain events.
AutoClaim.BridgeServiceFinder.BlockFinalityFinalizedBlockNoFinality level bounding the upper block of each event scan. Empty inherits the default.
AutoClaim.BridgeServiceFinder.BlockChunkSize10000NoMaximum number of blocks queried per FilterLogs request while scanning. 0 inherits the default.
AutoClaim.BridgeServiceFinder.HealthCheckPath/healthNoHTTP path probed to assert a resolved bridge service is alive. Empty inherits the default.
AutoClaim.BridgeServiceFinder.HealthCheckTimeout5sNoTimeout applied to each health-check HTTP request. 0 inherits the default.
AutoClaim.BridgeServiceFinder.RequireAllHealthyOnStartfalseNoWhen true, finder startup fails if any resolved bridge service is unreachable; when false, unreachable services are cached as unhealthy and may heal from a later on-chain update.
AutoClaim.BridgeServiceFinder.IgnoreNetworkIDs[]NoNetwork IDs to exclude entirely from on-chain resolution (e.g. [5, 12]): no RollupIDToRollupData call, no contract reads, no health probe during enumeration, and rollup-manager lifecycle events announcing them are ignored by live discovery too. Intended for known-dead networks whose unreachable on-chain reads/health checks would otherwise slow down startup and event processing. A network listed here is still served if also present in BridgeURLs.

The BlockFinality, BlockChunkSize, HealthCheckPath, HealthCheckTimeout, and RequireAllHealthyOnStart values above are the finder’s built-in defaults applied whenever the corresponding field is left unset (zero value); the shipped [AutoClaim.BridgeServiceFinder] default config template only sets RollupManagerAddr and PollInterval explicitly.

Claimer keys

Each enabled [[AutoClaim.Claimers]] entry owns one destination network. NetworkID = 0 (L1) is a valid destination, reachable only through the L2-to-Lx detector.

KeyRequiredDescription
EnabledYesDisabled claimers are ignored.
IDYesUnique operator-readable claimer ID. Duplicate enabled IDs are rejected.
NetworkTypeYesMust be EVM.
NetworkIDYesDestination network ID. 0 means L1. Duplicate enabled network IDs are rejected.
URLRPCYesDestination-chain JSON-RPC URL used for claim state checks and transaction submission.
BridgeAddrYesDestination bridge contract address.
PolicyNameYesOne of allow-all, api-approve, no-message, or basic-filter.
PolicyPolicy-dependentStatic policy configuration.
GasOffsetNoExtra gas passed to EthTxManager.Add for claim transactions.
WaitPeriodYesClaimer poll period and transaction-result polling interval. Must be greater than zero.
RetryAfterNoRetry delay after a failed claim attempt. Defaults to WaitPeriod when omitted or zero.
MaxRetriesNoMaximum claim submission retries before the request is marked failed. 0 means failures are immediately final.
EthTxManagerYesIndependent transaction-manager configuration and storage path for this claimer.

Policies

PolicyBehavior
allow-allApproves every eligible request automatically, regardless of direction.
api-approveStores the request as manual-approval-required; an operator must approve or reject through the API.
no-messageRejects message bridge leaves and approves asset bridge leaves.
basic-filterSimulates the claim with eth_estimateGas on the destination chain for asset claims and, when AllowMessageClaims = true, message claims. It rejects claims whose simulated gas exceeds MaxGas (MaxGas = 0 disables the gas cap), rejects disallowed origins or asset tokens, and returns a blocking policy error when proof preparation, calldata packing, or simulation fails.

Policy.AllowMessageClaims, Policy.AllowedOrigins, Policy.AllowedTokens, Policy.ManualFallback, and Policy.MaxGas are policy configuration inputs. An empty AllowedOrigins or AllowedTokens list allows all origins or tokens respectively; token matching is case-insensitive. AllowedOrigins matches the bridged token’s origin_network, not the bridge exit’s source_network. Notes on basic-filter:

  • It does not honor ManualFallback; operational errors remain blocked with last_error instead of becoming manual-review requests, and claimer recovery stops until the process is restarted after the underlying issue is fixed.
  • It uses only normal JSON-RPC eth_estimateGas against latest target state. It does not require archive nodes, debug_* or trace_* APIs, historical state replay, or internal call traces.
  • It does not inspect direct or indirect nested bridge calls. Approved simulation metadata includes nested_bridge_detection = "skipped" so operators do not mistake the result for real nested-call inspection.

API

Auto Claim endpoints are split by audience so operators can expose request status publicly without exposing admin controls:

  • Public, read-only request inspection is served on the public API ([PublicREST] port, default 5577) under the /autoclaim/v1 prefix. These routes are registered only when the autoclaim component is running.
  • Admin manual decisions are served on the admin API ([AdminREST] port, default 5579) under the /autoclaim/v1 prefix, gated by [AutoClaim.API].Enabled, so it can be firewalled off.
Method and pathServerPurpose
GET /autoclaim/v1/bridgesPublic ([PublicREST])List tracked requests.
GET /autoclaim/v1/bridges/{id}Public ([PublicREST])Inspect one request by Auto Claim request ID (source_network:destination_network:deposit_count).
POST /autoclaim/v1/bridges/{id}/approveAdmin ([AdminREST])Approve a request currently in manual-approval-required.
POST /autoclaim/v1/bridges/{id}/rejectAdmin ([AdminREST])Reject a request currently in manual-approval-required.

List query parameters: source_network, origin_network, destination_network, status, policy_status (alias: policy_result), bridge_tx_hash, claim_tx_hash, from_block, to_block, page_number, and page_size (maximum 1000).

Manual approval and rejection bodies are optional JSON objects:

{
  "reason": "approved by operator",
  "metadata": {
    "ticket": "OPS-123"
  },
  "decider": "operator",
  "decider_id": "alice"
}

The API returns request fields including id, status, source_network, bridge identifiers (including origin_network), global_index, bridge_tx_hash, claim_tx_hash, tx_manager_id, l1_info_tree_index, ler (the source network’s local exit root observed at detection time, used to select the covering L1 info tree leaf; the leaf-to-LER Merkle proof itself is always fetched fresh from the source’s bridge service at claim time and is never stored, so it is not part of the API response; ler is omitted/empty for L1-origin requests), retry counters, policy decision metadata, manual decision metadata, timestamps, and last_error.

Example workflow for api-approve:

# Inspect via the public API ([PublicREST] port, e.g. 5577).
curl "http://localhost:5577/autoclaim/v1/bridges?status=manual-approval-required"
curl "http://localhost:5577/autoclaim/v1/bridges/0:1:42"
# Approve via the admin API ([AdminREST] port, e.g. 5579).
curl -X POST "http://localhost:5579/autoclaim/v1/bridges/0:1:42/approve" \
  -H "Content-Type: application/json" \
  -d '{"reason":"approved after bridge review","decider":"operator","decider_id":"alice"}'

Approving or rejecting a request in any status other than manual-approval-required returns 409 Conflict.

The L2-to-Lx bridge detector discovers rollup-origin bridges by calling the source rollup’s own bridge service (a remote node, not this one) — see the Bridge service claim-candidates endpoint for that API’s contract.

API documentation

The swagger definition is generated with make generate-swagger-docs, which writes autoclaim/api/docs/autoclaim_swagger.json and copies it to docs/assets/swagger/autoclaim/swagger.json for the rendered documentation. Rerun it after changing API annotations in autoclaim/api.

Storage

Auto Claim owns one SQLite database (AutoClaim.StoragePath) with four tables, created by migrations autoclaim/storage/migrations/autoclaim0001.sql and autoclaim0002.sql:

TableKeyPurpose
autoclaim_requestrequest_key; UNIQUE(source_network, destination_network, deposit_count)One row per tracked request: source_network, status, policy result, global index, L1 info tree index, ler and verify_block_num (rollup-origin requests only), retry counters, last_error, and JSON blobs for the bridge, proof, policy decision, and manual decision. The leaf-to-LER Merkle proof is never stored here — it is fetched fresh from the source’s bridge service every time a claim is prepared (see the L2-to-Lx proof preparation steps above).
autoclaim_transaction_attempt(request_key, attempt_number)One row per claim transaction attempt with transaction-manager ID, claim transaction hash, status, and timestamps.
autoclaim_bridge_cursorcursor_nameDurable per-detector block-window cursor (block window and position); one row for the L1-to-L2 detector and one (l2-to-lx) for the L2-to-Lx detector.
autoclaim_ler_cursorsource_networkDurable per-source-network cursor tracking the last local exit root (LER) and L1 verify block the L2-to-Lx detector has fully processed for that source.

autoclaim0002 also re-keyed every pre-existing autoclaim0001 row’s request_key from origin_network:destination_network:deposit_count to source_network:destination_network:deposit_count (equivalent for those rows, since every one is L1-origin, i.e. source_network = 0). autoclaim0002 (unshipped when this change landed) no longer defines a leaf_proof_json column: it briefly held the detection-time leaf proof, which is now always fetched fresh at claim time instead of being persisted, so the column was dropped from the migration in place rather than removed by a follow-up migration.

Each claimer’s EthTxManager keeps its own independent database at Claimers.EthTxManager.StoragePath. There is no per-claimer GER-syncer database: readiness for an L2-destination claimer is checked with an HTTP call to that network’s own bridge service, not a locally-synced database, so no per-claimer SQLite database or L2 reorg detector is created for GER tracking.

Operational notes

  • Disable Auto Claim entirely by not selecting the autoclaim component (there is no [AutoClaim].Enabled flag).
  • Disable the API independently with [AutoClaim.API].Enabled = false; automatic claiming continues for non-manual policies.
  • Disable either direction independently: [AutoClaim.L1ToL2BridgeDetector].Enabled = false or [AutoClaim.L2ToLxBridgeDetector].Enabled = false. Both detectors are always constructed; a disabled one is a no-op that never polls.
  • Use separate StoragePath values for Auto Claim storage and each claimer’s EthTxManager.StoragePath.
  • Both bridge detectors advance their block-window cursor after each successfully processed poll window, even when nothing was enqueued. Bridges already claimed on the target bridge are skipped before enqueue; duplicate bridge exits are deduplicated by the request key and enqueue is idempotent. The L2-to-Lx detector additionally advances a per-source-network LER cursor, but only after every claim-candidate page for that source’s new LER has been enqueued — a finder miss or an unsynced source leaves that source’s LER cursor untouched so nothing is missed.
  • GER readiness is checked per-claimer during proof preparation, not by either bridge detector, and the same mechanism applies to both directions. For an L2-destination claimer, readiness is gated by an HTTP call to that destination network’s own aggkit bridge service (GET /bridge/v1/injected-l1-info-leaf, resolved through bridgeservicefinder.Finder) rather than a locally-synced GER syncer; a 404 means no injected GER covers the bridge yet, and the proof preparer returns “not ready” so the request is retried next claimer cycle without consuming retry budget. An L1-destination claimer has no such gate: it is ready as soon as l1infotreesync has the relevant leaf.
  • Auto Claim logs startup, API startup, bridge detector polling errors, claimer recovery errors, and per-request errors through the standard Aggkit logger. Request-level error details are also stored in last_error and exposed by the API. The component does not export Prometheus metrics.
  • Failed or evicted transaction-manager results are retried while retry budget remains. Exhausted requests become failed and require operator investigation.
  • Use api-approve when an operator must explicitly inspect each request before claim submission. Expose the API only on trusted networks or behind access controls; it can approve or reject pending manual requests.

Testing

Unit tests live next to each package; run them with the standard targets:

make build
make lint
make test-unit

The focused end-to-end tests run against the two-chain anvil-2chains environment by default (see End-to-end tests):

go test -v -run 'TestAutoClaimL1ToL2(AllowAll|APIApprove|BasicFilter)|TestAutoClaimL2ToL1AllowAll' -timeout 30m ./test/e2e

TestAutoClaimL1ToL2AllowAll exercises the fully automatic L1-to-L2 flow with the allow-all policy; TestAutoClaimL1ToL2APIApprove exercises the manual flow, approving the request through the API; TestAutoClaimL1ToL2BasicFilter exercises the basic-filter policy with target-chain gas simulation; TestAutoClaimL2ToL1AllowAll exercises the fully automatic L2-to-L1 flow (L2-to-Lx detector, RollupPreparer, an NetworkID = 0 claimer).

L2-to-L2 uses the same default environment:

go test -v -run 'TestAutoClaimL2ToL2AllowAll' -timeout 30m ./test/e2e

TestAutoClaimL2ToL2AllowAll exercises the fully automatic L2-to-L2 flow end to end: the L2-to-Lx detector and bridgeservicefinder resolving both the source and destination networks, the destination-bridge-service GER-injection gate, and the claim-time leaf-proof fetch. Mocks for the interfaces in autoclaim/types and the other touched packages are generated with make generate-mocks.

Bridge service component

The bridge service abstracts interaction with the unified LxLy bridge. It represents decentralized indexer, that sequences the bridge data. Each bridge service sequences L1 network and a dedicated L2 one (which is uniquely defined by the network id parameter). Therefore, each agglayer connected chain runs its own bridge service. It is implemented as a JSON RPC service.

Bridge flow

Bridge flow L2 -> L2

The diagram below describes the basic L2 -> L2 bridge workflow.

sequenceDiagram
    participant User
    participant L2 (A)
    participant Aggkit (A)
    participant AggLayer
    participant L2 (B)
    participant Aggkit (B)
    participant L1

    User->>L2 (A): Bridge assets to L2 (B)
    L2 (A)->>L2 (A): Index bridge tx & updates the local exit tree
    Aggkit (A)->>AggLayer: Build & send certificate (Aggsender)
    AggLayer->>L1: Settle batch
    L1->>L1: update GER
    Note right of L1: rollupmanager updates the GER & RER (PolygonZKEVMGlobalExitRootV2.sol)
    AggLayer-->>L2 (A): L1 tx hash

    Aggkit (A)->>L1: Aggoracle fetches last finalized GER from L1
    Aggkit (A)->>L2 (A): Aggoracle injects the GER on L2 (A) GlobalExitRootManagerL2SovereignChain.sol
    Aggkit (B)->>L1: Aggoracle fetches last finalized GER from L1
    Aggkit (B)->>L2 (B): Aggoracle injects the GER on L2 (B) GlobalExitRootManagerL2SovereignChain.sol

    User->>Aggkit (A): Call bridge_l1InfoTreeIndexForBridge endpoint on the origin network(A)
    Aggkit (A)-->>User: Returns L1InfoTree index X for which the bridge was included
    loop Poll destination network, until `L1InfoTreeLeaf` is retrieved  
      User->>Aggkit (B): Poll bridge_injectedInfoAfterIndex on destination network L2(B) until a non-null response.  
      Aggkit (B)-->>User: Returns the first L1InfoTreeLeaf(GER=Y) for the GER injected on L2(B) at or after L1InfoTree index X
    end 
    User->>Aggkit (A): Call bridge_getProof on origin network(A) to generate merkle proof for bridge using l1InfoTreeIndex of GER Y and networkID(A)
    
    Aggkit (A)-->>User: Return claim proof
    User->>L2 (B): Claim (proof)
    L2 (B)->>L2 (B): Send claim tx<br/>(bridge is settled on the L2 (B))
    L2 (B)-->>User: Tx hash

Bridge flow L1 -> L2

The diagram below describes the basic L1 -> L2 bridge workflow.

sequenceDiagram
    participant User
    participant L1
    participant Aggkit
    participant L2

    User->>L1: Bridge assets to L2
    L1->>L1: Updates the mainnet exit tree
    L1->>L1: Update GER
    Note right of L1: bridgeContract updates the GER<br/>only if `forceUpdateGlobalExitRoot` is true in the bridge transaction.
    Aggkit->>L1: Aggoracle fetches last finalized GER
    Aggkit->>L2: Aggoracle injects the GER on L2 GlobalExitRootManagerL2SovereignChain.sol

    User->>Aggkit: Call bridge_l1InfoTreeIndexForBridge endpoint on the origin network
    Aggkit-->>User: Returns L1InfoTree index X for which the bridge was included
    loop Poll destination network, until `L1InfoTreeLeaf` is retrieved  
      User->>Aggkit: Poll bridge_injectedInfoAfterIndex on destination network (L2) until a non-null response.  
      Aggkit-->>User: Returns the first L1InfoTreeLeaf(GER=Y) for the GER injected on L2 at or after L1InfoTree index X
    end 

    User->>Aggkit: Call bridge_getProof on origin network to generate merkle proof for bridge using l1InfoTreeIndex of GER Y and networkID=0 (L1)
    Aggkit-->>User: Return claim proof
    User->>L2: Claim (proof)
    L2->>L2: Send claimAsset/claimBridge tx on the destination network<br/>(bridge is settled on the L2)
    L2-->>User: Tx hash

Notes:

  1. In CDK-Erigon, the Global Exit Root (GER) on the L2 smart contract (PolygonZKEVMGlobalExitRootL2.sol) is automatically updated by the sequencer. In a sovereign chain, the GER is injected on L2 (GlobalExitRootManagerL2SovereignChain.sol) by the Aggoracle component.

  2. A non-null response from bridge_injectedInfoAfterIndex indicates that the bridge is ready to be claimed on the destination network.

  3. If forceUpdateGlobalExitRoot is set to false in a bridge transaction, the GER will not be updated with that transaction. The user must wait until the GER is updated by another bridge transaction before claiming. This is done to save gas costs while bridging.

  4. Over the REST API, bridge_injectedInfoAfterIndex is served by GET /bridge/v1/injected-l1-info-leaf, which now responds 404 Not Found (not 500) when no injected global exit root covers the requested L1 info tree index yet — callers should treat 404 as “not ready yet, retry later” rather than a hard failure. The Go client (bridgeservice/client.Client.GetInjectedL1InfoLeaf) surfaces this as the client.ErrNotFound sentinel. This endpoint also backs the Auto Claim destination-readiness gate for L2-destination claimers — see Auto Claim Service for how it is used to decide when a bridge is ready to claim.

  5. The same 404-for-not-ready contract from note 4 now also applies to GET /bridge/v1/l1-info-tree-index (bridge_l1InfoTreeIndexForBridge) and GET /bridge/v1/claim-proof: both previously returned 500 whenever the L1 info tree syncer or a bridge syncer had simply not caught up yet to the requested deposit/leaf, and now return 404 for that condition, reserving 500 for genuine faults. The Go client surfaces this as client.ErrNotFound on Client.GetL1InfoTreeIndex and Client.GetClaimProof. In addition, the 404 semantics described in note 4 for /injected-l1-info-leaf now cover its L1 path (network_id=0) as well as its L2 path — previously the L1 path fell through to 500 when l1infotreesync had not yet indexed the requested leaf; it now answers 404 there too.

  6. /l1-info-tree-index, /claim-proof, and /injected-l1-info-leaf can also respond 503 Service Unavailable when a syncer they read from is halted or in an inconsistent state (e.g. resolving a reorg). 503 is a second retry-later code, but it does not mean the same thing as 404: 404 means the syncer is healthy and simply hasn’t indexed the requested data yet, while 503 means a syncer is in an operational fault state. Retrying is appropriate for both, but operators and client authors should not conflate them — persistent 503s warrant investigating the syncer, whereas persistent 404s only indicate lag.

  7. The fallback inside getFirstL1InfoTreeIndexForL1Bridge, which backs bridge_l1InfoTreeIndexForBridge in the flow diagrams above, was corrected. When the primary GetRootByLER lookup misses because the L1 bridge syncer has not yet caught up to the tip of the L1 info tree, the fallback now clamps to the most recent L1 info tree leaf at or before the last block the L1 bridge syncer has indexed. It previously reused a position from the L1 bridge exit tree (a deposit count) as if it were an L1 info tree index — two different counters in two different trees — which could surface as a 500 sql: no rows in result set error for a deposit that was already settled. The flow itself is unchanged; only the correctness of this internal fallback lookup was fixed.

Bridge flow L2 -> L1

The diagram below describes the basic L2 -> L1 bridge workflow.

sequenceDiagram
    participant User
    participant L2
    participant Aggkit
    participant AggLayer
    participant L1

    User->>L2: Bridge assets to L1
    L2->>L2: Index bridge tx & updates the local exit tree
    Aggkit->>AggLayer: Build & send certificate (Aggsender)
    AggLayer->>L1: Settle batch
    L1->>L1: update GER
    Note right of L1: rollupmanager updates the GER & RER (PolygonZKEVMGlobalExitRootV2.sol)
    AggLayer-->>L2: Return L1 tx hash
    Aggkit->>L1: Fetch last finalized GER (Aggoracle)
    Aggkit->>L2: Aggoracle injects GER on L2 (GlobalExitRootManagerL2SovereignChain.sol)

    User->>Aggkit: Query bridge_l1InfoTreeIndexForBridge endpoint on the origin network(L2)
    Aggkit-->>User: Returns L1InfoTree index X for which the bridge was included 
    loop Poll destination network, until `L1InfoTreeLeaf` is retrieved
      User->>Aggkit: Poll bridge_injectedInfoAfterIndex on destination network (L1) until a non-null response.
      Aggkit-->>User: Returns the first L1InfoTreeLeaf(GER=Y) for the GER injected at or after L1InfoTree index X
    end

    Aggkit-->>User: Return claim proof
    User->>L1: Claim (proof)
    L1->>L1: Send claimAsset/claimBridge tx on the destination network<br/>(bridge is settled on the L1)
    L1-->>User: Tx hash

Indexers

The bridge service relies on specific data located on different chains (such as bridge, claim, and token mapping events, as well as the L1 info tree). These data are retrieved using indexers. Indexers consists of three components: driver, downloader and processor.

Driver

Driver is in charge of retrieving the blocks and also monitors for the reorgs (using the reorg detector component). The idea is to have driver implementation per chain type (so far we have the EVM driver, but in future, each non-evm chain would require a new driver implementation).

Downloader

Downloader is in charge of parsing the blocks and logs that are retrieved by the driver. Downloader (indirectly, via the driver) passes the parsed data to the processor.

Processor

Processor represents the persistance layer, which writes retrieved indexer data in a format suitable for serving it via API. It utilizes SQL lite database.

The diagram below depicts the interaction between components of each indexer.

sequenceDiagram
    participant Driver
    participant Downloader
    participant Processor

    Driver->>Driver: Fetch blocks in a loop
    Driver->>Driver: Monitor reorgs & finalization
    Driver-->>Downloader: Send finalized blocks & logs
    Downloader->>Downloader: Parse blocks & event logs
    Downloader-->>Processor: Send parsed data
    Processor->>Processor: Persist data in SQLite DB

Syncers

In this paragraph, we will list and briefly describe syncers that are of interest for the bridge service.

L1 Info Tree Sync

It interacts with L1 execution layer (via RPC) in order to:

  • Sync the L1 info tree,
  • Generate merkle proofs,
  • Build the relation bridge <-> L1InfoTree index for bridges originated on L1
  • Sync the rollup exit tree (namely a tree consisted of all local exit trees, that tracks exits per rollup network), persist, generate proofs

Bridge Sync

It interacts with the L2 or L1 execution layer (via RPC) in order to:

  • Sync bridges, claims and token mappings. Needs to be modular as it’s execution client specific.
  • Build the local exit tree
  • Generate merkle proofs

Claim candidates endpoint

GET /bridge/v1/claim-candidates lists bridges originated on the network that this bridge service instance itself syncs (its own bridgesync) that are candidates for claiming against a requested local exit root. It is intended for a remote consumer (e.g. a node running Auto Claim for a different network) that needs to discover claimable bridges from a source network it does not sync locally.

The response does not include a Merkle proof for each bridge. A consumer that needs the leaf-to-local-exit-root proof for a specific bridge fetches it separately, at claim time, from GET /bridge/v1/claim-proof (see the Auto Claim RollupPreparer, which always fetches this proof fresh when preparing a claim rather than caching one derived at discovery time).

There is no network_id selector: the endpoint always answers for the bridge service’s own source network. If that instance has no L2/source bridgesync configured, it returns 503.

ParamRequiredMeaning
destination_network_idsyesDestination network IDs to filter by, sent as a repeated query parameter (?destination_network_ids=1&destination_network_ids=2), not comma-separated. Maximum 5.
to_leryesLocal exit root (0x-prefixed 32-byte hex hash) the proofs are built against. Must resolve to a root this bridge service has synced.
from_lernoExclusive lower-bound local exit root (hex hash). When omitted, the full history is considered.
page_numbernoPage number (default 1).
page_sizenoPage size (default 100).

Bridges are matched by deposit_count ∈ (index(from_ler), index(to_ler)] and destination_network ∈ destination_network_ids. If to_ler (or from_ler, when provided) has not been synced yet, the endpoint responds 404 with a body of the form {"error": "to_ler 0x... not found (not synced yet)"} (same pattern for from_ler) — callers should treat this as “not ready yet, retry later” rather than a hard failure.

Response shape:

{
  "claim_candidates": [
    {
      "bridge": { "...": "a BridgeResponse, see /bridges" }
    }
  ],
  "count": 1
}

The bridge field is the only content per candidate — there is no per-bridge proof or local exit root field. to_ler (and from_ler, when provided) still define the deposit-count range the candidates are drawn from; they are request parameters, not part of each candidate.

Example request:

GET /bridge/v1/claim-candidates?destination_network_ids=0&destination_network_ids=2&to_ler=0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757

The Go client exposes this as client.GetClaimCandidates(ctx, client.GetClaimCandidatesParams{...}) (bridgeservice/client/client.go), which returns client.ErrNotFound when to_ler/from_ler is not synced yet.

Sync status

GET /bridge/v1/sync-status reports the synchronization status of the L1 and L2 bridge indexers, plus (when applicable) the l2gersync (injected-GER) syncer. Response shape (types.SyncStatus):

{
  "l1_info": {
    "contract_deposit_count": 100,
    "synchronized_deposit_count": 100,
    "is_synced": true,
    "is_active": true,
    "last_processed_block": 1234,
    "network_block": 2555
  },
  "l2_info": {
    "contract_deposit_count": 200,
    "synchronized_deposit_count": 200,
    "is_synced": true,
    "is_active": true,
    "last_processed_block": 5678,
    "network_block": 5680
  },
  "l2_ger_info": {
    "is_active": true,
    "last_processed_block": 12345678
  }
}

l1_info / l2_info (NetworkSyncInfo) compare on-chain bridge deposit counts against the local bridgesync database counts, per network.

l2_ger_info (L2GERSyncInfo) reports the l2gersync (injected-GER) syncer’s own progress, independent of l2_info:

  • is_activetrue when this bridgeservice instance has an l2gersync syncer wired in. It is always false on an L1 bridgeservice (l2gersync only runs against an L2 sovereign chain), and false when running against an L2 that isn’t configured with l2gersync.
  • last_processed_block — the last L2 block l2gersync has processed. Compare this against the L2 chain head (l2_info.network_block) to tell whether l2gersync is keeping up. A value that stays pinned below a known block while the chain head keeps advancing indicates l2gersync is stuck — most commonly because an invalid GER was injected and not yet removed on-chain; see the remove-GER runbook for the blocking/automatic recovery behavior and how to use this field to confirm recovery.

Public configuration

GET /bridge/v1/config returns a sanitized view of this instance’s configuration, useful e.g. to configure a proxy in front of the bridge service without duplicating its contract addresses. It never exposes RPC URLs, DB paths, private keys, or any other internal/sensitive configuration value. Response shape (types.PublicConfigResponse):

{
  "network_id": 10,
  "components": {
    "L1InfoTreeSync": {
      "block_finality": "FinalizedBlock",
      "initial_block": 0,
      "sync_block_chunk_size": 100
    },
    "BridgeL1Sync": {
      "block_finality": "LatestBlock",
      "initial_block": 0,
      "sync_block_chunk_size": 100
    },
    "BridgeL2Sync": {
      "block_finality": "LatestBlock",
      "initial_block": 0,
      "sync_block_chunk_size": 100
    },
    "L2GERSync": {
      "block_finality": "LatestBlock",
      "initial_block": 0,
      "sync_block_chunk_size": 100,
      "sync_mode": "SovereignChain"
    }
  },
  "contracts": {
    "L1": {
      "GlobalExitRootAddr": "0x0000000000000000000000000000000000000000",
      "RollupManagerAddr": "0x0000000000000000000000000000000000000000",
      "BridgeAddr": "0x0000000000000000000000000000000000000000"
    },
    "L2": {
      "GlobalExitRootAddr": "0x0000000000000000000000000000000000000000",
      "BridgeAddr": "0x0000000000000000000000000000000000000000"
    }
  },
  "internal_config_checksum": "1f6d1a8b3c2e9f04",
  "public_config_checksum": "af63bd4c8601b7df"
}

network_id is the rollup/network ID this bridge service instance’s bridge/claim syncers are listening on (the destination network for L2, 0 for L1). components mirrors the public subset of each syncer’s own configuration (SyncComponentConfig) for every syncer actually running on this instance — a component is omitted entirely (not just left empty) when it isn’t running, so a client can’t be misled into configuring itself against a component that isn’t backing this instance. contracts deduplicates the smart contract addresses used by this instance instead of repeating them once per component (as they appear in the raw aggkit configuration).

components.L2GERSync.sync_mode is not configuration — it’s the GER manager mode (Legacy or SovereignChain) l2gersync auto-detected by probing the L2 GER contract at startup (see l2_ger_syncer.go) — but useful operational information, so it’s reported alongside that component’s config.

internal_config_checksum and public_config_checksum are hex-encoded FNV-1a checksums (not cryptographically secure — they’re not meant to be, just fast fingerprints for detecting incidental change):

  • internal_config_checksum covers this instance’s entire fully-resolved configuration (public and private alike), so it changes on any config change, even one that isn’t exposed on this endpoint.
  • public_config_checksum covers only what’s actually published in this response (network_id, components, contracts), so a caller (e.g. a proxy) can detect when the public-facing configuration it depends on has changed, without reacting to unrelated internal-only config changes that also move internal_config_checksum.

Bridging custom ERC20 token

When a non-native ERC20 token, not yet mapped on a destination network, is bridged, its representation is deployed on the destination network using the CREATE2 opcode. The mapping process emits the NewWrappedToken event on the destination network.

Mapped token details are available via the bridge_getTokenMappings endpoint.

The following diagram depicts the basic flow of bridging the custom ERC20 token.

sequenceDiagram
    participant User
    participant OriginERC20 as Origin ERC20 Token
    participant OriginBridge as Origin Bridge Contract
    participant DestIndexer as Destination Bridge Indexer
    participant DestBridge as Destination Bridge Contract

    %% Step 1: Approve Transaction
    User->>OriginERC20: approve(amount)
    Note right of OriginERC20: User authorizes bridge to transfer tokens

    %% Step 2: Call Bridge Asset
    User->>OriginBridge: bridgeAsset(amount, destinationNetwork)
    OriginBridge-->>User: Transaction receipt (bridge asset event emitted)

    %% Step 3: Indexing on Destination
    DestIndexer-->>OriginBridge: Polls for bridge asset event
    OriginBridge-->>DestIndexer: Emits bridge asset event
    Note right of DestIndexer: Indexes bridge asset transaction

    %% Step 4: Polling for Claim Readiness
    loop Poll until ready for claim
        User->>DestIndexer: Is bridge ready for claim?
        DestIndexer-->>User: Not ready yet / Ready signal
    end

    %% Step 5: Claim Bridge on Destination
    User->>DestBridge: claimBridge(leafValue, proofLocalExitRoot, proofRollupExitRoot)
    Note right of DestBridge: `leafValue` consists of bridge data <br/> (e.g. globalIndex, originNetwork, originTokenAddress, <br/>destinationNetwork, destinationAddress etc.)
    DestBridge-->>DestBridge: Deploys wrapped token
    DestBridge-->>DestBridge: Performs token mapping
    DestBridge-->>DestBridge: Mints wrapped token to the destination address

    %% Step 6: Final Transaction Hash to User
    DestBridge-->>User: Transaction hash (wrapped token deployed and tokens minted to the destination address)
    Note right of User: Bridge process completed successfully

Prometheus Metrics

The bridge service exposes several Prometheus metrics to track the number of handled requests and their latencies for different API endpoints. These metrics help monitor service performance, request volume, and latency distribution across various handlers. Each handler is described with a unique handler id and these are the values, depending of what data they are providing:

  • get_bridges,
  • get_claims,
  • get_token_mappings,
  • get_legacy_token_migrations,
  • l1_info_tree_index_for_bridge,
  • injected_info_after_index,
  • claim_proof,
  • get_claim_candidates,
  • last_reorg_event,
  • get_sync_status,
  • health_check,
Metric NameTypeDescription
bridge_total_requestsCounterVecTotal number of requests handled per endpoint (handler_id) and HTTP status code (status_code).
bridge_request_latency_secondsHistogramVecLatency of requests in seconds, recorded per endpoint (handler_id). Useful for analyzing request duration distributions.

Usage Notes

All metrics are counters, meaning they only increase over time. Each metric helps monitor usage and performance of its corresponding API endpoint.

API Documentation

Bridge Tracker component

The bridge tracker gives a client a single endpoint to follow one bridge (identified by its creating transaction) from the moment it is sent until it is claimed, instead of the client polling the bridge service, the Global/Local Exit Root state and the agglayer certificate status itself and stitching the result together. It is served by the aggkit-proxy binary (TRACKER component), alongside the bridge service finder.

How it works

Registering a bridge (GET .../tx/{tx_hash}, or connecting over the WebSocket) adds it to an in-memory supervised list. A background engine resolves each supervised bridge’s creating transaction (FindBridge, over the origin network’s JSON-RPC endpoint) and then walks it through its expected path, one milestone at a time, checking the fact behind the current step and advancing once it is met:

StepMeaning
WaitingGERUpdateL1-originated bridge: the L1 Global Exit Root has not been updated with this deposit yet.
WaitingLERUpdateL2-originated bridge: the origin network’s Local Exit Root has not been updated yet.
PendingInclusionThe bridge is not yet part of any certificate sent to the agglayer.
CertificatePendingIncluded in a certificate; waiting for it to settle (covers Pending/Proven/Candidate/InError).
WaitL1SettledGERL2-originated only: the certificate settled, waiting for its settlement tx to confirm on L1.
WaitingGERInjectionL1 → L2 and L2 → L2 only: waiting for the covering Global Exit Root’s injection tx to land on the destination network — an L2-side fact; skipped for L2 → L1, since mainnet needs no injection.
WaitingL1InfoLeafAvailableAlways right before WaitingClaim, on every route: waiting for the bridge-service instance that will build the claim proof — the origin network’s own instance, or the destination’s when the origin is mainnet (which has no bridge-service deployment of its own) — to have its L1 info tree sync caught up to this deposit (GET /bridge/v1/l1-info-tree-index). Unlike WaitingGERInjection, this is never skipped or inferred from a sibling step: injecting a GER on the destination is not the same fact as the proof-building instance having caught up, and that sync can lag behind the finality this tracker uses elsewhere (see #1823).
WaitingClaimThe bridge is claimable: the proof-building instance has the bridge’s L1 info tree index.
ClaimedTerminal: the bridge has been claimed on the destination network.

Which steps apply, and in which order, depends on the bridge’s direction:

  • L1 → L2: WaitingGERUpdateWaitingGERInjectionWaitingL1InfoLeafAvailableWaitingClaimClaimed
  • L2 → L1: WaitingLERUpdatePendingInclusionCertificatePendingWaitL1SettledGERWaitingL1InfoLeafAvailableWaitingClaimClaimed
  • L2 → L2: WaitingLERUpdatePendingInclusionCertificatePendingWaitL1SettledGERWaitingGERInjectionWaitingL1InfoLeafAvailableWaitingClaimClaimed

The whole route is published the moment the creating tx resolves, so a client sees every step it will walk through before any milestone has been checked — not just the current one.

TrackingStatus summarizes the bridge’s lifecycle for a client that only needs the high-level state: registered (added to the list, not resolved yet), running, error (a step, or the initial resolution itself, failed terminally), or finished (claimed).

Endpoints

All routes are served under /tracker/v1.

MethodPathDescription
GET/tracker/v1/healthHealth status, instance identity and build info.
GET/tracker/v1/network/{network_id}/tx/{tx_hash}Registers (or looks up) the bridge and returns its current TrackingData.
GET/tracker/v1/network/{network_id}/tx/{tx_hash}/wsSame bridge, pushed as a status WebSocket message on every change instead of polled.

The response, both over REST and as each WebSocket status message, is a TrackingData: its bridge_status field stays null until the tracker resolves the creating tx, and all_steps is null until then too. bridge_status.event carries the facts taken directly from the on-chain BridgeEvent log (origin/destination network and address, amount, leaf type); block_number, log_index and block_timestamp sit alongside it as the block-level context the event was found in, not the event’s own fields.

The WebSocket connection closes normally (code 1000) once the bridge reaches a terminal state — Claimed, or the tracker giving up trying to resolve the creating tx at all (invalid tx / not a bridge transaction). A step-level error on an otherwise-resolved bridge is reported in TrackingData.error but is not terminal: the engine keeps retrying it.

Configuration

Enable the TRACKER component (--components TRACKER,...) and configure the [Tracker] section:

[Tracker]
RetentionPeriod = "10m"
IdleTimeout = "30m"
RegisterResolveTimeout = "3s"
L1BlockFinality = "LatestBlock"
L2BlockFinality = "LatestBlock"
MaxTrackedBridges = 100000
L2InjectionLookbackBlocks = 1000

# Workaround only: uncomment for a destination network whose bridge-service instance does not
# report the L2 block a covering GER was injected at.
# [Tracker.L2GlobalExitRootAddress]
# 1 = "0x..."

[Tracker.AgglayerClient]
Cached = true
[Tracker.AgglayerClient.ConfigurationCache]
TTL = "1s"
Capacity = 100
SendCertificate = "forbidden"
GetCertificateHeader = "cached"
GetEpochConfiguration = "cached"
GetLatestPendingCertificateHeader = "cached"
GetNetworkInfo = "cached"
[Tracker.AgglayerClient.GRPC]
URL = "https://agglayer-dev.polygon.technology"
UseTLS = false
  • RetentionPeriod: how long a terminal bridge (finished, or failed to ever resolve) stays queryable before the tracker forgets it and a later request re-registers it from scratch.
  • IdleTimeout: how long a bridge — terminal or still active — stays supervised once nobody has read it (REST poll) and it has no active WebSocket subscriber. Unlike RetentionPeriod, this applies regardless of status: a bridge that never resolves and that nobody is watching would otherwise stay in memory forever.
  • RegisterResolveTimeout: how long the first request for a freshly registered tx waits for the engine’s immediate resolution attempt before answering, so it has a shot at real progress instead of the bare registered state; a lookup of an already-registered tx never waits.
  • L1BlockFinality / L2BlockFinality: the finality a bridge’s creating tx receipt must reach before the tracker accepts it, so a later reorg cannot leave it permanently following an orphaned deposit (a resolved bridge is never re-checked).
  • MaxTrackedBridges: caps the in-memory supervised list; a request beyond it fails instead of registering the bridge — reaching the cap never evicts an existing entry to make room, so RetentionPeriod and IdleTimeout are what keep the registry under it during normal operation.
  • L2GlobalExitRootAddress: workaround only — a networkID → GlobalExitRootManagerL2 contract address map, used solely as a fallback for a destination network whose bridge-service instance does not report the L2 block a covering GER was actually injected at. For a network present here, the tracker scans that network’s own L2 for the UpdateHashChainValue event instead of leaving it absent. A network absent from this map (the default, empty map) never gets this fallback attempted; it should not be set otherwise.
  • L2InjectionLookbackBlocks: bounds how many blocks that same fallback scans backwards from the destination network’s head before giving up, instead of continuing all the way back to genesis. Defaults to 1,000 blocks when unset or <= 0.
  • AgglayerClient: the client used to resolve an L2-originated bridge’s covering certificate and its status (PendingInclusion/CertificatePending/WaitL1SettledGER). Cached is the master switch for ConfigurationCache’s per-method policy (false ignores it entirely). Each method is cached (served from its own TTL cache), passthrough (always calls the agglayer directly, the default for a method left unset), or forbidden (refused without ever reaching the agglayer — the tracker only ever reads agglayer state, so SendCertificate is forbidden here). GetLatestSettledCertificateHeader is intentionally left unset (passthrough): its “latest” answer must always be fresh.

API Documentation

EthTxManager

EthTxManager is responsible for managing transactions

EthTxManager Configuration

ParameterTypeDescriptionExample/Default
FrequencyToMonitorTxsdurationFrequency to monitor pending transactions."1s"
WaitTxToBeMineddurationWait time before retrying mining confirmation."2s"
GetReceiptMaxTimedurationMax wait time for getting transaction receipt."250ms"
GetReceiptWaitIntervaldurationInterval between retries for fetching receipt."1s"
PrivateKeysarrayList of private key configurations (keystore path + password).[ { Path = "/app/keystore/claimsponsor.keystore", Password = "testonly" } ]
ForcedGasuint64Fixed gas value override (0 = no override).0
GasPriceMarginFactorfloat64Gas price multiplier margin.1.0
MaxGasPriceLimituint64Maximum gas price allowed for sending.0
StoragePathstringPath to EthTxManager’s local database."/tmp/aggkit/ethtxmanager-claimsponsor.sqlite"
ReadPendingL1TxsboolWhether to read pending L1 transactions.false
SafeStatusL1NumberOfBlocksuint64Number of blocks to consider a transaction safe.5
FinalizedStatusL1NumberOfBlocksuint64Number of blocks to consider a transaction finalized.10

Etherman

Etherman handles the communication with the network.

Etherman Configuration

ParameterTypeDescriptionExample/Default
URLstringJSON-RPC URL for the network.
MultiGasProviderboolUse multiple gas providers if true.false
L1ChainIDuint64The Chain ID of the network to which transactions will be sent.

Note: This can be either the L1 or L2 Chain ID.
HTTPHeadersarrayCustom HTTP headers to add to RPC calls.[]

Note: If the L1ChainID field is set to 0, Etherman will automatically determine and populate the correct Chain ID at runtime, provided that a valid JSON-RPC URL is supplied.

Release lifecycle

This document presents the Aggkit Software release lifecycle. The Aggkit team has adopted a process grounded in industry-standard best practices to avoid reinventing the wheel and, more importantly, to prevent confusion among new developers and users. By adhering to these widely recognized practices, we ensure that anyone in the industry can intuitively understand and follow our internal procedures with minimal explanation.

Versioning

The versioning process follows the standard Semantic Versioning to tag new versions

Summary

  1. MAJOR version when you make incompatible API changes
  2. MINOR version when you add functionality in a backward compatible manner
  3. PATCH version when you make backward compatible bug fixes

At this time the project is in development phase so refer to the FAQ for the current versioning criteria:

How should I deal with revisions in the 0.y.z initial development phase?

The simplest thing to do is start your initial development release at 0.1.0 and then increment the minor version for each subsequent release.

How do I know when to release 1.0.0?

If your software is being used in production, it should probably already be 1.0.0. If you have a stable API on which users have come to depend, you should be 1.0.0. If you’re worrying a lot about backward compatibility, you should probably already be 1.0.0.

Pre-Releases

Refer to the Software release lifecycle Wikipedia article for a definition and criteria this project is following regarding pre-releases.

Release process

The release process is based on the Gitflow workflow for managing the source code repository.

For a quick reference you can check https://cheatography.com/mikesac/cheat-sheets/gitflow/

As a quick reference this is the diagram of the branching cycle:

FAQ

Should I cherry pick commits made to a release branch while it’s still unmerged?

As stated by the Gitflow workflow, release branches should be short-lived and merged back to main and develop branches, but it can happen from time to time that develop branch needs a commit from a release branch before it’s released.

In that case, a cherry-pick commit can be merged into develop containing the desired changes, as they would have end-up in develop at some point in the future anyway.

How do we manage several developments in parallel?

Sometimes there’s a necessity to release a new stable version of the previous branch with certain features while simultaneously working on the next version. In that case, we’ll maintain two release branches like release/4.0.0 and release/5.0.0. These branches will evolve in parallel, but most of the changes from the lower release will need to be cherry-picked onto the newest release. Additionally, if any critical fix is made to the newest release, it should be back-ported to the older release.

How to create a hotfix for an older release?

When a release branch is merged into main and develop, it is removed, and only the tag is left. To create a hotfix release, a new release branch will be created from the tag so the necessary fixes can be applied. Then follow the normal release cycle: create a new beta for the release, test it in all environments, then create the final tag and release it.

The fixes may need to be cherry-picked into any open release branches.

Why we should not squash merge when merging a release branch to main or develop ?

This is opinionated but in general there’s quite a lot of downsides when squash merging release branches, see this response for some of them https://stackoverflow.com/questions/41139783/gitflow-should-i-squash-commits-when-merging-from-a-release-branch-into-master/41298098#41298098

Another big downside is that main and develop branch will distance more and more in terms of commits as time passes, making them totally different after some time.

Reference

Comparison of popular branching strategies https://docs.aws.amazon.com/prescriptive-guidance/latest/choosing-git-branch-approach/git-branching-strategies.html

End-to-end tests

This document enumerates and summarizes the e2e tests. The tests are implemented using Bats framework and are assuming there is a running cluster to run them against. They are placed in the test/bats folder and divided into two major categories:

  • the ones that involve single L2 (pessimistic proof) and L1 network. They are found in the test/bats/pp folder.
  • the ones that involve two L2 (pessimistic proof) and single L1 network. They are found in the test/bats/pp-multi folder. Reusable helper functions are placed in the test/bats/helpers folder and they consist of sending and claiming bridge transactions, fetching proofs, sending transactions, querying contracts etc. Most of the functions rely on the cast command from Foundry.

Single L2 network

It involves single L2 network (and single L1 network), that are attached to the same agglayer.

Transfer message

Bridges message from L1 to L2, by invoking bridgeMessage function on the bridge contract and then claiming once the global exit root is injected to the destination L2 network.

Native gas token deposit to WETH

Bridges and claims native token from L1 to L2, that is mapped to the WETH token on L2.

Test Bridge APIs workflow

Bridges the native token from L1 to L2 and then invokes the aggkit bridge service endpoints to verify they are working as expected: bridge_getBridges, bridge_l1InfoTreeIndexForBridge, bridge_injectedInfoAfterIndex and bridge_claimProof.

Custom gas token deposit L1 -> L2

Bridges custom gas token, that pre-exists on L1 and is mapped to a native token on L2, claims it on the L2 and asserts that the native token balance has increased when settled on L2.

Custom gas token withdrawal L2 -> L1

Bridges and claims native token on L2 network, that is pre-deployed and mapped to custom gas token on an L1 network and asserts that the gas token balance for the receiver address has increased after it got claimed on L1 network.

ERC20 token deposit L1 -> L2

It deploys the ERC20 token on the L1 and bridges and claims it to the L2. In this process of claiming the bridge, a token representation of given ERC20 token is automatically deployed on the L2.

Auto Claim L1 -> L2

Validates the L1 to L2 Auto Claim service with the existing e2e environment. The focused Go e2e command is:

go test -v -run 'TestAutoClaimL1ToL2(AllowAll|APIApprove)' -timeout 30m ./test/e2e

TestAutoClaimL1ToL2AllowAll enables Auto Claim with the allow-all policy and waits for the request to reach confirmed without a manual claim. TestAutoClaimL1ToL2APIApprove enables the API, waits for manual-approval-required, approves the request through POST /autoclaim/v1/bridges/{id}/approve, and then waits for confirmed.

The e2e environment must be able to start the docker compose stack, which requires enough host resources. If the host kills docker compose up (signal: killed) before the tests start, rerun the command on a host with more memory.

Remove GER (invalid-GER recovery)

Exercises the remove-GER runbook end to end against the anvil-2chains env: inject an invalid GER on L2, confirm l2gersync blocks on it, run the remove_ger tool’s recovery flow (freeze bridge -> removeGlobalExitRoots -> category-specific claim correction -> restore bridge), and confirm l2gersync recovers automatically and resumes normal processing. Implemented in test/e2e/removeger_test.go:

go test -v -run 'TestRemoveGER_(NoProblematicClaims|CategoryA|CategoryB1|CategoryB2)|TestGenerateInvalidGER' -timeout 60m ./test/e2e
  • TestRemoveGER_NoProblematicClaims — invalid GER with no problematic claims; recovery is just freeze/remove/restore.
  • TestRemoveGER_CategoryA — invalid GER used by a claim that would under-collateralize the bridge; recovery adds an unsetMultipleClaims step.
  • TestRemoveGER_CategoryB1 — invalid GER used by a claim with correct bridge content and index but a wrong GER; recovery adds a forceEmitDetailedClaimEvent step.
  • TestRemoveGER_CategoryB2 — invalid GER used by a claim with correct bridge content but a wrong index; recovery adds unset + set claims + force-emit steps.
  • TestGenerateInvalidGER — exercises the remove_ger tool’s generate subcommand (which crafts and injects a synthetic invalid GER via cast) as a standalone check of the generation path. This test drives cast send/cast call from the host (outside Docker) against the L2 RPC port published by the Anvil compose env; on a dev machine whose local foundry cast cannot open outbound connections to that Docker-published port (while the Go ethclient used elsewhere in the harness reaches it fine — a machine-local cast networking quirk, not an aggkit or test defect), the test detects this via a preflight probe and cleanly t.Skips rather than failing. CI installs cast fresh and reaches the compose network normally, so the test runs in full there.

Each of the four TestRemoveGER_* scenarios asserts, via GET /bridge/v1/sync-status’s l2_ger_info (see Bridge service component):

  1. l2gersync is genuinely stalled on the invalid GER’s insert block while the L2 chain head keeps advancing (assertL2GERSyncStalledAt);
  2. after the recovery tool’s removeGlobalExitRoots call, l2gersync’s last_processed_block catches up past the actual removal transaction’s block (waitForL2GERSyncCaughtUp, targeting the block number returned by remove_ger.ExecuteRecovery’s RecoveryResult.RemovalBlock, not a post-hoc chain-head read — an earlier iteration of this assertion targeted an overshot, post-hoc head read and could time out a few blocks short of a real, successful recovery);
  3. l2gersync is genuinely alive afterwards, via a fresh, valid L1->L2 bridge and claim (assertL2GERSyncStillAlive).

Complementary log-based detection (detectInvalidGERFromAggkitLogs) is kept alongside the /sync-status assertions.

CI matrix

.github/workflows/test-go-e2e.yml runs the remove-GER tests on anvil-2chains in three dedicated matrix groups, so each group gets an isolated compose stack and cannot leak mutated chain state into the default group. The anvil-2chains / default group’s regex explicitly excludes them (Go’s -run has no negation syntax, so the default group is enumerated as a positive, anchored regex instead):

Matrix group (env / group)Tests
anvil-2chains / removeger-fastTestRemoveGER_NoProblematicClaims, TestRemoveGER_CategoryA, TestGenerateInvalidGER
anvil-2chains / removeger-b1TestRemoveGER_CategoryB1
anvil-2chains / removeger-b2TestRemoveGER_CategoryB2
anvil-2chains / defaultEverything else (positive-regex list, remove-GER tests excluded)

Two L2 networks

It involves two L2 networks (and single L1 network), that are attached to the same agglayer.

Test L2 to L2 bridge

It bridges native tokens from L1 to both L2 networks and claims them. Afterwards, it bridges from L2 (PP2) to L2 (PP1) network and claims it on the destination network.

Common configuration

SignerConfig

The SignerConfig struct is the primary configuration object used to initialize a signer. It’s defined in the go_signer library and specifies how and where cryptographic signing operations are performed.

The configuration supports multiple signer types. To use it, set the desired signer type in the Method field. The remaining configuration parameters will vary depending on the selected method.

The main methods are:

Keystore (local)

Use this method to sign with a local keystore file.

NameTypeExampleDescription
MethodstringlocalMust be local
Pathstring/opt/private_key.kestorefull path to the keystore
PasswordstringxdP6G8gV9PYspassword to unlock the keystore

Example:

[AggSender]
AggsenderPrivateKey = { Method="local", Path="/opt/private_key.kestore", Password="xdP6G8gV9PYs" }

Google Cloud KMS (GCP)

Use this method to sign using the Google Cloud KMS infrastructure.

NameTypeExampleDescription
MethodstringGCPMust be GCP
KeyNamestringprojects/your-prj-name/locations/your_location/keyRings/name_of_your_keyring/cryptoKeys/key-name/cryptoKeyVersions/versionid of the key in Google Cloud

Example:

[AggSender]
AggsenderPrivateKey = { Method="GCP", KeyName="projects/your-prj-name/locations/your_location/keyRings/name_of_your_keyring/cryptoKeys/key-name/cryptoKeyVersions/version"}

Amazon Web Services KMS (AWS)

Use this method to sign using the AWS KMS infrastructure. The key type must be ECC_SECG_P256K1 to ensure compatibility.

NameTypeExampleDescription
MethodstringAWSMust be AWS
KeyNamestringa47c263b-6575-4835-8721-af0bbb97XXXXid of the key in AWS

Example:

[AggSender]
AggsenderPrivateKey = { Method="AWS", KeyName="a47c263b-6575-4835-8721-af0bbb97XXXX"}

Others

Additional signing methods are available. For a complete list and detailed configuration options, please refer to the go_signer library documentation (v0.0.7)

ClientConfig

The ClientConfig structure configures the gRPC client connection. It includes the following fields:

Field NameTypeDescription
URLstringThe URL of the gRPC server
MinConnectTimeouttypes.DurationMinimum time to wait for a connection to be established
RequestTimeouttypes.DurationTimeout for individual requests
UseTLSboolWhether to use TLS for the gRPC connection
Retry*RetryConfigRetry configuration for failed requests

RetryConfig

The RetryConfig structure configures the retry behavior for failed gRPC requests:

Field NameTypeDescription
InitialBackofftypes.DurationInitial delay before retrying a request
MaxBackofftypes.DurationMaximum backoff duration for retries
BackoffMultiplierfloat64Multiplier for the backoff duration
MaxAttemptsintMaximum number of retries for a request
Excluded[]MethodList of methods excluded from retry policies

Example:

[AggSender]
    [AggSender.AgglayerClient]
		URL = "http://localhost:9000"
		MinConnectTimeout = "5s"
		RequestTimeout = "300s" 
		UseTLS = false
		[AggSender.AgglayerClient.Retry]
			InitialBackoff = "1s"
			MaxBackoff = "10s"
			BackoffMultiplier = 2.0
			MaxAttempts = 16

Method

The Method type represents a gRPC method configuration with the following fields:

Field NameTypeDescription
ServiceNamestringThe gRPC service name (including package)
MethodNamestringThe specific gRPC function name (optional)

This type is used to specify methods that should be excluded from retry policies. The ServiceName field is required and should include both the package and service name.

Example:

[AggSender]
    [AggSender.AgglayerClient]
        [AggSender.AgglayerClient.Retry]
            Excluded = [
                { Service = "agglayer.Agglayer", Method = "SubmitCertificate" },
                { Service = "agglayer.Agglayer", Method = "GetStatus" }
            ]

RateLimitConfig

The RateLimitConfig structure configures rate limiting behavior. If either NumRequests or Interval is set to 0, rate limiting is disabled.

Field NameTypeDescription
NumRequestsintMaximum number of requests allowed within the interval
Intervaltypes.DurationTime window for rate limiting

Example:

[AggSender]
    [AggSender.MaxSubmitCertificateRate]
        NumRequests = 20
        Interval = "1h"

When rate limiting is enabled, if the number of requests exceeds NumRequests within the specified Interval, the system will wait until the next interval before allowing more requests. This helps prevent overwhelming the system with too many requests in a short period.

RESTConfig

RESTConfig configures a shared Gin-based HTTP server. It backs the REST sections in the config: [PublicREST], [AdminREST], and the proxy’s [REST].

Field NameTypeDescription
HoststringHostname or IP address the REST service listens on
PortintPort number the REST service is accessible on
ReadTimeouttypes.DurationHTTP server read timeout
WriteTimeouttypes.DurationHTTP server write timeout
MaxRequestsPerIPAndSecondfloat64Unused; kept for config compatibility. See below
CORSCORSConfigCross-Origin Resource Sharing settings for this REST service. See below

MaxRequestsPerIPAndSecond is not enforced: aggkit does not rate-limit requests in-process. Its default is 0 (unlimited). If you need per-IP request throttling, apply it at the fronting reverse proxy / API gateway / ingress — that is also where it is most effective, since a service sitting behind a proxy typically sees every client as the proxy’s single IP, making in-process per-IP limiting ineffective anyway.

CORSConfig

CORSConfig configures Cross-Origin Resource Sharing headers, so the REST service can be called from a browser-based client hosted on a different origin. Disabled by default, which preserves the current behavior (no CORS headers, so browsers block cross-origin requests).

Field NameTypeDescription
EnabledboolTurns on CORS header handling
AllowedOrigins[]stringOrigins allowed to make cross-origin requests. "*" allows any origin; empty denies every origin once Enabled is true
AllowedMethods[]stringHTTP methods allowed for cross-origin requests
AllowedHeaders[]stringRequest headers allowed for cross-origin requests
AllowCredentialsboolAllows cookies / HTTP auth on cross-origin requests. When true, the request’s Origin is reflected back instead of *, since the CORS spec forbids combining credentials with a wildcard origin
MaxAgetypes.DurationHow long browsers may cache a preflight (OPTIONS) response. 0 (default) omits the header

Example, enabling CORS for the proxy’s [REST] section for a frontend hosted at https://example.com:

[REST.CORS]
Enabled = true
AllowedOrigins = ["https://example.com"]
AllowedMethods = ["GET", "POST", "OPTIONS"]
AllowedHeaders = ["Content-Type", "Authorization"]
AllowCredentials = false
MaxAge = "12h"

Note that the [RPC] section is not a RESTConfig: it is the JSON-RPC server config from github.com/0xPolygon/cdk-rpc, whose MaxRequestsPerIPAndSecond is enforced (via tollbooth). There, 0 does not mean unlimited — tollbooth.NewLimiter(0, ...) produces a limiter with a burst of 1 and no refill, i.e. one request per IP ever. Its default stays 10.

RPCClientConfig

RPCClientConfig configures the JSON-RPC client used to connect to Ethereum nodes. It is used in multiple places, notably [L1NetworkConfig.RPC] (L1 node) and [Common.L2RPC] (L2 node).

FieldTypeDefaultDescription
URLstringJSON-RPC endpoint URL
Modestring""Client mode: "" or "basic" for standard nodes, "op" for Optimism nodes
HashFromJSONboolfalseWhen true, fetches block hashes via JSON-RPC (eth_getBlockByNumber). When false, computes them locally from the RLP-encoded header (go-ethereum default). Enable this for nodes where RLP hashing does not match the canonical block hash
BatchBlockHeaderRetrievalbooltrueWhen true, uses JSON-RPC batch requests to fetch block headers in bulk (faster). Disable if the node does not support batch calls
RetryModestring"backoff"Retry strategy: "backoff" for exponential backoff, "delays" for fixed delay list, "" for no retries
MaxRetriesint5Maximum number of retry attempts
InitialBackoffduration5sInitial wait time before the first retry (backoff mode)
MaxBackoffduration60sMaximum wait time between retries (backoff mode)
BackoffMultiplierfloat642.0Multiplier applied to the backoff duration on each retry
Delays[]duration[]Explicit list of wait times for each retry attempt (delays mode)

Example:

[L1NetworkConfig.RPC]
URL = "http://localhost:8545"
Mode = "basic"
HashFromJSON = false
BatchBlockHeaderRetrieval = true
RetryMode = "backoff"
MaxRetries = 5
InitialBackoff = "5s"
MaxBackoff = "60s"
BackoffMultiplier = 2.0

[Common.L2RPC]
URL = "http://localhost:8123"
Mode = "basic"
HashFromJSON = true
BatchBlockHeaderRetrieval = true
RetryMode = "delays"
MaxRetries = 6
Delays = ["1s", "2s", "5s", "10s", "30s", "60s"]

AutoClaim

AutoClaim configures the optional Auto Claim runtime, which automates both L1-to-L2 and L2-to-Lx (L2-to-L1, L2-to-L2) bridge claims. It is disabled by default. To enable it, select the autoclaim component (there is no separate enable flag), configure storage, and add at least one enabled EVM claimer for the destination network.

Auto Claim requires l1bridgesync and l1infotreesync when enabled. [AutoClaim.BridgeServiceFinder].RollupManagerAddr is required whenever [AutoClaim.L2ToLxBridgeDetector].Enabled = true or any enabled claimer has an L2 destination (NetworkID != 0), in either direction — the finder resolves rollup bridge service URLs both as sources (bridge discovery and claim-proof fetch) and as destinations (the per-claimer GER-injection readiness gate, which replaced the previous per-claimer l2gersync instance). There are no longer per-claimer BlockFinality / InitialBlockNum config keys, since claimers no longer run their own GER syncer. The optional REST API uses /autoclaim/v1 for request inspection and manual approvals.

See Auto Claim Service for the complete configuration table, policy behavior, lifecycle, and API workflow.