Skip to content

Repository files navigation

nscv

CI Python License

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/nscv

How this relates to zmd1

The 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 idea

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.

What this package is

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  # 3

SPENT 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.

The formats

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 output

Anchor, 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) == original

ak 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.

Command line

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.hex

Verification 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.

Backends

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.

What this does not do

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.

Implementation requirements

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.

Status

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.

Development

pip install -e ".[dev]"
pytest -q
ruff check . && ruff format --check .
mypy

58 tests. Most are rejection cases, because the useful property of a verifier is not that it accepts good bundles.

License

MIT.

About

NFTs on Zcash without waiting for the asset layer: an item is an ordinary shielded note, provenance is verified against the chain rather than a registry, and ownership is never public. Byte formats, verification procedure and conformance vectors.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages