PATINA docs

Identity and derivations

Five derivations, all with an ASCII domain tag and no separator byte. Four of them hash, each a single SHA-256, and the fifth is the message a holder signs. Every hex string below was computed by docs/tools/vectors.mjs.

What you will know after this page
  • How to build a preimage: an ASCII tag concatenated straight onto the rest, with no length prefix and no separator byte.
  • How to recompute a commit commitment from a claimant key and a 16 byte salt, and check your answer with one command.
  • Which 68 byte leaf a new commit uses, what its BIP-341 tapleaf hash is, and why the 70 byte legacy leaf is still accepted.
  • Why the artifact id takes the txid in wire order and the carrier index as four little endian bytes.
  • Which fixed width encoding each of the last two derivations hashes, and what settles an argument about a state root.

Identity constants

Six values that never move. A parser compares the magic bytes and the version byte before it looks at anything else, so those two rows are the ones to read carefully.

Frozen identity values.
NameValueBytes
Protocol namePATINA
Protocol slugpatina
Marker magicASCII PTNA50 54 4e 41
Marker version byte0x0101
Genesis assetFirstlight Seals
Networksregtest, signet, mainnetmainnet gated by activation control

The version byte is protocol data, not a product name. It lives in the marker so a parser can reject bytes it does not understand. It is never used in a URL, a folder, or a label.

Hashing conventions

  • Every derivation that hashes is a single SHA-256. Not double, not tagged in the BIP-340 sense.
  • The domain tag is plain ASCII, concatenated directly with the rest of the preimage. There is no length prefix and no separator byte.
  • Display ids are lowercase hex.
  • reveal_txid_wire is the txid in internal byte order, which is the reverse of the txid a block explorer shows.
The five domain tags as bytes.
TagLengthHex
PTNA/commit1150544e412f636f6d6d6974
PTNA/artifact1350544e412f6172746966616374
PTNA/event1050544e412f6576656e74
PTNA/state1050544e412f7374617465
PTNA/attest1150544e412f617474657374

Example inputs

The worked examples below all use these two values. The key is a valid secp256k1 x coordinate found by hashing a fixed phrase, so no private key for it exists.

claimant_xonly (32)  f13dc99dc544736fca031f924b0ae1c54ff4a331f5c3b2951c6d4d5e1878866a
salt (16)            2fe862993a92197e084e4070cc8aa1c3
Do not reuse these

A real salt must come from a cryptographically secure random generator, and must never be reused across commits. A salt that appears in public documentation is a salt anyone can grind against.

Commit commitment

One tag, one key, one salt, concatenated in that order and hashed once. The worked example prints the whole 59 byte preimage so you can line your own bytes up against it before you blame the hash.

commitment = SHA256("PTNA/commit" || claimant_xonly(32) || salt(16))
preimage
  50544e412f636f6d6d6974                                            "PTNA/commit"
  f13dc99dc544736fca031f924b0ae1c54ff4a331f5c3b2951c6d4d5e1878866a  claimant_xonly
  2fe862993a92197e084e4070cc8aa1c3                                  salt

commitment
  ac2e3090ef3265ff4fc99463cec7e2b8d9227ac29f3ac0f5fc8e24dbaa2afdd6
node -e '
const { createHash } = require("crypto");
const key = Buffer.from("f13dc99dc544736fca031f924b0ae1c54ff4a331f5c3b2951c6d4d5e1878866a", "hex");
const salt = Buffer.from("2fe862993a92197e084e4070cc8aa1c3", "hex");
const pre = Buffer.concat([Buffer.from("PTNA/commit", "ascii"), key, salt]);
console.log(createHash("sha256").update(pre).digest("hex"));
'

Commit output shape

A qualifying commit is a Taproot output whose spend reveals one of the two exact leaves below. New commits use the reduced-data leaf:

<claimant_xonly(32)> OP_CHECKSIG PUSH32(commitment) OP_DROP
The 68 byte reduced-data leaf, byte by byte.
OffsetBytesMeaning
020Push 32 bytes
1f13dc99d ... 1878866aclaimant x-only public key
33acOP_CHECKSIG
3420Push 32 bytes
35ac2e3090 ... aa2afdd6commitment
6775OP_DROP
20f13dc99dc544736fca031f924b0ae1c54ff4a331f5c3b2951c6d4d5e1878866aac20ac2e3090ef3265ff4fc99463cec7e2b8d9227ac29f3ac0f5fc8e24dbaa2afdd675

OP_CHECKSIG leaves the authorization result on the stack. The commitment is pushed and dropped afterwards, so the result and claimant control are unchanged and no conditional opcode executes.

The permanent legacy leaf <claimant_xonly> OP_CHECKSIG OP_0 OP_IF PUSH32(commitment) OP_ENDIF remains accepted for historical and already-committed reveals. It is 70 bytes and is not used for new construction because active BIP-110 rules reject execution reaching its OP_IF.

For completeness, the BIP-341 tapleaf hash of that script with leaf version 0xc0, which is standard Taproot and not a PATINA rule:

tagged_hash("TapLeaf", 0xc0 || compact_size(68) || leaf_script)
= 9ce5c961ee584cc444555feecd3d7d26201e69b85cf23ea4a830d237ac081111

Artifact id

The id is a hash of where the artifact was born, and both halves of that location are byte ordered in a way that catches people out. The worked example prints the txid in both orders for exactly that reason.

artifact_id = SHA256("PTNA/artifact" || reveal_txid_wire(32) || carrier_vout_le(4))

Two details trip people up. The txid goes in wire order, reversed from what an explorer shows. The vout is four bytes little endian, not one byte, even though the marker payload carries it as one byte.

txid, display order  1d29e5ba13cecf357a9218518914617f872eeaa83a33bf6be6fd78dfdd1ce9c6
txid, wire order     c6e91cdddf78fde66bbf333aa8ea2e877f6114895118927a35cfce13bae5291d
carrier_vout         1
carrier_vout_le      01000000

preimage
  50544e412f6172746966616374                                        "PTNA/artifact"
  c6e91cdddf78fde66bbf333aa8ea2e877f6114895118927a35cfce13bae5291d  txid wire order
  01000000                                                          vout, 4 byte LE

artifact_id
  d3b8d3013c23dd3df76882034df80935ba55aa75772007f0b142b63adce5eff0

The id is fixed at birth. Moving the artifact to a new carrier does not change it.

Attestation message

message = "PTNA/attest" || artifact_id_hex || block_hash_hex

This one is a string, not bytes: the two hashes appear as lowercase hex text, and the message is signed off chain with BIP-322. A complete message is 11 + 64 + 64 = 139 characters.

PTNA/attestd3b8d3013c23dd3df76882034df80935ba55aa75772007f0b142b63adce5eff06b86b9521630996bec957312d95d273f4e19e1d87e00ca8928f3c736c6d73308

Usage is on Attestations.

Event leaf and state root

These two are stated as definitions here. The note underneath says where the encodings they hash are set out field by field.

event_leaf = SHA256("PTNA/event" || canonical event encoding)
state_root = SHA256("PTNA/state" || canonical snapshot encoding)
Where the two encodings are defined

Both are fixed width, with no optional fields and nothing to guess: 86 bytes for an event, 88 bytes for a snapshot. The baseline gives the offset, the width and the byte order of every field in each of them.

The golden vectors are still what settles an argument. An implementation matches by reproducing the vector state roots byte for byte. If your encoding produces a different root on the same block, your encoding is wrong regardless of how reasonable it looks.

See Golden vectors and state roots.

Reproduce every value on this page

node docs/tools/vectors.mjs

The script prints each preimage and each digest. If a number in these pages disagrees with the script, the script is right and the page is a bug.

If you would rather do it against a live artifact than a fixture, the public verify walkthrough takes the same derivations with a block explorer open beside them.