Privacy
The three privacy levels, the PrivacyOptions that implement level 1, private mode with adaptor signatures, and what an observer can still see.
Paralock removes the middleman, but privacy depends on what the two chains reveal. A swap has two legs: ZEC locked on Zcash and stablecoins locked on an EVM chain. Both locks are public. The goal is that neither your shielded history nor the link between the two legs leaks.
Three levels
| Level 1: hygiene | Level 2: adaptor signatures | Level 3: fully shielded leg | |
|---|---|---|---|
| Status | Shipped | Experimental | In research |
| How | Privacy and shielding in your wallet | mode: "adaptor" | FROST 2-of-2 shielded notes |
| Swap visible on Zcash? | Yes: a transparent HTLC | Yes, but only as an ordinary 2-of-2 multisig | No: a normal shielded transaction |
| A shared value links the two chains? | Yes: the same sha256 hashlock | None | None |
| Linked to your wallet history? | No, if you shield before and after and use a fresh recipient | No | No |
| Amount and timing correlation | Reduced: standard amounts, jitter | Reduced | Reduced |
Level 1 and level 2 combine: use private mode and every hygiene option together.
Level 1: hygiene
These measures don't hide that a swap happened, or the link between its two legs. They keep a swap from linking to the rest of your activity.
Shield before and after
The taker spends from one transparent key. Keep your savings in the shielded pool, and before each swap move only the swap amount (plus fees) from your shielded balance to that key's transparent address, taker.zcashAddress. Change and refunds go to zcash.returnAddress (by default the same address); shield them again. The maker does the same with the ZEC it claims, for example in its afterClaim hook.
The SDK doesn't move shielded funds: your wallet does. The devnet shows the pattern: the CLI unshields each swap from ths account 1 into the dedicated swap address (account 2) and reshields change afterwards, and examples/payroll does the same for a treasury.
Fresh recipient
freshRecipient: true pays the stablecoins to a new EVM address created for this swap, instead of an address that already has a history. Its private key is kept in the store and passed to onFreshRecipient; read it back with taker.freshRecipient(id). That key controls the funds, so the taker refuses this option unless the store is durable or you set onFreshRecipient. If you pass a recipient (a payment), it wins over freshRecipient.
Relayed claim
A fresh address has no gas. relayClaim: true lets the relayer account configured on that chain submit the claim and pay its gas. The relayer can't redirect the funds: the escrow always pays the recipient fixed at lock time. It could refuse or fail to relay; then the swap fails with SwapFailedError (the SDK doesn't fall back by itself), and taker.resume(id, { relayClaim: false }) submits the claim from your own EVM account before the lock expires. That account pays the gas and is linked to the claim; the recipient stays the one in the lock.
Standard amounts
standardAmountsOnly: true refuses any amount other than 0.1, 0.5, 1 or 5 ZEC (STANDARD_AMOUNTS_ZAT), so an unusual amount doesn't single out your swap. It applies to the ZEC amount, so use it with exact-in quotes (taker.quote). An exact-out payment almost never lands on a standard amount.
Timing jitter
jitterMs: n waits a random 0 to n milliseconds before claiming, so the claim doesn't line up exactly with the maker's lock. Keep it well inside the EVM lock: the taker re-checks the time left after the jitter, and if fewer than minClaimWindowSeconds remain it refuses to claim, and both sides refund.
All together
import { type Hex, createTaker } from "@paralock/sdk";
const taker = createTaker({
zcash: { rpcUrl: "http://127.0.0.1:8232", network: "mainnet", key: process.env.ZEC_KEY!, returnAddress: process.env.ZEC_RETURN_ADDRESS },
evm: {
base: {
rpcUrl: "https://mainnet.base.org",
chainId: 8453,
htlc: process.env.HTLC as Hex,
adaptorEscrow: process.env.ESCROW as Hex,
account: process.env.EVM_KEY as Hex,
relayer: process.env.RELAYER_KEY as Hex, // pays the claim gas; can't redirect the funds
fromBlock: 1,
},
},
maker: "https://maker.example",
store: "./paralock-taker", // fresh-recipient keys are kept here too
onFreshRecipient: (account, swap) => console.log(`swap ${swap.id} pays ${account.address}; store its key safely`),
});
const quote = await taker.quote(50_000_000n, "base", "adaptor"); // 0.5 ZEC, private mode
const swap = await taker.swap(quote, {
privacy: {
freshRecipient: true, // pay a new address created for this swap
relayClaim: true, // the relayer submits the claim, so that address needs no gas
standardAmountsOnly: true, // refuse anything but 0.1, 0.5, 1 or 5 ZEC
jitterMs: 60_000, // wait a random 0 to 60 s before claiming
},
});
const recipient = await taker.freshRecipient(swap.id); // { address, privateKey }: it controls the stablecoinsThe same options on the devnet CLI:
pnpm paralock swap --zec 0.5 --chain base --private --fresh-recipient --relay --standard-only --jitter 10000The devnet saves fresh-recipient keys to .paralock/recipients.jsonl.
What level 1 still leaks: the HTLC is transparent, and the same hashlock appears on both chains, so anyone can pair the two legs of a swap.
Level 2: adaptor signatures
Private mode (mode: "adaptor", or --private in the CLI) removes every shared value from the chains:
- On Zcash, the ZEC sits in a plain 2-of-2 multisig of a fresh taker key and a fresh maker key made for this quote. Spending it looks like any other multisig spend.
- On the EVM chain,
ParalockAdaptorEscrowpays the recipient when given a signature from a one-timesignerkey overclaimDigest(id). The escrow id is a hash of the lock's parameters, including a random salt. - The secret is
y, the discrete logarithm of an adaptor pointY = y·Gthat only the two parties ever see. The taker's claim publishes a completed signature. Only the maker, who holds the matching pre-signature, can computeyfrom it, and then complete the taker's pre-signature on the Zcash claim. The maker's public swap view (GET /swaps/:id) never containsY, the pre-signatures, the signer's key or the salt: the maker sends them only in answers to the taker's own requests.
How it works walks through the ten steps. The cryptography is in @paralock/adaptor: ECDSA adaptor signatures on secp256k1 with Chaum–Pedersen DLEQ proofs and hedged nonces.
What level 2 still leaks:
- the amounts and timing of the two legs (use standard amounts and jitter);
- the fact that a 2-of-2 multisig was used on Zcash;
- an interaction with
ParalockAdaptorEscrowon the EVM chain; - to whoever learns the swap id (the quote id): both legs. The maker's public view of a swap lists both chains' transactions and the amounts to anyone who asks with the id, so keep swap ids out of logs and shared links.
An observer who suspects a swap can still correlate amounts and time windows, but has no cryptographic link between the legs.
Level 3: a fully shielded Zcash leg
Status: In research. Level 2 still shows a transparent multisig on Zcash. Removing it needs the shielded pool:
- FROST 2-of-2 shielded notes. Orchard spend authorization uses RedPallas, which FROST already supports. The taker would pay into a note that both parties control jointly.
- Schnorr adaptor signatures, which are simpler and better studied than the ECDSA version and extend naturally to FROST.
- A cross-group DLEQ proof, showing the same
yis valid on Pallas (Zcash) and on secp256k1 (EVM), as BTC⇄XMR swaps do for ed25519. - A time-locked refund. A v5 transaction's
lock_timeapplies to the whole transaction, so a pre-signed shielded refund can be time-locked too.
The result would be an ordinary shielded transaction on Zcash, with only the EVM leg visible. Open work: a cross-curve proof implementation, Ironwood support in FROST (in progress), and wallet support for jointly controlled notes (PCZT helps).
What observers can still see
| Who | htlc mode | Private mode |
|---|---|---|
| Anyone watching Zcash | The transparent swap address funding the HTLC, the amount, the hashlock, the claim or refund, and the secret once claimed | The swap address funding a 2-of-2, the amount, and the spend with two ordinary signatures |
| Anyone watching the EVM chain | The maker's lock, recipient, amount, expiry, the hashlock, and the secret in Claimed | The maker's lock, recipient, amount, expiry, the one-time signer, and the signature in Claimed |
| Both chains together | The same hashlock pairs the legs | Only amount and timing correlation |
| The maker | Both legs of your swap, and whatever your connection reveals, such as your IP address | The same: it is your counterparty |
| Your lightwalletd server | The addresses you query and the transactions you send | The same |
| The relayer | The claim it submits, which becomes public anyway | The same |
Honest limitations
- The Zcash leg is transparent in both modes. Shielding before and after keeps your history out of it, but the lock, its amount and its spend are public.
- The maker sees everything about your swap. No mode hides your swap from your counterparty. Reach it over a network path you are comfortable with.
- Amounts and timing correlate. Standard amounts and jitter reduce this; they don't remove it, especially when few swaps happen.
- What you do next can re-link. Moving funds from a fresh recipient into an address you already use links them.
- Light servers learn your addresses. A lightwalletd server sees which transparent addresses you query and what you broadcast. Run your own node or server if that matters.
- On Tempo, transfers carry a memo: the hashlock (htlc mode) or the escrow id (private mode). In private mode the escrow id doesn't relate to anything on Zcash.