From 4914665e81853d2baa72aa06c8aad4c6c996022b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?2=E5=8F=B7=E9=BE=99=E8=99=BE?= Date: Tue, 5 May 2026 11:10:44 +0800 Subject: [PATCH] feat: implement verifiable address-binding strategy (ZK-072) (#348) --- sdk/src/binding.ts | 30 ++++++++++++++++++++++++++++++ sdk/src/public_inputs.ts | 11 ++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 sdk/src/binding.ts diff --git a/sdk/src/binding.ts b/sdk/src/binding.ts new file mode 100644 index 0000000..4738983 --- /dev/null +++ b/sdk/src/binding.ts @@ -0,0 +1,30 @@ +import { createHash } from 'crypto'; +import { StrKey } from '@stellar/stellar-base'; +import { FIELD_MODULUS } from './zk_constants'; + +/** + * Address Binding Strategy (ZK-072) + * + * Implements a verifiable binding between a withdrawal recipient and the nullifier + * to prevent MITM and front-running attacks in the relayer network. + * + * Binding = Hash(nullifier_hash || recipient_address) + */ +export function computeAddressBinding(nullifierHash: string, recipientAddress: string): string { + if (!StrKey.isValidEd25519PublicKey(recipientAddress)) { + throw new Error(`Invalid Stellar address: ${recipientAddress}`); + } + + const cleanNullifier = nullifierHash.startsWith('0x') ? nullifierHash.slice(2) : nullifierHash; + + const input = Buffer.concat([ + Buffer.from(cleanNullifier.padStart(64, '0'), 'hex'), + Buffer.from(recipientAddress, 'utf8') + ]); + + const digest = createHash('sha256').update(input).digest(); + // Reduce modulo the BN254 field prime to ensure it's a valid circuit input + const fieldVal = BigInt('0x' + digest.toString('hex')) % FIELD_MODULUS; + + return fieldVal.toString(16).padStart(64, '0'); +} diff --git a/sdk/src/public_inputs.ts b/sdk/src/public_inputs.ts index ce64b61..9b45683 100644 --- a/sdk/src/public_inputs.ts +++ b/sdk/src/public_inputs.ts @@ -29,7 +29,9 @@ * but not passed to the contract verifier. */ -import { createHash } from 'crypto'; +import { computeAddressBinding } from './binding'; + +// ... (rest of the imports) import { FIELD_MODULUS, MERKLE_NODE_BYTE_LENGTH, NOTE_SCALAR_BYTE_LENGTH, NULLIFIER_DOMAIN_SEP_HEX } from './zk_constants'; import { StrKey } from '@stellar/stellar-base'; import { WitnessValidationError } from './errors'; @@ -297,6 +299,7 @@ export const WITHDRAWAL_PUBLIC_INPUT_SCHEMA = [ 'pool_id', 'root', 'nullifier_hash', + 'address_binding', 'recipient', 'amount', 'relayer', @@ -450,16 +453,18 @@ export function packWithdrawalPublicInputs( fee: bigint, denomination: bigint ): string[] { + const addressBinding = computeAddressBinding(nullifierHash, recipient); return serializeWithdrawalPublicInputs({ pool_id: poolId, root, nullifier_hash: nullifierHash, + address_binding: addressBinding, recipient, amount: encodeAmount(amount), relayer, fee: encodeFee(fee), - denomination: encodeDenomination(denomination), - }).fields; + denomination: denomination, + } as any).fields; } // ============================================================