diff --git a/package.json b/package.json index 31ca941..ecf78bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mirageprivacy/sdk", - "version": "0.4.3", + "version": "0.5.0", "description": "SDK for private transfers on Mirage", "type": "module", "main": "./dist/index.cjs", diff --git a/src/internal/api.ts b/src/internal/api.ts index e05f4cc..8b0d9de 100644 --- a/src/internal/api.ts +++ b/src/internal/api.ts @@ -99,6 +99,17 @@ export function isApprovalStale(approvedAt: number, nowSecs = Date.now() / 1000) export type ExecutionMode = "private" | "native"; +/** + * The ZK intent an ERC-20 escrow settles against. The commitment hides the + * recipient and amount, so the API cannot derive it: the client computes it and + * the API encodes it verbatim into the constructor. + */ +export interface ZkIntentRequest { + commitment: `0x${string}`; + instance_domain: `0x${string}`; + request_id: `0x${string}`; +} + export interface PricingSignalRequest { asset: string; execution_mode: ExecutionMode; @@ -202,6 +213,8 @@ export async function fetchPricingQuote( sender: Address; escrowType: EscrowKind; blindedSigners: Address[]; + /** Required for ERC-20 escrows and rejected for every other kind. */ + intent?: ZkIntentRequest; signals: PricingSignalRequest[]; }, ): Promise { @@ -210,6 +223,7 @@ export async function fetchPricingQuote( sender: params.sender, escrow_type: params.escrowType, blinded_signers: params.blindedSigners, + ...(params.intent ? { intent: params.intent } : {}), signals: params.signals, }); // A signed request must never yield preview fields. Reject rather than diff --git a/src/internal/zk.ts b/src/internal/zk.ts new file mode 100644 index 0000000..2d6778d --- /dev/null +++ b/src/internal/zk.ts @@ -0,0 +1,108 @@ +import { concatHex, keccak256, pad, toHex, type Address } from "viem"; + +/** + * Byte-exact encoders for the ZK receipt protocol, mirroring + * `nomad-types::zk`. The escrow recomputes the statement from its own storage + * and the circuit reproves the relation, so a single byte of disagreement + * makes every proof fail with no indication of which side is wrong. Widths are + * fixed by the protocol spec and are not negotiable per call site. + */ + +/** + * Protocol relation version committed by the intent commitment. Bumping this + * invalidates every previously generated proof. + */ +export const RELATION_VERSION = 2; + +/** Domain tag separating salt derivation from every other use of the scalar. */ +const SALT_DOMAIN = "mirage/zk/intent-salt/v1"; + +/** Settlement asset class committed inside the intent commitment. */ +export const ASSET_KIND_NATIVE = 0; +export const ASSET_KIND_ERC20 = 1; + +/** + * Classifies a settlement asset. The zero address is native by convention + * everywhere in the protocol. The commitment carries an explicit byte rather + * than inferring native settlement from a zero token address, so a native row + * can never be read as an ERC-20 row whose token encodes as zero. + */ +export function assetKind(asset: Address): number { + return /^0x0{40}$/i.test(asset) ? ASSET_KIND_NATIVE : ASSET_KIND_ERC20; +} + +/** Private opening of one settlement row. Never leaves the client or enclave. */ +export interface IntentOpening { + instanceDomain: `0x${string}`; + chainId: number; + /** Predicted escrow address, fixed so a proof cannot be replayed elsewhere. */ + escrow: Address; + requestId: `0x${string}`; + rowIndex: number; + /** Settlement token, or the zero address for native ETH. */ + asset: Address; + recipient: Address; + amount: bigint; + salt: `0x${string}`; +} + +function uint32(value: number): `0x${string}` { + return pad(toHex(value), { size: 4 }); +} + +/** + * Derives the commitment salt from material both sides already hold. + * + * The salt must be secret, and both the depositor and the enclave must arrive + * at the same value. Deriving it from the blinding scalar avoids threading a + * new field through the SDK, the API, and the Signal envelope. The instance + * domain and request id are folded in so a scalar reused across deployments + * still yields distinct salts. + */ +export function deriveSalt( + blindingScalar: `0x${string}`, + instanceDomain: `0x${string}`, + requestId: `0x${string}`, + rowIndex: number, +): `0x${string}` { + return keccak256( + concatHex([ + toHex(SALT_DOMAIN), + pad(blindingScalar, { size: 32 }), + pad(instanceDomain, { size: 32 }), + pad(requestId, { size: 32 }), + uint32(rowIndex), + ]), + ); +} + +/** Exact commitment preimage, exposed so fixtures can compare bytes. */ +export function intentPreimage(opening: IntentOpening): `0x${string}` { + return concatHex([ + pad(toHex(RELATION_VERSION), { size: 1 }), + pad(opening.instanceDomain, { size: 32 }), + pad(toHex(opening.chainId), { size: 32 }), + opening.escrow, + pad(opening.requestId, { size: 32 }), + uint32(opening.rowIndex), + pad(toHex(assetKind(opening.asset)), { size: 1 }), + opening.asset, + opening.recipient, + pad(toHex(opening.amount), { size: 32 }), + pad(opening.salt, { size: 32 }), + ]); +} + +/** Value stored on the escrow and recomputed inside the circuit. */ +export function intentCommitment(opening: IntentOpening): `0x${string}` { + return keccak256(intentPreimage(opening)); +} + +/** Fresh 32-byte random value for the per-deployment domain and request id. */ +export function randomBytes32(): `0x${string}` { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return `0x${Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("")}`; +} diff --git a/src/transfer.ts b/src/transfer.ts index 96ed485..fc73180 100644 --- a/src/transfer.ts +++ b/src/transfer.ts @@ -29,6 +29,7 @@ import type { ObfuscationResult, PricingQuote, PricingSignalRequest, + ZkIntentRequest, } from "./internal/api.js"; import { fetchComplianceApproval, @@ -41,11 +42,15 @@ import { import type { VerifyAttestationOptions } from "./internal/attestation.js"; import { approveQuotedForDeployment, + buildQuotedApprovalBuckets, deployQuotedApproved, deployQuotedAtomic, estimateQuotedApprovalGas, + predictContractAddress, } from "./internal/escrow.js"; import { deriveBlindedSigners } from "./internal/bond.js"; +import { deriveSalt, intentCommitment, randomBytes32 } from "./internal/zk.js"; +import type { IntentOpening as ZkIntentOpening } from "./internal/zk.js"; import { submitSignal } from "./internal/nomad.js"; import { pollTransfers } from "./internal/poll.js"; import { checkAbort } from "./internal/abort.js"; @@ -156,6 +161,64 @@ function attestationOptions(network: NetworkConfig): { }; } +/** + * Builds the intent an ERC-20 escrow settles against. + * + * The commitment binds the escrow address, so the deployment nonce is read here + * and the resulting address must be the one the deployment actually takes. The + * salt is derived from the blinding scalar rather than transmitted: the enclave + * already receives the scalar inside the Signal and recovers the same value. + */ +async function buildZkIntent(params: { + network: NetworkConfig; + sender: Address; + rows: TransferRow[]; + row: TransferRow; + blindingScalar: `0x${string}`; + publicClient: PublicClient; +}): Promise<{ request: ZkIntentRequest; opening: ZkIntentOpening }> { + const instanceDomain = randomBytes32(); + const requestId = randomBytes32(); + // Deployment follows its approvals, so the escrow lands that many nonces + // ahead. An atomic batch spends one nonce for the whole sequence. The deposits + // that set the count come from an unsigned preview, since the signed quote is + // what this commitment is being built for. + const preview = await fetchPricingPreview(params.network.apiServer, { + chainId: params.network.chainId, + escrowType: "erc20", + signals: buildPricingSignals(params.rows), + }); + const nonceOffset = params.network.enableAtomicBatch + ? 0 + : buildQuotedApprovalBuckets(preview.depositByAsset).length; + const escrow = predictContractAddress( + params.sender, + (await params.publicClient.getTransactionCount({ + address: params.sender, + blockTag: "pending", + })) + nonceOffset, + ); + const opening = { + instanceDomain, + chainId: params.network.chainId, + escrow, + requestId, + rowIndex: 0, + asset: params.row.tokenAddress, + recipient: params.row.recipientAddress, + amount: params.row.amount, + salt: deriveSalt(params.blindingScalar, instanceDomain, requestId, 0), + }; + return { + request: { + commitment: intentCommitment(opening), + instance_domain: instanceDomain, + request_id: requestId, + }, + opening, + }; +} + /** * One SDK transfer request becomes one Signal per asset. Asset groups preserve * first-appearance order, so the first transfer selects the reward asset. @@ -328,12 +391,27 @@ async function buildContext(params: TransferParams): Promise { } const blinded = deriveBlindedSigners(networkKey.publicKey, rows.length); + // An ERC-20 escrow settles against a commitment rather than stored transfer + // details, and the commitment binds the escrow address, so it must be + // predicted before the constructor is priced. + const intent = + escrowType === "erc20" + ? await buildZkIntent({ + network: params.network, + sender, + rows, + row: rows[0], + blindingScalar: blinded.blindingScalar, + publicClient: params.publicClient, + }) + : undefined; const [quote, obfuscation] = await Promise.all([ fetchPricingQuote(params.network.apiServer, { chainId: params.network.chainId, sender, escrowType, blindedSigners: blinded.blindedSigners, + intent: intent?.request, signals: buildPricingSignals(rows), }), fetchObfuscation(params.network.apiServer, escrowType), diff --git a/test/fixtures/zk_vectors.json b/test/fixtures/zk_vectors.json new file mode 100644 index 0000000..1211c20 --- /dev/null +++ b/test/fixtures/zk_vectors.json @@ -0,0 +1,421 @@ +{ + "fixture_version": 1, + "relation_version": 2, + "note": "Generated by `cargo test -p nomad-types --test zk_vectors`. The circuit and the Solidity escrow must reproduce every preimage and digest here byte for byte. Set VECTORS_CHECK=1 to verify instead of regenerate.", + "cases": [ + { + "name": "erc20_baseline", + "description": "First row, 1 USDC settlement, native ETH payout, first bond attempt.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1000000", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000001a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48444444444444444444444444444444444444444400000000000000000000000000000000000000000000000000000000000f42405555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0xa4abd620fc7199a0d4586a11bd6d362b2307ffec6ce7acac21e0303fbde55207" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "intent_commitment": "0xa4abd620fc7199a0d4586a11bd6d362b2307ffec6ce7acac21e0303fbde55207", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0x0000000000000000000000000000000000000000", + "payout_amount": "500000000000000000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x02111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000012222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000a4abd620fc7199a0d4586a11bd6d362b2307ffec6ce7acac21e0303fbde55207000000010000000001406f400000000001406ff46666666666666666666666666666666666666666000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b200000000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0xed07b7a9871d537dafeca0e65d0c7d5315b01594b84888530fa3e2c7ae216fed", + "public_signals": [ + "315067106191367668287550802297222888787", + "28828069871988020029990350371212586989" + ] + } + }, + { + "name": "native_baseline", + "description": "Native ETH settlement of 1 ETH. Differs from erc20_baseline only in asset and amount, isolating the asset-kind byte.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "asset_kind": 0, + "asset": "0x0000000000000000000000000000000000000000", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1000000000000000000", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000000000000000000000000000000000000000000000044444444444444444444444444444444444444440000000000000000000000000000000000000000000000000de0b6b3a76400005555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0xb2396f7452bc9769ee02e1b4f2f2797998463bf1421b5e99af037690bf7695b3" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "intent_commitment": "0xb2396f7452bc9769ee02e1b4f2f2797998463bf1421b5e99af037690bf7695b3", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0x0000000000000000000000000000000000000000", + "payout_amount": "100000000000000000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x02111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000012222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000b2396f7452bc9769ee02e1b4f2f2797998463bf1421b5e99af037690bf7695b3000000010000000001406f400000000001406ff466666666666666666666666666666666666666660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a00000000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0xe6a8482ec26636a814557f0e87f6a86e9329110204510d494db166ec9f7a45be", + "public_signals": [ + "306596208940915100833148732559542626414", + "195609744512338074130204399403575952830" + ] + } + }, + { + "name": "erc20_max_row_index", + "description": "Row index 31, the last valid index in a 32-row batch.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 31, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1000000", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000001f01a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48444444444444444444444444444444444444444400000000000000000000000000000000000000000000000000000000000f42405555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0xfd07dc5083fb6c4bea35fc53ac4c0d0688588db222a51874f521a10187f80dcb" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 31, + "intent_commitment": "0xfd07dc5083fb6c4bea35fc53ac4c0d0688588db222a51874f521a10187f80dcb", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0x0000000000000000000000000000000000000000", + "payout_amount": "500000000000000000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000001ffd07dc5083fb6c4bea35fc53ac4c0d0688588db222a51874f521a10187f80dcb000000010000000001406f400000000001406ff46666666666666666666666666666666666666666000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b200000000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0x5d183ad51c6a4a3aa27bd4e92f3679976dc163e7b8a109c1735e548e079c4bb5", + "public_signals": [ + "123744011996751716326943171883386173847", + "145889991151649279123173885175026961333" + ] + } + }, + { + "name": "erc20_max_amount", + "description": "Settlement amount of 2^256 - 1. Catches truncation to a narrower type.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000001a0b86991c6218b36c1d19d4a2e9eb0ce3606eb484444444444444444444444444444444444444444ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0xece247711f05d327f7b5ed465e89aeb8f3bbdce88f564b5fb8e6600dacce5055" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "intent_commitment": "0xece247711f05d327f7b5ed465e89aeb8f3bbdce88f564b5fb8e6600dacce5055", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "payout_amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x02111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000012222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000ece247711f05d327f7b5ed465e89aeb8f3bbdce88f564b5fb8e6600dacce5055000000010000000001406f400000000001406ff46666666666666666666666666666666666666666a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0x4363a8ef3b50fa771d7ae1273b157e1187f783f26720919d22aa1b7c314f1000", + "public_signals": [ + "89575739505286078243523549474944548369", + "180730952955811481366659105505464487936" + ] + } + }, + { + "name": "erc20_min_amount", + "description": "Settlement amount of 1 atomic unit, left-padded to 32 bytes.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000001a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48444444444444444444444444444444444444444400000000000000000000000000000000000000000000000000000000000000015555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0xbb52ca00c57ce9898356399928c89977b6c9827c4e70262cef2df1838ffa24f3" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "intent_commitment": "0xbb52ca00c57ce9898356399928c89977b6c9827c4e70262cef2df1838ffa24f3", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0x0000000000000000000000000000000000000000", + "payout_amount": "1", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x02111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000012222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000bb52ca00c57ce9898356399928c89977b6c9827c4e70262cef2df1838ffa24f3000000010000000001406f400000000001406ff46666666666666666666666666666666666666666000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0x34d4e843cd362a7df8bbf970c17aef606fb60e8c1f58fd9a1499d38f815e5966", + "public_signals": [ + "70225333605649854162288819093651517280", + "148489600615757822018699320415559244134" + ] + } + }, + { + "name": "erc20_token_payout", + "description": "USDC payout instead of native ETH, exercising a nonzero payout asset in the statement hash.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 7, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1000000", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000701a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48444444444444444444444444444444444444444400000000000000000000000000000000000000000000000000000000000f42405555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0x5e2db4aba20795d72d4bac1c2f8db19b7749d738f245e2551962676938c8c7f4" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 7, + "intent_commitment": "0x5e2db4aba20795d72d4bac1c2f8db19b7749d738f245e2551962676938c8c7f4", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "payout_amount": "1010000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x021111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000122222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333000000075e2db4aba20795d72d4bac1c2f8db19b7749d738f245e2551962676938c8c7f4000000010000000001406f400000000001406ff46666666666666666666666666666666666666666a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000f69500000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0xa38c06014b4b53f6b124c1c848d09ca306057a388f73429208a017f55ec790a9", + "public_signals": [ + "217391206670124389255755339554236701859", + "8003808394146612921902176078561513641" + ] + } + }, + { + "name": "erc20_second_bond_attempt", + "description": "Identical to erc20_baseline except bond_attempt = 2. Its statement hash must differ, which is what rejects a proof from the prior lease.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1000000", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000001222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000001a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48444444444444444444444444444444444444444400000000000000000000000000000000000000000000000000000000000f42405555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0xa4abd620fc7199a0d4586a11bd6d362b2307ffec6ce7acac21e0303fbde55207" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 1, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "intent_commitment": "0xa4abd620fc7199a0d4586a11bd6d362b2307ffec6ce7acac21e0303fbde55207", + "bond_attempt": 2, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0x0000000000000000000000000000000000000000", + "payout_amount": "500000000000000000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x02111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000012222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000a4abd620fc7199a0d4586a11bd6d362b2307ffec6ce7acac21e0303fbde55207000000020000000001406f400000000001406ff46666666666666666666666666666666666666666000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b200000000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0xc374a6759414dee1d3474bccc0b0a380b17e99b3e29afa222b66813ad599f5cc", + "public_signals": [ + "259805141809166868068421984825988129664", + "235930702118746914561841968803997152716" + ] + } + }, + { + "name": "erc20_sepolia_chain_id", + "description": "Sepolia chain id, a multi-byte value left-padded to a 32-byte word.", + "intent": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 11155111, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "asset_kind": 1, + "asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "recipient": "0x4444444444444444444444444444444444444444", + "amount": "1000000", + "salt": "0x5555555555555555555555555555555555555555555555555555555555555555" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000aa36a7222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333330000000001a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48444444444444444444444444444444444444444400000000000000000000000000000000000000000000000000000000000f42405555555555555555555555555555555555555555555555555555555555555555", + "preimage_len": 226, + "commitment": "0x7b6f6f6900dead098ba590f667d7fff0c18f4c0d458dda27ca0bb9746c9bc26f" + }, + "statement": { + "inputs": { + "instance_domain": "0x1111111111111111111111111111111111111111111111111111111111111111", + "chain_id": 11155111, + "escrow": "0x2222222222222222222222222222222222222222", + "request_id": "0x3333333333333333333333333333333333333333333333333333333333333333", + "row_index": 0, + "intent_commitment": "0x7b6f6f6900dead098ba590f667d7fff0c18f4c0d458dda27ca0bb9746c9bc26f", + "bond_attempt": 1, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0x0000000000000000000000000000000000000000", + "payout_amount": "500000000000000000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x0211111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000aa36a722222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333000000007b6f6f6900dead098ba590f667d7fff0c18f4c0d458dda27ca0bb9746c9bc26f000000010000000001406f400000000001406ff46666666666666666666666666666666666666666000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b200000000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0x1391dcde2310350ef29853e728f8713286880b894e820286bff45af3886240c2", + "public_signals": [ + "26012694694017401843505205076324544818", + "178822937793000371202222476440813846722" + ] + } + }, + { + "name": "erc20_high_bytes", + "description": "Every 32-byte and address field set to 0xff, catching sign extension or signed handling in a non-Rust implementation.", + "intent": { + "inputs": { + "instance_domain": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "chain_id": 1, + "escrow": "0xffffffffffffffffffffffffffffffffffffffff", + "request_id": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "row_index": 0, + "asset_kind": 1, + "asset": "0xffffffffffffffffffffffffffffffffffffffff", + "recipient": "0xffffffffffffffffffffffffffffffffffffffff", + "amount": "1000000", + "salt": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "preimage": "0x02ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000f4240ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "preimage_len": 226, + "commitment": "0x10e50c9454d21a02535967c8c9ffbb3b9b3c1fccf890adb2d339dc05aa71196a" + }, + "statement": { + "inputs": { + "instance_domain": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "chain_id": 1, + "escrow": "0xffffffffffffffffffffffffffffffffffffffff", + "request_id": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "row_index": 0, + "intent_commitment": "0x10e50c9454d21a02535967c8c9ffbb3b9b3c1fccf890adb2d339dc05aa71196a", + "bond_attempt": 4294967295, + "bond_start_block": 21000000, + "bond_deadline": 21000180, + "bonded_collector": "0x6666666666666666666666666666666666666666", + "payout_asset": "0xffffffffffffffffffffffffffffffffffffffff", + "payout_amount": "500000000000000000", + "witness_block_number": 21000050, + "witness_block_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "preimage": "0x02ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000010e50c9454d21a02535967c8c9ffbb3b9b3c1fccf890adb2d339dc05aa71196affffffff0000000001406f400000000001406ff46666666666666666666666666666666666666666ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000006f05b59d3b200000000000001406f72aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "preimage_len": 285, + "statement_hash": "0xaf50a810242832b90d3eee2d1a7544a85971ec6c1759c7e80d61769f93335b68", + "public_signals": [ + "233033691734697134595588566698459219112", + "118892816382406655200606823064410479464" + ] + } + } + ] +} diff --git a/test/integration/mock-api.ts b/test/integration/mock-api.ts index 94cec4d..980e883 100644 --- a/test/integration/mock-api.ts +++ b/test/integration/mock-api.ts @@ -162,9 +162,14 @@ function createServer(port: number, nomadUrl?: string): http.Server { try { const request = JSON.parse(body) as { chain_id: number; - sender: string; + sender?: string; escrow_type: "erc20" | "native" | "batch"; - blinded_signers: string[]; + blinded_signers?: string[]; + intent?: { + commitment: string; + instance_domain: string; + request_id: string; + }; signals: Array<{ asset: string; execution_mode: "private" | "native"; @@ -183,9 +188,21 @@ function createServer(port: number, nomadUrl?: string): http.Server { rowIndex: itemIndex, })), ); - if (rows.length === 0 || request.blinded_signers.length !== rows.length) { + // A request without a sender is an unsigned preview: it carries no + // signers and receives no deployable fields. + const preview = request.sender === undefined; + if (rows.length === 0) { + throw new Error("pricing requires at least one row"); + } + if (!preview && (request.blinded_signers?.length ?? 0) !== rows.length) { throw new Error("pricing requires one blinded signer per row"); } + if (!preview && request.escrow_type === "erc20" && !request.intent) { + throw new Error("ERC-20 escrow requires a ZK intent"); + } + if (request.intent && request.escrow_type !== "erc20") { + throw new Error("only ERC-20 escrows take a ZK intent"); + } if (request.escrow_type !== "batch" && rows.length !== 1) { throw new Error("single escrow pricing requires exactly one row"); } @@ -201,6 +218,27 @@ function createServer(port: number, nomadUrl?: string): http.Server { } const rewardKey = rewardAsset.toLowerCase(); deposits.set(rewardKey, (deposits.get(rewardKey) ?? 0n) + rewardAmount); + if (preview) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + chain_id: request.chain_id, + service_fee: { asset: rewardAsset, amount: rewardAmount.toString() }, + deployment: { + escrow_type: request.escrow_type, + constructor_args: null, + quote_commitment: null, + reward_asset: rewardAsset, + reward_amount: rewardAmount.toString(), + deposit_by_asset: Object.fromEntries( + [...deposits].map(([asset, amount]) => [asset, amount.toString()]), + ), + msg_value: (deposits.get(zeroAddress) ?? 0n).toString(), + }, + sealed_pricing_authorization: null, + })); + return; + } + const firstRow = rows[0]; const constructorArgs = request.escrow_type === "erc20" diff --git a/test/transfer-pricing.test.ts b/test/transfer-pricing.test.ts index be66739..1bfee2b 100644 --- a/test/transfer-pricing.test.ts +++ b/test/transfer-pricing.test.ts @@ -146,7 +146,7 @@ describe("prepareTransfer pricing flow", () => { recipientAddress: RECIPIENT_A, amount: 100n, senderAddress: SENDER, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, }); @@ -165,7 +165,7 @@ describe("prepareTransfer pricing flow", () => { recipientAddress: RECIPIENT_A, amount: 100n, senderAddress: SENDER, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, }), ).rejects.toMatchObject({ code: "INVALID_PRICING_QUOTE" }); @@ -178,7 +178,8 @@ describe("prepareTransfer pricing flow", () => { amount: 100n, senderAddress: SENDER, publicClient: { - getTransactionCount: vi.fn().mockRejectedValue(new Error("RPC unavailable")), + getTransactionCount: vi.fn().mockResolvedValue(5), + estimateContractGas: vi.fn().mockRejectedValue(new Error("RPC unavailable")), } as any, network, }); @@ -196,7 +197,7 @@ describe("prepareTransfer pricing flow", () => { { tokenAddress: NATIVE_TOKEN_ADDRESS, recipientAddress: RECIPIENT_B, amount: 50n }, ], senderAddress: SENDER, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, }); @@ -219,7 +220,7 @@ describe("prepareTransfer pricing flow", () => { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 100n, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, }), ).rejects.toMatchObject({ code: "SENDER_REQUIRED" }); @@ -233,7 +234,7 @@ describe("prepareTransfer pricing flow", () => { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 100n, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, resume: { ...VALID_RESUME, [field]: undefined } as any, }), @@ -249,7 +250,7 @@ describe("prepareTransfer pricing flow", () => { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 100n, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, resume: { ...VALID_RESUME, escrowType: "erc20" }, }); @@ -266,7 +267,7 @@ describe("prepareTransfer pricing flow", () => { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 100n, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, resume: { ...VALID_RESUME, escrowType: "batch" }, }), @@ -280,7 +281,7 @@ describe("prepareTransfer pricing flow", () => { { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 40n }, { tokenAddress: USDC, recipientAddress: RECIPIENT_B, amount: 60n }, ], - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, resume: { ...VALID_RESUME, escrowType: "batch" }, }), @@ -292,7 +293,7 @@ describe("prepareTransfer pricing flow", () => { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 100n, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, resume: VALID_RESUME, }); @@ -323,7 +324,7 @@ describe("nomad proxy routing", () => { recipientAddress: RECIPIENT_A, amount: 100n, senderAddress: SENDER, - publicClient: {} as any, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, network, }); @@ -494,3 +495,69 @@ describe("previewTransfer", () => { ).rejects.toMatchObject({ code: "INVALID_PRICING_QUOTE" }); }); }); + +describe("zk intent", () => { + it("sends an intent for an ERC-20 escrow", async () => { + await prepareTransfer({ + tokenAddress: USDC, + recipientAddress: RECIPIENT_A, + amount: 100n, + senderAddress: SENDER, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, + network, + }); + + expect(pricingBody.escrow_type).toBe("erc20"); + expect(pricingBody.intent.commitment).toMatch(/^0x[0-9a-f]{64}$/); + expect(pricingBody.intent.instance_domain).toMatch(/^0x[0-9a-f]{64}$/); + expect(pricingBody.intent.request_id).toMatch(/^0x[0-9a-f]{64}$/); + // A zero commitment is openable by anyone who guesses the empty preimage. + expect(pricingBody.intent.commitment).not.toBe(`0x${"00".repeat(32)}`); + }); + + it("omits the intent for native and batch escrows", async () => { + await prepareTransfer({ + tokenAddress: NATIVE_TOKEN_ADDRESS, + recipientAddress: RECIPIENT_A, + amount: 100n, + senderAddress: SENDER, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, + network, + }); + expect(pricingBody.intent).toBeUndefined(); + + await prepareTransfer({ + transfers: [ + { tokenAddress: USDC, recipientAddress: RECIPIENT_A, amount: 100n }, + { tokenAddress: USDC, recipientAddress: RECIPIENT_B, amount: 200n }, + ], + senderAddress: SENDER, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, + network, + }); + expect(pricingBody.intent).toBeUndefined(); + }); + + it("binds a fresh domain and request id per preparation", async () => { + const args = { + tokenAddress: USDC, + recipientAddress: RECIPIENT_A, + amount: 100n, + senderAddress: SENDER, + network, + }; + await prepareTransfer({ + ...args, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, + }); + const first = pricingBody.intent; + await prepareTransfer({ + ...args, + publicClient: { getTransactionCount: vi.fn().mockResolvedValue(5) } as any, + }); + + expect(pricingBody.intent.instance_domain).not.toBe(first.instance_domain); + expect(pricingBody.intent.request_id).not.toBe(first.request_id); + expect(pricingBody.intent.commitment).not.toBe(first.commitment); + }); +}); diff --git a/test/zk.test.ts b/test/zk.test.ts new file mode 100644 index 0000000..fab48cd --- /dev/null +++ b/test/zk.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { deriveSalt, intentCommitment, intentPreimage } from "../src/internal/zk.js"; + +/** + * Vectors are generated by `cargo test -p nomad-types --test zk_vectors`. The + * circuit, the Solidity escrow, and this encoder must reproduce every preimage + * and digest byte for byte. + */ +const VECTORS = JSON.parse( + readFileSync(new URL("./fixtures/zk_vectors.json", import.meta.url), "utf-8"), +); + +describe("zk intent vectors", () => { + it("matches the pinned relation version", () => { + expect(VECTORS.relation_version).toBe(2); + }); + + for (const testCase of VECTORS.cases) { + it(`reproduces ${testCase.name}`, () => { + const input = testCase.intent.inputs; + const opening = { + instanceDomain: input.instance_domain, + chainId: input.chain_id, + escrow: input.escrow, + requestId: input.request_id, + rowIndex: input.row_index, + asset: input.asset, + recipient: input.recipient, + amount: BigInt(input.amount), + salt: input.salt, + }; + expect(intentPreimage(opening)).toBe(testCase.intent.preimage); + expect(intentCommitment(opening)).toBe(testCase.intent.commitment); + }); + } +}); + +describe("deriveSalt", () => { + const scalar = `0x${"11".repeat(32)}` as const; + const domain = `0x${"22".repeat(32)}` as const; + const request = `0x${"33".repeat(32)}` as const; + + it("is deterministic", () => { + expect(deriveSalt(scalar, domain, request, 0)).toBe(deriveSalt(scalar, domain, request, 0)); + }); + + it("separates every input", () => { + const baseline = deriveSalt(scalar, domain, request, 0); + expect(deriveSalt(`0x${"44".repeat(32)}`, domain, request, 0)).not.toBe(baseline); + expect(deriveSalt(scalar, `0x${"44".repeat(32)}`, request, 0)).not.toBe(baseline); + expect(deriveSalt(scalar, domain, `0x${"44".repeat(32)}`, 0)).not.toBe(baseline); + expect(deriveSalt(scalar, domain, request, 1)).not.toBe(baseline); + }); +});