Guide

Worked examples,
using the published bytes.

Every number on this page is taken from vectors/generated/golden.json in this repository. Nothing here is invented for illustration. If you follow along you will get the same values, and the pair check tool will confirm them in your browser.

Protocol finalized. Mainnet is not active.

Example 1

Fix a binding and derive its identity

Nothing in Tandem means anything until you choose a network, an INIT transaction, and a specification digest. The golden corpus fixes one regtest binding, so we will use it.

The binding used by the published corpus
InputValue
Networkregtest, code 0x03
Configured INIT txid (display)1010101010101010101010101010101010101010101010101010101010101010
Specification digestcaa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f

The specification digest is not a magic constant. It is the SHA256 of tandem.md read as raw bytes, with no whitespace or line-ending normalization. Confirm it before you trust anything downstream:

Confirm the specification digest
$ sha256sum tandem.md
caa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f  tandem.md

Now derive the namespace commitment by rule ID-5. The INIT txid must be converted from display order to wire order first, which reverses its 32 bytes exactly once. This particular txid is 32 copies of 0x10, so its reversal happens to be identical, which is convenient for a first example and dangerous to generalize from.

Namespace commitment
SHA256(
  "TANDEM/NAMESPACE\0"                                                 
  || 03                                                                
  || 1010101010101010101010101010101010101010101010101010101010101010 
  || caa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f
)
= 38025b6fb909d2f22226ee9ea7e3a0e895c589792a93393c136ad502c608d221

That value appears in the corpus as identity.namespace, and it is also the 32 bytes carried in the namespace field of every non-INIT marker in the corpus. Those two facts agreeing is the first real consistency check available to you.

The object key follows from the namespace and the CREATE transaction, by rule ID-6. The corpus fixes a CREATE txid of 32 copies of 0x20:

Object key
SHA256(
  "TANDEM/OBJECT\0"                                                    
  || 38025b6fb909d2f22226ee9ea7e3a0e895c589792a93393c136ad502c608d221 
  || 2020202020202020202020202020202020202020202020202020202020202020 
  || 01000000                                                          
)
= 5e355bac07710f3bc3b271902b8deda97c52893518bf9bac8a080c318659cb52

The human-readable form of the same object is tandem:regtest:1010…1010:2020…2020:1, but never use it as a key. Ordering, equality, database indexes, and both Merkle trees all use the 32 binary bytes.

Example 2

Build a carrier from two halves

This is the moment where two independent people become one object. Take two compressed public keys, sort them, and the address is fully determined. There is nothing to negotiate and nothing to configure.

SIDE A key0

0279be667ef9dcbbac55a0
6295ce870b07029bfcdb2d
ce28d959f2815b16f81798

Lower by unsigned bytewise comparison, so it takes the key0 slot.

SIDE B key1

02c6047f9441ed7d6d3045
406e95c07cd85c778e4b8c
ef3ca7abac09b95c709ee5

Higher by the same comparison, so it takes the key1 slot.

Sorting is not a convention you may vary. Compare the two 33-byte strings as unsigned bytes, left to right. Here the first bytes tie at 0x02, and the second bytes decide: 0x79 is below 0xc6. If you swap the two keys the resulting witness script is different, the P2WSH address is different, and the protocol treats the transaction as bound to the wrong carrier. The failure is BAD_KEY_ORDER_OR_BINDING, code 0x0015.

Witness script, exactly 71 bytes
52                                                                   
21 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 
21 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 
52                                                                   
ae                                                                   
Carrier scriptPubKey, exactly 34 bytes
SHA256(witnessScript) = 9b984c7bae3efddc3a3f0a20ff81bfe89ed1fe07ff13e562149ee654bed845db

scriptPubKey = 0020 9b984c7bae3efddc3a3f0a20ff81bfe89ed1fe07ff13e562149ee654bed845db

Both values appear verbatim in the corpus under carrier. Note the hash here is a plain SHA256, not a double SHA256 and not a HASH160. That is the native P2WSH rule, and getting it wrong produces a valid-looking address that no Tandem operation will ever match.

Try it with your own keys.

The pair check tool does exactly this derivation in your browser, shows you which side each key landed on, and names the rule behind every check. It never transmits what you paste.

Example 3

Decode a marker byte by byte

Here is a real MARK marker script from the corpus, taken apart completely. Every byte has a job and there are no spare ones.

MARK marker script, 81 bytes
6a4c4e544e444d0103020138025b6fb909d2f22226ee9ea7e3a0e895c589792a
93393c136ad502c608d221010000000000de2e2206a8bf7075457a777a271a46
7a26faa75f82ceec3a2ee708c0c732b84a

The first three bytes are script framing, not payload:

Script framing
BytesMeaning
6aOP_RETURN
4cOP_PUSHDATA1
4ePush length 78, the exact MARK payload size

OP_PUSHDATA1 is required here rather than a direct push, because a direct data push only reaches 0x4b, which is 75. A 78-byte payload cannot use one, so OP_PUSHDATA1 is the minimal encoding and using anything larger would fail rule REC-6 with BAD_MARKER_ENCODING_OR_LENGTH. CREATE and ROTATE, at 40 and 44 bytes, are small enough for a direct push and must use one.

The 78 payload bytes, decoded in full
RangeBytesFieldValue
[0,4)544e444dmagicTNDM
[4,5)01marker format1, the only defined format
[5,6)03networkregtest
[6,7)02opcodeMARK
[7,8)01state_vout1, the carrier output index
[8,40)38025b6f…c608d221namespacematches the derived namespace from example 1
[40,44)01000000state_seq1, little-endian
[44,45)00kindnote
[45,46)00flagsmust be zero
[46,78)de2e2206…c732b84achapter commitmentnonzero, as required

A sequence of 1 tells you this MARK is the first operation after the CREATE that produced sequence 0. It is not a claim the marker gets to make on its own: the parser must find an active carrier at sequence 0 and confirm that this transaction consumes it. If no such predecessor exists, the failure is PREDECESSOR_NOT_ACTIVE, code 0x001a, and the sequence check at 0x001b is never reached.

The other four markers, for comparison

Every marker in the published corpus, with its distinguishing fields
OperationPrefixstate_voutstate_seqTrailing fields
INIT6a3bnonenoneH_open 1108, H_close 5428, carrier 20,000, refund delay 52,560, spec hash
CREATE6a280x01nonenamespace only
MARK6a4c4e0x011kind, flags, chapter commitment
ROTATE6a2c0x012none, successor keys come from the inputs
CLOSE6a4c500xff3reason 0 (mutual completion), three zero reserved bytes, close commitment

CLOSE is the only operation whose state_vout is 0xff rather than 0x01. That single byte is how the marker declares there is no successor carrier. The INIT payload confirms the founding window arithmetic from rule OP-1: 1108 plus 4,320 is 5,428, and since the INIT lead is 1,008 blocks, this INIT must have confirmed at height 100 or lower.

Example 4

Split a fee between two sides

Tandem does not let the two participants agree on who pays. The split is arithmetic, so an implementation can check it without knowing anything about the relationship.

The corpus records a CREATE fee of 3,030 satoshis. Applying the CREATE equations from rule PAIR-3.3:

Even fee, CREATE
F  = 3030
D0 = 10000 + ceil(3030 / 2) = 10000 + 1515 = 11515   
D1 = 10000 + floor(3030 / 2) = 10000 + 1515 = 11515  

D0 + D1 - 20000 = 23030 - 20000 = 3030 = F           

An even fee divides cleanly and both sides pay the same. An odd fee is where the tie-break matters. Take a fee of 3,031 on the same CREATE:

Odd fee, CREATE
F  = 3031
D0 = 10000 + ceil(3031 / 2) = 10000 + 1516 = 11516    
D1 = 10000 + floor(3031 / 2) = 10000 + 1515 = 11515

D0 + D1 - 20000 = 23031 - 20000 = 3031 = F

Side A always absorbs the odd satoshi, on both CREATE and ROTATE. A wallet that gives it to side B instead produces a transaction that fails with BAD_FEE_SPLIT_OR_CHANGE, code 0x0019, even though the total fee is correct and both parties consented. The rule exists so that two implementations never round differently and then disagree about validity.

Note the ordering of the fee checks. Positivity is 0x0018 and the split is 0x0019, so a transaction that both fails to pay a positive fee and misallocates it reports the positivity failure. This matters when you write tests: the reason code you assert must be the lowest failing one, not merely a true one.

Fees recorded by the published corpus for each valid operation
OperationFee, satoshisWho pays
INIT1,000The single funding input
CREATE3,030Both sides, split by the equations above
MARK1,000One sponsor, who must be side A or side B
ROTATE1,000Both successor sides
CLOSE1,000Taken from the 20,000-satoshi carrier before the equal payout
REFUND340Taken from the carrier the same way

MARK is the one operation where a single side can carry the cost. The fee sponsor is one extra P2WPKH input, and its key must equal current key0 or current key1. It cannot be an unrelated third party. Both signatures on the carrier are still required, so paying the fee alone grants no unilateral power.

Example 5

Follow an object from first to last

The corpus fixes one object snapshot with real values. Here is what each field means as the object moves.

One object across four operations A CREATE at height 1108 starts the object at sequence 0 with chapter count 0. A MARK moves it to sequence 1 and chapter count 1, keeping the same pair. A ROTATE moves it to sequence 2 with a new pair and an unchanged chapter count. A CLOSE moves it to sequence 3 and the CLOSED status with no successor. CREATE seq 0 height 1108 chapters 0 pair 1 MARK seq 1 height 1200 chapters 1 pair 1 ROTATE seq 2 chapters 1 pair 2 CLOSE seq 3 CLOSED no successor pair 2
Sequence numbers in this figure are the ones carried by the corpus markers: MARK at 1, ROTATE at 2, CLOSE at 3. The heights are the ones the corpus records for the INIT opening and for the example block.

Reading a snapshot leaf

The corpus publishes an object snapshot for the object we derived in example 1. Its fields decode as follows.

Object-state snapshot from the corpus, decoded
FieldValueWhat it tells you
object_key5e355bac…8659cb52The key derived in example 1
founding1Created inside the 4,320-block founding window
status0ACTIVE
create_height1108Exactly H_open, the first permitted height
state_seq1One operation has happened since CREATE
current_outpoint3030…3030, vout 1The live carrier, so the object is spendable
key0, key1The pair from example 2Both keys are still required
terminal_txidzero32Absent, consistent with an active object
chapter_count1Exactly one valid MARK so far

create_height being exactly 1108 is worth noticing. That is H_open itself, and rule OP-2 uses h >= H_open, so this is the earliest height at which any object could exist under this binding. One block earlier the same transaction would be invalid with BAD_HEIGHT_OR_PHASE.

The active and terminal fields are mutually exclusive by construction. An active object has a current outpoint and a zero terminal txid. A terminal object has zero36 as its current outpoint and a real terminal txid. There is no state where both are populated, and no state where neither is.

Example 6

Classify a failure the way the protocol does

Getting the verdict right is only half of conformance. You must also select the same reason code, and the selection is ordered.

Suppose a transaction spends an active carrier, carries a single MARK marker, and has three separate problems: its locktime is 7 instead of 0, its sponsor input reveals a key belonging to neither participant, and its chapter commitment is all zeroes.

  1. Dispatch runs first. One marker remains and one carrier is spent, so we take the single-marker branch rather than any of the multiple-marker or markerless paths.
  2. Marker structure passes. The push is minimal, the payload is exactly 78 bytes, format is 0x01, network matches, opcode is defined, and the namespace matches.
  3. Group 0x0010 is the first failing group: locktime is not 0. That is BAD_TX_VERSION_OR_LOCKTIME.

The answer is 0x0010. Not 0x0015 for the sponsor key, and not 0x001d for the zero commitment, even though both are genuinely wrong. An implementation that reports either of those is nonconforming, because rule INV-3 requires the lowest failing code.

The consequences do not stop at the reason code. Because a carrier was consumed, the event is class 2, its type is EXITED_NONCANONICAL rather than the attempted MARK, and the object terminates. A reader who only checks that the transaction was rejected would miss that an object died.

Rejection is not the same as no effect.

An invalid transaction with no carrier spend leaves state untouched. An invalid transaction that consumes a carrier destroys the object it consumed. The distinction is rule ST-2, and it exists because Bitcoin has already spent the output regardless of what Tandem thinks of the spend.

Precedence beats numeric order

One case inverts what you might expect. A transaction with two markers and two carriers gets MULTIPLE_MARKERS (0x0001), while a transaction with one marker and two carriers gets MULTIPLE_CARRIERS (0x0020). The numeric order does not decide this, dispatch step order does, and rule INV-2 says so explicitly. In the two-carrier cases both objects terminate and each gets its own terminal event, ordered by binary object key with consecutive sub_index values starting at zero.

Example 7

Commit to content without putting it on chain

A MARK commits 32 bytes. Those bytes are not the content, and they are not a hash of the content either. They are a hash of a manifest, bound to the exact object and position in its history.

Expected chapter commitment
SHA256(
  "TANDEM/CHAPTER\0" ||     
  namespace_commitment ||
  genesis_outpoint36 ||     
  predecessor_outpoint36 || 
  state_seq_u32le ||
  kind_u8 ||
  manifest_sha256
)

Because the object, the exact predecessor, the sequence, and the kind are all inside the preimage, the same manifest committed at a different point in the same object's history produces a different commitment. A commitment cannot be lifted from one object and replayed into another.

CLOSE has the parallel construction with the TANDEM/CLOSE\0 domain, the close reason in place of the kind, and one extra allowance: a CLOSE commitment may be all zeroes, which means no close manifest. A MARK commitment may not, which is rule BAD_COMMITMENT, code 0x001d.

Availability is never validity.

A MARK is protocol-valid when its on-chain commitment is nonzero, even if no manifest is available anywhere or a supplied manifest does not match. Manifest matching is a presentation result. It never changes an on-chain event's validity or the object's state. Treat a missing or mismatched manifest as a display problem, not a consensus one, and never let it change what you report about the chain.

The manifest itself is ordinary JSON, constrained by the published schemas. Normalize it with RFC 8785 JCS before hashing so that two implementations producing semantically identical manifests produce identical manifest_sha256 values.