Golden vectors and state roots
Two implementations agree when they produce the same state root from the same blocks. The vectors are how you find out before real money depends on it.
- What a state root is, how it is derived, and why 32 matching bytes end an argument that a field by field comparison cannot.
- The twelve areas a vector suite has to cover, including every one of the eighteen reason codes at least once.
- Why a vector asserts after every block instead of only at the end.
- How to bisect two running indexers down to the single block where their roots diverge.
- Why a root that does not match is usually an encoding difference before it is a logic bug.
Why a state root
Comparing two indexers field by field is slow and misses things. A state root reduces a whole snapshot to 32 bytes: same root, same state, no argument.
state_root = SHA256("PTNA/state" || canonical snapshot encoding)
The baseline freezes the hash and the domain tag. It does not spell out the field order and widths of the canonical encoding. That makes the vectors the authority: an implementation is correct when it reproduces the published roots, not when its encoding looks reasonable.
If you are building from scratch and your roots do not match, the problem is almost always encoding before it is logic. Compare the underlying records first, then the bytes you feed the hash.
What the vectors have to cover
Twelve areas, listed with the cases each one needs. A suite that skips any of them leaves a way for two implementations to disagree in production. Write the rows naming an exact number first: 144 and 143, 100000 and 99999, 10000 and 9999. Those pairs are there because the two values either side of a threshold have to behave differently, and that is where implementations part company.
| Area | Cases |
|---|---|
| Derivations | Commit commitment, artifact id, event leaf, state root, attestation message, each with at least one known answer |
| Marker grammar | Valid SEED, valid KEEP at 1, 2 and 8 entries, non minimal push, multiple pushes, extra opcodes, oversize script, unknown version, unknown opcode, two PTNA outputs in one transaction |
| SEED validity | Each of the six checks passing, and each failing with its own reason code. Commit age at exactly 144 and at 143. |
| Founding boundaries | Commit one block before h_open, at h_open, at
h_close - 1, at h_close. Reveal at the last grace block and one past
it. |
| Minimums | Carrier at exactly 100000 and 99999 for founding, exactly 10000 and 9999 for open, successor at exactly 10000 and 9999 |
| Successor selection | Default rule with an OP_RETURN at vout 0, with a dust output before an eligible one, KEEP overriding the default, and every void condition falling through |
| Relic | No eligible output, everything to fees, all outputs below the minimum |
| Bundles | Two artifacts routed onto one carrier, then moved together |
| Rings | Depth arithmetic across several stretches, carried_value per ring, ring index
ordering |
| Tiers | Depth exactly at each of the seven thresholds, and one below each |
| Reorgs | Roll back one block, roll back past a SEED, roll back past a carrier spend, and replay to the same root |
| Invalid events | Every one of the eighteen reason codes produced at least once |
Vector shape
Each vector is a scenario: a starting deployment, a list of blocks with the transactions in them, and the expected result after each block.
{
"name": "seed-commit-too-young",
"deployment": { "network": "regtest", "h_open": 0, "h_close": 4032, "commit_min_age": 144 },
"blocks": [
{ "height": 0, "txs": ["..."] }
],
"expect": [
{
"height": 0,
"state_root": "...",
"artifacts": 0,
"invalid_events": [ { "reason": "SEED_COMMIT_TOO_YOUNG" } ]
}
]
}
Two properties make a vector useful. It must be self contained, so no network access is needed to run it. And it must assert per block, not only at the end, so a failure points at the block that broke rather than at the finish line.
Running them
- Load a scenario and apply its blocks in order.
- After each block, compute your state root and compare with the expected root.
- Stop at the first mismatch and report the height. Later mismatches are usually consequences of the first.
- Compare invalid events too, including their order.
- Then replay the reorg scenarios and confirm the roots return to the earlier values.
Comparing two live implementations
Vectors cover what somebody thought to write down. A differential run over a real chain covers the rest.
-
Confirm they are comparable
curl -s https://a.example.org/patina/status curl -s https://b.example.org/patina/statusCompare
network,protocol_id, andspec_sha256. Different specs are allowed to differ, and comparing them proves nothing. -
Index the same range with both
Same start height, same end height, same chain.
-
Compare roots at the same height
Not at the same wall clock time. One indexer being ahead is not a disagreement.
-
Bisect to the first differing height
Halve the range until you find the single block where the roots diverge. That block contains the disagreement, and usually one transaction in it explains everything.
-
Reduce it to a vector
Extract the transaction into a new scenario with the smallest surrounding context that still reproduces the mismatch. That vector is the deliverable, and it becomes a permanent test.
When roots differ
Do not assume the older or larger implementation is right. Work out what the baseline requires, in writing, before either side changes code. The public site puts the same rule in one line on Verify it yourself: if the answers disagree, the chain wins. Then follow Report a disagreement. A disagreement resolved privately is a disagreement that will happen again to somebody else.