Swapper M

Hinkal’s SwapperM circuit is a generalized “multi‑token swapper” that allows a user to consume up to inputCount UTXOs for each of tokenCount different tokens, and to create up to outputCount new UTXOs per token type. The circuit verifies membership proofs for the consumed commitments, computes and checks new commitments, enforces balance equations per token type, and proves that the user holds a valid access token. It also derives stealth addresses and helper values needed by the smart contract and exposes them as public outputs. Because the circuit must handle an arbitrary number of tokens, inputs and outputs, it takes matrix‑shaped inputs and loops over them internally, ensuring correctness and preventing overflow.

The circuit supports multiple token types (tokenCount), multiple input UTXOs per token type (inputCount) and multiple output UTXOs per token type (outputCount). It therefore takes matrix‑shaped inputs for amounts, timestamps, randomization values and Merkle proofs. The only special encoding used is the derivation of stealth addresses and commitments from a user’s shielded private key. The circuit uses Poseidon‑based hash functions and the StealthAddressCalculator to derive sender and per‑UTXO public keys, and an OriginalCommitmentCalculator to build note commitments. Additional public inputs provide the roots of the commitments tree (rootHashHinkal) and the access‑token tree (rootHashAccessToken). The circuit also derives an extra stealth address from a supplied extraRandomization, and uses the ApprovalConversionHandlerBatch gadget to compute additional addresses (conversionInHinkalAddress) for external approvals. To prevent integer overflows, each input and output amount is passed through an OverflowPreventer. Finally, to ensure that the circuit is instantiated for distinct tokens, a set of IsEqual constraints asserts that every erc20TokenAddress is unique.

Statement to Prove

The SwapperM circuit has the following public inputs:

  • rootHashHinkal – the Merkle root of the on‑chain note‑commitment tree against which all input UTXOs must be proven.

  • erc20TokenAddresses[tokenCount] – an array of ERC‑20 token addresses (the zero address denotes ETH). All addresses must be distinct.

  • inNullifiers[tokenCount][inputCount] – the nullifier for each input UTXO; each must equal the nullifier computed from the corresponding commitment and signature.

  • outCommitments[tokenCount][outputCount] – the claimed commitments for the outputs.

  • rootHashAccessToken – the Merkle root of the access‑token tree; the sender’s access token must be proven against this root.

  • calldataHash – a call‑data digest; it is exposed but not otherwise used by the circuit.

  • interactionAddress – an address combined with the shielded private key to compute the inHinkalAddress.

  • externalApprovalAddresses[tokenCount][conversionCount()] – for each token type and conversion index, an external approval address used to derive conversionInHinkalAddress.

  • outTimeStamp – common timestamp applied to all output commitments.

  • amountChanges[tokenCount] – net change for each token type; the sum of output amounts must equal the sum of input amounts plus amountChanges[i].

The circuit exposes several outputs:

  • inHinkalAddress – a Poseidon hash of the shielded private key and interactionAddress used by the contract to index the transaction.

  • conversionInHinkalAddress[tokenCount][conversionCount()] – per‑token, per‑conversion addresses derived from the shielded private key and externalApprovalAddresses.

  • extraRandomizedStealthAddress, extraRandomizedStealthAddressSigns, extraRandomizedStealthAddressH0Ay, extraRandomizedStealthAddressH1Ay – a Poseidon‑based stealth address (and its elliptic‑curve components) computed from shieldedPrivateKey and extraRandomization. These values can be output on‑chain to save gas.

It also takes the following private data (witness values):

  • shieldedPrivateKey – the sender’s secret key.

  • inAmounts[tokenCount][inputCount] – the amounts of each input UTXO.

  • inTimeStamps[tokenCount][inputCount] and inRandomizations[tokenCount][inputCount] – timestamps and randomization values used to derive input commitments and stealth addresses.

  • inCommitmentSiblings[tokenCount][inputCount][treeDepth] and inCommitmentSiblingSides[tokenCount][inputCount][treeDepth] – Merkle proofs showing membership of each input commitment in the tree rootHashHinkal.

  • outAmounts[tokenCount][outputCount] – amounts of each output UTXO.

  • outPublicKeys[tokenCount][outputCount] – stealth public keys for the recipients of each output.

  • extraRandomization – randomization used to compute the extra stealth address.

  • accessTokenSiblings[treeDepth] and accessTokenSiblingSides[treeDepth] – Merkle proof showing membership of the sender’s access token in the tree rootHashAccessToken.

  • Other internal signals such as signatures and commitments are computed by the circuit and need not be provided explicitly.

Conditions Enforced by the Circuit

The circuit proves that all of the following conditions hold true (all hashes are Poseidon hashes, and amounts are interpreted as little‑endian field elements):

  1. Correct nullifiers: For every token type i and input index j, it computes a stealth public key from shieldedPrivateKey and inRandomizations[i][j], uses this key along with inAmounts[i][j], inTimeStamps[i][j] and erc20TokenAddresses[i] to build an input commitment, signs the commitment with the shielded private key to derive a signature, and uses both to compute a nullifier. It asserts that inNullifiers[i][j] matches the computed nullifier. This binds each input to the sender and prevents double‑spends.

  2. Valid Merkle proofs for inputs: For each non‑zero input amount inAmounts[i][j], it verifies that the Merkle root produced by hashing the input commitment with inCommitmentSiblings[i][j] and inCommitmentSiblingSides[i][j] equals rootHashHinkal. (If the amount is zero, the root check is skipped via a ForceEqualIfEnabled gadget.)

  3. Correct output commitments: For every token type i and output index j, it computes an output commitment from outAmounts[i][j], erc20TokenAddresses[i], outPublicKeys[i][j] and outTimeStamp, and asserts that outCommitments[i][j] equals the computed commitment.

  4. Balanced amounts per token: For each token type i, it sums all input amounts to inTotal and all output amounts to outTotal. It enforces inTotal + amountChanges[i] = outTotal. This ensures that the difference between total inputs and outputs equals the claimed amountChanges[i], allowing swaps or refunds without creating or destroying tokens.

  5. Access token ownership: It verifies that the sender holds a valid access token. It hashes the shieldedPrivateKey to derive a public key, then uses accessTokenSiblings and accessTokenSiblingSides to check that this public key exists in the access‑token tree rooted at rootHashAccessToken. It also ensures isStakeOrUnstake = 0, indicating that this operation is neither a stake nor an unstake.

  6. Distinct token addresses: It asserts that all entries in erc20TokenAddresses are distinct. For every pair (i, j) with i < j, an IsEqual gadget forces erc20TokenAddresses[i] ≠ erc20TokenAddresses[j] by requiring its output to be zero.

  7. Derived addresses: It computes inHinkalAddress = Poseidon(shieldedPrivateKey, interactionAddress) and exposes it as a public output. Similarly, for each token type and conversion index, it computes conversionInHinkalAddress[i][k] by hashing shieldedPrivateKey with externalApprovalAddresses[i][k].

  8. Extra stealth address: It computes an extra stealth address from shieldedPrivateKey and extraRandomization and exposes it (and its associated curve coordinates) as outputs. These values can be used by the contract to reconstruct commitments without recomputing the expensive elliptic‑curve operations.

  9. Overflow prevention: OverflowPreventer gadgets ensure that each inAmounts[i][j] and outAmounts[i][j] fits within the field size (preventing wrap‑around), and all hashes and signatures are computed using Poseidon‑based primitives imported from circomlib.

Collectively, these constraints prove that the sender owns and spends the input UTXOs, that the net token flows match the claimed amountChanges, that the outputs are correctly formed, that the sender is authorized via an access token, and that no double‑spending or value overflow occurs.

Last updated