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 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:
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.
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:
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.
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.
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.
The first three bytes are script framing, not payload:
Script framing
Bytes
Meaning
6a
OP_RETURN
4c
OP_PUSHDATA1
4e
Push 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
Range
Bytes
Field
Value
[0,4)
544e444d
magic
TNDM
[4,5)
01
marker format
1, the only defined format
[5,6)
03
network
regtest
[6,7)
02
opcode
MARK
[7,8)
01
state_vout
1, the carrier output index
[8,40)
38025b6f…c608d221
namespace
matches the derived namespace from example 1
[40,44)
01000000
state_seq
1, little-endian
[44,45)
00
kind
note
[45,46)
00
flags
must be zero
[46,78)
de2e2206…c732b84a
chapter commitment
nonzero, 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
reason 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 // side A debit
D1 = 10000 + floor(3030 / 2) = 10000 + 1515 = 11515 // side B debit
D0 + D1 - 20000 = 23030 - 20000 = 3030 = F // closes exactly
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 // side A pays the extra satoshi
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
Operation
Fee, satoshis
Who pays
INIT
1,000
The single funding input
CREATE
3,030
Both sides, split by the equations above
MARK
1,000
One sponsor, who must be side A or side B
ROTATE
1,000
Both successor sides
CLOSE
1,000
Taken from the 20,000-satoshi carrier before the equal payout
REFUND
340
Taken 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.
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
Field
Value
What it tells you
object_key
5e355bac…8659cb52
The key derived in example 1
founding
1
Created inside the 4,320-block founding window
status
0
ACTIVE
create_height
1108
Exactly H_open, the first permitted height
state_seq
1
One operation has happened since CREATE
current_outpoint
3030…3030, vout 1
The live carrier, so the object is spendable
key0, key1
The pair from example 2
Both keys are still required
terminal_txid
zero32
Absent, consistent with an active object
chapter_count
1
Exactly 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.
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.
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.
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" || // 15 bytes
namespace_commitment ||
genesis_outpoint36 || // the object's CREATE outpoint
predecessor_outpoint36 || // the carrier this MARK consumes
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.