Skip to content

Release 0.8.0

Choose a tag to compare

@elasticLove1 elasticLove1 released this 06 Nov 10:05
· 5 commits to main since this release
51a15b0

[0.8.0] – 2026-11-06

Breaking Changes

  • Shellnet runs on a new AckiNackiBridge, 0xcEe5D884BC1b7F4CC84A31FEA74f3C8809D19Bd5, on
    Arbitrum Sepolia from block 326573145, with eccUSDCBridge 1.15.0. eccUSDCBridge no longer
    trusts 0x3FB082a905a895151224F6d93bD074E816338638. Set BRIDGE_ADDRESS,
    BRIDGE_DEPLOY_BLOCK and FROM_BLOCK as README.md lists them.
  • AAVE is mandatory, automatic, and Arbitrum Sepolia only (#142). Redeploy the bridge:
    storage slots from owner on move down by one (supplyMultiplierBps is slot 5), and the
    constructor reverts InvalidAaveAddress when either AAVE address is zero, on every chain.
    • supplyToAave(uint256) is removed, and with it the errors AaveDisabled and
      NothingToSupply. A keeper that called it has nothing left to call. deposit now keeps
      liquidReserveBps of the book liquid and supplies the rest once the liquid principal reaches
      supplyMultiplierBps times that; below it a deposit makes no pool call. A pool that refuses
      never fails a deposit: it emits AaveSupplyRefused and the USDC stays on the bridge. USDC sent
      to the bridge directly is never supplied.
    • withdrawByProof pays from the bridge's USDC while the liquid principal covers the payout.
      Otherwise it pulls from AAVE what the USDC on hand cannot pay — if the pool cannot return that,
      the payout reverts and its nullifier stays unused, as before — and tries to pull enough more to
      leave the target of the new book liquid; a refused refill emits AaveRefillFailed and the
      payout still goes through.
    • setLiquidReserveBps reverts ReserveBpsTooLow below 1_000 (10 %); it and
      setSupplyMultiplierBps revert ReserveBandTooWide(reserveBps, multiplierBps) when their
      product exceeds 100 %, and setSupplyMultiplierBps reverts MultiplierTooLow at 1× or below.
    • excessUsdc() returns the liquid USDC above the principal that a pool loss does not need,
      min(U + S − B, U + A − B): USDC sent to the bridge is skimmable while principal sits in the
      pool, and never while it covers a loss.
    • DeployRealBridge deploys on Arbitrum Sepolia (421614) only, on the AAVE market there: pool
      0xBfC91D59fdAA134A4ED45f7B584cAf96D7792Eff, aToken
      0x460b97BD498E1157530AEb3086301d5225b91216, Circle USDC
      0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d, from script/ArbitrumSepoliaAave.sol with no
      override. Chain ids 1 and 11155111 and USE_AAVE are gone; USE_AXIOM_ORACLE=true reverts.
      deployment_real.json gains aave_pool and aave_ausdc. DeployShellnetE2EBridge gets the
      same market on 421614 and reverts on any chain but 421614 and anvil; DeployTestBridge runs on
      anvil only and needs USDC_ADDRESS.
    • DeployReuseVerifiersBridge.s.sol and crates/bridge-prover-libraries/scripts/deploy_bridge_bundle.sh
      are removed. Deploy with DeployShellnetE2EBridge on Arbitrum Sepolia, fed by
      compute_bridge_anchors, as tests/e2e/local-net/prover-host/jobs/deploy.sh does.
    • Replacing the live 421614 bridge 0x3FB082a905a895151224F6d93bD074E816338638 follows
      docs/aave-yield.md; it predates migrateTreasury, so the new bridge's owner covers its
      treasuryBalance() with fundTreasury. Before migrateTreasury on a bridge with this change,
      pull the position back with withdrawFromAave(type(uint256).max) (TreasuryNotLiquid
      otherwise).
  • A withdrawal proof names the one EVM bridge that may pay it (bridge-audit#140). A second
    AckiNackiBridge on the same chain for the same Acki Nacki bridge could pay a withdrawal the
    first had already paid; it no longer can, and neither bridge has to be paused for that. Burns
    made before the switch below are paid by the bridge they named. Upgrade in this order:
    eccUSDCBridge on Acki Nacki, then a newly deployed AckiNackiBridge with the new verifier,
    then setEvmBridge for its chain, then the relayers and ackinacki-bridge. Replacing an
    existing bridge closes withdrawals to its chain (setEvmBridge(<chainId>, 0)) until the old
    bridge has paid the burns bound to it and its principal has moved; deposits stay open. The
    order is in docs/aave-yield.md, "Replacing a bridge on the same chain".
  • Circuit 4 verification key rotated. Withdrawal proofs built for the previous key do not
    verify against the new BridgeWithdrawalAggregatorVerifier, and new proofs do not verify
    against the old one. BridgeWithdrawalAggregatorVerifier.{bin,sol} and its
    _calldata.bin are regenerated; the vkDigest pinned in ShplonkDeployLib
    (WITHDRAWAL_VK_DIGEST) and the Yul code hash (WITHDRAWAL_YUL_CODEHASH) change with them.
    The verifier is immutable in AckiNackiBridge, so a bridge needs a new deployment. Circuit 4
    has twelve public inputs, the twelfth being dstBridge; proof_event_*.json carries twelve
    public_instances_hex, the aggregated calldata is 3 712 bytes with the inner-VK digest at
    word 24, and the witness JSON is schema_version 2. Event keys under params/ carry circuit
    revision 4: the first prover or CLI run regenerates event_pk.bin / event_vk.bin. Burns made
    before the upgrade are proven with the release their bridge was deployed for.
  • eccUSDCBridge 1.15.0 (code hash 71074c89af517458c73566d3e84937279547622e01e827eb9bf7161e156135a3):
    WithdrawalInitiated gains uint160 dstBridge between tokenId and sender (event id
    0x728804fc); WithdrawalRefused is unchanged. New owner call setEvmBridge(dstChainId, evmBridge) (0 clears) and getter getEvmBridge(dstChainId). A withdrawal to a chain with no
    EVM bridge set is refused with reason 239 (ERR_NO_EVM_BRIDGE) and burns nothing. The binding
    is not carried through updateCode: call setEvmBridge for every chain right after the
    upgrade, before withdrawals reopen. The zerostate binds BRIDGE_ZS_L1_BRIDGE for
    BRIDGE_ZS_L1_CHAIN_ID.
  • AckiNackiBridge.withdrawByProof: WithdrawalPublicInputs gains dstBridge as its last
    field, so the selector is 0xc7d38069 (was 0xa9753d18) and
    IBridgeWithdrawalVerifier.verifyWithdrawal is 0xbcd3e6c9. A proof whose dstBridge is not
    the bridge's own address reverts WrongDstBridge(uint256). Tooling that encodes the call by
    hand needs the twelve-field tuple.
  • ackinacki-bridge withdraw needs eccUSDCBridge 1.15.0. Before burning it requires
    getEvmBridge(<chain>) to equal --bridge-address and stops with exit 2 otherwise, naming
    the bridge Acki Nacki binds that chain to. After the burn it checks that the captured event
    names --bridge-address and stops with exit 13 otherwise, before proving; re-run with the
    bridge the message names and --allow-retry — the idempotency key does not include the
    bridge, so the run resumes without a second burn; without the flag it stops with exit 3. Deposits still work against 1.14.0.
  • relayer daemon-withdraw and relayer daemon-bridge park a proof_event_*.json whose
    dstBridge is another bridge without sending it, and treat WrongDstBridge as permanent;
    relayer submit-withdraw and relayer withdraw-e2e refuse such a proof. bridge-verifier-daemon
    accepts twelve public instances and refuses eleven.
  • deposit-relayer arbitrum-anchors and deposit-relayer arbitrum-vector move to eth-lc-relayer
    (arbitrum-anchors needs a build with --features live-submit): change the binary in the unit
    file and drop deposit-relayer's global --state if the command line has it; the subcommands'
    flags and environment variables carry over; the ARBITRUM_ANCHOR_STATE file gives way to the
    state directory of the next entry.
  • EthBeaconLightClient and ArbitrumBlockVerifier are built with the sold 0.81.1 release and
    move to contracts/an/0.81.1_compiled/exchange/ (code hashes d0fa5bca…, 7bf4ec29…); ABIs
    are unchanged, and the relayers build them in from the new folder. Rebuild with
    make SOLD_0_81_1=<sold 0.81.1>.
  • AckiNackiBridge constructor: BridgeWithdrawConfig gains a last field, predecessor —
    the bridge on the same chain whose treasury this one may take over with migrateTreasury /
    fundTreasury, or zero. A non-zero one must have code on this chain and the same
    WITHDRAW_DAPP_FR / WITHDRAW_ACC_FR, else the deploy reverts InvalidPredecessor; it need not
    be paused. New view: predecessor(). Update tooling that encodes the constructor; deployed
    bridges are unchanged. The bridge deploy scripts read it from PREDECESSOR_BRIDGE (optional,
    zero by default); deploy_bridge_bundle.sh requires PREDECESSOR_BRIDGE to be set, to the zero
    address if there is none.
  • Every long-running service takes three directories and writes nowhere else
    (bridge-audit#132): --state-dir, --work-dir, --log-dir, each with an environment
    variable (DEPOSIT_RELAYER_*_DIR, ETH_LC_RELAYER_*_DIR, ARBITRUM_ANCHORS_*_DIR,
    BRIDGE_WORK_DIR and BRIDGE_LOG_DIR beside BRIDGE_STATE_DIR for relayer daemon-live).
    The state is a directory now: deposit-relayer daemon --state / DEPOSIT_RELAYER_STATE,
    eth-lc-relayer daemon --state, eth-lc-relayer arbitrum-anchors --anchor-state /
    ARBITRUM_ANCHOR_STATE and relayer --state / RELAYER_STATE_PATH are gone. An old
    command line stops: without the directory flags clap names the missing ones, and an old
    --state flag or *_STATE variable beside them fails with the migration hint. Move each
    state file into
    its service's state directory (deposit-relayer-state.json, eth-lc-relayer-state.json,
    arbitrum-anchors-state.json, relayer-state.json next to prover_state.json); the
    daemons name the files; deposit-relayer, eth-lc-relayer daemon and arbitrum-anchors
    refuse to start when they find other JSON files in their state directory but not their own
    (relayer daemon-live re-reads the bridge's on-chain state at start, so its cursor file is
    rebuilt). deposit-relayer status and prove-one, eth-lc-relayer prove-one,
    set-committee, submit-rotate and the other relayer daemon modes take the directory flags
    too; set-committee and submit-rotate take the daemon's state lock, so the daemon is
    stopped while they run. The provers' files go under the work directory instead of the system
    temp directory, their transcripts and each daemon's own log (a file per day, the newest 14
    kept, beside stdout) into the log directory.
  • The contract ABIs are built into the binaries: AN_BRIDGE_ABI_PATH,
    AN_ARBITRUM_VERIFIER_ABI_PATH, AN_LC_ABI_PATH, AN_USDC_ABI_PATH and their flags are
    removed from deposit-relayer and eth-lc-relayer; crates/eth-light-client-relayer/abi/
    is gone. Remove the variables from the unit files.
  • The Docker Compose kit moved from crates/bridge-relayer-daemon/deploy/shellnet-l2 to
    docker/ and covers every service (bridge-audit#135): docker/compose.yaml runs
    daemon-live, deposit-relayer, eth-lc-relayer and arbitrum-anchors from the images
    docker/Dockerfile builds, with the one-shot commands preflight, lc-keygen,
    fold-bk-set and cli under --profile ops. The old compose.yaml, compose.env.example
    and Dockerfile are deleted; the scripts are docker/relayer/, the runtime env template is
    docker/env/daemon-live.env.example, the compose service relayer is daemon-live. The
    BK set file is no longer baked into the image: it is mounted from $BRIDGE_OPT/keys/bk_set.json,
    and EXPECTED_BRIDGE_COMMIT is the commit of the image. EXPECTED_RELAYER_SHA256 and
    EXPECTED_AGGREGATE_PROOF_SHA256 are optional. The layout and the migration are in
    docker/README.md.
  • eth-lc-relayer daemon proves the committee rotate itself (bridge-audit#145). With rotate on
    (the default) it needs two more prover binaries beside --rotate-prover-bin:
    --shard-prover-bin / LIGHT_CLIENT_SHARD_PROVER_BIN (export_shard_snark) and
    --step-snark-prover-bin / LIGHT_CLIENT_STEP_SNARK_PROVER_BIN (export_step_snark), all three
    built from eth-light-client-prover with --features aggregation, and two SRS files in
    --prover-dir: kzg_bn254_21.srs (the release asset of that name) and kzg_bn254_20.srs,
    cut from it once with downsize_srs --input kzg_bn254_21.srs --output kzg_bn254_20.srs --k 20
    (eth-light-client-prover's, shipped in the bridge-eth-lc-relayer image). Without them it
    refuses to start and names what is missing; --no-rotate starts it without.
    scripts/ursus/eth-light-client-relayer.service and its env example carry the new binaries
    and the --state-dir / --work-dir / --log-dir flags (ETH_LC_RELAYER_*_DIR in the env
    file) in place of --state: update an installed unit before restarting it. In Docker the
    bridge-eth-lc-relayer image ships the binaries; put kzg_bn254_21.srs into
    $BRIDGE_DATA/params/lc-prover/ and cut kzg_bn254_20.srs beside it (docker/README.md,
    Install, item 2). The daemon no longer auto-rotates the one committed fixture period.
    Verification keys are unchanged.
  • export_shard_snark, export_step_snark and rotate_tree_n8 no longer fall back to an SRS
    under data/: SHARD_SRS_PATH, STEP_SRS_PATH and OUTER_SRS are required, and STEP_SRS
    with a step snark. rotate_tree_n8 fails when STEP_DIR is set and holds no
    step_proof.bin, instead of building the tree without the step snark, which is a different
    circuit than the one the light client pins.

New / Improvements

  • eth-lc-relayer daemon starts the rotate of period P as soon as the contract holds P and the
    beacon serves light_client/updates?start_period=P, in the background while the steps of P go
    on. Its stages run as child processes and keep their inputs and outputs in
    <work-dir>/rotate-<P>/: a restart re-runs only the stage that had not finished, and the
    directory goes once the contract is past P. The stages' proving keys are written to
    <prover-dir>/rotate-keys/ on the first rotate after an install or a binary upgrade and
    reused after that. Before submitRotate the daemon checks the bundle: a bundle for a period the
    contract has left is dropped, and one whose verification key is not the pinned
    rotate_vk_blob.bin or whose current committee is not the contract's is logged and not sent.
    Each stage is killed after --rotate-timeout-secs (default 10800) and on daemon shutdown;
    transcripts are rotate-<P>-<stage>-<time>-{stdout,stderr}.log in --log-dir. A failed
    stage is started again after 10, then 20 minutes, from the beacon node's answers fetched anew;
    after the third failure in a period the daemon leaves that period alone. A submitRotate
    whose outcome stays unknown no longer holds up the steps: it is sent again on the next ticks
    until it lands or the contract moves past it. A bundle proven for another Gloas fork slot than the contract's
    is proven again instead of sent. To prove a period again by hand, stop the daemon, remove
    <work-dir>/rotate-<P>/ and start it. The prove takes about 1.5 h; the tree stage peaks at
    about 50 GB RSS and the step prover keeps running beside it, so plan for about 75 GB (a
    container memory limit has to sit above that). rotate-keys/ takes about 40 GB of disk. The
    nodes need the tvm-sdk#284 decider (tvm-sdk v3.1.0.an).
  • --owner-hop works with rotate on: at the first update of a new period without a sent rotate,
    the daemon logs an error naming why (still proving, failed, refused) and falls back to the owner
    hop; a rotate sent and not yet confirmed gets five sends first. Once the contract refuses
    the hop (after disableOwnerRotation) the daemon stops hopping and proves the rotate past the
    boundary instead; take --owner-hop off when disabling owner rotation. It no longer requires
    --no-rotate. docker/compose.yaml runs eth-lc-relayer that way
    (--owner-hop --rotate-timeout-secs 10800, no --no-rotate).
  • export_shard_snark, export_step_snark and rotate_tree_n8 prove a live hand-off:
    ROTATE_UPDATE_PATH names the beacon update of period P (/updates?start_period=P) and, for
    export_step_snark, COMMITTEE_JSON_PATH the update of P−1, whose next committee signed it.
    ROTATE_KEY_DIR keeps their proving keys between runs; a key written by another build is
    regenerated, not used.
  • AckiNackiBridge.migrateTreasury(address successor) (owner, paused only) sends the whole
    treasuryBalance to a contract whose predecessor() returns this bridge; emits
    TreasuryMigrated. The bridge cannot tell a real successor from any contract giving that answer:
    the owner key decides where the book goes. Pull AAVE principal back first (TreasuryNotLiquid).
  • AckiNackiBridge.fundTreasury(uint256 amount) (owner or predecessor) adds USDC to
    treasuryBalance with no Deposit event and no MAX_BRIDGE_TVL check; emits TreasuryFunded.
  • deposit-relayer needs an EVM RPC that serves eth_getLogs and transaction receipts for at
    least the last 7 days (30 preferred); one that keeps ~10 000 blocks cannot prove a deposit
    older than that. Check RPC_URL before the next restart.
  • Images for every service, built from the sources of one commit by docker/Dockerfile:
    docker.gosh.sh/bridge-relayer, bridge-deposit-relayer, bridge-eth-lc-relayer and
    bridge-cli (.woodpecker/images.yaml: built on every pull request, pushed on main and
    on a version tag, tagged sha-<12>, latest and the release). --target tools gives
    every binary as plain files.
  • deposit-relayer reads RPC_URL, BRIDGE_ADDRESS and EXPECT_CHAIN_ID from the
    environment, eth-lc-relayer arbitrum-anchors reads BRIDGE_ADDRESS, PARENT_LOG_SPAN and
    ARBITRUM_LOG_SPAN: a service is configured from an env file alone.
  • deposit-relayer and eth-lc-relayer cut every URL in their logs and prover transcripts to
    scheme://host: RPC URLs carry API keys, and transport errors quote them.
  • ackinacki-bridge deposit --log-dir / BRIDGE_DEPOSIT_LOG_DIR keeps each prover tool's
    stderr, one file per run; withdraw keeps the aggregator's calldata under --work-dir.
  • The five *.patch files in the repository root and scripts/check_eth_beacon_lc_sources.sh
    are removed: acki-nacki takes the AN contracts through contracts/an/place.json. Nothing to do.
  • AckiNackiBridge gains setSupplyMultiplierBps(uint256) (owner) and the view
    supplyMultiplierBps(), default 20_000 (2×); it emits SupplyMultiplierBpsSet(uint256). A
    deposit supplies once the liquid principal reaches that multiple of the target. New events to
    alert on: AaveSupplyRefused(uint256 amount, bytes reason) — the pool refused a deposit's
    supply, the USDC stayed on the bridge — and AaveRefillFailed(uint256 amount, bytes reason) —
    a payout went through but the pool did not return the refill above the shortfall. reason is
    the pool's revert data. supplyForSelf(uint256) is external only for the bridge's own use and
    reverts NotSelf for any other caller.
  • DeployShellnetE2EBridge, DeployTestBridge and GenDepositSetup deploy a mock AAVE market
    on local anvil (31337). DeployShellnetE2EBridge and DeployRealBridge expose deploy(),
    which returns the bridge. The local e2e (tests/e2e/local-net) checks that the bridge is on the
    421614 market, that the deposit supplied part of its USDC, and that withdrawByProof pulled it
    back.
  • .woodpecker/solidity.yaml runs the Arbitrum Sepolia fork suite
    (AckiNackiBridgeArbSepoliaAaveForkTest) against https://sepolia-rollup.arbitrum.io/rpc on
    every pull request; an outage of that RPC fails the check.

Fixes

  • eccUSDCBridge 1.15.0 (contracts/an/0.80.0_compiled/exchange/, code hash 71074c89…):
    disableOwnerAnchorsOn(l2ChainId) for a chain with an Arbitrum block verifier throws
    ERR_PARENT_OWNER_ANCHORS_ENABLED (240) until the owner's anchors are off for the verifier's
    parent chain, and deployL2Verifier throws 240 on a closed chain for a parent chain the owner
    still anchors. Before, the owner could still reach a closed Arbitrum chain by accepting a
    parent-chain block, while isOwnerAnchorsEnabled reported it closed. Turn off Sepolia
    (or Ethereum) first, then the Arbitrum chain. updateCode drops the verifier code and the
    verifiers: install and deploy them again after the upgrade.
  • scripts/deposit_anchor_params.py --call takes only setAcceptedBlockHash: eccUSDCBridge has
    no attestBlockHash. The deposit-relayer hint on exit code 224 names the Arbitrum block
    verifier (eth-lc-relayer arbitrum-anchors) as the anchor writer for an Arbitrum chain.
  • eth-lc-relayer daemon no longer hangs on a rotate prover that prints more than a pipe
    holds: both streams are read to the end, and its transcript is written like the step
    prover's.
  • Version tags publish their release assets again (ackinacki-bridge-linux-x86_64.tar.gz,
    kzg_bn254_21.srs, kzg_params_18.srs, SHA256SUMS). v0.3.0 to v0.7.0 have none;
    install.sh needs a tag cut after this change.
  • relayer daemon-live no longer stalls after a late applyBkSetUpdate or verifyBlock receipt,
    or when its bundle cursor is behind a rotation the contract already accepted. If the receipt is
    not back within 180 seconds, the update counts as applied when the contract already shows that
    seq_no — and, for a rotation, the new BK-set commitment. A deferred rotation ack no longer fails
    the tick: the daemon acks the bundles the contract already has, then rotates. The rotation
    proof already built for that ack is kept until the cursor passes it, instead of being proved
    again on every tick of the catch-up. Restart the relayer. Do not delete prover_state.json
    because of this stall. Verification keys are unchanged.
  • A withdrawal in the last 128 blocks before an L2 boundary can be proven.
    The Circuit 4 witness put that boundary's own L2 root into leaf 0 of its L1
    tree (every 16 384 blocks). The rebuilt root did not match the root the bridge
    stores, so withdrawByProof reverted UnknownAnchor on an L1 anchor, and an
    L2 anchor failed while the witness was built. The USDC was already burned on
    Acki Nacki. Leaf 0 is now the L2 root of the previous L2 boundary, or 32 zero
    bytes at the first boundary (block 16 384). A missing layer root fails the
    build instead of becoming a tree of zeros, and the rebuilt L1 root must match
    the root the key block published. No verification key changes.
    A proof_event_*.json already built with the old leaf 0 still makes
    withdrawByProof revert UnknownAnchor. relayer daemon-withdraw and
    relayer daemon-bridge treat that revert as transient and submit the same
    file again. Move those files out of --proofs-dir. Prove the withdrawal
    again with ackinacki-bridge withdraw --allow-retry (the burn is already
    recorded), or let daemon-withdraw / daemon-bridge pick up the new
    proof_event_*.json. Do this while that L2 root is still inside the bridge
    window (about 8 days at 3 blocks/s).
  • eth-lc-relayer daemon exits cleanly on SIGTERM, as it does on SIGINT.
  • eth-lc-relayer arbitrum-anchors and arbitrum-vector give up an RPC request after 60 s and
    retry on the next tick, instead of waiting forever on a node that does not answer.
  • eth-lc-relayer arbitrum-anchors locks its --anchor-state file (<file>.lock next to it) and
    refuses to start while another instance holds it.
  • ackinacki-bridge accepts bridges 1.14.0 and newer (MIN_BRIDGE_VERSION): a release build works
    with the deployed bridge, and release.yaml publishes the binaries again.
  • A bridge on a fresh network can be seeded at the network's genesis instead of waiting for the
    first 16384-block boundary: compute_bridge_anchors --seed-seqno 0 --level 2 prints the genesis
    (zero anchor, the BK set file's commitment) without reading the chain, and relayer daemon-live
    with BRIDGE_BOOTSTRAP_SEQNO=0 proves its first bundle at seq_no 16384. L2 anchoring only.
  • A bridge seeded on a key block of higher order than its anchor level (L2 on a W³ boundary, L1 on
    a W² boundary) no longer stalls verifyBlock at the next boundary of that order. On a
    prover_state.json an earlier build seeded that way, relayer daemon-live and
    bridge-prover-daemon refuse to start and name the layer: for relayer daemon-live remove the
    file and restart; for bridge-prover-daemon move it and prover_bk_set.json aside and restart
    with BRIDGE_BOOTSTRAP_SEQNO set to the bridge's seed seq_no.