@xanaverse/contracts
v1.6.0
Published
The Bitcoin covenants behind zanaadu.com (trustless social media on BSV): lean hand-emitted covenants for posts, upvotes, downvotes, replies, sharded on-chain name and picture registries (SMT heads with a built-in marketplace), plus the sCrypt user-number
Maintainers
Readme
@xanaverse/contracts
The Bitcoin covenants behind zanaadu.com, trustless social media on BSV. Posts, upvotes, downvotes, replies, user numbers, names and profile pictures are all UTXOs whose payment, ownership and uniqueness rules are enforced by Bitcoin Script and validated by the network, not by a platform. This package ships every covenant the product has ever written, the exact bytes running on production today, the specs they were built from, and the interpreter harnesses that prove them, so anyone can verify the chain without trusting us.
What runs on production
zanaadu.com, mainnet, as of 2026-10-14. Every family below is recognised on chain by the
sha256 of its static script suffix (the bytes after the state pushes). tools/verify-stamps.cjs
recomputes these from the shipped code.
| Surface | Contract | Entry point | Written since | Suffix | sha256 of the suffix |
|---|---|---|---|---|---|
| Posts with a payout slot | PostAnchorV4 | /v3 | parsed by production's Workers since 2026-10-14; written from the frontend's two-key release | 1,099 B | 6205d253057ac07bfcae6a6235e30674443aaf0df27014098631bf2217cc7aa9 |
| Posts | PostAnchorV3 | /v3 | 2026-07-16 | 1,082 B | f82ab0b117f36fd65afc5256bc8214fc088ef89d9b320cbab9f08fe0c55386cc |
| Upvotes | UpvoteProofV3 | /v3 | 2026-07-16 | 1,805 B | 1856a697babeba7d3703c26aed7dc15856cfcd9096a2cf130e538a3e2918bc90 |
| Downvotes | DownvoteProofV3 | /v3 | 2026-07-16 | 1,805 B | 4ef614df5f7f6025589fe544be6cf9e87750b5d8ef80f09fc6e55861ee6d8449 |
| Replies | ReplyProofV3 | /v3 | 2026-07-16 | 1,826 B | 8378c40116038ec7052324b04965406678fee552d3645386944c6eb924b83aad |
| Names | PfNameHeadV3 | /v3 | 2026-09-16 | 8,029 B | 4e1bd1ef10191ad54d1e6a15c136bc292daabbefa8875283294a10b23a2ad5c4 |
| Profile pictures | PfPictureHeadV1 | /v3 | 2026-09-16 | 7,522 B | 4020fdb24166a9fb593fae4e0aacbbafe7c4358bcedb72e45c6a34bd01f82ce5 |
| User numbers (@N) | UserRegistry + UserProof | main (sCrypt) | production launch, 2026-06 | compiled artifact | see dist/artifacts/ |
The two identity registries are sharded: 256 name heads under namespace
eb6023d17b39d0dc4b3467452c2d029fcdbb061931ab5d6b38e806e55ad40a08 and 64 picture heads under
5e9779ee96d24707f10dc85fb6a280cf03d5a7a9a9abe3f2cc1408437335c457. The genesis outpoint of
every head is in docs/HEAD-TABLE-name-production.json and
docs/HEAD-TABLE-picture-production.json, the same tables the production indexer embeds
and hash-verifies at boot. The user-number registry is a single sCrypt UTXO chain whose
genesis is a9b16035cce2c62e1c86af85081beb658d65c2f5f53f052f7b70d72c38a549bb.
On chain, read only. These were written by earlier versions of the app and are still
parsed, counted and reclaimable, but nothing new is written in them: PostAnchorV0,
PostAnchor (v1), PostAnchorV2 (lockable posts) and the v1 sCrypt UpvoteProof,
DownvoteProof, ReplyProof. Since 2026-07-16 every new post and engagement is a V3 lean
covenant, including locked and hash-only posts (PostAnchorV3 is a superset of V2).
Frozen history, not in the product. Exported so old outputs stay verifiable and so the
harnesses keep proving them byte for byte: PfNameHeadV2 (ec1ad66d…, the beta registries of
2026-08, superseded on 2026-09-08 by V3 which commits the name's casing), PfPictureProofV1
(abfc46d1…, a per-token picture asset, mainnet-proven 2026-08-26, replaced by the picture
registry on 2026-08-28), PfNameProofV1 (its name twin, rehearsal only), PfNameHeadV1
(the register-only head of the first mainnet rehearsals) and PfMintQuotaHead (designed, never
deployed).
Standalone. TimeLockedGift (sCrypt, since 1.4.0) is a one-shot time-locked payment
covenant. It is complete and mainnet-tested but zanaadu.com does not use it.
Install
npm install @xanaverse/contractsThe main entry needs the peer dependency scrypt-ts ^1.4.0 (contract classes and
artifacts). The /v3 entry has zero dependencies: plain functions that return hex.
Verify what you installed
# 1. The shipped bytes are the frozen production stamps (three sources compared,
# sha256 recomputed with node:crypto). Exit 0 = all match.
node node_modules/@xanaverse/contracts/tools/verify-stamps.cjs
# 2. Execute every covenant through the BSV Script interpreter (needs scrypt-ts installed).
# Positives and negatives per arm, mainnet relay flags, real signatures and preimages.
node node_modules/@xanaverse/contracts/poc-shrink/v3/harness.cjs # posts + engagement, 59 cases
node node_modules/@xanaverse/contracts/poc-shrink/v3/harness.cjs --v4 # V4 posts + engagement witnessing the payout, 16 cases
node node_modules/@xanaverse/contracts/tools/post-v4-mutations.cjs # V4 covenant mutants, 3 of 3 killed
node node_modules/@xanaverse/contracts/poc-shrink/v3/harness-pf-name-v3.cjs --fuzz 200 # names, 156 cases + 400 fuzz
node node_modules/@xanaverse/contracts/poc-shrink/v3/harness-pf-picture-head.cjs --fuzz 200 # pictures, 129 cases + 400 fuzzpoc-shrink/v3/emit.ts is the emitter source of record and compile.sh is the exact tsc
recipe that reproduces emit.compiled.cjs, the file the harnesses execute. The raw sCrypt
sources are in src/contracts/; the artifacts in dist/artifacts/ are what npm run compile
produces from them.
Entry points
// sCrypt contracts: user numbers, legacy posts and engagement, TimeLockedGift
import { UserRegistry, UserProof, UserRegistryArtifact, parsePostAnchor } from '@xanaverse/contracts'
// Lean covenant emitters: posts, engagement, names, pictures, SMT, PUSHTX machinery
import { emitPostAnchorV3Lock, emitPfNameV3Lock, SmtTree256 } from '@xanaverse/contracts/v3'Both are shipped as CommonJS and ESM with type declarations.
The /v3 covenants
Every V3-family output is [state prefix][static suffix]: the state is a fixed sequence of
data pushes and the suffix is a constant script that reads its own state back off the stack.
Recognition is the end-anchored sha256 of the suffix (table above); the descriptors export
those stamps:
import { V3_DESCRIPTORS, PF_NAME_V3_DESCRIPTORS, PF_PICTURE_HEAD_DESCRIPTORS } from '@xanaverse/contracts/v3'
for (const d of [...V3_DESCRIPTORS, ...PF_NAME_V3_DESCRIPTORS, ...PF_PICTURE_HEAD_DESCRIPTORS]) {
console.log(d.contract, d.suffixLen, d.suffixSha256, d.stateFields)
}Spending a covenant is PUSHTX: the unlock carries the BIP143 sighash preimage of the spending
transaction, the script verifies it against a fixed key and then reads the outputs it needs
to constrain straight out of the preimage. Engagement and posts use
SIGHASH_SINGLE|ANYONECANPAY|FORKID (0xc3, one output bound); the registries use
SIGHASH_ALL|ANYONECANPAY|FORKID (0xc1, the whole output set bound). Builders must produce
MINIMALDATA pushes and low-S signatures; poc-shrink/ENGAGEMENT_V3_DESIGN.md §3 and
docs/PF-SPEC.md §3.8 list the mainnet relay rules a builder follows.
Shared machinery exported for builders and verifiers: buildSighashPreimage,
pushtxBlock, pushData, scriptNum, pushInt, p2pkhLock, sha256Hex, hash256Hex,
the OP table and the Asm symbolic-stack builder every suffix is emitted with.
Posts: PostAnchorV3
State prefix, 11 pushes in order: creatorProtocolPubKey (33 B), slot2 (32 B commitment,
becomes contentHash after publish), nonce (32 B), lockUntil (4 B LE), contentMode
(4 B LE), committedSats (8 B LE), createdAt, parentHash, content, tags,
creatorIdentityKey. The last five are empty in the template and set at publish.
| Selector | Arm | What the covenant enforces |
|---|---|---|
| OP_1 | publish | creator signature; reveals the body (mode 1) or its hash (modes 2/3) against the template commitment; rebuilds the anchor at committedSats with lockUntil, contentMode, committedSats copied verbatim |
| OP_2 | addContent | mode 3 only; proves sha256(content) == contentHash and reveals the body |
| OP_0 | reclaim | creator signature; the spending tx must carry nLockTime >= lockUntil with a non-final sequence, so the network refuses it early; pays committedSats back |
import {
emitPostAnchorV3Lock, buildPostAnchorV3PublishedState,
emitPostV3PublishUnlock, emitPostV3AddContentUnlock, emitPostV3ReclaimUnlock,
type PostAnchorV3State,
} from '@xanaverse/contracts/v3'Content modes: 1 body on chain at publish, 2 hash only (body off chain), 3 hash at
publish and body revealed later with addContent. Design: poc-shrink/SINGLE_TX_POST_DESIGN.md
and ENGAGEMENT_V3_DESIGN.md §3.9.
Engagement: UpvoteProofV3, DownvoteProofV3, ReplyProofV3
State prefix (votes, 7 pushes): voterProtocolPubKey, slot2 (commitment, then the
post's contentHash), nonce, creatorProtocolPubKey, creatorIdentityKey,
voterIdentityKey, paymentAmount. Replies add replyContent, parentContentHash,
createdAt. The act arm proves the target post exists (the post transaction rides in the
unlock and is walked in script), forces the payment output to the post creator's key at or
above the baked-in minimum (100 sats for votes, 50 for replies) and rebuilds the proof with
the revealed fields; reclaim is voter-signed. Payment amounts are read from the proof, never
estimated: earnings are the sum of paymentAmount over proof UTXOs.
import {
emitUpvoteProofV3Lock, emitDownvoteProofV3Lock, emitReplyProofV3Lock,
emitVoteActUnlock, emitReplyActUnlock, emitEngagementReclaimUnlock,
ENGAGEMENT_SUFFIXES, type EngagementV3State,
} from '@xanaverse/contracts/v3'Names: PfNameHeadV3
A name is a leaf in a depth-256 sparse Merkle tree whose root lives in one of 256 shard
heads (a UTXO chain per shard). Each head is
20 <namespaceId 32> <shardId> <shardMask> 20 <root 32> [SUFFIX 8,029 B], with
namespaceId, shardId and shardMask immutable and copied forward by every arm.
- Charset
[A-Za-z0-9_], 1 to 20 bytes, checked in script. - Uniqueness is case-insensitive:
leafKey = sha256(fold(name))with the ASCII lowercase fold computed in script, soAliceandaliceare one name and a second mint dies on the non-membership proof. The leaf commits the cased bytes the holder chose. leaf = sha256(name ‖ ownerSigner33 ‖ ownerIdentity33 ‖ minterIdentity33 ‖ price8); home shard= leafKey[0] & shardMask.- Names are permanent, like user numbers: there is no expiry. Ownership, listing, sale to a buyer (seller paid by a script-forced P2PKH output) and release back to the pool are all arms of the head, all miner-enforced.
- Every owner signature is
SIGHASH_ALL|ANYONECANPAY|FORKID; the covenant binds the whole output set, so nothing can be smuggled into a spend.
| Selector | Arm | Signer | Leaf transition |
|---|---|---|---|
| 1 | mint | new owner | empty → leaf(name, S, I, M = I, 0) |
| 2 | list | owner | price 0 → argA (1 ≤ p < 2^48) |
| 3 | updatePrice | owner | listed → argA |
| 4 | delist | owner | listed → price 0 |
| 5 | transfer | owner | owner → (argA, argB), price 0 |
| 6 | buy | buyer (argA) | listed → (argA, argB), price 0; extra output P2PKH(hash160(S)) at the listed price |
| 7 | recase | owner | name → argA with the same fold; price kept |
| 0 | release | current owner | leaf → empty |
Unlock pushes, bottom to top: 256 siblings (bottom-to-top proof order), preimage, sig,
name, ownerSigner, ownerIdentity, minterIdentity, price8, argA, argB, tail
(the spender's outputs after the head and the arm's extra output), sel.
import {
emitPfNameV3Lock, emitPfNameV3Unlock, pfNameV3Next,
pfNameFold, pfNameV3LeafKey, pfNameV3Leaf, pfNameV3ShardOf, pfNameV3KeySlotOk,
PF_NAME_V3_SEL, type PfNameHeadState, type PfNameV3UnlockArgs,
} from '@xanaverse/contracts/v3'Spec: docs/PF-SPEC.md §1 to §3 and §3c.
Profile pictures: PfPictureHeadV1
The same head shape and arms over 64 shards, with leafKey = imageHash = sha256(exact image
bytes) witnessed directly (32-byte width check in script, no re-hash), so one image can be
registered once across the whole network, and selector 7 is touch, an owner-signed no-op
that re-emits the leaf. The image bytes never ride in a head spend; they are published in a
separate content transaction (OP_FALSE OP_RETURN envelope, docs/PF-SPEC.md §4.5) by
anyone, before or after the mint, and a reader only trusts bytes that hash to the committed
imageHash.
import {
emitPfPictureHeadLock, emitPfPictureHeadUnlock, pfPictureHeadNext,
pfPictureLeafKey, pfPictureLeaf, pfPictureShardOf,
PF_PICTURE_HEAD_SEL, type PfPictureHeadState, type PfPictureHeadUnlockArgs,
} from '@xanaverse/contracts/v3'Spec: docs/PF-SPEC.md §3b.
Sparse Merkle tree reference
SmtTree256, smtComputeRoot, smtDirectionsForKey, SMT_EMPTY_ROOT_HEX: depth 256,
single SHA-256, node = sha256(left ‖ right), empty leaf = 32 zero bytes, proofs emitted
bottom to top. Bit-exact with the UserRegistry covenant (the empty root is asserted equal
at module load) and with the production indexer.
The sCrypt contracts (main entry)
import { UserRegistry, UserRegistryArtifact, UserProof, UserProofArtifact } from '@xanaverse/contracts'
UserRegistry.loadArtifact(UserRegistryArtifact)
UserProof.loadArtifact(UserProofArtifact)
const registry = UserRegistry.fromLockingScript(lockingScriptHex)
console.log(`Users registered: ${registry.nextUserId - 1n}`)Contract classes
| Contract | Status on production |
|---|---|
| UserRegistry | LIVE. Singleton assigning sequential user numbers, SMT duplicate prevention |
| UserProof | LIVE. Tradeable user-number ownership NFT with a built-in marketplace |
| PostAnchor (v1) | on chain, read only since 2026-07-16 |
| PostAnchorV0 | on chain, read only (frozen legacy format) |
| PostAnchorV2 | on chain, read only since 2026-07-16 (lockable posts; V3 carries the same fields) |
| UpvoteProof, ReplyProof, DownvoteProof (v1) | on chain, read only since 2026-07-16 |
| TimeLockedGift | standalone, not used by the product |
Artifacts are exported next to every class (PostAnchorArtifact, UserProofArtifact, …).
Load an artifact before instantiating or parsing.
Protocol constants
import {
PROTOCOL_ID, PROTOCOL_KEY_ID, PROTOCOL_COUNTERPARTY, PROTOCOL_BASKETS, PROTOCOL_SHARED_CONFIG
} from '@xanaverse/contracts'
PROTOCOL_ID // [0, 'xanaverse'] BRC-42 key derivation
PROTOCOL_KEY_ID // '1'
PROTOCOL_COUNTERPARTY // 'self'
PROTOCOL_BASKETS // { posts, upvotes, replies, downvotes } BRC-100 basket namesParsing legacy posts (version-aware)
parsePostAnchor detects v0, v1 and v2 from the script itself and returns a normalised
shape; getPostAnchorInstance returns the raw contract for reclaim (a v2 post must reclaim
through the v2 instance, whose reclaim() carries the CLTV gate and pays out
committedSats). getUpvoteProofInstance, getDownvoteProofInstance,
getReplyProofInstance do the same for v1 engagement proofs. V3 outputs are not sCrypt
contracts: recognise them by suffix stamp and read the state pushes in the order given above.
import { parsePostAnchor, isPostAnchor, getPostAnchorInstance } from '@xanaverse/contracts'
import type { ParsedPostAnchor } from '@xanaverse/contracts'
if (isPostAnchor(lockingScript)) {
const post: ParsedPostAnchor = parsePostAnchor(lockingScript)
post.version // 0 | 1 | 2
post.content // post text
post.contentHash // sha256
post.tags // v1+
post.lockUntil // v2 only; 0n = unlocked
post.contentMode // v2 only; 1n full | 2n hash-only | 3n hash+data
}UserRegistry and UserProof
interface UserRegistryState {
nextUserId: bigint // next sequential number
registeredKeysRoot: ByteString // SMT root of registered identities (depth 256)
treasuryAddress: PubKeyHash // registration fee recipient
platformAddress: PubKeyHash // marketplace fee recipient
}
UserRegistry.REGISTRATION_FEE = 1000n
UserRegistry.SMT_DEPTH = 256
interface UserProofState {
userId: bigint // immutable
originalRegistrant: PubKey // immutable provenance
ownerPubKey: PubKey // current owner's protocol key
ownerIdentityKey: PubKey // current owner's identity key
isForSale: boolean
salePrice: bigint
platformAddress: PubKeyHash
}
UserProof.PLATFORM_FEE_BPS = 100n // 1 % marketplace feeMarketplace methods, all covenant-enforced with the owner signature first: list(sig, price),
updatePrice(sig, newPrice), delist(sig), buy(sig, newOwner, newOwnerIdentityKey) (the
covenant validates the seller payout), transfer(sig, newOwner, newOwnerIdentityKey),
reclaim(sig).
PostAnchorV2 and TimeLockedGift (timelocks on BSV)
OP_CHECKLOCKTIMEVERIFY is a NOP on BSV post-Genesis. What Genesis kept is consensus
enforcement of transaction finality: a transaction with a future nLockTime and a non-final
input sequence is not mined until median-time-past passes the lock. Both covenants read the
spending transaction's own nLockTime and nSequence out of the sighash preimage
(timeLock()) and force the spender to build a transaction the network refuses to confirm
early. PostAnchorV3 uses the same mechanism for reclaim.
import { TimeLockedGift, TimeLockedGiftArtifact } from '@xanaverse/contracts'
TimeLockedGift.loadArtifact(TimeLockedGiftArtifact)
const gift = new TimeLockedGift(PubKey(recipientPubKeyHex), BigInt(unlockUnixTime), BigInt(satoshis))
// unlock(sig): timelock elapsed, recipient signature, output pinned to P2PKH(recipient, amount)What is in the tarball
| Path | What |
|---|---|
| dist/ | CommonJS + ESM builds, type declarations, compiled sCrypt artifacts |
| src/contracts/ | raw sCrypt sources of the main-entry contracts |
| src/v3/index.ts | the /v3 entry (re-exports the emitter) |
| poc-shrink/v3/emit.ts | the emitter source of record, every clause annotated with its spec section |
| poc-shrink/v3/emit.compiled.cjs, compile.sh | the compiled emitter the harnesses run, and the exact recipe that reproduces it |
| poc-shrink/v3/harness*.cjs | interpreter harnesses: posts + engagement, name head V3 / V2 / V1, picture head, asset tokens, quota head |
| poc-shrink/ENGAGEMENT_V3_DESIGN.md, SINGLE_TX_POST_DESIGN.md, v3/SIZES.md | the V3 designs and measured sizes |
| docs/PF-SPEC.md | the name and picture registry spec (snapshot at this release) |
| docs/HEAD-TABLE-*-production.json | the production genesis head tables (namespace, shards, outpoints, table hash) |
| tools/verify-stamps.cjs | the stamp verifier |
Trust model
- Counts are derived by querying proof UTXOs; an indexer can cache them but cannot invent them.
- Every payment rule (engagement minimums, seller payouts, platform fees) is enforced by Bitcoin Script at the network level.
- Identity is a Merkle proof: a name, a picture or a user number is verifiable against a head's committed root by anyone with the chain.
- Timelocks are enforced by transaction-finality consensus, not application code.
- The bytes are frozen and stamped; the harnesses and
verify-stampslet you check both before you rely on either.
License
MIT
