bridge-sdk: aleo-bridge-sdk Python package (lifecycle verbs, agent tools, live suite) - #71
Open
iamalwaysuncomfortable wants to merge 94 commits into
Open
iamalwaysuncomfortable wants to merge 94 commits into
iamalwaysuncomfortable wants to merge 94 commits into
Conversation
…payload and hook data
…s) with typed views and validation
…nd fake facade fixtures
…nfirmation timeouts
…mported modules del sys.modules[...] permanently replaced aleo_bridge's module/class objects for the rest of the pytest session, breaking isinstance checks in any test file that runs later alphabetically and compares against classes imported at collection time. Use monkeypatch.delitem so the original modules are restored at teardown.
…estation client, nullifier read
…eo-only profile, CLI entry point
…o and verify the root on chain
…d nullifier reads, registry drift) and README
Add checkpoint.py: Checkpoint (frozen dataclass with to_dict/to_json and from_dict/from_json round-tripping), create_checkpoint(plan, receipt, registry) reducing a Receipt to the documented recovery allowlist (intent, route, source/destination transaction ids, delivery verification) while excluding keys, record plaintext, secretNonce, attestation bodies, payloads, message hashes, nonces and quote internals, CheckpointStore protocol, and FileCheckpointStore (one 0600 file per sanitized receipt id, atomic temp+os.replace writes). Export Checkpoint/CheckpointStore/FileCheckpointStore/create_checkpoint from __init__.py. Update the two test_client.py cases that were placeholders for plan 4's checkpoint wiring (BRIDGE_CHECKPOINT_DIR, Profile.checkpoint_dir) now that checkpoint.py exists and client.py's lazy imports resolve.
…e JSON-RPC web3 double Adds aleo_bridge.eth.Ethereum (rpc_url+private_key, w3+signer, or bare w3 read-only/default-account forms), cached chain_id, send_transaction (local signer or default-account path), wait_for_receipt/get_receipt, and Ethereum.from_env. web3/eth_account stay lazily imported so the package still imports without the evm extra. Adds tests/fakes/fake_web3.py: a real Web3 over a hand-rolled JSON-RPC provider so tests exercise real signing/calldata against fake transport state, reusing tests.conftest.FakeAleo rather than a second Aleo fake. Updates the two plan-1 placeholder assertions in test_client.py that anticipated this change (Bridge.eth / Bridge.from_env now construct a real Ethereum connection instead of raising MissingExtraError).
…ABI fragments, _plan_for
private_burn's record plaintext (input 0) and private_mint's secret nonce (input 3) could end up in logs via repr(). Print only the input count; .inputs stays the explicit accessor for callers that need the values.
… ConfigurationError Receipt.replace() forwarded protocol_state/next_action straight to dataclasses.replace() unless the caller overrode them, so a replace() copy aliased the original's dicts and mutating one leaked into the other. setdefault() a shallow copy of each first. to_progress()'s missing-routeId check raised a bare ValueError; every other failure in this package is a BridgeError subclass, so make this one ConfigurationError too (same message).
_verified_tree() previously fell back to an unverified (possibly empty) Merkle proof whenever the on-chain freeze_list_root was unreadable, which would silently hand out a proof against the wrong tree. Raise ConfigurationError instead and tell the caller to pass merkle_proof= explicitly. freeze_list_program()'s import-lookup also narrowed its except clause from bare Exception to (ProgramNotFound, AleoError), so a transient RPC failure of another type propagates instead of silently falling back to the static freeze-list table. Updated every fake facade setup that expects a proof to seed usdcx_freezelist.aleo/freeze_list_root[1u8] with the matching root (EMPTY_TREE_ROOT for an empty list), and added tests for the unreadable-root and unrelated-exception-propagates cases.
…e 0700 Profile.load_or_create()'s first-write path used a tmp-file-plus-rename, which is atomic but not exclusive: two processes racing a fresh $ALEO_BRIDGE_HOME could each "win", the second silently overwriting the first's already-in-use key. Open profile.json with O_EXCL instead; on FileExistsError, load whichever profile got there first instead of clobbering it. The home directory is now created (or healed) at mode 0700, matching the file's existing 0600.
…tate burn() silently ignored record=/merkle_proof= for public/public-as-signer modes instead of rejecting them, and an unknown mode wasn't caught until deep inside build_burn_inputs (after route/amount/chain work already ran). Validate mode first, and reject record=/merkle_proof= for any non-private mode with a ConfigurationError. Also de-duplicated the Aleo-chain lookup: XReserveModule._aleo_chain_id() and HyperlaneModule._aleo_chain() were exact copies of Bridge.aleo_chain(); both modules now call the client's version instead of keeping their own.
- units._DECIMAL_RE: \d -> [0-9] (Python's \d matches any Unicode decimal
digit, not just ASCII).
- encoding.xreserve_deposit_nonce: bound source_domain to uint32 before
encoding it, consistent with the payload's remote_domain check.
- registry.Asset.matches_address: re.fullmatch instead of re.search, so a
trailing newline can no longer sneak past a "$"-anchored regex.
- encoding's hex/shape/width checks (hex_to_bytes, bytes32_to_u128_limbs,
solana_address_to_hyperlane_recipient, xreserve_deposit_payload's
per-field width check) now raise InvalidRecipientError, and _uint_be
raises InvalidAmountError, instead of bare ValueError — all BridgeError
subclasses, same remedy messages. Updated the callers (circle.py,
hyperlane.py, xreserve.py) that caught the old ValueError type, and the
tests that asserted on it. xreserve.get_attestation("nope") now raises a
BridgeError instead of a naked one.
- client.checkpoints_from_env: removed the dead ImportError branch (and its
"plan 4" wording) now that checkpoint.py always exists.
- client.balance_program: one-line comment recording that arc20_<sym>.aleo's
balances mapping is verified to be what the warp/xreserve programs'
mint_public/burn_public spend.
- hyperlane.py: removed the unused Callable import.
…and checkpoint-before-poll ordering - fake_web3._decode_raw now keeps nonce/gas/gasPrice/maxFeePerGas/maxPriorityFeePerGas from the signed raw tx instead of discarding them, so tests can assert send_transaction's exact fee-filling arithmetic (gas = estimate * 1.2; EIP-1559 maxFeePerGas = baseFee * 2 + tip). - FakeRpcProvider gains a legacy=True knob that omits baseFeePerGas from eth_getBlockByNumber, exercising eth.py's previously-unreachable legacy gasPrice branch. - FakeRpcProvider gains receipt_delay/receipt_poll_counts so a receipt can stay pending for N polls per hash; test_evm_call's ordering test now proves each checkpoint fires with zero receipt polls for its own hash, that polling only starts after, and that approvals fully confirm before the main call is broadcast. No production code changes: eth.py's fee formula and _calls.py's checkpoint-before-poll ordering were both already correct; the gap was in test strength only.
…e split and chain assert
… row from the registry Bridge(solana=...) now takes an RPC URL string as a read-only connection and refuses any object that is not a solana-py client with a ConfigurationError, instead of wrapping it and failing later with an AttributeError from the first RPC read. status() appends the Solana row only when the environment actually has a chain with family == solana, and takes the chain id and native asset id from that chain rather than the 'solana' / 'solana/sol' literals — a testnet client no longer reports a chain the registry does not define for it.
…m __exit__ _AsyncClientAdapter.close() now awaits the wrapped client's own close() on the private loop before stopping it — solana-py's AsyncClient owns an aiohttp session that can only be closed from its own loop — and closes the loop only once the thread has actually exited, instead of raising on a loop that is still running. SolanaRpcClient gains close(), so Solana.close() releases the HTTP pool of the DEFAULT transport too, and Solana.__exit__ swallows whatever close() raises so releasing a transport cannot replace the caller's own exception.
… at build, accept a plan alone M7: keypair_from_private_key raises its ConfigurationError outside the except block on both the JSON and base58 paths, so neither __cause__ nor __context__ survives — a chained JSONDecodeError carries the whole document in .doc, which IS the private key. M8: _build_transaction re-resolves the route through outbound_route() instead of the one snapshotted when transfer_remote() was called, and refuses a route that no longer matches the quoted plan; the instruction can no longer be compiled against a stale deployment. M11: recipient is optional on quote_transfer_remote/transfer_remote — a plan supplies it along with the amount and sender. Neither plan nor recipient raises InvalidRecipientError, plan with sender= or a differing amount raises ValueError (an identical amount is tolerated), mirroring EthModule's rule.
…arations README lists BRIDGE_SOLANA_PRIVATE_KEY / BRIDGE_LIVE_SOLANA_RPC_URL as aliases with the same caveat as the Ethereum pair: they are not live-test-only, so an exported alias points every from_env() call at that endpoint. Removes _sealevel.ALEO_MAINNET_HYPERLANE_DOMAIN (nothing imported it; the domain comes from the route metadata) and the unused decimals parameter of SolModule._make_plan.
…unk_blocks on assignment Without plan=, eth.quote_transfer_remote/transfer_remote/quote_deposit_usdc/deposit_usdc now raise InvalidRecipientError instead of letting recipient=None reach _recipient_bytes32 as an opaque TypeError (deposit_usdc did not fail until send()). log_scan_chunk_blocks becomes a validating property so eth.log_scan_chunk_blocks = 0 cannot make _scan_logs spin forever; the constructor runs through the same check.
…gs and EVM/Solana dispatch execute() commits the source-chain legs of a prepared Plan and returns Progress. Aleo legs prove (DPS or local), checkpoint the exact prepared transaction BEFORE broadcasting it, submit with wait=False and checkpoint the id; EVM and Solana legs go through the modules' own plan= surface so the route, registry version, sender and every plan field are re-validated where the transaction is built. A private xReserve mint now requires its own secret_nonce (never the 0scalar default), the Hyperlane hook payment is re-quoted at the last moment unless pinned, and a destination-balance baseline is captured only when the destination connection is the recipient. _Emitter feeds both checkpoint channels: a module's already-reduced Checkpoint and execute()'s own post-send emission reduce to the same value, so the caller sees each boundary once and the bound store keeps exactly one record per transfer, superseding the id it replaces. The write-side fakes gain the real plan= signatures, and lifecycle.quote now calls bridge.sol.quote_transfer_remote(plan=plan) without the positional recipient workaround.
… adapter close survives a raising client R1: a node that echoes a different hash still ANSWERED, so the signed bytes may sit in its mempool under the hash we computed — the same ambiguity as a lost response. The mismatch branch now carries broadcast_id, so EvmCall refuses a resend instead of signing a second transfer. R2: _AsyncClientAdapter.close() bounds the wrapped client's close() by the timeout, swallows its failure and stops the loop in a finally, setting _closed only once that stop is issued — a third-party transport that cannot close no longer leaves the private loop thread running with a second close() turned into a no-op. R3: comments the default-account path, where the hash is not knowable until the node answers, so none of these protections apply; a local signer is the right choice for funds-moving calls.
…ound xReserve and balance-diff fallback
…connected address, and make destination-balance reads best-effort Task 4 review carry-overs: _persist now saves (or drops, if terminal) the new checkpoint before deleting the id it supersedes, never the reverse, so a crash between the two steps can't leave the store holding neither. A module-emitted checkpoint's supersede is deferred on the _Emitter (_pending_supersede) until the module has actually saved it -- flushed at the next emission or via execute()'s new finalize() call. Aleo hyperlane/xreserve legs now call _assert_sender against a guarded bridge.aleo_address() before proving, refusing a plan built for a different account. _read_destination_balance wraps its RPC read in try/except so an advisory baseline read never blocks funds movement.
…or tolerance and timeout-as-not-failure Polls get_status() to a caller boundary (types.CALLER_BOUNDARIES, extendable via until=) or a terminal state, tracking observed changes into the bound checkpoint store and firing on_update only when the receipt actually changed. A transient read error (flaky RPC/HTTP transport) is retried with the normal poll interval up to max_consecutive_errors (default 5) before the last one is re-raised; on_error lets callers log the retries. Everything else propagates on the first attempt. Hitting timeout_seconds raises PollingTimeoutError carrying the last status/progress -- a timeout is not a failure, the transfer is still in flight.
…swallowing a missing Solana connection _message_id no longer falls back to receipt.id when it equals receipt.source_tx_id (an EVM Hyperlane receipt whose DispatchId log was unreadable carries the tx hash as its id, not a message id). get_status's Solana message-id fill-in now resolves the sol module outside the log-read try, so a missing Solana connection raises ConfigurationError instead of being swallowed as "still unavailable". Also adds the carried "delivered wins" regression test for get_status's inbound xReserve nullifier-first check on ATTESTATION_PENDING.
…t signing Appends recover(bridge, checkpoint) plus its helpers (_coerce_checkpoint, _plan_from_intent, _assert_prepared_id, _finish) to lifecycle.py. Accepts a Checkpoint, its dict, or its JSON; rebuilds the Plan via prepare() against the live registry and validates route id + registry version before touching any chain. Branches on source-chain family: Aleo (prepared-but-unbroadcast resumes with no network read; submitted gets one get_status refresh), Solana (validates the blockhash pair and reads source_status once), EVM Hyperlane (delegates to eth.recover_source(plan, cp, required=False)), and inbound xReserve (delegates to eth.recover_source, then layers a submitted or prepared private-mint destination leg on top). Checkpoint deletion on a terminal outcome is keyed on the checkpoint's own id, never the receipt's (they diverge once a source tx confirms into a message id). Adds tests/test_recover.py (8 tests) covering checkpoint validation, all three source-chain families, xReserve destination recovery paths, unsupported routes, terminal checkpoint cleanup, and a round-trip check that a plan rebuilt from a checkpoint is field-identical to the plan that produced it for an EVM, a Solana, and an Aleo-origin route.
The best-effort try/except lived inside _read_destination_balance, so a flaky RPC looked identical to 'no reader configured' in get_status branch 6 - the delivery signal degraded to 'not delivered yet' forever instead of being retried by wait()'s transient classifier. Move the swallow to _delivery_verification, the one caller that genuinely must not raise (execute's advisory pre-broadcast baseline).
…rivate mint resume() finishes the source leg an interruption left unsubmitted. An Aleo leg rebroadcasts the checkpointed bytes byte-for-byte after checking the serialized payload's own id, and treats the node's duplicate answer as success (plan 1's is_duplicate_submission, re-exported as is_duplicate_broadcast_error rather than reimplemented) - so the transfer can only ever exist once on chain. An EVM leg re-scans source history with recover_source(required=True) and authorizes only the step the scan proves is still missing, keeping veil's two guards: the re-quoted hook data must equal the checkpointed approval's hook, and the allowance must still cover the deposit. Solana legs have no resumable state and say so instead of guessing. complete() submits the one user-signed Aleo transaction a private USDCx mint needs: it rebroadcasts an already-proved destination transaction, or builds the mint (the module re-verifies that (recipient, secret_nonce) opens the attested commitment, so a wrong nonce never reaches proving), proves it, checkpoints the exact bytes BEFORE broadcast, then broadcasts. secret_nonce is mandatory for a private resume/complete and is refused before any RPC; it is never written to a receipt, a checkpoint or a Progress.
… cleans up by checkpoint id
…lete never persists secrets (store-level test) The hook-equality guard in resume's xReserve branch only ran when protocol_state carried a hookData string, so a receipt without one passed it vacuously and the deposit could be re-hooked to a freshly derived commitment. Require the hook to be present and a 65-byte 0x hex string (encoding.HOOK_DATA_BYTES, the same shape EthModule._recover_xreserve enforces) in the pre-RPC block, before the history scan, so a transfer resume() cannot finish is not even scanned; the equality guard below is now unconditional. Every receipt recover() produces already carries hookData, so this only refuses hand-built, truncated or legacy ones. Also: assert at the store level that complete() writes neither the secret nonce, Circle's attestation nor the committed hook into any checkpoint file, and say in resume's docstring that a SOURCE_APPROVAL_PENDING receipt needs recover() first.
…ated writes
bridge_tools() returns the six read and five write tools in the Claude API
tools= shape; dispatch_tool() runs one and returns JSON-serializable data.
Reads are open; every write needs confirm=true and otherwise returns the quote
(or recovered progress / built call) plus how_to_confirm, having moved nothing.
bridge_execute takes the quote inputs, never a plan, and re-quotes internally;
confirmed writes hand back {progress, checkpoint} so the model can feed the
checkpoint to bridge_get_progress / bridge_resume / bridge_complete later.
Three safety rules on top of the plain surface:
- a private mint never gets a defaulted secret: mint_mode=private without a
secret_nonce (and bridge_complete without one, unless the mint is already
proved) is a structured error that quotes nothing and moves nothing;
- secrets never leave the process: rendered receipts drop the private-mint
secret, record plaintext, the Circle attestation body and the proved
transaction bytes, and no tool echoes its own arguments back;
- BridgeErrors come back as {error, error_type, how_to_fix} instead of
crashing the model's loop - unconfigured Ethereum/Solana connections are
probed up front, and an ambiguous source send also carries next="recover"
with the last checkpoint, since these calls are single-use.
EvmXReserveQuote renders a synthetic "xReserve max fee" fee entry from
max_fee_atomic, which its empty fees tuple would otherwise hide.
Also lands the exports task 9 deferred: bridge_tools, dispatch_tool and
agent_guide() (the packaged AGENTS.md, or a pointer at codegen/gen_context.py
until task 12 generates it), with the export-pinning test extended.
…cord previews, copy schemas, guide recovery on write errors
…bridge prints the guide
…the hermetic mirror Ports veil's test/integration/live/config.ts and helpers.ts: the read-only gate functions (BRIDGE_LIVE_FUNDS + BRIDGE_LIVE_STATE_DIR, the mainnet acknowledgement and case list, the separate execution acknowledgement), one_atomic_unit, the namespaced state path, and the state/benchmark/polling/explorer helpers. The private-mint secret nonce is new relative to veil: it is generated once per case, written to <state>.secret with O_CREAT|O_EXCL and mode 0600, and the state JSON records only secretNoncePresent, so a state file can be shared without leaking the commitment secret. tests/test_live_helpers.py mirrors veil's helpers.test.ts (gating truth table, state round-trip and fail-closed cases, one-atomic-unit formatting, bytea hashes, rejected Aleo transactions) and adds the secret-file mode/exclusivity checks and the explorer 429/5xx tolerance.
… verbs tests/live/cases.py ports veil's five mainnet cases as functions the pytest suite (13b) and the operator CLI both call, so the two cannot drift. Each case loads its route-qualified state file, quotes, prints the route/amount/fee table, prechecks balances (Underfunded carries the shortfall so callers can skip), and — only when the caller passes execute=True — executes once, drops the in-memory progress, recovers from the checkpoint on disk and drives wait/resume/complete to done. execute is never retried after a broadcast. Case parametrization covers every registry route per case, in both directions, with non-active routes reported as skipped-by-registry rather than dropped. veil's literals are kept: '2' USDC for the private mint, '2.000001' USDCx for the private burn, one atomic unit elsewhere, signer mode for Aleo Hyperlane. The Aleo Hyperlane IGP payment comes from our own quote rather than veil's operator-supplied fee variable. scripts/rehearse.py runs one case over its routes with --case/--route/ --quote-only/--recover/--report and exits 0/1/2 (ok/failed/pending). It only reads the acknowledgement variables, prints no line that would set one, and prints the --recover command for anything left pending.
Documents the three gates and that nothing in the repository sets them, the state and secret files, the five cases with their routes and veil amounts, the funding each run needs (from the read-only mainnet quote sweep), and how to rehearse, run and recover through scripts/rehearse.py.
…, marks, balance delta Aleo-origin cases confirm the source transaction on chain before recovering (veil aleo-hyperlane:153, aleo-xreserve:136), so a rejected execution is reported as a rejection rather than as a delivery timeout. Adds the plan-prepared and clients-created benchmark marks veil records, logs the destination balance delta once a case is done, and looks the checkpoint up in the bound store once instead of twice.
One `live`-marked test per veil case, parametrized from an enumeration of DEFAULT_REGISTRY rather than a hand-written route list: a route no case covers raises at import, and a `metadata-required` route is parametrized and skipped with `registry:metadata-required` instead of disappearing. Plus the testnet deposit (3 USDC, the amount that clears the 2 USDCx withdrawal fee) and — beyond veil — the testnet RETURN burn, so "all the routes back and forth" holds on testnet too. Every funded test crosses a real process boundary: phase one quotes, prechecks and calls `execute` once, then returns; that client is dropped and a brand-new Bridge over the same FileCheckpointStore finishes the transfer through `pending()` → `recover()` → `wait`/`resume`/`complete`. `execute` is never called twice for one transfer. Underfunded is a skip with the shortfall (also when a quote refuses first), a timeout is pending with the resume command. Harness changes the first real runs forced: * `cases.run_case(stop_after_execute=...)` — the handover point above. * Aleo→EVM xReserve delivery is a balance rise, not a drive loop. lifecycle.py says Circle exposes no delivery query for that direction and leaves the receipt in DELIVERY_PENDING, so `wait` could only ever time out (it did, for 20 minutes, on a leg whose funds had already landed). veil polls the ERC-20 balance instead; so do we now, and we recover the withdrawal tx id from the Transfer log. * config: per-environment key/RPC resolution (testnet Aleo key, Sepolia RPC, public defaults) and `BRIDGE_LIVE_XRESERVE_AMOUNT`. * Delivery is asserted as "at least what was quoted", never equality — the testnet return delivered 0.996501 USDC against a quoted 0.000001, because the live withdrawal fee was 1.0035 USDC, not the registry's 2.
README: the exact invocations for quote-only, testnet and mainnet execution (the acknowledgement values shown only as `<see tests/live/config.py>`, never copy-pasteable), the metadata-required and testnet routes, and the two-phase recovery the suite performs. pyproject: PyNaCl moves into the base dependencies. Delegated proving is the default path, and the proving request is sealed with a NaCl box before it leaves the machine — without PyNaCl the first testnet private mint raised ImportError *after* the Sepolia deposit was already on chain. That is not an optional extra.
…P surface and live evidence Reorganizes README.md around quote/execute/wait/recover/resume/complete, connections, routes, shield/unshield, Tier 2 modules, agent tools and MCP, keeping every existing fact (aliases, single-use calls, timeout-is-not-failure, checkpoint-before-poll, Solana.close(), the full live-tests gates/cases/funding tables). Adds the 2026-09-18 testnet round trip evidence and the registry-vs-live xReserve fee discrepancy. Extends tests/test_package.py with version/extras lockstep, AGENTS.md packaging and README-coverage assertions.
Mirrors build-shield-swap/release-shield-swap: builds the pure-Python wheel against the built aleo-sdk wheel, runs the hermetic suite only (pytest -q -m "not live" — no key, no live RPC), checks AGENTS.md via gen_context.py --check, and smoke-tests the wheel both with every extra and without any (MissingExtraError only at point of use, never at import). release-bridge is tag-triggered, reuses the trusted-publishing pattern, and gates on build-bridge like the other release jobs; adds bridge-sdk/** to the push/pull_request path triggers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
bridge-sdk/— thealeo-bridge-sdkPython package (import aleo_bridge), a port of veil's@provablehq/aleo-bridge-sdk0.1.0 into the web3.py-style verb structure used by our other Python SDKs. It is a standalone package: no dependency on shield-swap journal/profile/stage abstractions.2026-08-31.solana-deposits.1(7 chains, 19 assets, 22 routes; metadata literal-identical to veil).Ethereum(rpc_url | w3, private_key | signer),Solana(rpc_url | client, private_key | signer)(own sync JSON-RPC client; solana-py is async-only),Bridge.from_env()/from_profile().Bridge:quote → execute → wait / get_status → recover → resume / complete, veil's checkpoint format (version 1 allowlist, secrets excluded), timeout ≠ failure, calls are single-use after a broadcast, a lost RPC response never loses the tx id.aleo_bridge.agent.bridge_tools()/dispatch_tool()(confirm-gated writes, secrets redacted), stdio MCP server (aleo_bridge.mcp), generatedAGENTS.md(codegen/gen_context.py --checkin CI),python -m aleo_bridge.test/integration/live/: same gates (BRIDGE_LIVE_FUNDS,BRIDGE_LIVE_STATE_DIR,BRIDGE_LIVE_MAINNET_ACK,BRIDGE_LIVE_MAINNET_CASES,BRIDGE_LIVE_MAINNET_EXECUTE), recover-first state files, one atomic unit per leg, parametrized over every registry route in both directions;scripts/rehearse.pyas the operator CLI.Test evidence
pytest -q -m "not live"), 51 live tests collected and gated;codegen/gen_context.py --checkclean.Bridgeverbs with a process boundary between execute and completion:0x6a89c5fa…8596, deposit0x08cd56e4…aab4, mintat1jdy6wyal4ndwwydy80hjxt5q9eh332vk6t8zrw8jsvgc0pg7jcxqc85day.at1cufuy7v4lc4dn5ek3vdfa0tpeh8rrqqnahyelv06d2ll5n0kvczsrv9ugq, withdrawal0xbacb3ff2…4b54.Known findings
withdrawalFeeAtomic(2 USDC) does not match the live testnet fee (≈1.0035 USDC delivered 0.996501 of 2.000001). veil uses the same literal. The quote marks the fee as estimated; the literal is kept for the burn-minimum guard.aleo.records.register, shares the view key) — documented and opt-in.aleo-sdk[dps]is now a base dependency.metadata-requiredin the registry (ALEO token to Ethereum/Solana/Base/HyperEVM, USAD) and cannot move funds until reviewed.Funding needed to run the full mainnet matrix once
Rulings made during the agentic loop
71 rulings (decisions I made where the spec, the plans or a review disagreed)
plan 1-core-aleo (11)
bridge-sdk/.venv(already provisioned); it only runs the editable install oncepyproject.tomlexists — why: saves minutes and avoids re-downloading; cost if wrong: none (the editable install is idempotent).Process.add_program/contains_programtakeProgram/ProgramIDobjects, not strings — plan text says "raw_program"; implementers wrap withnet.Program.from_string/net.ProgramID.from_string— cost if wrong: a TypeError caught by the fake-facade tests.errorfrom protocol_state["destinationError"] / ["sourceError"] / default; export all 25 names) — cost if wrong: none, additive.TransactionConfirmationTimeout(consistent with the facade and shield-swap; timeout ≠ failure is the LIFECYCLE's job via submit_prepared(wait=False) + get_status) but document it ondelegate/submit_preparedand add a fake-timeout test asserting the exception propagates and the tx was submitted — cost if wrong: none.FreezeListresolves the freeze-list program from the token program's imports (bridge.program(token).importsentry ending infreezelist.aleo), with a static fallback table {usdcx_stablecoin.aleo→usdcx_freezelist.aleo, test_usdcx_stablecoin.aleo→test_usdcx_freezelist.aleo};tree()verifies the computed root against on-chainfreeze_list_rootwhen readable and raises ConfigurationError on mismatch (a wrong proof burns a fee) — cost if wrong: an extra mapping read per proof. Ruling:client.ethereum_from_env()/client.solana_from_env()are thin hooks returning None in plan 1 (no connection classes yet); plans 2/3 route them throughEthereum.from_env()/Solana.from_env()per the cross-plan ruling — cost if wrong: a two-line edit later. Facts passed:Transaction.to_json()/from_json(),.id()is a METHOD,.bytes();network.submit_transactiondoesstr(transaction)so a JSON string is accepted; DPS payload nests the full tx under "transaction";Process.add_program(Program)/contains_program(ProgramID)take typed objects.Bridge.eth/Bridge.solstay properties raising ConfigurationError when unconfigured (contract); plan 4 must gate onbridge.ethereum is None/bridge.solana is None(contract attributes) instead ofgetattr(bridge, "eth", None), and plan 4's FakeBridge must modeleth/solas raising properties so the suite catches this — why: keeps the actionable error for Tier-2 callers while giving lifecycle a None-safe probe — cost if wrong: a wrong DeliveryUnknownError path, caught by plan 4 tests.SolCall.send(on_checkpoint=…)passes aCheckpointbuilt bycheckpoint.create_checkpoint, same as plan 2's EvmCall — why: spec §8 convention, one idiom for stores — cost if wrong: none (plan 3 not started).SolanaRpcClient(requests) or a caller-supplied solana-pyAsyncClient(adapted);solana.rpc.api.Clientdoes not exist in solana-py 0.40 — cost if wrong: docs only.Bridge.from_profile(..., ethereum=None)to pick up EVM keys viaEthereum.from_env()like plan 3 does for Solana; bothEthereum.from_env()andSolana.from_env()returnX | None(None when the key env var is unset; ConfigurationError when only one of EVM_PRIVATE_KEY/ETHEREUM_RPC_URL is set) — cost if wrong: small API asymmetry.__import__placeholder and dead branch; duplicatetests/fakes/__init__.pycreation made idempotent).plan 2-ethereum (14)
checkpoint.py(Checkpoint dataclass,create_checkpoint,CheckpointStore,FileCheckpointStore) is executed as plan 4 Task 2 BEFORE plan 2 Task 2, under the plan-4 workspace/ledger, becauseEvmCall.sendandSolCall.sendboth depend on it — why: the contract puts it in plan 4 but plans 2/3 consume it; building it once first avoids stubs — cost if wrong: plan 4's later tasks find it already done (they must check the plan-4 ledger).tests/fakes/fake_web3.pyimportsFakeAleofromtests.conftestrather than redefining it — cost if wrong: a small import cycle to untangle.bridge-sdkfor every plan (plan 1 precedent) — cost if wrong: none.recoverDispatchFromHistory({required:true})makes only the inability to scan fatal (missing sender / no confirmed approval block); a clean zero-match scan means "nothing was dispatched, resume may re-authorize" and must NOT raise (raising would makeresumeimpossible after an approval-only checkpoint). The controller's dispatch text mis-stated this. Code stands; small round queued (behind Task 10): docstring stating the semantic + a test for both protocols asserting required=True + clean zero-match scan → SOURCE_SUBMISSION_PENDING — cost if wrong: a resume that re-dispatches when a scan silently missed a dispatch (mitigated by the sender/to filters and the bounded scan window).ETHEREUM_RPC_URL=https://ethereum-rpc.publicnode.com(chainId 0x1 verified) andSEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com(0xaa36a7 verified) at run time; fund-moving leg 1 still needs the user'sEVM_PRIVATE_KEY— cost if wrong: a rate-limited public RPC makes a live read flaky (skip on 429).plan=kwarg (not bare route=/sender=) — plan 4 execute/resume hold a Plan and the registry trust model re-resolves by id — cost if wrong: one kwarg to rename.Bridge.status()keeps propagating ChainMismatchError (loud misconfiguration beats a silently degraded read) — cost if wrong: one try/except in client.py.EvmXReserveQuote.feesstays(); plan 4quotemust rendermax_fee_atomicas the protocol fee line — carried to plan 4 dispatch notes._plan_forpromoted toaleo_bridge/_plan.py:build_plannow (plan 3_make_planand plan 4preparemust import it; byte-identical Plans are a correctness requirement for_route_for_plancomparisons) — cost if wrong: a module move.build_planderives the wallet executor fromregistry.chain(source.chain_id).family(evm-wallet / solana-wallet / aleo-wallet) so plan 3_make_planand plan 4prepareshare one builder; EVM output byte-identical — cost if wrong: one string mapping.log_scan_chunk_blocksstays onEthModule; plan 4 may thread it throughBridge(...)if the rehearsal needs it — cost if wrong: one kwarg.eth.py(or plan 4's final fix wave), not a third plan 2 round — cost if wrong: two footguns live a few hours longer.execute(plan)/resumefor private xReserve must take the secret nonce from the intent/checkpointed hook and enforce veil's hookData-equality guard; never rely on the default nonce — cost if wrong: a private mint committed to the wrong hook (funds-adjacent; flagged in the plan 4 dispatches).plan 3-solana (12)
fake_solana.pymust not redefine an Aleo fake; reusetests.conftest.FakeAleo— cost if wrong: a small import.pytest.skip(rate limit), not failure — cost if wrong: a silently skipped live check (reported in output)._plan_for→_plan.build_plan;SolModule._make_planmust call it so Plans are byte-identical across chains) — cost if wrong: idle implementer slot for one round.Status.EXPIRED(+ blockhashExpired/sourceError, no exception), as the brief and Tasks 7/9 specify; controller note 4's "pending" wording applied only to the plain deadline timeout — cost if wrong: one status mapping.(?![0-9a-fA-F])+ a test for each; no second checkpoint after the message id is known (Task 7source_status/plan 4get_statusre-derive it from logs) — cost if wrong: a store record without messageId until the next refresh.eth.py/_calls.pyEvmCall) in this fix wave even though plan 2 is complete — same shape, same funds hazard, one review — cost if wrong: a slightly larger re-review diff.broadcast_id = local_hashon the echo-mismatch branch + a resend test; fix the adapter_closedordering + timeout on the client close; leave the middleware path (hash unknowable; documented) and the fake-nonce note — cost if wrong: two small diffs. Serialized behind plan 4 Task 4 (it edits eth.py).plan 4-lifecycle-agent (34)
preparefrom Task 1 is not needed bycreate_checkpoint, which takes aPlan) — cost if wrong: Task 1/3 later findcheckpoint.pypresent; they must not overwrite it.quote/execute/resumecalleth.*/sol.*withplan=plan(never re-derived asset/recipient/amount); Task 3'sFakeEth/FakeSolaccept and validateplan=the way the real modules do — cost if wrong: fakes and real modules diverge silently.lifecycle.prepareparses intent, resolves route + amount, then returnsaleo_bridge._plan.build_plan(...); Task 7_plan_from_intentusesbuild_plantoo; add a test thatprepare()==build_plan()field-by-field for every active route — cost if wrong: a recovered plan fails_assert_plan_matches.__init__.__all__; never replaces; add a test pinning the pre-existing export set (AleoCall, EvmCall, CircleClient, EthModule, FreezeList, EMPTY_MERKLE_PROOF_PAIR, HyperlaneModule, PrivacyModule, XReserveModule, DEFAULT_ENDPOINT, Profile, Locator, Privacy, CALLER_BOUNDARIES, TERMINAL) — cost if wrong: silent public-API regression.EvmXReserveQuote.max_fee_atomicis rendered as the protocol fee line inrehearse._print_quoteandagent._serialize(label "xReserve max fee", asset usdc) — cost if wrong: an operator submits leg 3 without seeing the fee.rehearse._destination_readerprobesbridge.ethereum is not None/bridge.solana is not Nonebefore touchingbridge.eth/bridge.sol— cost if wrong: rehearsal crashes on Aleo-only configs.tests/fakes/fake_bridge.pyextendstests.conftest.FakeAleo/FakeNetwork(addsget_confirmed_transaction, duplicate-broadcast raisesaleo.AleoNetworkError) instead of defining a second Aleo fake — cost if wrong: two drifting fakes.log_scan_chunk_blockssetter guard) are folded into plan 4 Task 4 — cost if wrong: none beyond a slightly larger Task 4 diff.SolModule.source_status, exports and Bridge wiring; plan 4 Task 1 may run any time — cost if wrong: dead real code paths behind passing fakes.log_scan_chunk_blockssetter raises ConfigurationError (matches the constructor; one contract) — the note said ValueError — cost if wrong: one exception class._Emitterdedupes on checkpoint equality (both channels reduce the same receipt against the same plan), not (id, status) — cost if wrong: a duplicate callback.receipt.id != receipt.source_tx_idguard;_moduleoutside the try) are folded into Task 7's dispatch — cost if wrong: Task 7's diff grows by ~20 lines.DeliveryUnknownErrorwhen a baseline exists but no destination reader is configured is correct (veil parity; a false "still pending" is worse) — cost if wrong: one exception → unchanged mapping.tests/live/test_lifecycle_live.py+scripts/rehearse.pymust exercise EVERY lifecycle verb (prepare/quote/execute/wait/get_status/recover/resume/complete, agent tools' read path) against real chains per spec §12's leg matrix at minimal amounts, with balance prechecks and per-leg tx ids in the report; the fakes-only tests are not sufficient evidence of "done". Testnet legs (Sepolia leg 1, Aleo testnet) run unconditionally; mainnet fund-moving legs run in this session withBRIDGE_LIVE_MAINNET_EXECUTEset on the rehearsal invocation only (never exported), because the user has explicitly asked for actual e2e tests and the standing rule (memory: live round trips at minimal amounts are the definition of done; keys from env) authorizes it — cost if wrong: minimal mainnet fees on the user's own test wallets, every tx id reported.Bridge.recover/wait/resume/completefrom the on-disk checkpoint, so recover/resume/complete are validated against live state, not fakes._delivery_verification's call site in execute;get_statussees the real exception) — cost if wrong: Task 8's diff grows by ~10 lines._finishstays storeless (a read-only recovery must never raise fromcreate_checkpoint; wait/get_status persist) — cost if wrong: one extra log scan after a restart.CheckpointInvalidErrorguard for malformeddeliveryVerification— cost if wrong: two small additions.tests/live/config.py/helpers.pymirroring veil's gates + state files + explorer lookup, hermetictests/test_live_helpers.py, the per-case functions,scripts/rehearse.pyCLI); 13b: the parametrized live suitetests/live/test_lifecycle_live.pyover every active route plus the first real runs (testnet immediately; mainnet when the wallets are funded and the user's acknowledgements are set) — cost if wrong: one extra review seat.is_duplicate_broadcast_erroris exactly plan 1'sis_duplicate_submission(only "already exists"); a bare "Duplicate transaction" raises — the safer direction — cost if wrong: a manual resume after a benign duplicate.completedoes not call_require_active(funds are already deposited; XReserveModule still validates the route) — cost if wrong: a mint attempted on a parked route that the module then refuses.bridge_tools/dispatch_tool/agent_guidepackage exports; Task 12 updates__main__.pyto print the generated guide — cost if wrong: none (same files, later tasks).pending()builds each Progress OFFLINE —Checkpoint→ its reduced receipt/intent →to_progress(plan, receipt)withplanfrom_plan_from_intent(pure) — and returns a list even when a checkpoint is malformed (that entry becomes aProgresswithnext="failed"and the error text); callers refresh withget_status/recover— cost if wrong: a stalenextuntil the caller refreshes.worktree-bridge-sdkto origin and open a PR againstmaster(title/body without any AI attribution; body lists the leg-by-leg live results, the funding still needed for the unrun mainnet legs, and the collected rulings) — cost if wrong: a PR the user closes.next: "recover"guidance onbridge_resume/bridge_completeerrors +bridge_pendingdelegating to the (now offline)Bridge.pending()with per-entry checkpoints and per-entry failures; queued behind Task 9's fix round — cost if wrong: ~40 lines.Review status
Every task had a fresh-implementer + task-review cycle (funds-critical fixes from those reviews are on the branch); a final whole-branch review is in flight and its fix wave will be pushed to this branch.