diff --git a/build-on-celo/nightfall.mdx b/build-on-celo/nightfall.mdx index dd7ff1d3e..60c2978b4 100644 --- a/build-on-celo/nightfall.mdx +++ b/build-on-celo/nightfall.mdx @@ -174,7 +174,12 @@ Update the following variables in `celo-sepolia.env`: **Funding Your Address** -These addresses must be funded with CELO on the Celo Sepolia testnet. You can get testnet CELO from a [faucet](https://faucet.celo.org/celo-sepolia) if needed. +`CLIENT_ADDRESS` must be funded on Celo Sepolia with: + +- **CELO for gas** on deposit and de-escrow transactions. Get it from the [Celo Sepolia faucet](https://faucet.celo.org/celo-sepolia). +- **A balance of whichever token you intend to deposit.** For example, to move USDT into Nightfall, `CLIENT_ADDRESS` must hold USDT on Celo Sepolia. + +The client calls `approve()` and `transferFrom()` on your behalf when you submit a deposit — no manual approval step is needed. @@ -207,7 +212,46 @@ curl -X POST http://localhost:3000/v1/deriveKey \ }' ``` -This will return your `root_key`, `nullifier_key`, `zkp_private_key`, and `zkp_public_key`. Save these values for future operations. +This will return your `root_key`, `nullifier_key`, `zkp_private_key`, and `zkp_public_key`. Save these values for future operations — in particular, the recipient's `zkp_public_key` is what a sender needs to route a private transfer. + + +**Run one `nightfall_client` instance per user identity** + +A `nightfall_client` process tracks exactly one ZKP key pair at a time. The client only decrypts L2 blocks with whichever keys are currently loaded, so swapping mnemonics via `/v1/deriveKey` on a running client **will not reveal funds addressed to the new keys** — past blocks are never re-decrypted. + +For a sender → recipient flow, run **two independent stacks** (two `nightfall_client` + MongoDB pairs, e.g. on different host ports). Each stack derives its own mnemonic once and keeps it for its lifetime. + +**Do not drop the client's MongoDB while you hold unspent commitments.** The commitment salts and preimages live only there; they cannot be reconstructed from L1, and the underlying L1 escrow for those funds will be permanently stranded. + + + +#### Amount Encoding + +Every `value`, `fee`, and `deposit_fee` in Nightfall's API is a **64-character hex string of the raw token amount, without `0x` prefix**. The number of decimals depends on the token: + +| Token (Celo Sepolia) | Address | Decimals | 1 whole unit | +| ------------------------------------- | -------------------------------------------- | -------- | ------------------------------------------------------------------ | +| CELO | `0x471EcE3750Da237f93B8E339c536989b8978a438` | 18 | `0000000000000000000000000000000000000000000000000de0b6b3a7640000` | +| USD₮ (Tether USD testnet) | `0xd077A400968890Eacc75cdc901F0356c943e4fDb` | 6 | `00000000000000000000000000000000000000000000000000000000000f4240` | + +Common amounts: + +- `0.1 CELO` → `000000000000000000000000000000000000000000000000016345785d8a0000` +- `1 CELO` → `0000000000000000000000000000000000000000000000000de0b6b3a7640000` +- `1 USDT` → `00000000000000000000000000000000000000000000000000000000000f4240` +- `10 USDT` → `0000000000000000000000000000000000000000000000000000000000989680` + +#### Diagnostic Endpoints + +Useful read-only endpoints for checking state and debugging: + +| Endpoint | Returns | +| ---------------------------------------- | ----------------------------------------------------------------------------------------- | +| `GET /v1/health` | `"Healthy"` when the client is ready | +| `GET /v1/balance/$ercAddress/$tokenId` | Total balance of the token under the currently-loaded ZKP keys (64-char hex) | +| `GET /v1/commitments` | All commitments (preimages, status, nullifiers) known to this client | +| `GET /v1/proposers` | Registered proposers on-chain and their URLs | +| `GET /v1/request/$uuid` | Status of a submitted request (`Queued` → `Processing` → `Submitted` → `Confirmed`) | #### Operations @@ -239,12 +283,12 @@ curl -X POST http://localhost:3000/v1/deposit \ **Parameters:** -- `ercAddress`: The ERC20/ERC721/ERC1155/ERC3525 token contract address. Use `0x471EcE3750Da237f93B8E339c536989b8978a438` for CELO token (ERC20 via Celo Token Duality) -- `tokenId`: Token ID (use all zeros for ERC20) -- `tokenType`: `0` for ERC20, `1` for ERC721, `2` for ERC1155, `3` for ERC3525 -- `value`: Amount in hex format (without `0x` prefix) -- `fee`: Transaction fee in hex format -- `deposit_fee`: Deposit fee in hex format +- `ercAddress`: The ERC20/ERC721/ERC1155/ERC3525 token contract address. Use `0x471EcE3750Da237f93B8E339c536989b8978a438` for CELO token (ERC20 via Celo Token Duality). +- `tokenId`: Token ID (use all zeros for ERC20 — full 64-char form without `0x`). +- `tokenType`: `0` for ERC20, `1` for ERC721, `2` for ERC1155, `3` for ERC3525. +- `value`: Amount in hex format, without `0x` prefix (see [Amount Encoding](#amount-encoding)). +- `fee`: Transaction fee in hex format. +- `deposit_fee`: Deposit fee in hex format. **Step 3:** Check deposit status: @@ -290,9 +334,9 @@ curl -i -H "Content-Type: application/json" \ **Parameters:** - `ercAddress`: Token contract address -- `tokenId`: Token ID (`0x00` for ERC20) -- `recipientData.values`: Array of amounts to send (in hex without `0x` prefix) -- `recipientData.recipientCompressedZkpPublicKeys`: Array of recipient public keys +- `tokenId`: Token ID. For **transfers only**, use the short form `"0x00"` — the deposit, withdraw, and de-escrow endpoints expect the full 64-zero form (`"0000…0000"`) without `0x`. +- `recipientData.values`: Array of amounts to send (in hex without `0x` prefix; see [Amount Encoding](#amount-encoding)) +- `recipientData.recipientCompressedZkpPublicKeys`: Array of recipient public keys (from each recipient's `deriveKey` response — the recipient must be running their own `nightfall_client`) - `fee`: Transaction fee **Step 3:** Check transfer status: @@ -303,76 +347,118 @@ curl -i "http://localhost:3000/v1/request/$TRANSFER_ID" **Withdraw from Nightfall** -Withdraws tokens from Nightfall back to Layer 1. The withdrawal process involves two steps: initiating the withdrawal (which creates an escrow) and then de-escrowing to complete the withdrawal. +Withdraws tokens from Nightfall back to Layer 1. The withdrawal process involves two on-chain phases: initiating the withdrawal (an L2 transaction that nullifies your private commitment), and then de-escrowing (an L1 transaction that releases the tokens from the Nightfall contract to your recipient). + +**Step 1: Build the padded recipient address** -**Step 1: Initiate Withdrawal** +`recipientAddress` must be the **32-byte** (64-char) hex representation of the L1 recipient — the 20-byte EOA left-padded with 12 zero bytes, **no `0x` prefix**. The short `0x…` form used elsewhere is not accepted here: + +```bash +L1_ADDR="0xf39fd6e51aad88f6f4ce6ab8827279cfffb92267" +L1_PADDED="000000000000000000000000${L1_ADDR#0x}" +echo "$L1_PADDED" +# 000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92267 +``` + +**Step 2: Initiate the withdrawal** ```bash WITHDRAW_ID=$(uuidgen) curl -X POST http://localhost:3000/v1/withdraw \ -H "Content-Type: application/json" \ -H "X-Request-ID: $WITHDRAW_ID" \ - -d '{ - "ercAddress": "0x471EcE3750Da237f93B8E339c536989b8978a438", - "tokenId": "0000000000000000000000000000000000000000000000000000000000000000", - "tokenType": "0", - "value": "000000000000000000000000000000000000000000000000016345785d8a0000", - "recipientAddress": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92267", - "fee": "0000000000000000000000000000000000000000000000000000000000000000" - }' + -d "{ + \"ercAddress\": \"0x471EcE3750Da237f93B8E339c536989b8978a438\", + \"tokenId\": \"0000000000000000000000000000000000000000000000000000000000000000\", + \"tokenType\": \"0\", + \"value\": \"000000000000000000000000000000000000000000000000016345785d8a0000\", + \"recipientAddress\": \"$L1_PADDED\", + \"fee\": \"0000000000000000000000000000000000000000000000000000000000000000\" + }" ``` **Parameters:** - `ercAddress`: Token contract address -- `tokenId`: Token ID (all zeros for ERC20) +- `tokenId`: Token ID (all zeros for ERC20 — same 64-char form as deposit) - `tokenType`: `0` for ERC20, `1` for ERC721, `2` for ERC1155, `3` for ERC3525 -- `value`: Amount to withdraw in hex format -- `recipientAddress`: Layer 1 address to receive the tokens +- `value`: Amount to withdraw in hex format (see [Amount Encoding](#amount-encoding)) +- `recipientAddress`: **32-byte** (64-char) hex L1 recipient, no `0x` prefix (see Step 1) - `fee`: Transaction fee -**Step 2:** Check withdrawal status: +**Step 3: Capture the `withdrawFundSalt`** + +The salt is needed for de-escrow in Step 5. It is returned in the **client stdout logs** (and in the webhook payload if you configured `WEBHOOK_URL`), **not** in `/v1/request/$WITHDRAW_ID`: + +```bash +SALT=$(docker logs nf4_indie_client 2>&1 \ + | grep -o '"withdraw_fund_salt":"[^"]*"' \ + | tail -1 \ + | sed 's/.*"withdraw_fund_salt":"\([^"]*\)".*/\1/') +echo "$SALT" +``` + +**Step 4: Wait until the withdrawal is included in an L2 block** + +The commitment state is the authoritative signal. When the commitment you just spent flips to `Spent` with `nullifier == withdraw_fund_salt`, the withdrawal has landed on L2 and is ready to be de-escrowed: ```bash -curl -i "http://localhost:3000/v1/request/$WITHDRAW_ID" +curl -s http://localhost:3000/v1/commitments \ + | jq '.[] | select(.nullifier == "'"$SALT"'") | .status' +# Expect "PendingSpend" initially, then "Spent" once the block lands ``` -The withdrawal response will include a `withdrawFundSalt` value that you'll need for the de-escrow step. + +In some client builds `/v1/request/$WITHDRAW_ID` can remain stuck at `Submitted` even after the withdrawal has landed. Treat the commitment status (above) as the source of truth. The status-tracking fix is on the `celo` branch of [celo-org/nightfall\_4\_CE](https://github.com/celo-org/nightfall_4_CE) from commit `12a85a8` onward. + + -**Step 3: De-escrow (Complete Withdrawal)** +**Step 5: De-escrow (release tokens on L1)** -After the withdrawal is processed and included in a block, complete the withdrawal by de-escrowing: +After the commitment is `Spent`, call `/v1/de-escrow` to release the tokens from the Nightfall contract to the L1 recipient: ```bash curl -X POST http://localhost:3000/v1/de-escrow \ -H "Content-Type: application/json" \ - -d '{ - "ercAddress": "0x471EcE3750Da237f93B8E339c536989b8978a438", - "tokenId": "0000000000000000000000000000000000000000000000000000000000000000", - "tokenType": "0", - "value": "000000000000000000000000000000000000000000000000016345785d8a0000", - "recipientAddress": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92267", - "fee": "0000000000000000000000000000000000000000000000000000000000000000", - "withdrawFundSalt": "178a2ac1938304e86ada919e6c3931702df4c0b78ffb8e314322d289e4fb197a" - }' + -d "{ + \"ercAddress\": \"0x471EcE3750Da237f93B8E339c536989b8978a438\", + \"tokenId\": \"0000000000000000000000000000000000000000000000000000000000000000\", + \"tokenType\": \"0\", + \"value\": \"000000000000000000000000000000000000000000000000016345785d8a0000\", + \"recipientAddress\": \"$L1_PADDED\", + \"fee\": \"0000000000000000000000000000000000000000000000000000000000000000\", + \"withdrawFundSalt\": \"$SALT\" + }" ``` +Expect `HTTP 200 OK`. The L1 balance of `$L1_ADDR` is now higher by `value` (minus the de-escrow L1 gas). + **Parameters:** -- All parameters from the withdrawal request -- `withdrawFundSalt`: The salt value returned from the withdrawal operation +- All parameters from the withdrawal request — in particular, the same padded `recipientAddress` form +- `withdrawFundSalt`: The salt captured in Step 3 **Important Notes:** -- All hex values should be provided without the `0x` prefix (except for `tokenId` in transfer which uses `0x00`) -- The client must be healthy before making requests. Check health with: `curl http://localhost:3000/v1/health` -- Transaction status can be checked using the request ID returned from each operation -- The webhook is automatically started with docker-compose and will receive notifications about transaction status changes -- For Celo Sepolia, the default ERC20 token address is `0x471EcE3750Da237f93B8E339c536989b8978a438` (CELO) -- Operations (deposits, transfers, withdrawals) may take up to 1 hour to complete and be included in a block by the proposer. Client execution should take few seconds. +- All `value` / `fee` / `tokenId` fields are hex **without** the `0x` prefix — **except** `tokenId` in `/v1/transfer`, which uses the short form `"0x00"`. +- `recipientAddress` on `/v1/withdraw` and `/v1/de-escrow` must be 32-byte (64-char) left-padded hex, **no** `0x` prefix. +- The client must be healthy before making requests. Check with `curl http://localhost:3000/v1/health`. +- The webhook is automatically started with docker-compose and receives notifications about transaction status changes, including the `withdraw_fund_salt`. +- For Celo Sepolia, the default ERC20 token address is `0x471EcE3750Da237f93B8E339c536989b8978a438` (CELO). +- **Timing expectations** (with a healthy proposer): + - Client-side work (proof generation, L1 escrow transaction) typically completes in a few seconds. + - L2 block confirmation of a deposit, transfer, or withdrawal is usually **~30 minutes** (the proposer batches for 120 s, then generates the rollup proof before posting the `BlockProposed` transaction). + - This can extend up to **~1 hour** under adverse conditions. If an operation stays at `Submitted` substantially longer, call `GET /v1/proposers` to confirm a registered proposer is available. +- **De-escrow must come after** the withdrawal has been included on L2 (commitment `Spent`). Calling `/v1/de-escrow` earlier will fail. + + +A worked end-to-end example of this entire flow (deposit → transfer → withdraw → de-escrow), with sample mnemonics, state variables, and diagnostic checkpoints, is available at [`doc/celo_sepolia_client_playbook.md`](https://github.com/celo-org/nightfall_4_CE/blob/celo/doc/celo_sepolia_client_playbook.md) on the `celo` branch. + + + ### Integration Steps 1. **Review Technical Documentation**: Start with the [Nightfall GitHub documentation](https://github.com/EYBlockchain/nightfall_4_CE/blob/master/doc/nf_4.md)