Skip to content

feat: resolve freeze-list compliance proofs automatically (bridge + Shield Swap) - #80

Merged
iamalwaysuncomfortable merged 12 commits into
masterfrom
feat/automatic-freezelist-proofs
Oct 22, 2026
Merged

iamalwaysuncomfortable merged 12 commits into
masterfrom
feat/automatic-freezelist-proofs

Conversation

@iamalwaysuncomfortable

@iamalwaysuncomfortable iamalwaysuncomfortable commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Every Shield Swap and bridge call that takes freeze-list exclusion proofs now resolves them itself, following the accounting in veil's feat/automatic-freezelist-proofs branch and the slot tables in amm-v3/MerkleProofReference.md (f6694b7). Callers can still pass fully formed proofs; the parameter is compliance_proofs everywhere, and the old keywords keep working.

Shared layer (aleo)

  • AleoNetworkClient.get_compliance_freeze_list(program) and the async / facade twins read GET /programs/{id}/compliance/freeze-list (the published tree, leaves first and root last; None when a program has no list — the endpoint answers 404 today, the docs say null, both are handled).
  • New aleo.compliance: the tree math (moved from the bridge, same vectors), MerkleProofInput / MerkleProofPair / ComplianceProof (one proof) with a per-package ComplianceProofs alias for a call's whole argument, as in veil, generate_freezelist_proof (checks the tree against the on-chain root and recomputes every witness), freezelist_program (program name → *freezelist.aleo import → known table; credits.aleo is a placeholder slot), and FreezeListProofs / AsyncFreezeListProofs: one root read per list per call, the tree fetched only when the root changed, witnesses cached per client (16 lists × 256 addresses) and discarded when the root moves. An empty list yields the fixed empty-tree pair without fetching anything beyond the root.

Bridge

  • unshield, xreserve.burn, execute take compliance_proofs= (a literal, a MerkleProofPair, a (MerkleProofInput, MerkleProofInput) tuple, or a one-tuple of any). merkle_proof= is kept as an alias; passing both raises. aleo_bridge.freezelist re-exports the shared helpers and FreezeList.exclusion_proof delegates to the shared resolver.

Shield Swap

  • New aleo_shield_swap.compliance: pure slot planners for swap / claim / mint / increase / collect / rebalance (every entrypoint in the reference, including all 14 rebalance variants) and tuple-union aliases on the bundle count: SwapProofs 0–1, ClaimProofs 1–3, MintProofs 3–5, IncreaseProofs 0–2, CollectProofs 2–4, RebalanceProofs 2–6.
  • compliance_proofs= on the six sync write methods and on async swap / claim_swap_output. A wrong count raises ValueError naming the slots the route expects. wrapper_proofs= still fills every wrapped-side slot while the AMM slots are built by the SDK. Receiver slots screen the position's withdrawal address; sender slots the signer; AMM slots signer / recipient / withdrawal.

Backward compatibility

  • No existing call breaks: every old keyword still works, inputs are emitted in the same order, and the empty-tree literal is unchanged. The only behavioural change is that calls which previously hard-coded the empty pair now read each list's current root (one mapping read per list per call) and would raise FrozenAddressError / FreezeListError instead of submitting a transaction the contract would reject.
  • The tree helpers in aleo_bridge.freezelist now raise aleo.compliance.FreezeListError (an AleoError) instead of ConfigurationError; FreezeList methods still raise ConfigurationError.
  • The shield-swap agent-page budget moves 32k → 34k chars (six signatures and one-line notes).
  • bridge-sdk/pyrightconfig.json adds ../sdk/python to extraPaths so the bridge type-checks against the editable sdk (CI already installs the sdk wheel from this repo). All four packages are bumped to 0.7.0 in lockstep, and the shield-swap and bridge aleo-sdk floors rise to >=0.7.0 because both now import aleo.compliance.

Naming follows veil 0.13's compliance vocabulary (MerkleProofInput, ComplianceProofContext, generate_freezelist_proof, literal_compliance_proof, format_merkle_proof_pair, KNOWN_TOKEN_FREEZELISTS, freezelist_program, get_compliance_freeze_list); none of these existed in a release. Kept deliberately: compliance_proofs= as the parameter, FreezeListProofs as the resolver (no ProofProvider callback), and "bundle" for one [MerkleProof; 2] argument, which is the amm-v3 reference's word.

Tests

  • Unit: sdk 23 new (tree vectors, witnesses against rebuilt trees, cache / eviction / stale-tree handling, endpoint parsing), bridge (alias and conflict handling on unshield / burn / execute, cache through the fake endpoint), shield-swap 31 new (every slot table from the reference, count validation, legacy interleaving, automatic resolution against empty and populated roots, credits placeholder). All three fast suites and pyright pass (sdk 21 and shield-swap 15 pyright errors pre-exist on master).
  • Live, testnet (Shield Swap, SDK-built proofs on every call): read tier tests/integration/test_compliance_live.py (both lists, published tree == on-chain root); test_swap_lifecycle.py; a USDCx → ETH swap + claim (sender bundle against test_usdcx_freezelist.aleo, AMM bundle against shield_swap_freezelist.aleo); test_liquidity_lifecycle_live.py (mint → increase → rebalance → decrease → collect → burn on a pool with a wrapped USDCx side).
  • Live, mainnet (bridge): read tier tests/live/test_compliance_live.py; a 0.1 USDCx unshield with an SDK-built proof (at1xc73qqqpugs0njsuq927skyq8leldn4wu6sf6tqn5dkcezwv3v8qg9yqhd, public USDCx 13.534309 → 13.634309); the 2.000001 USDCx private xReserve burn through execute on both routes (test_lifecycle_live.py::test_aleo_xreserve): to Ethereum at1uupu8mt465sxsaf96yuftphf39mv5taj6xt5992y0tyljt4hnyfq6mceml → 0x8be667e0…b1655 (+0.996501 USDC), to Arc at1qhh39e8pfh4ynkrxpxhl44acczwyrpt0ujret590mj00vhrzqvfqg03yhp → 0xf2828fff…2df0d (+1.983601 USDC).

Every freeze list is empty on both networks as of 2026-10-20, so the live runs exercise the empty-root branch end to end; populated trees are covered by the unit tests against locally rebuilt trees whose roots are checked.

Testnet with a populated list (2026-10-21)

FROZEN_ADDRESS_1 was added to shield_swap_freezelist.aleo (the USDCx list stayed empty). With real witnesses from that tree: the swap lifecycle, the USDCx swap + claim, and the full liquidity lifecycle (ETH/USDCx pool) passed; a fresh profile whose address sorts below the frozen leaf claimed two swaps (at1yynlq…, execute accepted), so both bracket shapes are proven on chain. New live modules tests/integration/test_frozen_live.py (shield-swap) and tests/live/test_frozen_live.py (bridge) check that the frozen account is listed, refused by the resolver, and refused before proving on swap / mint / unshield / private burn; they skip with a message for a list that does not contain it yet.

The regular shield-swap live suite showed 8 failures unrelated to this branch (API drift: 401s no longer returned for bad credentials, GET /positions 404, POST /referral/activity 410, codes_per_user_limit null, and a test sizing bug in the fresh-profile mint). Those belong to a separate amm-api sync.

Also on this branch

  • MerkleProofInput accepts siblings and leaf_index as ints or as "…field" / "Nu32" literals (normalised to ints; sibling_literals renders them back).
  • veil #193 port, bridge data fix: the BAT, USDG and ZEC Aleo warp-route token_owner values rotated on 2026-10-02 and the stale shared owner failed every Aleo-origin transfer's app_metadata check. Registry version unchanged, owners verified against the live app_metadata mappings, regression test over the five outbound routes. Not yet exercised with a live Aleo-origin BAT/USDG/ZEC transfer.

…ge and Shield Swap SDKs

Add aleo.compliance (tree math, MerkleProof/MerkleProofPair, witness
verification, freeze_list_program, FreezeListProofs/AsyncFreezeListProofs with a
per-client cache pinned to each list's root) and the freeze-list endpoint on
both network clients and facade network modules.

Bridge: unshield, xreserve.burn and execute take compliance_proofs=; the
merkle_proof= keyword remains as an alias.

Shield Swap: aleo_shield_swap.compliance plans the proof slots of every
entrypoint (swap, claim, mint, increase, collect, all rebalance variants) per
MerkleProofReference.md; the write methods take compliance_proofs= as a tuple
in input order (typed per-call unions on the bundle count) and build every
bundle themselves when it is unset; wrapper_proofs= remains as an alias.

Live-verified on testnet (swap, claim, USDCx leg, liquidity lifecycle) and on
mainnet (USDCx unshield, private xReserve burns to Ethereum and Arc).
FROZEN_ADDRESS_1 / FROZEN_ADDRESS_2 name the frozen accounts. While a list
does not contain them the checks skip and say so; once it does they prove the
resolver refuses a proof for the address and that a swap, mint, unshield, or
private burn involving it is refused before anything is proved or paid.
MerkleProof -> MerkleProofInput, ProofSlot -> ComplianceProofContext,
exclusion_proof_from_tree -> generate_freezelist_proof, one_bundle ->
literal_compliance_proof, bundle_literal -> format_merkle_proof_pair,
KNOWN_FREEZE_LISTS -> KNOWN_TOKEN_FREEZELISTS, freeze_list_program ->
freezelist_program, get_program_freeze_list -> get_compliance_freeze_list.
None of these shipped in a release; compliance_proofs=, merkle_proof=,
wrapper_proofs=, FreezeListProofs and the released bridge helpers are unchanged.
…roofInput

siblings take ints or "…field" strings and leaf_index an int or "Nu32";
values normalise to ints so equality and hashing are unchanged, and
sibling_literals renders the literal form back out.
…l #193)

The three warp programs changed token_owner on 2026-10-02; the stale shared
owner made every Aleo-origin transfer fail the on-chain app_metadata check.
Registry version is unchanged (data fix, as upstream). Owners verified
against the live app_metadata mappings; fixture mirrors veil's reviewed
metadata with a regression test over the five outbound routes.
One [MerkleProof; 2] argument as a caller supplies it (literal, MerkleProofPair,
or a pair of MerkleProofInput); the name follows veil's ComplianceProofs.
… for the argument

ComplianceProof is one [MerkleProof; 2] value (literal, MerkleProofPair, or a
pair of MerkleProofInput). Each SDK package now defines ComplianceProofs as its
calls' whole compliance_proofs argument, as veil does: the bridge's is one proof
bare or in a one-tuple or None; Shield Swap's is the union of the per-method
tuple aliases or None.
Explain how the private Merkle inputs establish freezelist compliance inside
the transaction proof, list the functions that take proofs with their slots
and subjects, then describe automatic generation, caching, errors, and the
optional proof inputs. Wording follows veil #194.
aleo-sdk, aleo-contract-abi-generator, shield-swap-sdk and aleo-bridge-sdk
move to 0.7.0 in lockstep (manifests, lockfiles, docs conf, __version__ and
the version tests). The shield-swap and bridge aleo-sdk floors rise to
>=0.7.0: both now import aleo.compliance, which first ships in this release.
…s missing

aleo_bridge and aleo_shield_swap check for aleo.compliance on import and raise
an ImportError that names the installed aleo-sdk version and the pip command,
instead of a bare ModuleNotFoundError from the first private call.
…ve runners

With maturin-action's sccache input, sccache starts on the first rustc call
and a transient failure to reach the GitHub cache service (a DNS error on a
macOS runner in run 39753109410) fails the whole build before compiling.
SCCACHE_IGNORE_SERVER_IO_ERROR does not cover server startup. Native runners
now start sccache themselves and build without a compiler cache when it does
not come up; the manylinux containers keep the action input.
@iamalwaysuncomfortable
iamalwaysuncomfortable merged commit 550d6c3 into master Oct 22, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants