> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Shielded Payment Circuits

> The sender proof, recipient proof, payment identifiers, and encrypted payloads for a MIP410 shielded payment

This page defines the proof boundaries for a MIP410 shielded payment. See
[Shielded Payments](https://docs.metalntwx.com/learn/mip410/shielded-payments.md) for the payment lifecycle
and [Proof and Verification](https://docs.metalntwx.com/learn/mip410/proof-and-verification.md) for proof
composition.

## Submitted proof set

Each initiation, acceptance, or rejection submits exactly two proofs:

| Proof | Binding |
| - | - |
| Main proof | The sender's initiation statement or the recipient's resolution statement |
| Policy-wrapper proof | The sender or recipient VASP's signature and private policy proof for the same payment context |

The pool constructs both sets of public inputs. It supplies the same payment
effect hash, recipient VASP binding commitment, and payload commitment to both
proofs. Shielding and unshielding each use a single proof without a policy wrapper.

## Payment identifiers

A shielded payment uses three derived values: nullifiers, input lock IDs, and
the payment effect hash. The sender circuit derives the first two from the
sender's nullifier key `nk` and each input note's `leafIndex` in the commitment
tree. This shared derivation binds both identifiers to the same input notes.

A nullifier permanently identifies a spent note:

```text theme={null}
nullifier = Poseidon2(nk, leafIndex, DOMAIN_NULLIFIER)
```

Once the pool records a nullifier in the spent set, the note cannot be spent
again. A payment's nullifiers remain private until an `Accept` resolution.

An input lock ID prevents concurrent reservations of the same note:

```text theme={null}
lockId = Poseidon2(nk, leafIndex, DOMAIN_LOCK)
```

Two holds derived from the same input produce the same input lock ID and
collide at initiation. The separate domain tag prevents observers from deriving
the note's nullifier from its input lock ID. Rejection or reclaim releases the
lock. Acceptance retains it permanently, preventing a spent note from being
reserved again while also recording its nullifier in the spent set.
After rejection or reclaim, the note's next hold, unshield, or bridge withdrawal
presents the same input lock ID and links back to the failed attempt.

The payment memo also carries the sender's input nullifiers inside the payment
effect. A recipient VASP that opens it can recognize the spend that eventually
consumes those notes, even after rejecting the payment or letting it expire and
be reclaimed. The nullifiers remain unchanged and become public at a later
`Accept`, unshield, or bridge withdrawal.

The payment effect hash commits the payment outcome to one value:

```text theme={null}
paymentEffectHash = Poseidon2(DOMAIN_EFFECT,
                              nullifier_0, nullifier_1,
                              recipientNoteCommitment,
                              changeNoteCommitment)
```

There are exactly two input slots. A payment spends one or two real notes;
when there is only one, the second slot's nullifier and input lock ID are zero.
A zero change note commitment means no change note.

The payment effect hash is a public input of initiation and serves as the hold
ID. The nullifiers require the sender's `nk`, and the output commitments contain
fresh secrets, so observers cannot reconstruct the payment effect from its
hash. The sender proof binds the sealed nullifiers to the input notes and
balances their value against the recipient note and optional change.

### Onchain use

| Value | Public from | Onchain checks |
| - | - | - |
| Input lock IDs | Initiation calldata | Recorded at initiation; retained after acceptance; released after rejection or reclaim; also checked by unshielding |
| Payment effect hash | Initiation calldata | Identifies the hold; recomputed from the disclosed effect at acceptance |
| Nullifiers | Acceptance calldata | Required to be unspent and then recorded permanently; unshielding uses the same spent set |

## Payment request

The recipient provides a payment request signed by its VASP's messaging key.
The current request is version 2, with a fixed 433-byte binary encoding:

| Field | Size | Meaning |
| - | -: | - |
| Version | 1 byte | `2` |
| Diversified address | 96 bytes | `d`, `pk_x`, and `pk_y` |
| VASP identifier | 32 bytes | `fi_id` of the VASP that must resolve the payment |
| Registry index | 8 bytes | The VASP's slot in the pool's VASP tree |
| Expiry | 8 bytes | Unix timestamp through which the request is valid |
| Record thresholds | 4 × 48 bytes | Asset and amount pairs indicating when the recipient VASP expects a Travel Rule record; unused slots are zero |
| Signature | 96 bytes | Grumpkin Schnorr signature over the request |

The sender checks the signature against the VASP's admitted messaging key and
checks expiry offchain. Neither the pool nor the sender circuit verifies this
request signature or expiry. The request supplies the recipient address and
VASP identifier used to build the payment. There is no address-issuance proof
or onchain customer designation.

The sender circuit derives the recipient VASP binding commitment from the
request's `fi_id` and payment effect hash. At resolution, the recipient proof
and policy wrapper must reproduce that binding. A request naming the wrong
VASP can produce an unresolvable hold; expiry then permits reclaim. Record
thresholds guide payload preparation, while the VASP's policy determines
whether that payload permits the payment.

## Initiation

The sender proof establishes this statement:

> I know the openings of one or two notes in the commitment tree and authorize
> this payment with their spending key. The inputs share one asset and fund the
> recipient note plus any change. My `affilID` is absent from the sanctioned
> set. The payment effect, recipient VASP binding, and payload commitment match
> the values fixed for this initiation.

### Ownership and value

The proof witnesses each input
[note opening](https://docs.metalntwx.com/reference/privacy/shielded-note.md#the-commitment) and its Merkle
path. From `(ak, nk)`, it derives the sender's `ivk` and checks each note's
address. A Schnorr signature under `ak` covers all eleven initiation public
inputs, including the payment effect hash, input lock IDs, nonce, deadline,
payload commitment, and sanctions root.

Input amounts equal the recipient amount plus change. Each amount is
range-checked to 128 bits and the recipient amount must be nonzero. Any change
address must derive from the sender's `ivk`. Wallets should choose a fresh
change diversifier; the circuit enforces ownership but does not check prior
use. Both output notes inherit the minimum `shieldTimestamp` of the real inputs.

The pool checks input lock availability separately. The sender proof derives
the input lock IDs and nullifiers from the same `(nk, leafIndex)` pairs, so
the reserved notes are the notes acceptance spends.

### Payload commitment

The witness includes the payload salt seed, optional Travel Rule record, and
payment identifiers. The circuit recomputes the salted payload commitment,
including a payment leaf derived from the public payment effect hash,
recipient VASP binding commitment, nonce, and deadline. This binds the payload
to the hold without publishing its plaintext.

The main circuit does not verify an X.509 credential, an external
screening verdict, or a standard beneficiary-alignment proof. Its sanctions
check proves non-membership of the sender's `affilID`. Additional requirements
belong in the VASP's private policy and offchain checks; the payload's evidence
leaf is reserved as zero.

### Witness

| Witness | Role |
| - | - |
| Two input slots | Note openings, address base-point roots, commitment paths, and flags distinguishing real notes from the unused slot |
| `(ak, nk)` and authorization signature | Establish ownership and authorize the exact initiation |
| Recipient note material | Address, amount, and note secret |
| Change note material | Amount, diversifier, base-point root, and note secret when change exists |
| Recipient VASP identifier | Derive the recipient VASP binding commitment |
| Sanctions path | Prove non-membership of the sender's `affilID` |
| Payload seed, record fields, and identifiers | Recompute the payload commitment from its plaintext |

### Public inputs

The circuit receives these eleven field elements in order:

| Position | Public input | Pool constraint |
| - | - | - |
| 1–2 | `chain_id`, `pool_address` | Bound to this deployment |
| 3 | `commitment_root` | One of the pool's retained commitment roots |
| 4–5 | `lock_ids[2]` | Real input slots must be unlocked; the pool skips zero dummy slots |
| 6 | `payment_effect_hash` | Must identify a new hold |
| 7 | `fi_binding_commitment` | Stored as the recipient VASP binding |
| 8 | `nonce` | Bound by the signature and payload commitment |
| 9 | `deadline` | Later than the inclusion block timestamp and no more than `maxHoldDuration()` seconds ahead |
| 10 | `payload_commitment` | Nonzero canonical field element, stored on the hold |
| 11 | `sanctions_root` | Current sanctions root |

The registration root is an input to the accompanying policy wrapper, not the
main initiation circuit. After verifying both proofs, the pool records the
input locks and creates the hold.

## Policy wrapper

The same wrapper circuit serves initiation, acceptance, and rejection. It
witnesses the VASP's registry leaf, messaging-key signature, private
policy verification key, and policy proof. It checks that the witnessed key's
hash matches the leaf and verifies the policy proof recursively. It also checks
a messaging-key signature over the chain, pool, payment effect hash, phase,
and payload commitment.

The wrapper has eight public inputs in order:

| Position | Public input | Meaning |
| - | - | - |
| 1–2 | `chain_id`, `pool_address` | Deployment binding for the VASP's signature |
| 3 | `payment_effect_hash` | Payment being judged |
| 4 | `phase` | `0` initiation, `1` acceptance, `2` rejection |
| 5 | `payload_commitment` | Payload the VASP signs and its policy judges |
| 6 | `commitment_root` | Initiation's root; zero at resolution |
| 7 | `registry_root` | Current VASP registry root or the previous root retained after an admission |
| 8 | `fi_binding_commitment` | At resolution, must identify the VASP whose leaf and signature the wrapper verifies |

The inner policy exposes five values to the wrapper: payment effect hash,
phase, payload commitment, commitment root, and registry root. The wrapper
requires them to match its own public inputs. The private policy is responsible
for binding its own witness and checks to that context.

At initiation, the sender VASP signs the context. The initiation proof does
not bind the policy signer to the sender's VASP. At resolution, the wrapper
binds the signing VASP to the hold's recipient VASP. Customer affiliation is
maintained offchain. The registry contains VASPs, policy verification-key
hashes, and messaging public keys.

## Encrypted payment envelope

Payment data travels in two encrypted parts with different recipients:

| Part | Cleartext header | Encrypted content | Decryption key |
| - | - | - | - |
| Note memo, version 2 | Version, payment effect hash, ephemeral public key, one-byte view tag, payload commitment | Recipient note opening, payment effect, nonce, deadline | Recipient's `ivk` |
| Institutional payload, version 1 | Version, payment effect hash, ephemeral public key | Payload salt seed, payment identifiers, optional Travel Rule record, sender VASP identifier, sender payload signature | Recipient VASP's messaging secret key |

The memo is 666 bytes; the institutional payload is 2,230 bytes. Both use
ChaCha20-Poly1305 with the cleartext header authenticated as associated data.
Each encryption draws a fresh ephemeral secret, producing a one-time key.
Delivery and retrieval are the integrator's responsibility.

### Encryption

For the note memo, Grumpkin Diffie-Hellman uses the recipient address's base point:

```text theme={null}
epk    = esk · g_d
shared = esk · pk_d          sender
       = ivk · epk           recipient
```

A domain-separated Poseidon2 derivation turns the shared point and ephemeral
public key into the symmetric key. A one-byte view tag helps scanners discard
unrelated memos before decryption. A VASP with delegated viewing access can
open the memo without the customer's spending key.

The institutional payload instead uses `epk = esk · G` and
`shared = esk · mpk = msk · epk`, where `mpk` and `msk` are the recipient
VASP's messaging public and secret keys. A separate domain tag derives its
symmetric key. Possessing `ivk` alone does not reveal the institutional record.

The recipient joins both plaintexts, recomputes the payload commitment, compares
it with the hold, and verifies the sender VASP's payload signature against its
messaging key in the pool's institution registry. These intake checks run
offchain. The sender's payload signature is separate from the context signature
verified inside each policy wrapper.

## Resolution

The recipient proof establishes this statement for either outcome:

> I know the recipient note and payment effect sealed by this hold. The note's
> address derives from my `ivk`, whose `affilID` is absent from the sanctioned
> set. The recipient VASP identifier reproduces the hold's binding. Acceptance
> discloses the sealed effect; rejection discloses an empty effect.

| Witness | Role |
| - | - |
| Recipient note opening and base-point root | Recompute the note commitment and its diversified address |
| Payment effect | Recompute the hold's payment effect hash, including the sender's nullifiers |
| Recipient's `ivk` | Establish ownership of the recipient note and derive `affilID` |
| Recipient VASP identifier | Reproduce the hold's recipient VASP binding commitment |
| Sanctions path | Prove non-membership of the recipient's `affilID` |

The eleven public inputs, in order, are:

| Position | Public input | Pool constraint |
| - | - | - |
| 1–2 | `chain_id`, `pool_address` | This deployment |
| 3 | `payment_effect_hash` | The active hold ID |
| 4 | `resolution` | `0` acceptance or `1` rejection; distinct from the wrapper's phase values |
| 5–6 | `nullifiers[2]` | Disclosed effect on acceptance; zeros on rejection |
| 7 | `recipient_note_commitment` | Disclosed effect on acceptance; zero on rejection |
| 8 | `change_note_commitment` | Disclosed effect on acceptance; zero if no change or on rejection |
| 9 | `fi_binding_commitment` | Loaded from the hold |
| 10 | `payload_commitment` | Loaded from the hold and shared with the policy wrapper |
| 11 | `sanctions_root` | Current sanctions root |

The pool's `acceptPayment` and `rejectPayment` calls require both the recipient
proof and recipient VASP's policy-wrapper proof. Acceptance records the
nullifiers, appends the recipient note and any change, and retains input locks.
Rejection releases locks without spending notes or appending commitments.
Both require an active hold at or before its deadline.

Rejection still requires the sealed payment effect and an unsanctioned
recipient. If the recipient cannot produce those witnesses, it cannot reject
the hold. Once the block timestamp exceeds the deadline, anyone can call
`reclaimExpired` to release the sender's input locks without proofs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.