Zcash has no custom-asset layer on mainnet and none is scheduled, so this does NFTs without one: an item is an ordinary shielded note, its provenance is checked against the chain rather than against a registry, and ownership is never public.
The name is an acronym for nullifier-sealed client-side validation, after the
mechanism in The idea below. This repository is the byte formats
and the checking procedure; the specification is SPEC.md and
conformance vectors are in vectors/.
No consensus change, no network upgrade, no new cryptography.
Not on PyPI yet. Install from source:
pip install git+https://github.com/Zecbit-nft/nscvThe same organisation publishes zmd1, and the two target different chain states rather than competing.
| zmd1 | nscv | |
|---|---|---|
| Assumes | ZIP 226/227 custom assets exist | only the Orchard pool as deployed |
| An item is | a finalized asset with supply 1 | a shielded note |
| Runs today | no, ZSA is not scheduled for any upgrade | yes |
| Ownership | public once assets exist | never public |
| Metadata | a JSON manifest, canonicalized | a byte string, not specified |
If custom assets ship, zmd1 is the better design and this one becomes a legacy mechanism: items created here do not migrate, because no consensus rule can carry a client-side chain into a native asset. Until then, this is what works.
The canonicalization advice differs between the two repositories for a reason worth stating, since it looks like a contradiction. zmd1 binds a JSON document, and JSON has many byte encodings of the same value, so it must fix one: RFC 8785. nscv binds fixed-layout binary, which has exactly one encoding already, so canonicalization has nothing to do and introducing it would only add a step that could disagree.
The Orchard action circuit sets a created note's rho to the nullifier of the
note spent in the same action. It does this so that rho never repeats — a
protocol-internal reason with nothing to do with assets.
The consequence is that every note in the pool carries, in a field the circuit constrains and no user chooses, the identity of the note whose destruction paid for it. The pool is not a set of independent notes. It is a forest of chains.
| A seal | a shielded note, named by its commitment |
| Closing it | spending it, which publishes its nullifier |
| The successor | the unique note whose rho equals that nullifier |
| Enforced by | the action circuit, not by convention |
| Unique because | consensus rejects a duplicate nullifier |
An item is a note. A transfer is an ordinary shielded payment that happens to place the spend and the output in the same action. Provenance is 96 bytes per hop handed to the buyer, who checks it against a node of their own choosing.
Uniqueness is not something this protocol invents. It is the same rule that stops the currency being double-spent.
The formats and the procedure. The Orchard cryptography and the node access are interfaces you supply.
That split is deliberate. Commitments are Sinsemilla and nullifiers are Poseidon
over the Pallas curve; they live in the orchard crate. A reimplementation here
would be slower, unaudited, and wrong in ways nobody would notice until an item
failed to verify.
import nscv
result = nscv.verify(consignment, chain_backend, crypto_backend, fetch_manifest)
result.outcome # <Outcome.CURRENTLY_HELD> or <Outcome.SPENT>
result.hops_checked # 3SPENT is an answer, not a failure. It means the chain is valid and the seller
no longer holds the item. Reporting it as an error makes the stale-proof case
look like a corrupt disclosure, which teaches users to retry a purchase they
should walk away from.
from nscv import Anchor, Consignment, FullViewingKey, Item, Manifest
from nscv import manifest
body = Manifest(
"My Collection",
start_index=100,
items=(Item(cm=note_commitment, metadata_digest=metadata_hash),),
).encode()
payload = Anchor(manifest.digest(body)).encode() # 38 bytes, into one data outputAnchor, 38 bytes, written once per collection. The transaction carrying it is the collection's identity — no field says so, because the identifier is not known until it confirms. Display it; a buyer who does not check it can be sold an item from a different collection with the same index.
Manifest, a byte string hashed as published and parsed only after the hash matches. That ordering removes the whole class of canonicalization bugs by refusing to canonicalize. The genesis identifier is absent by necessity: the manifest is committed by the anchor, so a manifest naming it could never be built.
Consignment, 96 bytes per hop, chunked across 512-byte memos.
chunks = Consignment(genesis_txid, item_index=1, hops=(...)).encode()
# 4 hops per memo; 20 hops needs 5
assert nscv.reassemble(chunks) == originalak in a hop is the spend validating key, not the spend authorizing
key. One letter, and the whole security of the construction: a recipient can
watch for the spend and cannot perform it. There is no field for a spending key
and there must never be one — there is a test asserting so.
nscv capacity
nscv manifest-digest manifest.bin
nscv anchor-encode manifest.bin
nscv anchor-decode 5a42543101...
nscv manifest-decode manifest.bin
nscv consignment-decode chunk0.hex chunk1.hexVerification is not exposed here. It needs an Orchard backend and a node, and a command that pretended otherwise would be the least honest thing in this repository.
Two protocols, in nscv.backends.
ChainBackend is seven operations against a node the user chose. It is
deliberately small, and it contains nothing a marketplace could serve. A
verifier that reaches the venue has not verified anything — there is no
technical barrier to building one, only this sentence and a test.
CryptoBackend is five Orchard operations: derive_ivk, try_decrypt,
address_for, note_commitment, nullifier.
tests/fakes.py has in-memory stand-ins built from BLAKE2b. They exist to test
the procedure — which checks run, in what order, what each rejects. They are not
Orchard and must not be used for anything else.
No public ownership lookup, and there cannot be one. Zcash has exactly two resources protected against double consumption: transparent outputs, publicly labelled, and nullifiers, public but unlinkable. The first destroys privacy by construction. The second is this. There is no third.
No privacy from prior holders. Verifying hop k needs the keys for hops 1 through k, so every buyer learns the item's full prior chain, permanently. The one-account-per-item rule is what keeps that to n anonymous hops instead of n people's financial histories.
No atomic settlement. Orchard has no conditional spending, so a venue holds funds between payment and delivery. That is custody, and calling it escrow would be false.
No fungibility. An action has one successor; a divisible balance needs two.
No royalties. Transfers are private, so they are not merely unenforceable but undetectable.
Each of these produces working-looking software when violated. SPEC.md section 7 is normative.
- Place the item spend and the buyer's output in the same action, and assert it before signing. The failure is silent and surfaces months later.
- One seed-derived account per item. Never reuse, never random entropy.
- Exclude item notes at the coin-selection layer, not in a dialog.
- Store consignments in self-addressed memos, so a seed restore recovers proofs and not just keys.
- Disclose full viewing keys. An
ivk-only disclosure proves history, not present ownership.
Not on PyPI. Not audited. The construction is new and this is its first implementation.
The assumption set is the currency's plus one: curve discrete log, commitment hiding and binding, proof soundness, and preimage resistance of the nullifier hash. A break in the last degrades the forgery argument from three cases to two rather than collapsing it.
pip install -e ".[dev]"
pytest -q
ruff check . && ruff format --check .
mypy58 tests. Most are rejection cases, because the useful property of a verifier is not that it accepts good bundles.
MIT.