backward_forward_let

package
v0.10.0-rc1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 20, 2026 License: Apache-2.0, MIT Imports: 39 Imported by: 0

README

backward-forward-let

backward-forward-let diagnoses and recovers Local Exit Tree divergence between the AggLayer's settled state and the current L2 bridge state.

It also includes staging-only helper commands for certificate injection drills.

What the tool does

The main command:

  • reads the settled AggLayer state,
  • reads the current L2 bridge state,
  • queries aggsender for settled certificate bridge exits,
  • finds the divergence point,
  • classifies the recovery case,
  • prints the recovery plan,
  • activates emergency state when needed,
  • executes BackwardLET and/or ForwardLET,
  • verifies the post-step deposit count and LER,
  • deactivates emergency state before exit.

Configuration

The tool reads the same TOML config format as aggkit and requires these sections:

  • Common.L2RPC.URL
  • BridgeL2Sync.BridgeAddr
  • AgglayerClient
  • BackwardForwardLET.BridgeServiceURL
  • BackwardForwardLET.AggsenderRPCURL
  • BackwardForwardLET.L2NetworkID
  • BackwardForwardLET.GERRemoverKey
  • BackwardForwardLET.EmergencyPauserKey
  • BackwardForwardLET.EmergencyUnpauserKey

Example:

[Common.L2RPC]
URL = "http://localhost:8545"

[BridgeL2Sync]
BridgeAddr = "0x1111111111111111111111111111111111111111"

[AgglayerClient.GRPC]
URL               = "http://localhost:4443"
MinConnectTimeout = "5s"
RequestTimeout    = "300s"
UseTLS            = false

[BackwardForwardLET]
BridgeServiceURL = "http://localhost:8080/bridge/v1"
AggsenderRPCURL  = "http://localhost:5576"
L2NetworkID      = 1

[BackwardForwardLET.GERRemoverKey]
Method   = "local"
Path     = "/path/to/ger-remover.keystore"
Password = "secret"

[BackwardForwardLET.EmergencyPauserKey]
Method   = "local"
Path     = "/path/to/emergency-pauser.keystore"
Password = "secret"

[BackwardForwardLET.EmergencyUnpauserKey]
Method   = "local"
Path     = "/path/to/emergency-unpauser.keystore"
Password = "secret"

Role requirements:

  • GERRemoverKey must be able to call backwardLET and forwardLET.
  • EmergencyPauserKey must be able to activate emergency state.
  • EmergencyUnpauserKey must be able to deactivate emergency state.

Main recovery command

Diagnose and, after confirmation, execute recovery:

backward-forward-let --cfg aggkit-config.toml

Run non-interactively:

backward-forward-let --cfg aggkit-config.toml --yes

Use a bridge-exit override file when aggsender cannot provide settled certificate exits:

backward-forward-let --cfg aggkit-config.toml \
  --cert-exits-file certificate_exits_override.json
Output behavior

The command prints one of:

  • NoDivergence
  • a classified recovery case with divergence details
  • a missing-certificate report when certificate exits cannot be loaded

Recovery behavior by case:

  • Case 1 and Case 3: ForwardLET only
  • Case 2 and Case 4: BackwardLET, then ForwardLET, and a second ForwardLET when extra real L2 bridges must be replayed

Aggsender restart caveat after staged drills:

  • aggsender intentionally treats local certificate-state mismatches as fatal on startup,
  • if AggLayer is already on a further or different certificate than the aggsender DB, aggsender will not auto-reconcile,
  • the required operator action is to wipe the aggsender DB and restart aggsender.

Fallback when aggsender data is unavailable

If the tool reports missing certificate exits, fetch them from the AggLayer admin/debug endpoint and rerun with --cert-exits-file.

Detailed procedure:

That document covers:

  • enabling debug-mode = true,
  • reaching the AggLayer admin JSON-RPC API,
  • using admin_getCertificate,
  • building the override file,
  • handling heights whose cert ID is not auto-resolved,
  • wiping the aggsender DB when a post-drill restart fails because local cert state no longer matches AggLayer.

Commands

backward-forward-let

Diagnose and recover divergence.

Flags:

  • --cfg, -c: one or more config files
  • --yes: skip interactive confirmation
  • --cert-exits-file, -f: fallback JSON file with bridge exits keyed by certificate height
backward-forward-let send-cert

Send a certificate JSON to the AggLayer and optionally store it in the aggsender DB.

This is primarily useful for controlled staging drills and test tooling.

Example:

backward-forward-let send-cert \
  --cfg agglayer-only.toml \
  --cert-file /tmp/cert.json \
  --db-path /path/to/aggsender.sqlite

For fallback-mechanism drills where aggsender must not retain the certificate, send to AggLayer only:

backward-forward-let send-cert \
  --cfg agglayer-only.toml \
  --cert-file /tmp/cert.json \
  --no-db

Flags:

  • --cfg, -c: config file containing at least AgglayerClient
  • --cert-json: certificate JSON string
  • --cert-file, -f: certificate JSON file
  • --db-path: aggsender SQLite DB path
  • --no-db: skip aggsender DB storage entirely

Behavior:

  • sends the certificate to the AggLayer,
  • stores it in aggsender DB as the last sent certificate unless --no-db is set,
  • derives FromBlock from the previous certificate when possible so aggsender retry logic remains coherent.
backward-forward-let craft-cert

Build a signed malicious certificate JSON for staging drills.

This command is intentionally gated by --staging-only.

Example:

backward-forward-let craft-cert \
  --cfg aggkit-config.toml \
  --staging-only \
  --num-fake-exits 1 \
  --out /tmp/malicious-cert.json

By default craft-cert reuses AggSender.AggsenderPrivateKey from the config, so the same shared signer config used by aggsender can be used here as well, including GCP KMS and other go_signer backends.

To override the config for a one-off local keystore drill, pass the legacy CLI flags:

backward-forward-let craft-cert \
  --cfg aggkit-config.toml \
  --signer-key-path /path/to/sequencer.keystore \
  --signer-key-password 'secret' \
  --staging-only \
  --num-fake-exits 1 \
  --out /tmp/malicious-cert.json

If aggkit/aggsender is stopped and aggsender RPC is unavailable, add --db-path so the command can reconstruct prior settled bridge exits from the aggsender SQLite DB:

backward-forward-let craft-cert \
  --cfg aggkit-config.toml \
  --signer-key-path /path/to/sequencer.keystore \
  --signer-key-password 'secret' \
  --db-path /path/to/aggsender.sqlite \
  --staging-only \
  --num-fake-exits 2 \
  --out /tmp/malicious-cert.json

If aggsender is intentionally stopped and neither aggsender RPC nor the local DB can provide all historical bridge exits, reuse the same fallback override file used by the main diagnosis command:

backward-forward-let --cfg aggkit-config.toml \
  --cert-exits-file certificate_exits_override.json \
  craft-cert \
  --staging-only \
  --num-fake-exits 1 \
  --out /tmp/malicious-cert.json

Flags:

  • --cfg, -c: config file with normal tool connectivity settings
  • AggSender.AggsenderPrivateKey: default signer config reused by craft-cert
  • --signer-key-path: optional local-keystore override for the signer config
  • --signer-key-password: password for the local-keystore override
  • --out: write crafted JSON to a file instead of stdout
  • --db-path: optional aggsender SQLite DB path when aggsender RPC is unavailable
  • --num-fake-exits: number of fake exits to include
  • --starting-exit-index: start index used to derive unique destination addresses
  • --nonce: optional deterministic nonce used in fake destination derivation
  • --origin-network: fake exit origin network
  • --origin-token-address: fake exit origin token address
  • --destination-network: fake exit destination network
  • --amount: decimal amount for each fake exit
  • --staging-only: required acknowledgement

Behavior:

  • reads the current settled state from AggLayer,
  • reconstructs the existing leaf sequence from aggsender RPC, aggsender DB, fallback override data, and bridge service as needed,
  • builds one or more fake BridgeExits,
  • computes the resulting NewLocalExitRoot,
  • signs the crafted certificate,
  • writes JSON that can be consumed by send-cert.

Staging drill flow

To simulate divergence on a staging network:

  1. Stop aggkit/aggsender before crafting or sending any malicious certificate. This prevents a genuine pending certificate from taking the next height while the drill is being prepared.
  2. Confirm there is no unrelated non-error pending certificate already occupying the next height. If there is, wait for it to settle before continuing. Re-check this immediately before each send-cert, because staging can advance while you are waiting on settlement or bridge indexing.
  3. Craft a malicious certificate with craft-cert.
  4. Submit it with send-cert. Use --no-db if you specifically want to test the fallback path where aggsender cannot provide certificate bridge exits and operators must use the AggLayer admin/debug endpoint.
  5. Keep aggkit/aggsender stopped until every malicious certificate needed for the current drill has been submitted to AggLayer.
  6. Restart aggkit/aggsender and wait for the certificate to settle. On staging this settlement can take up to one hour.
  7. Optionally create extra real L2 bridges if you want a Case 2 or Case 4 drill. Wait for bridge service to index them before expecting diagnosis or recovery to use them.
  8. Run backward-forward-let --cfg ... to diagnose and recover.
  9. After recovery, rerun the main command until it reports NoDivergence. For Case 2 and Case 4, this may require restarting aggsender and waiting for the honest follow-up certificate to settle after replayed genuine L2 bridges.

Typical case mapping:

  • Case 1: one malicious cert, no extra L2 bridges
  • Case 2: one malicious cert, then extra real L2 bridges
  • Case 3: two malicious certs, no extra L2 bridges
  • Case 4: two malicious certs, then extra real L2 bridges

Intermediate expectations:

  • After only the first malicious cert in a Case 3 drill has settled, the network still looks like Case 1.
  • Final Case 3 classification only appears after the second malicious cert settles.

Safety notes

  • craft-cert and send-cert are for staging drills and controlled test environments.
  • Do not use the debug commands against a production network.
  • The recovery command itself is intended for real incidents, but only with the correct signer roles and a verified operator workflow.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BridgeExitLeafHash

func BridgeExitLeafHash(be *agglayertypes.BridgeExit) common.Hash

BridgeExitLeafHash returns the leaf hash for a BridgeExit using BridgeExit.Hash().

func BridgeExitToLeafData

func BridgeExitToLeafData(be *agglayertypes.BridgeExit) bridgesync.LeafData

BridgeExitToLeafData converts an agglayer BridgeExit to a bridgesync.LeafData.

func BridgeResponseLeafHash

func BridgeResponseLeafHash(br *bridgeservicetypes.BridgeResponse) common.Hash

BridgeResponseLeafHash computes the leaf hash for a BridgeResponse using the same algorithm as BridgeExit.Hash(). The bridge service stores raw metadata (from the BridgeEvent). The contract's getLeafValue takes keccak256(rawMetadata), so we hash it here — matching convertBridgeMetadata in aggsender.

func BridgeResponseToLeafData

func BridgeResponseToLeafData(br *bridgeservicetypes.BridgeResponse) bridgesync.LeafData

BridgeResponseToLeafData converts a bridge service BridgeResponse to a bridgesync.LeafData.

func ComputeBackwardLETParams

func ComputeBackwardLETParams(
	allLeafHashes []common.Hash,
	targetIndex uint32,
) (frontier [32]common.Hash, nextLeaf common.Hash, proof [32]common.Hash, err error)

ComputeBackwardLETParams computes the three parameters required for a BackwardLET call:

  • frontier: the append-only tree frontier after inserting leaves 0..targetIndex-1
  • nextLeaf: the hash of the leaf at targetIndex (the leaf being "rolled back from")
  • proof: a Merkle proof that nextLeaf is at targetIndex in the full tree

func ComputeLERForNewLeaves

func ComputeLERForNewLeaves(existingLeafHashes []common.Hash, newLeafHashes []common.Hash) (common.Hash, error)

ComputeLERForNewLeaves computes the LET Merkle root after appending newLeafHashes to an existing tree described by existingLeafHashes.

func ExecuteRecovery

func ExecuteRecovery(ctx context.Context, env *Env, diagnosis *DiagnosisResult) (retErr error)

ExecuteRecovery performs the on-chain recovery steps for the given diagnosis. It activates emergency state (if not already active), runs BackwardLET and/or ForwardLET as required by the case, and deactivates emergency state when done.

func PrintDiagnosis

func PrintDiagnosis(w io.Writer, result *DiagnosisResult)

PrintDiagnosis prints a human-readable diagnosis summary to w.

func Run

func Run(c *cli.Context) error

Run is the main entry point for the backward/forward LET CLI.

func RunCraftCert

func RunCraftCert(c *cli.Context) error

RunCraftCert is the CLI action for the craft-cert subcommand. It builds a signed malicious certificate JSON for staging drills.

func RunSendCert

func RunSendCert(c *cli.Context) error

RunSendCert is the CLI action for the send-cert subcommand. It reads a certificate from JSON (--cert-json or --cert-file), sends it to the agglayer, and optionally stores it in the aggsender SQLite DB.

Types

type BackwardForwardLETConfig

type BackwardForwardLETConfig struct {
	// GERRemoverKey is the signing key used for GER-removal and bridge admin operations.
	GERRemoverKey signertypes.SignerConfig `mapstructure:"GERRemoverKey"`

	// EmergencyPauserKey is the signing key with activateEmergencyState privileges.
	EmergencyPauserKey signertypes.SignerConfig `mapstructure:"EmergencyPauserKey"`

	// EmergencyUnpauserKey is the signing key with deactivateEmergencyState privileges.
	EmergencyUnpauserKey signertypes.SignerConfig `mapstructure:"EmergencyUnpauserKey"`

	// BridgeServiceURL is the URL of the aggkit bridge service REST API (required).
	BridgeServiceURL string `mapstructure:"BridgeServiceURL"`

	// AggsenderRPCURL is the JSON-RPC URL of the running aggsender (required for certificate queries).
	AggsenderRPCURL string `mapstructure:"AggsenderRPCURL"`

	// L2NetworkID is the network ID of the L2 chain.
	L2NetworkID uint32 `mapstructure:"L2NetworkID"`

	// CertificateExitsFile is an optional path to a JSON override file containing
	// pre-extracted bridge exits keyed by certificate height. When set, used as a
	// fallback if the aggsender RPC cannot supply bridge exits for a height.
	// Obtain the file by calling admin_getCertificate on the agglayer for each
	// cert ID reported in the tool's missing-cert output.
	CertificateExitsFile string `mapstructure:"CertificateExitsFile"`
}

BackwardForwardLETConfig contains configuration specific to the backward/forward LET tool.

type BridgeExitsOverride

type BridgeExitsOverride struct {
	NetworkID   uint32
	Description string
	// contains filtered or unexported fields
}

BridgeExitsOverride holds pre-extracted certificate bridge exits keyed by height. Load via LoadBridgeExitsOverride. Use GetExits to retrieve exits for a specific height.

NOTE: the JSON field names follow the Go agglayertypes.BridgeExit json tags (e.g., "dest_network", "dest_address"). The agglayer Rust serde may use different names (e.g., "destination_network"); if so, build the file by marshaling the Certificate.BridgeExits value obtained via json.Unmarshal from the admin API response, not from the raw Rust JSON text.

func LoadBridgeExitsOverride

func LoadBridgeExitsOverride(filePath string) (*BridgeExitsOverride, error)

LoadBridgeExitsOverride reads and validates a JSON override file containing pre-extracted certificate bridge exits keyed by certificate height.

Expected file format (heights are string-keyed; amount is a decimal string):

{
  "network_id": 1,
  "description": "optional description",
  "heights": {
    "0": [
      {
        "leaf_type": 0,
        "token_info": { "origin_network": 0, "origin_token_address": "0x..." },
        "dest_network": 0,
        "dest_address": "0x...",
        "amount": "0",
        "metadata": null
      }
    ],
    "1": []
  }
}

Returns an error when:

  • the file cannot be read
  • the JSON is malformed
  • network_id is zero
  • the heights map is absent
  • any height key is not a non-negative integer

func (*BridgeExitsOverride) GetExits

func (o *BridgeExitsOverride) GetExits(height uint64) ([]*agglayertypes.BridgeExit, bool)

GetExits returns the bridge exits for the given certificate height. The second return value is false when the height has no entry in the override.

type Config

type Config struct {
	// Common contains shared settings such as the L2 RPC URL.
	Common ethermanconfig.CommonConfig `mapstructure:"Common"`

	// BridgeL2Sync contains the L2 bridge contract address used to initialize the binding.
	BridgeL2Sync bridgesync.Config `mapstructure:"BridgeL2Sync"`

	// AgglayerClient is the AggLayer gRPC client configuration.
	AgglayerClient agglayer.ClientConfig `mapstructure:"AgglayerClient"`

	// AggSender contains the subset of aggsender config reused by craft-cert signer resolution.
	AggSender CraftCertAggsenderConfig `mapstructure:"AggSender"`

	// BackwardForwardLET contains tool-specific settings.
	BackwardForwardLET BackwardForwardLETConfig `mapstructure:"BackwardForwardLET"`
}

Config holds the subset of aggkit configuration fields required by the backward/forward LET tool.

func LoadConfig

func LoadConfig(c *cli.Context) (*Config, error)

LoadConfig reads the TOML config file(s) specified by --cfg and unmarshals the fields required by the backward/forward LET tool. Uses the same template rendering pipeline as the main aggkit binary.

type CraftCertAggsenderConfig

type CraftCertAggsenderConfig struct {
	// AggsenderPrivateKey is the shared signer config used to sign certificates.
	AggsenderPrivateKey signertypes.SignerConfig `mapstructure:"AggsenderPrivateKey"`
}

CraftCertAggsenderConfig contains the aggsender signer settings reused by craft-cert.

type DiagnosisResult

type DiagnosisResult struct {
	Case RecoveryCase

	// L1 settled state (from AggLayer NetworkInfo).
	L1SettledLER           common.Hash
	L1SettledDepositCount  uint32 // = SettledLETLeafCount from NetworkInfo
	L1SettledHeight        uint64
	L1SettledCertificateID common.Hash

	// L2 on-chain bridge state.
	L2CurrentLER          common.Hash
	L2CurrentDepositCount uint32

	// DivergencePoint is the number of leading leaves that match between
	// L1 settled and L2 bridge. It is also the target deposit count for BackwardLET.
	DivergencePoint uint32

	// ExtraL2Bridges contains real L2 bridges (bridgesync.LeafData) after DivergencePoint.
	// Populated for Cases 2 and 4.
	ExtraL2Bridges []bridgesync.LeafData

	// DivergentLeaves are the bridge exits settled on L1 that are absent or different on L2.
	DivergentLeaves []*agglayertypes.BridgeExit

	// Undercollateralization summarises token under-collateralization from DivergentLeaves.
	Undercollateralization []UndercollateralizedToken

	// IsEmergencyState reports whether the L2 bridge is already paused.
	IsEmergencyState bool

	// AggsenderAPIFailed is set when the aggsender RPC was unreachable during the divergence walk.
	AggsenderAPIFailed bool

	// MissingCerts lists the certificate heights for which no bridge exit data
	// was available. Populated when AggsenderAPIFailed is true.
	// The operator should fetch each cert from the agglayer admin API using
	// the provided CertID, then supply a JSON override file.
	MissingCerts []MissingCertInfo

	// Deprecated: use MissingCerts instead. FailedCertHeight is the first height
	// for which no bridge exit data was available.
	FailedCertHeight uint64

	// Deprecated: use MissingCerts instead. FailedCertID is the cert ID for
	// FailedCertHeight, if it could be resolved.
	FailedCertID common.Hash
}

DiagnosisResult holds the complete output of the diagnosis phase.

func Diagnose

func Diagnose(ctx context.Context, env *Env) (*DiagnosisResult, error)

Diagnose compares the AggLayer's settled L1 state against L2's on-chain bridge state, classifies the divergence into one of 4 runbook cases, and returns a DiagnosisResult.

func (*DiagnosisResult) IsCompleteNoDivergence

func (d *DiagnosisResult) IsCompleteNoDivergence() bool

IsCompleteNoDivergence reports whether diagnosis completed successfully and confirmed there is no divergence between settled L1 state and the L2 bridge.

type Env

type Env struct {
	// L2Client is the L2 Ethereum RPC client.
	L2Client *ethclient.Client

	// BridgeService is the aggkit bridge service REST client.
	BridgeService bridgeServiceClient

	// AgglayerClient is the gRPC client for the AggLayer node.
	AgglayerClient agglayer.AgglayerClientInterface

	// AggsenderRPC is the JSON-RPC client for the running aggsender process.
	AggsenderRPC aggsenderRPCClient

	// BridgeExitsOverride is loaded from CertificateExitsFile if configured.
	// nil when no override file is specified.
	BridgeExitsOverride *BridgeExitsOverride

	// L2Bridge is the bound L2 bridge contract.
	L2Bridge l2BridgeContract

	// L2NetworkID is the network ID of the L2 chain.
	L2NetworkID uint32

	// Config holds the loaded configuration.
	Config *Config
	// contains filtered or unexported fields
}

Env holds all connections and contract bindings needed by the backward/forward LET tool.

func SetupEnv

func SetupEnv(ctx context.Context, cfg *Config) (*Env, error)

SetupEnv dials L2, initialises contract bindings, bridge service, agglayer, and aggsender clients.

func (*Env) Close

func (e *Env) Close() error

Close closes the L2 RPC connection.

type MissingCertInfo

type MissingCertInfo struct {
	// Height is the certificate height that is missing.
	Height uint64

	// CertID is the agglayer CertificateId for this height, if it could be
	// resolved via the public gRPC. Zero-value when not resolvable.
	CertID common.Hash

	// CertIDResolved is true when CertID was successfully resolved.
	// When false, the operator must contact the agglayer admin.
	CertIDResolved bool
}

MissingCertInfo describes a certificate height for which bridge exits could not be obtained from any available source.

type RecoveryCase

type RecoveryCase string

RecoveryCase classifies the divergence between the L1 settled LET and the L2 bridge state.

const (
	// NoDivergence indicates L1 settled state and L2 on-chain state are in sync.
	NoDivergence RecoveryCase = "NoDivergence"
	// Case1 is ForwardLET only — a single divergent leaf batch, no extra L2 bridges.
	Case1 RecoveryCase = "Case1"
	// Case2 is BackwardLET + ForwardLET — single divergent leaf + extra real L2 bridges.
	Case2 RecoveryCase = "Case2"
	// Case3 is ForwardLET only — multiple divergent leaf batches, no extra L2 bridges.
	Case3 RecoveryCase = "Case3"
	// Case4 is BackwardLET + ForwardLET — multiple divergent leaves + extra real L2 bridges.
	Case4 RecoveryCase = "Case4"
)

type UndercollateralizedToken

type UndercollateralizedToken struct {
	TokenOriginNetwork uint32
	TokenOriginAddress common.Address
	Amount             *big.Int
}

UndercollateralizedToken tracks the net under-collateralization amount per token.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL