PATINA docs

Build it

Build in this order. Each step is testable on its own, and the order puts the cheap checks before the expensive ones.

What you will know after this page
  • Why the artifact id derivation has to pass first, and what a wrong txid byte order costs everything downstream.
  • Why the six SEED checks run in a fixed order, and how that order decides which reason code a failing transaction gets.
  • Why depth is computed at query time and never stored or ticked forward per block.
  • The inclusive comparisons that have to be exact: 144 blocks passes, 100000 sats passes, 10000 sats passes.
  • The five kinds of test to run, ending with a differential run against another implementation.

Step order

Twelve steps. Each one is testable without the one after it, and the cheap checks come first, which is why serving an API is last. If you want the shape of the whole protocol before the detail, How PATINA works draws it on one page.

  1. Read the baseline, not the docs

    These pages explain the baseline. The baseline is the authority. Where they differ, the baseline wins and the page is a bug you should report.

  2. Implement the five derivations

    Single SHA-256, ASCII tags, no separators. Test against the worked examples on Identity and derivations before writing anything else. If your artifact id does not match, nothing downstream can.

    SHA256("PTNA/commit" || claimant_xonly || salt)
      with the example key and salt
      = ac2e3090ef3265ff4fc99463cec7e2b8d9227ac29f3ac0f5fc8e24dbaa2afdd6
  3. Implement the marker parser

    Take a scriptPubKey, return either a decoded marker or a reason code. This is a pure function with no chain state, so it is the easiest part to test exhaustively.

    Cover: minimal push enforcement, the 83 byte ceiling, lowest index selection, duplicate voiding, unknown version, unknown opcode, and both payload layouts.

  4. Implement SEED validation

    All six checks from SEED rules, in that order, emitting the right reason code at the right step. Order matters because it decides which code you emit for a transaction that fails more than one check.

  5. Track carriers

    Maintain a map from outpoint to the artifacts riding it. Every block, check whether any input spends a tracked carrier. This is the hot path.

  6. Implement successor selection

    Valid KEEP entry first, then the default rule, then relic. Every void condition falls through rather than failing.

  7. Append rings

    One per confirmed carrier spend. Append only. Never edit a ring, and never recompute one except by replay after a rollback.

  8. Compute depth and tier at query time

    Do not store depth. Do not update anything per block. If you find yourself writing a job that ticks depths forward, you have the model wrong.

  9. Record invalid events

    Every marker that decodes but fails a rule produces an event with a reason code. These are part of the state you must reproduce, not logging.

  10. Implement rollback and replay

    Store block hashes per height. On a mismatch, roll back to the fork point and replay. Prove that rolling back to height H and replaying to H reproduces the same state root.

  11. Match the state root

    Reproduce the canonical snapshot encoding until your roots match the vectors. The vectors are the authority for the encoding.

  12. Serve the API

    Last, not first. An API over wrong state is worse than no API. Contract on the endpoint reference.

Conformance checklist

Sixteen areas. A no on any of them is a way for your implementation to answer differently from everybody else's, and for most of the rows that difference shows up as a state root on some block, so read each row as a test you owe rather than a question you answer from memory.

Answer yes to all of these before calling an implementation compatible.
AreaCheck
DerivationsAll five reproduce the documented worked examples byte for byte
GrammarNon minimal pushes are rejected, and two PTNA outputs void the marker
SEEDAll six checks, in order, with the right reason code per failure
AgeExactly 144 blocks passes, 143 fails
Foundingh_open inclusive, h_close exclusive, grace inclusive
Minimums100000 founding, 10000 open, 10000 successor, all inclusive
KEEPEvery void condition falls through to the default rule
Default ruleLowest index, not OP_RETURN, at or above 10000 sats
RelicTerminal, with a final ring carrying relic: true
DepthComputed, never stored per block
TiersThresholds inclusive, eight names, Elder has no next tier
BundlesSeveral artifacts on one carrier all move together
ReorgsRollback to fork point and replay reproduces state exactly
Invalid eventsRecorded with frozen codes and reproducible order
SerialisationSats as strings, heights as numbers, ids lowercase hex
VectorsEvery golden vector passes, including state roots

Pitfalls that have caught people

Every row below has produced a real mismatch. The first four are byte level, and code that gets them wrong still returns a hash that looks perfectly fine, so nothing complains until you compare roots with somebody else.

The mistakes most likely to make two implementations disagree.
PitfallWhat goes wrong
Txid byte orderThe artifact id uses wire order, the reverse of the display txid. Using the display order gives a wrong id that still looks like a hash.
Vout widthThe marker payload carries carrier_vout in one byte. The artifact id hashes it as four bytes little endian. Both are correct, in different places.
Double hashingBitcoin uses SHA-256 twice in many places. These derivations use it once.
Tagged hashingThese are not BIP-340 tagged hashes. The tag is concatenated as plain ASCII.
Non minimal pushesAccepting OP_PUSHDATA1 for a short payload makes you accept markers others reject.
Inclusive comparisons144 passes, 10000 sats passes, a threshold exactly equal to depth counts. Off by one here changes real outcomes.
Fee outputsSats paid to the miner are not an output and can never be a successor.
MempoolUnconfirmed transactions never affect state.
Reason code orderChecking rules in a different order emits different codes for the same transaction.
Storing depthAny stored depth drifts and disagrees at the tip.

Testing strategy

  1. Unit test the pure functions. Derivations and the marker parser need no chain.
  2. Property test the parser. Random bytes should never panic and should always produce either a marker or a code.
  3. Run the golden vectors. They are the shared definition of correct.
  4. Run a regtest scenario suite. Mint, move, bundle, relic, reorg. Compare state roots at every height.
  5. Differential test against another implementation. Index the same signet range with both and compare roots per block. This finds what unit tests do not.

When you find a difference, do not adjust your code to match ours by inspection. Work out which behaviour the baseline requires, then report the disagreement so the answer is written down for everyone.