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.
- 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.
| Name | Value | Bytes |
|---|---|---|
| Protocol name | PATINA | |
| Protocol slug | patina | |
| Marker magic | ASCII PTNA | 50 54 4e 41 |
| Marker version byte | 0x01 | 01 |
| Genesis asset | Firstlight Seals | |
| Networks | regtest, signet, mainnet | mainnet 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_wireis the txid in internal byte order, which is the reverse of the txid a block explorer shows.
| Tag | Length | Hex |
|---|---|---|
PTNA/commit | 11 | 50544e412f636f6d6d6974 |
PTNA/artifact | 13 | 50544e412f6172746966616374 |
PTNA/event | 10 | 50544e412f6576656e74 |
PTNA/state | 10 | 50544e412f7374617465 |
PTNA/attest | 11 | 50544e412f617474657374 |
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
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
| Offset | Bytes | Meaning |
|---|---|---|
| 0 | 20 | Push 32 bytes |
| 1 | f13dc99d ... 1878866a | claimant x-only public key |
| 33 | ac | OP_CHECKSIG |
| 34 | 20 | Push 32 bytes |
| 35 | ac2e3090 ... aa2afdd6 | commitment |
| 67 | 75 | OP_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)
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.
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.