Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions .github/workflows/sdk-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# sccache's GitHub Actions cache is a speed-up, not a requirement. maturin-action's
# `sccache: true` makes it one: sccache starts on the first rustc call and, when the
# runner cannot reach the cache service at that moment (DNS or network blips, seen on
# the macOS runners), the whole build fails before compiling anything. Native builds
# therefore start sccache themselves and drop the compiler cache if it does not come
# up; only the manylinux containers keep the action input. sccache's GHA backend
# reads the two ACTIONS_* values, which shell steps do not get without this export.
- name: Expose the Actions cache credentials to sccache
if: runner.os != 'Linux'
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Start sccache (best effort)
if: runner.os != 'Linux'
shell: bash
run: |
pip install "sccache>=0.10.0"
export SCCACHE_GHA_ENABLED=true SCCACHE_IDLE_TIMEOUT=0
if sccache --start-server; then
printf 'SCCACHE_GHA_ENABLED=true\nSCCACHE_IDLE_TIMEOUT=0\nRUSTC_WRAPPER=sccache\n' >> "$GITHUB_ENV"
else
echo "::warning::sccache could not reach the GitHub Actions cache; building without a compiler cache"
fi
# Build the mainnet extension module wheel.
- name: Build mainnet wheel
uses: PyO3/maturin-action@v1
Expand All @@ -73,7 +98,7 @@ jobs:
working-directory: ./sdk
target: ${{ matrix.platform.target }}
args: --release --out dist --features mainnet
sccache: 'true'
sccache: ${{ runner.os == 'Linux' }}
manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }}
# Install build tools in the manylinux container.
# aws-lc-sys needs cmake and a C toolchain.
Expand All @@ -97,7 +122,7 @@ jobs:
working-directory: ./sdk
target: ${{ matrix.platform.target }}
args: --release --out dist-testnet --no-default-features --features testnet
sccache: 'true'
sccache: ${{ runner.os == 'Linux' }}
manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }}
before-script-linux: |
if command -v dnf &> /dev/null; then
Expand Down Expand Up @@ -141,6 +166,10 @@ jobs:
assert testnet.Network.id() == 1
print('two-network wheel ok:', mpk.address, tpk.address)
"
- name: sccache stats
if: runner.os != 'Linux' && env.RUSTC_WRAPPER == 'sccache'
shell: bash
run: sccache --show-stats
- name: Upload wheel
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -175,13 +204,38 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# sccache's GitHub Actions cache is a speed-up, not a requirement. maturin-action's
# `sccache: true` makes it one: sccache starts on the first rustc call and, when the
# runner cannot reach the cache service at that moment (DNS or network blips, seen on
# the macOS runners), the whole build fails before compiling anything. Native builds
# therefore start sccache themselves and drop the compiler cache if it does not come
# up; only the manylinux containers keep the action input. sccache's GHA backend
# reads the two ACTIONS_* values, which shell steps do not get without this export.
- name: Expose the Actions cache credentials to sccache
if: runner.os != 'Linux'
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Start sccache (best effort)
if: runner.os != 'Linux'
shell: bash
run: |
pip install "sccache>=0.10.0"
export SCCACHE_GHA_ENABLED=true SCCACHE_IDLE_TIMEOUT=0
if sccache --start-server; then
printf 'SCCACHE_GHA_ENABLED=true\nSCCACHE_IDLE_TIMEOUT=0\nRUSTC_WRAPPER=sccache\n' >> "$GITHUB_ENV"
else
echo "::warning::sccache could not reach the GitHub Actions cache; building without a compiler cache"
fi
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
working-directory: ./sdk-abi
target: ${{ matrix.platform.target }}
args: --release --out dist
sccache: 'true'
sccache: ${{ runner.os == 'Linux' }}
manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }}
before-script-linux: |
if command -v dnf &> /dev/null; then
Expand All @@ -194,6 +248,10 @@ jobs:
run: |
pip install sdk-abi/dist/*.whl
python -c "import aleo_abi; print(aleo_abi.generate_abi.__name__)"
- name: sccache stats
if: runner.os != 'Linux' && env.RUSTC_WRAPPER == 'sccache'
shell: bash
run: sccache --show-stats
- name: Upload wheel
uses: actions/upload-artifact@v4
with:
Expand Down
37 changes: 28 additions & 9 deletions bridge-sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ solana-hyperlane / aleo-hyperlane / evm-xreserve / aleo-xreserve) with
``fees`` and ``amount_out`` in human units and ``quote.plan``. Show the
user fees + amount before ``execute``.

### `execute(self, plan, *, on_checkpoint=None, proving: 'str' = 'delegate', mode: 'str | None' = None, record: 'str | None' = None, merkle_proof: 'str | None' = None, gas_payment_microcredits: 'int | None' = None, secret_nonce: 'str | None' = None, poll_seconds: 'float' = 1.0, timeout_seconds: 'float' = 120.0)`
### `execute(self, plan, *, on_checkpoint=None, proving: 'str' = 'delegate', mode: 'str | None' = None, record: 'str | None' = None, compliance_proofs: 'ComplianceProofs' = None, merkle_proof: 'str | None' = None, gas_payment_microcredits: 'int | None' = None, secret_nonce: 'str | None' = None, poll_seconds: 'float' = 1.0, timeout_seconds: 'float' = 120.0)`

Commit funds on the source chain for ``quote.plan``; returns ``Progress``.

Expand All @@ -92,8 +92,10 @@ journal id is reserved before submission and stays fixed through recovery.
changing on-chain receipt identity. ``proving`` is ``"delegate"`` (DPS) or
``"local"``; ``mode`` is ``"caller"|"signer"`` (Aleo Hyperlane) or
``"private"|"public"|"public-as-signer"`` (Aleo xReserve burn, default
private; ``record``/``merkle_proof`` optional — the SDK selects a record
and computes the exclusion proof). The Hyperlane hook payment is
private; ``record``/``compliance_proofs`` optional — the SDK selects a
record and computes the freeze-list exclusion proof, caching it until
the list changes; ``merkle_proof`` is the earlier name for one literal
bundle). The Hyperlane hook payment is
re-quoted right before proving unless ``gas_payment_microcredits`` is
pinned. Irreversible once the source step is broadcast: afterwards use
``wait`` / ``recover``, never ``execute`` again.
Expand Down Expand Up @@ -274,11 +276,19 @@ lifecycle layer (plan 4) re-quotes at the last responsible moment by calling thi
Live relayer payment for the route (the exact u64 the hook asserts); quote right before proving.
``asset`` is an asset ref, a ``Route`` or a route id (BAT/USDG need the route: two destinations).

### `xreserve.burn(self, recipient: 'str', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, mode: 'str' = 'private', record: 'str | None' = None, merkle_proof: 'str | None' = None, route: 'Route | None' = None) -> 'AleoCall[BurnReceipt]'`
### `xreserve.burn(self, recipient: 'str', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, mode: 'str' = 'private', record: 'str | None' = None, compliance_proofs: 'ComplianceProofs' = None, merkle_proof: 'str | None' = None, route: 'Route | None' = None) -> 'AleoCall[BurnReceipt]'`

Burn USDCx for USDC on the selected EVM route (Ethereum by default). ``private`` spends a Token record via the wrapper and needs a
freeze-list exclusion proof — both are resolved from chain state when not supplied. Minimum: more than
the 2 USDCx withdrawal fee. The Aleo burn-attestation service forwards accepted burns to Circle.
Burn USDCx for USDC on the selected EVM route (Ethereum by default).

``mode="private"`` spends a USDCx Token record through the wrapper and
proves the signer is not on the USDCx freeze list. Both inputs are
resolved from chain state when not supplied: the record is the
smallest one covering the amount, and the exclusion proof is computed
from the published list and cached until the list changes. Pass one
``[MerkleProof; 2]`` bundle in ``compliance_proofs`` to use it as given
(``merkle_proof`` is the earlier name for the literal form). The amount
must exceed the 2 USDCx withdrawal fee. The Aleo burn-attestation
service forwards accepted burns to Circle.

### `xreserve.private_mint(self, attestation: 'Attestation', recipient: 'str', *, secret_nonce: 'str' = '0scalar', route: 'Route | None' = None) -> 'AleoCall[MintReceipt]'`

Expand All @@ -292,13 +302,22 @@ One Circle request for *message_hash*; ``None`` while pending (404).



### `unshield(self, asset: 'Any', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, record: 'str | None' = None, merkle_proof: 'str | None' = None, recipient: 'str | None' = None) -> 'AleoCall[PrivacyReceipt]'`
### `unshield(self, asset: 'Any', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, record: 'str | None' = None, compliance_proofs: 'ComplianceProofs' = None, merkle_proof: 'str | None' = None, recipient: 'str | None' = None) -> 'AleoCall[PrivacyReceipt]'`

Move a private record into the public balance; see ``privacy.unshield``.

An ARC-22 token (USDCx) needs a freeze-list exclusion proof for the
signer: computed and cached by the SDK when ``compliance_proofs`` is
unset, or taken as given from one ``[MerkleProof; 2]`` bundle.

### `freezelist.exclusion_proof(self, address: 'str', program: 'str') -> 'str'`

``[MerkleProof; 2]`` proving *address* is not frozen on *program*; veil's empty pair when the list is empty.
``[MerkleProof; 2]`` proving *address* is not frozen on *program*; the empty pair when the list is empty.

Reads the current root every call and the published tree only when
the root changed. Raises ``ConfigurationError`` when the root is
unreadable, the published tree does not match it, or *address* is on
the list.

### `eth.transfer_remote(self, asset: 'Any' = None, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, plan: 'Plan | None' = None) -> 'EvmCall[DispatchReceipt]'`

Expand Down
77 changes: 77 additions & 0 deletions bridge-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,3 +933,80 @@ Unknown versions and old version labels attached to new routes are rejected.

Runnable Arc examples and explicit live-test gates are described in
[examples/README.md](examples/README.md). No live test runs by default.

<a id="compliance"></a>

## Compliance

A USDCx unshield and a private xReserve burn prove in zero knowledge that the
signer's private address is not on the USDCx freezelist. The SDK builds that
Merkle exclusion proof locally and passes it as a private input of the
transaction. The program checks the Merkle paths inside the transaction proof,
and its finalize compares the reconstructed root with the root stored on
chain. The network verifies that the signer is absent from the list without
seeing the signer's address or the paths. The recipient of an unshield stays
public.

Proof generation is on by default. Leave `compliance_proofs` unset and the
SDK does the work; set it only to replace the proof the SDK would build.

### Functions that take a compliance proof

Each call takes one `[MerkleProof; 2]` bundle: two Merkle paths proving that
the signer is absent from the asset's freezelist.

| Function | When a proof is needed | Program function |
| --- | --- | --- |
| `bridge.unshield()` | Unshielding an ARC-22 asset such as USDCx | `transfer_private_to_public` |
| `bridge.execute()` | An Aleo USDCx xReserve burn with `mode="private"`, the default | `private_burn` |
| `bridge.xreserve.burn()` | The same private burn as a single call | `private_burn` |

The subject is always the signer, even when the unshield recipient is another
address. ARC-20 unshielding, `bridge.shield()`, and burns with `mode="public"`
or `mode="public-as-signer"` take no proof.

### Automatic generation

Before each call the SDK reads the list's current root
(`freeze_list_root[1u8]` on `usdcx_freezelist.aleo`). When the root changed
since the last call it downloads the published tree from
`GET /programs/{id}/compliance/freeze-list`, checks that the tree ends in that
root, builds the signer's witness, and checks that the witness reconstructs
the root. Trees and witnesses stay in memory per client, list, and root, so
while the root is unchanged nothing beyond the root is read. An empty list
uses the empty-tree witness without a download. A frozen signer, a missing
root, or a published tree that does not match the root raises
`ConfigurationError` before anything is proved or a fee is paid; the message
says which. The cache holds at most 16 lists and 256 addresses per list and
does not survive a restart.

The list is found through the token program's imports:
`usdcx_stablecoin.aleo` imports `usdcx_freezelist.aleo`, and the testnet
programs carry a `test_` prefix. `bridge.freezelist.exclusion_proof(address,
program)` returns the bundle for any address, and
`bridge.freezelist.clear_cache()` drops the cached trees.

### Optional proof inputs

`compliance_proofs` takes a `ComplianceProofs` value: one bundle, or `None`.

| Value | Meaning |
| --- | --- |
| `None` (the default) | The SDK builds the proof. |
| An encoded `[MerkleProof; 2]` string | Used as given. |
| `MerkleProofPair(...)` or `(MerkleProofInput, MerkleProofInput)` | The same proof as structured values. `MerkleProofInput` takes siblings as ints or `"...field"` literals. |
| A one-element tuple of any of the above | The same, in the tuple form the Shield Swap SDK uses. |

`merkle_proof=` is the earlier name for the string form and still works. A
supplied proof bypasses the root read and the witness check, so its freshness
is the caller's responsibility.

```python
from aleo_bridge import MerkleProofInput, MerkleProofPair

receipt = bridge.unshield("aleo/usdcx", amount="2.5").delegate() # proof built by the SDK

bundle = MerkleProofPair(MerkleProofInput(siblings=(0,) * 16, leaf_index=1),
MerkleProofInput(siblings=("0field",) * 16, leaf_index="1u32"))
receipt = bridge.unshield("aleo/usdcx", amount="2.5", compliance_proofs=bundle).delegate()
```
4 changes: 2 additions & 2 deletions bridge-sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "aleo-bridge-sdk"
version = "0.6.2"
version = "0.7.0"
description = "Bridge Aleo, Ethereum, Arc, Base, Arbitrum and Solana assets with Hyperlane, Circle xReserve and CCTP"
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -12,7 +12,7 @@ requires-python = ">=3.10"
# Solana legs alike, so a checkpoint written on one machine can always be recovered on another.
# The Privy and Dynamic server-wallet SDKs (aleo_bridge.privy / aleo_bridge.dynamic) ship the same way;
# dynamic-wallet-sdk itself requires Python 3.11, so on 3.10 that one module reports it missing.
dependencies = ["aleo-sdk>=0.5.0", "requests>=2", "pynacl>=1.5",
dependencies = ["aleo-sdk>=0.7.0", "requests>=2", "pynacl>=1.5",
"web3>=7,<9", "eth-account>=0.13",
"solders>=0.21", "solana>=0.35",
"privy-client>=0.7",
Expand Down
2 changes: 1 addition & 1 deletion bridge-sdk/pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"include": ["python/aleo_bridge"],
"extraPaths": ["python"],
"extraPaths": ["python", "../sdk/python"],
"venvPath": ".",
"venv": ".venv",
"reportMissingModuleSource": false
Expand Down
Loading
Loading