Safer Ironwood migration IP from Nozywallet

Hello everyone,

We’ve been focused on Ironwood migration, and for Nozy the bar is a “safer” migration not just a working one. This note is how we’re practicing that against ZIP 318.

Status today: CLI Ironwood migrate path is testnet-validated (v2.4.0). Safer-migration Priority 1 (local-node default + broadcast gate; Nym/Tor as Advanced attestation) is in CLI and desktop status. Priorities 2–3 are partially wired (ZIP 318 buckets/denoms + cover warnings); cross-user cohort health and the {1,2,5} alignment are next.

This is a short product/engineering note from NozyWallet on how we are thinking about practicing an safe Orchard - Ironwood migration at the stage we are at now.

What we already agree with in ZIP 318

We are building against the wallet migration draft in ZIP 318 PR #1317 (Orchard to Ironwood migration).

That draft is doing the hard on-chain privacy work:

  • note splitting into canonical denominations

  • scheduled / bucketed migration transactions (cohorts, shared anchors)

  • sync decoupled from broadcast

  • explicit user consent

  • and — importantly — a required network-privacy step before the schedule is committed (Tor toggle, VPN fallback, IP-correlation disclaimer)

We treat that as the right shape for migration. A one-shot “move everything now” path is not good enough for a privacy-first wallet.

On Nozy’s side, the CLI Ironwood path (scan, split, turnstile schedule/prebuild, Ironwood send) is already testnet-validated in v2.4.0. Desktop/API migration UX is still being wired. So we are past “can we migrate at all?” and into “can we migrate safely in the sense users actually care about?”

For us, safer migration has three priorities after the ZIP 318 mechanism itself:

  1. Protect the broadcasting IP (Nym / Tor / local node)

  2. Coordinate users so migration cover traffic is shared (real cohorts, not lonely turnstiles)

  3. Get the amounts and timing selection algorithm right (Zooko-style / ZIP 318 amount–timing research, as in our coordination-migration writeup)

Priority 1 — IP / network metadata (Nym / Tor / local node)

ZIP 318 correctly notes a residual leak: Without network-level privacy, the server or network operator that receives a broadcast can correlate the broadcasting IP address with the on-chain pool-crossing event.

Canonical amounts and shared timing buckets reduce on-chain fingerprinting. They do not by themselves stop:

  • ISP / local network observers seeing wallet egress

  • a lightwalletd or submit endpoint logging IP ↔ migration broadcast

  • timing joins between “this IP synced / opened the wallet” and “this cohort got a turnstile”

If we only ship beautiful turnstiles over clearnet, we have improved the chain graph and still left a track for attackers who watch the wire the nozy people.

At this stage we are not proposing to replace ZIP 318. We are proposing to take its network-privacy step seriously in product and stand of our name Nozy wallet.

  1. Prefer a local Zebrad for desktop when possible

    Strongest default for Nozy’s architecture: wallet → local JSON-RPC, not a public light server, for sensitive migration traffic.

  2. Offer / require private egress for migration broadcast

    Align with ZIP 318’s Tor opt-in — and treat Nym (e.g. NymVPN / mixnet) and Tor as first-class ways to protect the broadcasting IP.
    Practical v1 for us: guidance + a hard warn / gate before migrate-broadcast unless local node is healthy or the user attests they are on Nym/Tor (we are still deciding detection vs attestation).

  3. Do not default “skip network privacy”

    ZIP 318 says no network-privacy option may be pre-selected. We agree. For high-risk flows we may go further than “MAY proceed without” and make clearnet an explicit, discouraged exception.

  4. Keep claims honest

    Nym/Tor reduce IP↔session linkage; they do not erase KYC history, subset-sum on revealed migration amounts, or a compromised device. Orchard/Ironwood hide note plaintexts; mixnets hide who talked to the network.

Priority 2 — Shared migration cover traffic across users

ZIP 318’s privacy story for pool-crossing amounts is value collision + cohorts: many wallets emit the same canonical denominations into the same anchor-height buckets, so one user’s turnstile is hidden among others.

That only works if other people are actually there.

A perfectly scheduled, Nym-routed migration that lands alone in an empty bucket is still a lonely fingerprint. So our second priority is making cover traffic real:

  • Implement ZIP 318 bucketing faithfully so Nozy users land in the same network-wide cohorts as other ZIP 318 wallets (shared boundaries, canonical denoms, bounded multiplicity).

  • Avoid “migrate whenever the app is open” behavior that pulls users out of shared windows and into wallet-specific timing.

  • Explore light coordination so users (and ideally multiple wallet implementations) can prefer the same upcoming buckets when privacy matters — without building a centralized “migration server” that becomes a new metadata honeypot.

  • The honest part about thin cohorts early after activation: if a bucket is sparse, the wallet should warn, delay to a fuller window, or increase multiplicity carefully within K_MAX — not pretend one isolated turnstile is private.

Priority 3 — Amounts and timing

Even with private egress and other users in the cohort, which amounts you publish and when still decides whether an observer can reassemble you.

Pool-crossing transfers reveal migrated value on-chain. A naive schedule (full balance, unique leftovers, wallet-specific jitter) hands attackers:

  • subset-sum / amount fingerprinting against a known prior withdrawal or KYC’d size

  • timing fingerprints that separate one wallet’s schedule from the cohort

  • whale / residual shapes that do not collide with anyone else

This is where we want to stay aligned with:

  • ZIP 318 amount-selection and anchor-bucket timing (canonical denominations, shared boundaries, bounded multiplicity)

  • Zooko’s migration amount/timing proposal and the coordination-migration writeup we have been working from — including ideas such as {1,2,5}×10^k-style chunking, concurrency / same-decade overlap checks before large rounds, and never one-shotting a full tagged balance

Priority 3 is the algorithm layer: given a balance and a threat model, choose a sequence of (amount, bucket/window) pairs that maximize collision and cover, not just “empty the Orchard pool somehow.”

In product terms for Nozy that means:

  • Prefer collision-prone denominations over high-entropy custom sizes

  • Separate split from turnstile so we do not fake privacy with weird migration shapes

  • Gate large or sensitive rounds on cover / concurrency where the writeup calls for it

  • Warn when the only remaining path would publish an exact residual that completes a known subset-sum

  • Keep researching alignment between ZIP 318’s power-of-ten scheme and Zooko-style {1,2,5}×10^k heuristics so wallets do not fragment the anonymity set by disagreeing on “canonical”

None replaces the others. IP protection without cover still leaves on-chain loneliness. Cover without a good amount/timing algorithm still leaves subset-sum and schedule fingerprints. A perfect algorithm on clearnet still paints your IP on the wire.

2 Likes

On the {1,2,5}×10^k alignment and cohort questions, since that’s where most of the real linkability lives:

The anonymity set for any migration is the union of every wallet’s turnstile crossings at the same denomination in the same time window. That makes the cross-wallet ladder the load-bearing assumption: if Nozy uses {1,2,5}×10^k and another wallet uses plain powers of ten, their crossings are distinguishable by amount and the two cohorts never merge, so each wallet only anonymizes its own users rather than the ecosystem. That argues for the ladder being normative in ZIP 318 rather than a per-wallet choice, otherwise you get N small anonymity sets instead of one large one.

Two things worth folding into the same analysis:

  • Schedule is part of the fingerprint, not just amount. Two wallets both on {1,2,5} but with different bucketing cadences (migrate-on-activation vs spread over weeks) stay separable by their temporal distribution. The set is really over (denomination × time bucket), so the schedule shaping has to align too, not just the amounts.
  • The remainder is where linkability hides. Real balances aren’t clean sums of canonical denominations, so there’s always a leftover. If it stays in Orchard that’s its own signal (and Orchard is being sealed); if it’s sent as a non-canonical amount it’s often uniquely identifying, and a distinctive remainder is linkable by subset-sum. How a wallet handles that residual is arguably the most important privacy decision in the whole flow.

Structurally this is the same problem as equal-output CoinJoin, where anonymity is the count of identical outputs in a round, so the prior art on denomination selection and toxic-change handling should transfer fairly directly.

Ok, good points here. I agree this sharpens priority 3 for sure, and yes, the {1,2,5}×10^k alignment is important. Now I see and I agree the anonymity is the union of the same denomination crossings in the same time window across wallets. If Nozy ships {1,2,5}×10^k while others stay on plain powers of ten the cohorts never merge.

For the schedule I also agree the set over denomation × time bucket not amount alone. Migrate on activation vs spread over weeks with the same ladder still separates cohorts by temporal shape.

My question is how strict reminder and toxic change handling be? Or should absorb into next canonical slot and delay for cover refuse one shot exact leftover never invent a one-off size?

It’s more but I’ll leave this here and are concurrency or the cover gates before large migration rounds something ZIP 318 should normatively encourage?

On toxic change: I’d make it a hard invariant that only canonical amounts ever cross the turnstile, and lean on the split/turnstile separation you already have to get there. An Orchard-to-Orchard split reveals no value (the pool’s value balance is zero and note amounts stay hidden), so you can privately rearrange notes into exactly-canonical sizes before anything crosses. That shrinks the residual to just the irreducible dust below your smallest denomination. For that, I’d accumulate rather than emit: hold the sub-denomination remainder and roll it into the next round until it sums to a canonical unit, and never broadcast a one-off size to “finish.” A leftover sitting in Orchard is a small bounded signal (and yes, the pool is sealing); a unique on-chain amount is a permanent one, so accumulate-and-wait beats emit-the-remainder.

On mandating cover/concurrency: the thing to keep in mind is that cover isn’t locally verifiable. A wallet can’t see who else is about to migrate without a coordination channel, and that channel is the metadata honeypot you’re rightly avoiding. Whether ZIP 318 actually mandates any of this is the ZIP authors’ call, not mine (I’m just an outside dev reading the draft), but the line I’d argue for is: make normative only what a wallet can enforce unilaterally and that keeps cohorts mergeable, the canonical ladder, bucket boundaries, bounded multiplicity, no one-off amounts, no pre-selected clearnet. Cover-awareness (warn/delay on a sparse bucket) fits better as a SHOULD. The higher-leverage thing to standardize is the estimator, how a wallet reads available cover from public chain data (recent crossings per denomination per bucket), so wallets converge on the same “is this bucket full enough” without anyone running a coordinator. Standardize the measurement, recommend the behavior, mandate only the locally-enforceable invariants.

1 Like

Thanks, this the kind of feedback. I look for help with my understanding of what needs to be done and can be done. For the toxic change the hard invariant is best and only canonical amounts cross the turnstile. We will use Orchard to Orchard splits to rearrange privately first. For dust below the smallest denom accumulate and wait Nozy never emit a one off size to finish. A leftover in Orchard is a small, bounded signal. I had issue with a unique amount is permanent with free dust on ironwood testnet.

The cover isn’t locally verifiable without a coordination channel a HONYPOT mandate only what wallets can enforce unilaterally and that keeps cohorts mergeable. Thin buckets warn and delay is “should” higher leverage standardize a public chain cover estimator so wallets converge without a coordinator.

We can team up and compare estimator

Glad it’s useful, and yeah, happy to compare notes on the estimator in the open here, so other wallets and the ZIP folks can pick it up too. I’d rather it live in the thread than a side channel, since the whole point is that everyone converges on the same reading.

Here’s a concrete starting definition to argue about. Cover for a given (denomination D, bucket B) = the count of turnstile crossings of amount D that land in comparable recent buckets. You can read all of that off public chain data: a crossing is a tx with orchard.valueBalance > 0 and ironwood.valueBalance < 0, and the crossing amount is the revealed value. Tally those by exact canonical denomination and by anchor-height window, and that count is your cover.

The parts that actually need pinning (so two wallets compute the same number) are:

  • bucket boundaries: shared anchor heights, so everyone buckets identically;
  • the lookback window and recency weighting (it’s inherently backward-looking, recent crossings per denomination as a predictor of the bucket you’re about to join);
  • multiplicity: how you count and where you cap;
  • and the rule that only exact canonical amounts count as cover. A non-canonical crossing doesn’t add to anyone’s set, it’s just noise or a toxic leftover.

It’s computable from public chain data alone (the per-tx pool value balances are all you need), no coordinator and no shielded secrets, which is the point. Easiest way to actually compare: we each implement that against the same range of testnet blocks and diff the counts. Wherever they disagree tells us exactly which parameter needs pinning first.

Glad to keep comparing in-thread so other wallets / ZIP authors can converge on one reading.

Your starting definition matches what we implemented:

1 .Cover(D, B) = count of turnstile crossings of amount D in comparable recent buckets B
2 .Crossing ≈ tx with orchard.valueBalance > 0 and ironwood.valueBalance < 0
3 .Only exact canonical (power-of-ten) amounts count as cover
4 .Bucket = ZIP 318 256-block window (height - height % 256)
5. Public valueBalance fields only no coordinator

Ironwood testnet scan (local Zebrad): heights 4134000 - 4170495 (activation tip). 82 Orchard→Ironwood crossings across 35 buckets.

The first disagreement already showed up: fee placement splits “revealed D.” Same crossings, two Cover counts:

Amount key Canonical cover events Dens D Buckets with Cover
orchard.valueBalanceZat 1 1 ZEC only 1
|ironwood.valueBalanceZat 18 0.001 / 0.01 / 1 / 10 ZEC 13

Example: Nozy-style fee-from-Ironwood turnstile ff31a7fd…d6dc44 (h=4143641) has orchard VB = 100000000 (canonical) but |ironwood| = 99960000 (not). Other testnet turnstiles often flip that pattern.

So before lookback / multiplicity, we’d like to pin: is D orchard VB, |ironwood| VB, or something else?