The files, and how they bind to each other
| File | Purpose | SHA256 of the file |
|---|---|---|
| vectors/generated/manifest.json | Binds the specification, the fixture file, and the vector root | d443d9b6e178b95b707620593e471b2146c2747be0f7789dc06f54ce133c33ac |
| vectors/generated/golden.json | The fixture corpus itself | fc4bee2c20fe94a66a9849f1dc3d73bc407179474e936de29eddef85dcfb5856 |
| vectors/create-marker.example.json | A minimal standalone CREATE marker input, using a different binding from the corpus | 49ee02bc45526953c8fd2e5e71b35fc73458ce4c33811870119113d194308cc8 |
The manifest names the specification file, repeats its digest, names the fixture file, repeats the fixture's digest, and publishes a vector root that binds the two. The fixture in turn repeats the specification digest inside itself. Four independent places must agree before you can trust any single value, which is exactly the property you want from a corpus that other implementations are graded against.
| Value | Content |
|---|---|
| Specification digest | caa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f |
| Specification byte count | 47,343 bytes, 845 lines, UTF-8, no BOM, LF endings, exactly one final LF |
| Fixture digest | fc4bee2c20fe94a66a9849f1dc3d73bc407179474e936de29eddef85dcfb5856 |
| Vector root | b7f22caf5c9b9f3562f4d842a60a4bb0daa3f2805a5b8ff73e4d716721882c11 |
vector_root = SHA256(
"TANDEM/VECTOR-ROOT\0" ||
"golden.json" ||
00 ||
fixture_sha256_32
)
= b7f22caf5c9b9f3562f4d842a60a4bb0daa3f2805a5b8ff73e4d716721882c11
The independent Rust verifier deliberately does not copy the vector root into its input lock. It pins the fixture file's digest and then derives the root itself, so an incorrect published root would be caught rather than trusted.
Identity
The corpus fixes one regtest binding. Every namespace field in every marker below is this namespace, which is the first cross-check available to a new implementation.
| Value | Content |
|---|---|
| Protocol identifier | tndm:regtest:1010101010101010101010101010101010101010101010101010101010101010 |
| Object display identifier | tandem:regtest:1010…1010:2020…2020:1 |
| Namespace commitment | 38025b6fb909d2f22226ee9ea7e3a0e895c589792a93393c136ad502c608d221 |
| Object key | 5e355bac07710f3bc3b271902b8deda97c52893518bf9bac8a080c318659cb52 |
Both derivations are worked through in guide example 1 and recomputed live by the pair check tool.
The carrier pair
key0 side A
0279be667ef9dcbbac55a0
6295ce870b07029bfcdb2d
ce28d959f2815b16f81798key1 side B
02c6047f9441ed7d6d3045
406e95c07cd85c778e4b8c
ef3ca7abac09b95c709ee5| Value | Content |
|---|---|
| Witness script, 71 bytes | 52210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817982102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee552ae |
| scriptPubKey, 34 bytes | 00209b984c7bae3efddc3a3f0a20ff81bfe89ed1fe07ff13e562149ee654bed845db |
Marker encodings
One valid marker per operation, published as both payload and full script. These are the fastest way to find an encoding bug: if your serializer produces different bytes for the same field values, the difference is visible immediately.
| Operation | Payload bytes | Script bytes | Distinguishing fields |
|---|---|---|---|
| INIT | 59 | 61 | H_open 1108, H_close 5428, carrier 20,000, refund delay 52,560 |
| CREATE | 40 | 42 | state_vout 0x01, namespace |
| MARK | 78 | 81 | state_seq 1, kind note, flags 0, nonzero chapter commitment |
| ROTATE | 44 | 46 | state_seq 2 |
| CLOSE | 80 | 83 | state_vout 0xff, state_seq 3, reason mutual completion |
6a28544e444d0103010138025b6fb909d2f22226ee9ea7e3a0e895c589792a93
393c136ad502c608d221
6a4c50544e444d010304ff38025b6fb909d2f22226ee9ea7e3a0e895c589792a
93393c136ad502c608d221030000000000000042424527b3f6228a8ff8223239
35c83822439be948d634100c138faeff2402a1
83 bytes is the maximum Tandem marker script, corresponding to the 80-byte maximum
payload. Note the encoding boundary between them: CREATE and ROTATE, at 40 and 44 bytes,
use a direct push, while MARK and CLOSE, at 78 and 80 bytes, must use
OP_PUSHDATA1 because a direct push tops out at 75. Using the wrong one is
BAD_MARKER_ENCODING_OR_LENGTH. The
pair check tool will show you that failure if you
edit a push byte.
Two commitments
| Commitment | Value |
|---|---|
| Chapter, from the MARK | de2e2206a8bf7075457a777a271a467a26faa75f82ceec3a2ee708c0c732b84a |
| Close, from the CLOSE | 42424527b3f6228a8ff822323935c83822439be948d634100c138faeff2402a1 |
31 validation cases
The corpus fixes 31 named validation cases: six valid operations and 25 rejections. Every one of the 26 stable reasons in the registry is exercised. For each case the corpus records the emitted event type, validity class, and reason, which is what an implementation must reproduce.
| Case | Reason | Class | Event type | Terminal |
|---|---|---|---|---|
valid-init | VALID | 1 | INIT | no |
valid-create | VALID | 1 | CREATE | no |
valid-mark | VALID | 1 | MARK | no |
valid-rotate | VALID | 1 | ROTATE | no |
valid-close | VALID | 1 | CLOSE | yes |
valid-refund | VALID | 1 | REFUND | yes |
reject-multiple-markers | MULTIPLE_MARKERS | 0 | INVALID | no |
reject-marker-encoding | BAD_MARKER_ENCODING_OR_LENGTH | 0 | INVALID | no |
reject-marker-format | UNKNOWN_MARKER_FORMAT | 0 | INVALID | no |
reject-network | WRONG_NETWORK | 0 | INVALID | no |
reject-opcode | UNKNOWN_OPCODE | 0 | INVALID | no |
reject-namespace | WRONG_NAMESPACE | 0 | CREATE | no |
reject-reserved | UNSUPPORTED_OR_RESERVED_FIELD | 0 | INVALID | no |
reject-tx-version | BAD_TX_VERSION_OR_LOCKTIME | 0 | CREATE | no |
reject-input-count | BAD_INPUT_COUNT_OR_ORDER | 0 | CREATE | no |
reject-output-count | BAD_OUTPUT_COUNT_OR_ORDER | 0 | CREATE | no |
reject-unconfirmed-input | UNCONFIRMED_OR_SAME_BLOCK_PREVOUT | 0 | CREATE | no |
reject-input-script | BAD_INPUT_SCRIPT | 0 | CREATE | no |
reject-key-binding | BAD_KEY_ORDER_OR_BINDING | 0 | CREATE | no |
reject-signature | BAD_SIGNATURE_OR_SIGHASH | 0 | CREATE | no |
reject-output-value | BAD_OUTPUT_SCRIPT_OR_VALUE | 0 | CREATE | no |
reject-nonpositive-fee | NONPOSITIVE_OR_INVALID_FEE | 0 | CREATE | no |
reject-fee-split | BAD_FEE_SPLIT_OR_CHANGE | 0 | CREATE | no |
reject-inactive-predecessor | PREDECESSOR_NOT_ACTIVE | 0 | MARK | no |
reject-state-sequence | BAD_STATE_SEQUENCE | 2 | EXITED_NONCANONICAL | yes |
reject-successor | BAD_SUCCESSOR | 2 | EXITED_NONCANONICAL | yes |
reject-commitment | BAD_COMMITMENT | 2 | EXITED_NONCANONICAL | yes |
reject-height | BAD_HEIGHT_OR_PHASE | 0 | CREATE | no |
reject-refund-maturity | BAD_REFUND_SHAPE_OR_MATURITY | 2 | EXITED_NONCANONICAL | yes |
reject-multiple-carriers | MULTIPLE_CARRIERS | 2 | EXITED_NONCANONICAL, twice | yes |
reject-unmarked-carrier-spend | UNMARKED_CARRIER_SPEND | 2 | EXITED_NONCANONICAL | yes |
The three most instructive rows
reject-namespace emits event type CREATE, not INVALID, even
though it is class 0. That is rule INV-5's population rule at work: the marker exposed
marker format 0x01 and a defined opcode, so the class 0 event carries the
attempted operation type. Compare it with reject-opcode, where the opcode
was not defined, so the type falls back to INVALID.
reject-commitment is class 2 with a terminal event, not
class 0. A zero chapter commitment is a late failure, and by the time it is reached the
transaction has already consumed a carrier. The object dies. Compare it with
reject-tx-version, an early failure in the same corpus that is class 0
because the fixture has no carrier spend.
reject-multiple-carriers is the only case that emits two
events from one transaction, each terminal, ordered by binary object key with
sub_index 0 and 1. If your implementation emits one event here, or emits
two in a different order, your event root will differ.
Roots
The corpus publishes complete preimages, not just results. That means you can find the exact byte where your implementation diverges, rather than only learning that it does.
| Preimage | Domain tag | Field bytes | Total |
|---|---|---|---|
| Event leaf | 13 | 358 | 371 |
| Object-state leaf | 20 | 184 | 204 |
| Chained block root | 17 | 192 | 209 |
tandem.md section 13.2 states a 16-byte domain tag and a 374-byte total. The tag
TANDEM/EVENT\0 is 13 bytes and the published preimages are 371 bytes. The
358-byte field block in the same sentence is correct. See
erratum 1, and watch the
tool measure it in your browser.
Published example block
The roots section fixes one block-shaped example: three event leaves and two object
snapshot leaves, at height 1200 with block hash b0b0…b0b0.
tx_index | event_index | Type | Class | Reason |
|---|---|---|---|---|
| 0 | 0 | MARK | 1 | VALID |
| 1 | 0 | INVALID | 0 | BAD_MARKER_ENCODING_OR_LENGTH |
| 2 | 0xffffffff | EXITED_NONCANONICAL | 2 | UNMARKED_CARRIER_SPEND |
The third leaf shows the no-marker rule in practice: its event_index is
0xffffffff rather than a vout, because there was no marker to take a vout
from. The second leaf shows the namespace population rule: its namespace field is
zero32, because a payload that fails encoding is never structurally decoded
far enough to extract one.
| Root | Value |
|---|---|
| Event root | 475b25d221ecaa3abae67c5a4828da0b4cfe752d6cf1b55fa2dff861fae046ce |
| Object-state root | 67ec64ab4c8645ed0d500ea71032c46026254241575979c26b9f7b207e110d79 |
| Pre-INIT state root | 3b78dccfc48ff451703584532a1202fbccf98e456e514f7bb6ec50889eb640a0 |
| Chained block root | c54cd3c6423a7a35f6fa37ecceed0387e4398c55f8f6a90711c4fb906322e260 |
The event tree has three leaves, so it exercises the odd-leaf rule: at the first level the third leaf is duplicated and hashed with itself. The corpus publishes every intermediate Merkle level, so if your root differs you can see which level first diverges. The object tree has two leaves and needs no duplication.
The chained root closes the loop. Reconstructing it from the pre-INIT state root, the
block hash, height 1200, the two component roots, and the post-block counters
founding_created 1, all_objects 2, and
active_objects 1 reproduces the published value exactly. That single
32-byte number is what two independent pipelines actually compare.
Reproduce them yourself
You do not have to take any of this on faith, and you should not. There are three independent ways to check the corpus, in increasing order of effort.
- In your browser. Open the pair check tool. It loads this repository's published corpus, rehashes every preimage, rebuilds both Merkle trees, and reports each result individually.
-
With the repository verifier. Run
node scripts/verify-public.mjsfrom the repository root. It checks the specification byte contract, every published artifact digest, and the manifest-to-fixture binding. - With the independent Rust verifier. See the verifier page. It recomputes the vector root rather than trusting it, and shares no code with the implementation that produced the corpus.
There are no full raw transactions, no signatures to verify, no block data, and no live network results. The validation cases record the expected event for each named scenario, not the bytes of a transaction that produces it. A corpus that exercises signature checking, fee arithmetic against real amounts, and multi-block reorganization would be a larger artifact than this one, and this repository does not publish it. Treat the corpus as necessary conformance evidence, not sufficient.