The ZMAP Protocol

The Zcash Memo Asset Protocol — how a shielded note carries an asset.

▫ SupersededVersion 0.4 · 2026-09-04

A proposal whose idea, an id bound to the issuer’s key, lives on as identity v1 in v0.5. Kept as the record of how it was argued.

No independent security review yet. Wrong or underspecified? That is a defect in the text: tell us.

ZMAP v0.4 — draft proposal

Status: DRAFT. 2026-09-04. Supersedes the 2026-09-03 draft, which argued this the wrong way round.


The proposal in one line

Adopt ZIP 227's asset-identity derivation, because it is the best available design — not because ZSAs might activate.

Why the earlier framing was wrong

The first draft argued "prepare for L1 ZSAs, so assets survive migration." That invites the obvious reply: "so you're waiting for NU7?" — and it makes the whole case depend on an upgrade whose ZIP reads Status: Draft, Deployment: TBD, and which ECC has pushed back on.

Test it the other way. If ZSAs never activate, is this change still worth making?

Needs NU7?
asset_id bound to the issuer key → identity forgery becomes impossible No
Signed issuance record No
finalize → capped, publicly auditable supply No
Canonical asset_desc No
Explicit burn, so a destroyed asset is distinguishable from a silent holder No
Enforced supply — consensus rejects inflation Yes

Only the last line waits. Everything above it is a better protocol on 2026-09-04.

And following ZIP 227 exactly costs nothing over inventing a scheme. Same key, same hash, same work. It buys two things free: byte-compatibility if NU7 ever lands, and a derivation already reviewed by people who do this full time. A bespoke scheme buys a review burden.

So the claim is not "ZMAP is ready for ZSAs." It is:

ZMAP uses ZIP 227 asset identity, working on Zcash mainnet today.

Migration is a footnote, not the motivation.


§4 (revised) — asset_id derivation

What v0.3 does, and what it costs

asset_id = sha256_hex(image_bytes)

Content-addressed, and §4's reasoning for that is sound: anyone holding the artwork recomputes the id without trusting a registry.

But it binds identity to content and not to issuer. Anyone can hash the same PNG and mint a carrier claiming that asset_id. That is §12.1 identity forgery, and v0.3 closes it only afterwards — via proof-of-issuance, attestation, and a buyer who checks. Detectable, not impossible.

What ZIP 227 does

From zcash-shielded-assets/orchard, src/note/asset_base.rs:

asset_id.push(0u8);                               // version
asset_id.extend(ik.encode());                     // issuance validating key
asset_id.extend_from_slice(&asset_desc_hash[..]); // 32-byte description hash
// asset_digest = BLAKE2b-512(ZSA_ASSET_DIGEST_PERSONALIZATION, asset_id)
// AssetBase    = hash_to_curve(ZSA_ASSET_BASE_PERSONALIZATION)(asset_digest)

Identity commits to the issuer's key. Without ik, an id deriving from it cannot be constructed.

The change

Correcting an error in the first draft. It said sha256(image) "drops into the asset_desc_hash slot unchanged". It fits dimensionally — both are 32 bytes — but that is not what ZIP 227 puts there:

// src/zsa/issuance/mod.rs:157
pub fn compute_asset_desc_hash(asset_desc: &NonEmpty<u8>) -> [u8; 32] {
    if String::from_utf8(…).is_err() {
        panic!("asset_desc is not a well-formed Unicode string");
    }
    Params::new().hash_length(32).personal(b"ZSA-AssetDescCRH") …
}

asset_desc is a UTF-8 description string, hashed with BLAKE2b-256. Not artwork bytes.

ZIP 227 has no concept of images, because L1 ZSAs are fungible tokens — asset_desc is ticker-like, "Zecboat Gold Token". Binding artwork is ZMAP's problem, and the description string is the only hook the derivation offers.

Binding the artwork through the description

asset_desc      = "<name>|sha256:<64 lowercase hex>"
asset_desc_hash = BLAKE2b-256( personal = "ZSA-AssetDescCRH", asset_desc )
asset_id_bytes  = 0x00 ‖ ik ‖ asset_desc_hash
asset_id        = BLAKE2b-512( personal = "ZMAP_AssetDigest", asset_id_bytes )

Example:

Happy Birthday #001|sha256:26a1563f6aa4fab5ec0a50f88b2242b165649f76231043689e8941ef5d027190

Identity transitively commits to the exact file, and the construction stays ZIP 227-literal — it is only a string. v0.3's content-addressing is preserved inside the new shape rather than fighting it.

Why not the alternatives

Rejected because
Description is a URL commits to a location, not content. URLs change. This is how Ethereum-era NFTs got it wrong.
Description is canonical JSON a hash over JSON is a hash over your serialiser's habits — key order, whitespace, Unicode normalisation, number formatting. A known source of consensus bugs.
Image hash outside the identity two different files sharing a name would share an asset_id. Weaker than v0.3, which at least bound identity to content.

Rules the implementation imposes, so they belong in the spec

  • Valid UTF-8. compute_asset_desc_hash panics on invalid input; it does not return an error. A malformed description crashes the minter.
  • Non-empty. The parameter type is NonEmpty<u8>.
  • Byte-exact, one canonical form. A trailing space is a different asset. Specify the separator (|), the prefix (sha256:), and lowercase hex, and hash exactly those bytes.

What it costs

Content-addressing weakens. Under v0.3 artwork alone recomputes the id. Under v0.4 a verifier needs the artwork, the name, and ik.

  • image_hash becomes MANDATORY in the memo, as does the exact asset_desc string. Without both, the id cannot be recomputed and the asset is unverifiable.
  • ik is published in the issuance manifest (§7) and is not secret — it is a validating key.
  • Artwork verification is otherwise unchanged: fetch, sha256, exact match. A fetch failure is still not verification.

The guarantee earned is worth the extra input: not this is the file the memo names, but this issuer issued this exact file.


§4a (new) — Version byte

ZIP 227 prefixes 0x00. v0.3 asset ids carry no version at all. Adopt it: one byte, and every future change to the derivation becomes expressible rather than a flag day.

A parser MUST treat a 64-hex-char id with no known preimage as v0.3 legacy.


§12.3 (revised) — The issuance key. This is the blocker.

The issuance key MUST be a ZIP 227 IssueValidatingKey — secp256k1 Schnorr, BIP-340 x-only.

From src/zsa/issuance/auth.rs:29:

use secp256k1::{schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey};
pub struct ZSASchnorr;

The existing FROST vault cannot supply this

The existing vault holds a 2-of-2 FROST group key on RedPallas, used as an Orchard spend authorizing key:

zcash-sign generate --ak <GROUP_KEY>

ak authorises spending notes from an address. isk/ik authorises issuing an asset. Different curve, different role. secp256k1 and Pallas are unrelated groups — there is no derivation between them.

A new key ceremony is required. This is not negotiable and it blocks everything else in v0.4.

Why that is cheap rather than painful

ZMAP has never had an issuance key. Assets are identified by sha256(image); nothing signs them into existence. So there is no key to migrate — only one to create. The cost is one ceremony, and it will never be lower than it is now.

FROST supports secp256k1, so threshold issuance remains achievable. It is a second vault, not an extension of the first, and frost-tools / zcash-sign were built around the Orchard path.

This separation is the design working, not a wrinkle. Spending from the mint address and creating assets are different powers. ZIP 227 separates them deliberately; v0.3 §12.3 already applies the same reasoning to attestation keys.


§13 (new) — Finalization

ZIP 227 lets an issuer permanently close issuance. ZMAP has no equivalent, so a buyer cannot know the supply is capped.

finalization = Sign_issuance( "zmap:final:v1" ‖ asset_id ‖ total_units ‖ height )

Published in the issuance manifest. Say what it is: an issuer's signed, dated, public commitment — not a consensus guarantee. Breaking it is detectable. It maps one-to-one onto ZIP 227's finalize flag. §12.2 split-detection still does the real work of catching inflation.


§14 (new) — Burn

v0.3 treats burn only as a hazard — a carrier spent as fee change, which is why the carrier lock and assert_no_zsa_notes_at_risk exist.

{ "zmap": 1, "asset_id": "…", "burn": true }

A carrier spent to an output with this memo is deliberately destroyed. Attestation status becomes burned; supply arithmetic subtracts it.

Without this, an asset that is gone and an asset whose holder stopped answering look identical.


Legacy assets — the line is 2026-09-04

Existing ZMAP assets are not migrated. They are legacy, and that is the correct answer.

Happy Birthday, Cyber-Nomads, Zecoil, wSOLzsa. Their value was proving the mechanism works on mainnet, and they did that. The assets themselves carry little beyond that — the only holder of any HBD asset is the project.

So: no dual-id mapping, no doubled verification surface, no retro-binding. They cannot be retro-bound anyway — every one predates any issuance key existing.

  • v0.3 assets remain verifiable under v0.3 rules, which continue to work.
  • A parser MUST distinguish them: no version byte, id equals the image hash.
  • wSOLzsa stays a 1-of-1 historical artifact, as the market plan already treats it.

Drawing the line makes the second blocking question disappear. It was only hard while assuming the old assets had to come along.


Migration, as a footnote

If ZIP 226/227 activate:

  1. Every v0.4 asset already carries (ik, asset_desc_hash) — the two inputs ZIP 227 needs.
  2. AssetBase is computable for each without a new ceremony.
  3. Issue the real ZSA; publish a signed statement binding ZMAP asset_id to AssetBase; retire the carrier via §14 burn.
  4. Provenance carries across rather than restarting.

And do not build the §7a ZK circuit. provenance.md §7a — a verifier cannot confirm a spent nullifier belongs to the previous hop's carrier without the holder's nk — is a ZMAP problem that enforced supply dissolves. Expensive work that activation makes redundant. Build the attestation federation, which is needed regardless; skip the circuit.


Not proposed

  • No curve points in memos. No value base, no circuit, no per-asset commitments.
  • CARRIER_ZAT stays 10,000, exact.
  • The attestation federation stays. It is what makes audited supply work.
  • No claim that ZMAP becomes forgery-proof. Identity forgery closes. Supply inflation by splitting (§12.2) does not — that needs consensus.

Order of work

1. The issuance key ceremony. secp256k1 Schnorr, threshold if desired. Everything else is unimplementable until ik exists.

2. Implement §4 in zmap.rs — derivation, version byte, mandatory image_hash, and a legacy path for v0.3 ids.

3. Mint HBD_zsa_v0.4 and prove the round trip: mint → detect → send → receive → verify. See the ZSA update notes.

4. §13 and §14 — finalization and burn. Both are memo conventions plus a signature; neither blocks the mint test.

5. Update the published surfaces — Ocean /protocol, the registry, zmapSpec.ts, the reference implementations. Each is a compatibility break and should land together.


Open questions

  1. BLAKE2b-512 or SHA-256? BLAKE2b matches ZIP 227 and supports personalization; SHA-256 does not. Leaning BLAKE2b for that reason alone.
  2. Threshold or single-key issuance? FROST over secp256k1 works. A 2-of-2 raises the bar for forgery and the bar for minting. Decide before the ceremony, not after.
  3. Does asset_desc stay sha256(image), or become a structured description hashed to 32 bytes? ZIP 227 allows arbitrary bytes. Keeping the image hash preserves v0.3's content-addressing inside the new scheme.

Provenance

Derivation read from zcash-shielded-assets/orchard @ 4cf06bc19e52d1e8e43438cdbdd703ac2565bff6 — src/note/asset_base.rs, src/zsa/issuance/mod.rs, src/zsa/issuance/auth.rs. ZIP status from zips.z.cash/zip-0227. Competitive context: a separate analysis.