Build it
Build in this order. Each step is testable on its own, and the order puts the cheap checks before the expensive ones.
- 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.
-
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.
-
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 -
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.
-
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.
-
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.
-
Implement successor selection
Valid KEEP entry first, then the default rule, then relic. Every void condition falls through rather than failing.
-
Append rings
One per confirmed carrier spend. Append only. Never edit a ring, and never recompute one except by replay after a rollback.
-
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.
-
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.
-
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.
-
Match the state root
Reproduce the canonical snapshot encoding until your roots match the vectors. The vectors are the authority for the encoding.
-
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.
| Area | Check |
|---|---|
| Derivations | All five reproduce the documented worked examples byte for byte |
| Grammar | Non minimal pushes are rejected, and two PTNA outputs void the marker |
| SEED | All six checks, in order, with the right reason code per failure |
| Age | Exactly 144 blocks passes, 143 fails |
| Founding | h_open inclusive, h_close exclusive, grace inclusive |
| Minimums | 100000 founding, 10000 open, 10000 successor, all inclusive |
| KEEP | Every void condition falls through to the default rule |
| Default rule | Lowest index, not OP_RETURN, at or above 10000 sats |
| Relic | Terminal, with a final ring carrying relic: true |
| Depth | Computed, never stored per block |
| Tiers | Thresholds inclusive, eight names, Elder has no next tier |
| Bundles | Several artifacts on one carrier all move together |
| Reorgs | Rollback to fork point and replay reproduces state exactly |
| Invalid events | Recorded with frozen codes and reproducible order |
| Serialisation | Sats as strings, heights as numbers, ids lowercase hex |
| Vectors | Every 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.
| Pitfall | What goes wrong |
|---|---|
| Txid byte order | The 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 width | The 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 hashing | Bitcoin uses SHA-256 twice in many places. These derivations use it once. |
| Tagged hashing | These are not BIP-340 tagged hashes. The tag is concatenated as plain ASCII. |
| Non minimal pushes | Accepting OP_PUSHDATA1 for a short payload makes you accept markers others reject. |
| Inclusive comparisons | 144 passes, 10000 sats passes, a threshold exactly equal to depth counts. Off by one here changes real outcomes. |
| Fee outputs | Sats paid to the miner are not an output and can never be a successor. |
| Mempool | Unconfirmed transactions never affect state. |
| Reason code order | Checking rules in a different order emits different codes for the same transaction. |
| Storing depth | Any stored depth drifts and disagrees at the tip. |
Testing strategy
- Unit test the pure functions. Derivations and the marker parser need no chain.
- Property test the parser. Random bytes should never panic and should always produce either a marker or a code.
- Run the golden vectors. They are the shared definition of correct.
- Run a regtest scenario suite. Mint, move, bundle, relic, reorg. Compare state roots at every height.
- 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.