Reference

Test vectors

Everything on this page is published in this repository under vectors/generated/. The corpus is small on purpose: it fixes the values that two implementations are most likely to get subtly different, and it makes every one of them reproducible with SHA256 alone.

Protocol finalized. Mainnet is not active.

The files, and how they bind to each other

Published vector files and their digests
FilePurposeSHA256 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.

The binding chain
ValueContent
Specification digestcaa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f
Specification byte count47,343 bytes, 845 lines, UTF-8, no BOM, LF endings, exactly one final LF
Fixture digestfc4bee2c20fe94a66a9849f1dc3d73bc407179474e936de29eddef85dcfb5856
Vector rootb7f22caf5c9b9f3562f4d842a60a4bb0daa3f2805a5b8ff73e4d716721882c11
The vector root formula, confirmed against the published value
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.

Published identity values
ValueContent
Protocol identifiertndm:regtest:1010101010101010101010101010101010101010101010101010101010101010
Object display identifiertandem:regtest:1010…1010:2020…2020:1
Namespace commitment38025b6fb909d2f22226ee9ea7e3a0e895c589792a93393c136ad502c608d221
Object key5e355bac07710f3bc3b271902b8deda97c52893518bf9bac8a080c318659cb52

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
ce28d959f2815b16f81798

key1 side B

02c6047f9441ed7d6d3045
406e95c07cd85c778e4b8c
ef3ca7abac09b95c709ee5
Derived carrier, from rule PAIR-2
ValueContent
Witness script, 71 bytes52210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817982102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee552ae
scriptPubKey, 34 bytes00209b984c7bae3efddc3a3f0a20ff81bfe89ed1fe07ff13e562149ee654bed845db

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.

Published marker sizes, all matching rule REC-5 exactly
OperationPayload bytesScript bytesDistinguishing fields
INIT5961H_open 1108, H_close 5428, carrier 20,000, refund delay 52,560
CREATE4042state_vout 0x01, namespace
MARK7881state_seq 1, kind note, flags 0, nonzero chapter commitment
ROTATE4446state_seq 2
CLOSE8083state_vout 0xff, state_seq 3, reason mutual completion
CREATE marker script, 42 bytes
6a28544e444d0103010138025b6fb909d2f22226ee9ea7e3a0e895c589792a93
393c136ad502c608d221
CLOSE marker script, 83 bytes, the largest permitted
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

CommitmentValue
Chapter, from the MARKde2e2206a8bf7075457a777a271a467a26faa75f82ceec3a2ee708c0c732b84a
Close, from the CLOSE42424527b3f6228a8ff822323935c83822439be948d634100c138faeff2402a1

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.

All 31 cases, with the event each produces
CaseReasonClassEvent typeTerminal
valid-initVALID1INITno
valid-createVALID1CREATEno
valid-markVALID1MARKno
valid-rotateVALID1ROTATEno
valid-closeVALID1CLOSEyes
valid-refundVALID1REFUNDyes
reject-multiple-markersMULTIPLE_MARKERS0INVALIDno
reject-marker-encodingBAD_MARKER_ENCODING_OR_LENGTH0INVALIDno
reject-marker-formatUNKNOWN_MARKER_FORMAT0INVALIDno
reject-networkWRONG_NETWORK0INVALIDno
reject-opcodeUNKNOWN_OPCODE0INVALIDno
reject-namespaceWRONG_NAMESPACE0CREATEno
reject-reservedUNSUPPORTED_OR_RESERVED_FIELD0INVALIDno
reject-tx-versionBAD_TX_VERSION_OR_LOCKTIME0CREATEno
reject-input-countBAD_INPUT_COUNT_OR_ORDER0CREATEno
reject-output-countBAD_OUTPUT_COUNT_OR_ORDER0CREATEno
reject-unconfirmed-inputUNCONFIRMED_OR_SAME_BLOCK_PREVOUT0CREATEno
reject-input-scriptBAD_INPUT_SCRIPT0CREATEno
reject-key-bindingBAD_KEY_ORDER_OR_BINDING0CREATEno
reject-signatureBAD_SIGNATURE_OR_SIGHASH0CREATEno
reject-output-valueBAD_OUTPUT_SCRIPT_OR_VALUE0CREATEno
reject-nonpositive-feeNONPOSITIVE_OR_INVALID_FEE0CREATEno
reject-fee-splitBAD_FEE_SPLIT_OR_CHANGE0CREATEno
reject-inactive-predecessorPREDECESSOR_NOT_ACTIVE0MARKno
reject-state-sequenceBAD_STATE_SEQUENCE2EXITED_NONCANONICALyes
reject-successorBAD_SUCCESSOR2EXITED_NONCANONICALyes
reject-commitmentBAD_COMMITMENT2EXITED_NONCANONICALyes
reject-heightBAD_HEIGHT_OR_PHASE0CREATEno
reject-refund-maturityBAD_REFUND_SHAPE_OR_MATURITY2EXITED_NONCANONICALyes
reject-multiple-carriersMULTIPLE_CARRIERS2EXITED_NONCANONICAL, twiceyes
reject-unmarked-carrier-spendUNMARKED_CARRIER_SPEND2EXITED_NONCANONICALyes

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.

Measured lengths, taken from the published preimages themselves
PreimageDomain tagField bytesTotal
Event leaf13358371
Object-state leaf20184204
Chained block root17192209
The event leaf total is 371, not 374.

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.

The three published event leaves
tx_indexevent_indexTypeClassReason
00MARK1VALID
10INVALID0BAD_MARKER_ENCODING_OR_LENGTH
20xffffffffEXITED_NONCANONICAL2UNMARKED_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.

Published roots for that block, all reproducible with SHA256 alone
RootValue
Event root475b25d221ecaa3abae67c5a4828da0b4cfe752d6cf1b55fa2dff861fae046ce
Object-state root67ec64ab4c8645ed0d500ea71032c46026254241575979c26b9f7b207e110d79
Pre-INIT state root3b78dccfc48ff451703584532a1202fbccf98e456e514f7bb6ec50889eb640a0
Chained block rootc54cd3c6423a7a35f6fa37ecceed0387e4398c55f8f6a90711c4fb906322e260

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.

  1. 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.
  2. With the repository verifier. Run node scripts/verify-public.mjs from the repository root. It checks the specification byte contract, every published artifact digest, and the manifest-to-fixture binding.
  3. 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.
What the corpus does not contain.

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.