+ Conformance is not a verdict about individual transactions. It is a claim that your
+ implementation produces the same events, the same reason codes, the same state, and the same
+ three roots as any other conforming implementation, at every height. This page sets out what
+ that requires and gives you an order to build it in.
+
+
Protocol finalized. Mainnet is not active.
+
+
+
+
+
+
+
+
The conformance claim
+
+ The specification states it as invariant 12: given the same deployment binding, the same
+ exact specification bytes, and the same blocks on the active chain, independent
+ implementations produce identical events, reason codes, state, counters, event roots,
+ object-state roots, and chained roots at every height.
+
+
+ Everything else on this page follows from that sentence. Note how much it demands beyond
+ agreeing on valid or invalid. Two implementations that reject the same transactions for
+ different reasons are both nonconforming, because their event leaves differ, so their
+ event roots differ, so every chained root after that block differs forever.
+
+
+ There is no conformance authority.
+
+ Nobody issues a Tandem conformance certificate, and this repository does not run a
+ test service. Conformance is something you demonstrate with reproducible evidence
+ against published artifacts, and that anyone else can re-run. That is the whole
+ mechanism.
+
+
+
+
+
+
Requirements
+
+
+
+
What a conforming implementation must do, and the rules that define it
+
Requirement
Rules
+
+
Bind to exactly one network, one configured INIT txid, and one specification digest, selected by configuration and never by discovery
ID-1, ID-2
+
Derive the namespace commitment and object keys by the exact preimages, using wire byte order for transaction identifiers and never reversing raw digests
ID-5, ID-6
+
Detect marker candidates by the broad rule, so that malformed markers remain detectable and cannot evade multiple-marker counting
REC-1, REC-2
+
Remove foreign INIT candidates before counting markers
REC-3
+
Enforce the exact script and payload lengths, minimal push encoding, and absence of trailing bytes
REC-4, REC-5, REC-6
+
Enforce every fixed payload field, including reserved bytes, flags, defined kinds, and defined reasons
REC-7, REC-8, REC-9
+
Enforce the key pair rules: valid points, distinct, sorted, and bound to their required roles
PAIR-1, PAIR-4
+
Enforce the exact 71-byte witness script and the four-element carrier witness stack in order
PAIR-2, PAIR-3
+
Enforce sequence continuity, carrier continuity, and equal terminal payouts
PAIR-5, PAIR-6, PAIR-7
+
Enforce the exact fee arithmetic, including the ceiling and floor split that gives the odd satoshi to side A
PAIR-3.3
+
Enforce every operation template exactly, with exact input and output counts, roles, and sequences
OP-1 to OP-6
+
Require confirmation in a strictly earlier block for every required prevout
OP-6 note
+
Apply the exact state machine, including terminating an object on any confirmed carrier spend that does not validate
ST-1, ST-2, ST-3
+
Maintain the three post-block counters exactly
ST-6
+
Apply the dispatch order, including its precedence over numeric reason order
INV-1, INV-2
+
Return the lowest failing reason code within a single-marker operation
INV-3, INV-4
+
Use the stable reason registry with its permanent names and numeric values
INV-5
+
Compute event leaves, object-state leaves, both Merkle roots with odd-leaf duplication, and the chained root, for every block including empty ones
ROOT-1 to ROOT-4
+
Reverse block application exactly on disconnection, before applying any replacement branch
ROOT-4
+
Exclude mempool observations from state, counters, and roots entirely
ST-6
+
+
+
+
+
+
+
Evidence you can produce today
+
+ These are reproducible now, against published artifacts, with no network and no
+ permission required.
+
+
+
+
+
Available conformance evidence and its coverage
+
Evidence
Covers
Does not cover
+
+
+
Specification digest and byte contract
+
That you are reading the same protocol as everyone else
+
Anything about your parser
+
+
+
The five published marker encodings
+
Payload field layout, exact lengths, push minimality boundaries
+
Transaction-level rules, signatures, state
+
+
+
Identity derivations
+
Namespace and object key preimages, and byte-order handling
+
Everything downstream of identity
+
+
+
The carrier derivation
+
Key sorting, witness script bytes, P2WSH construction
+
Signature verification
+
+
+
The 31 validation cases
+
All 26 reason codes, and the event type and class each produces
+
Fee arithmetic against real amounts, real signatures, multi-block behaviour
+
+
+
The published preimages and Merkle levels
+
Leaf field order, domain tags, odd-leaf duplication, both roots
+
Root computation over a real chain
+
+
+
The chained root reconstruction
+
The chaining formula and counter encoding
+
Chaining across many blocks and across a reorganization
+
+
+
+
+
+
+ Read the right-hand column carefully. The published corpus is a necessary conformance
+ gate, not a sufficient one. It contains no raw transactions, no signatures, no block
+ data, and no reorganization scenarios. An implementation that passes every published
+ vector has demonstrated that its encodings and hashing are right. It has not yet
+ demonstrated that its transaction validation or its rollback is.
+
+
+
+ Do not claim conformance from the corpus alone.
+
+ The accurate statement is that your implementation reproduces every published Tandem
+ vector. That is a real and checkable claim. Claiming full protocol conformance
+ requires evidence for the parts the corpus does not reach, which you would have to
+ produce and publish yourself.
+
+
+
+
+
+
Implementation checklist
+
+ Build in this order. Each stage is checkable before the next one is written, which is
+ the only realistic way to find a hashing or ordering mistake before it is buried.
+
+
+
Stage 1: bytes
+
+
Hash tandem.md as raw bytes. Compare the digest, byte count, and line count against release/spec.json. Do not normalize whitespace or line endings before hashing.
Derive the vector root from the fixture digest and confirm it equals the published root. Derive it, do not copy it.
+
Implement the primitive encodings: unsigned little-endian integers, the wire and display hash orders, and the rule that a raw digest is never reversed.
+
+
+
Stage 2: identity
+
+
Implement the namespace commitment. Confirm it reproduces the published namespace for the published binding.
+
Implement the object key. Confirm it reproduces the published object key.
+
Implement the protocol identifier and object display identifier strings, and use binary keys everywhere internally.
+
+
+
Stage 3: markers
+
+
Implement candidate detection by the broad rule, including partial payloads and non-minimal pushes.
+
Implement foreign INIT removal, keyed on payload byte 6 and the configured txid.
Implement all five payload grammars, with every fixed field, reserved byte, and defined-value set enforced.
+
Confirm all five published marker scripts parse to their published operation, payload size, and script size.
+
+
+
Stage 4: transactions
+
+
Implement P2WPKH and carrier witness shape checks, including the leading empty element and signature order.
+
Implement key validation: on-curve, distinct, sorted, HASH160-bound to the correct role.
+
Implement signature verification with strict DER, low-S, SIGHASH_ALL, SegWit v0 hashing, and the exact prevout amount.
+
Implement each operation template exactly, with exact counts, roles, and sequences.
+
Implement checked fee arithmetic that cannot wrap, then the exact splits including the ceiling and floor tie-break.
+
Implement confirmation provenance as a strictly-earlier-block test.
+
Implement REFUND recognition, including the exact sequence value and consensus maturity.
+
+
+
Stage 5: classification and state
+
+
Implement the top-level dispatch in its exact step order.
+
Implement single-marker validation in ascending reason-code order, returning the lowest failing code, and deferring state-dependent checks that cannot be made.
+
Implement the state machine and every state delta, including terminal transitions.
+
Implement the three counters as post-block values.
+
Confirm all 31 published validation cases produce the published reason, class, and event type.
+
+
+
Stage 6: roots
+
+
Implement the event leaf preimage. Confirm each published preimage hashes to its published leaf, and confirm its length is 371 bytes, not 374.
+
Implement the object-state leaf preimage. Confirm the published preimages and the 204-byte length.
+
Implement both Merkle trees with odd-leaf duplication and the single-leaf shortcut. Confirm every published intermediate level.
+
Implement the empty-set roots for both trees.
+
Implement the chained root, and confirm it for the published block including its counters.
+
Emit a root for every block from the INIT confirmation block onward, including blocks with no events.
+
+
+
Stage 7: chain handling
+
+
Implement block connection as an atomic unit: events, deltas, both roots, counters, chained root.
+
Implement block disconnection as an exact reversal, completing before any replacement branch is applied.
+
Confirm that a disconnect and reconnect of the same block returns you to an identical chained root.
+
Keep mempool observations in storage with no path into state, counters, or roots.
+
+
+
Stage 8: publishing your view
+
+
Emit agreement tuples matching the published schema, with counters and heights as decimal strings.
+
Normalize the tuple with RFC 8785 JCS and sign only the tuple.
+
Record your own release provenance per height, and do not rewrite historical provenance on a later release.
+
Compare only the nine semantic fields against another pipeline, never the four release fields.
+
Fail closed on disagreement, on any missing dependency, and on a stale tip.
+
+
+
+
+
Where implementations go wrong
+
+ Each of these is a real hazard that the rules single out, and each produces silent
+ divergence rather than an obvious error.
+
+
+
+
+
Common divergence causes
+
Mistake
Symptom
+
+
Reversing a raw digest as though it were a txid
Namespace and object keys differ from everyone else's, and nothing else works
+
Accepting a same-block prevout
Occasional extra valid events that another implementation rejects
+
Returning a true but not lowest reason code
Verdicts agree, event roots do not
+
Padding the event preimage to 374 bytes
Every event leaf differs, so every root differs from the first event onward
+
Skipping blocks with no Tandem events
Chained roots diverge permanently from the first skipped block
+
Giving the odd fee satoshi to side B
Valid transactions rejected, or invalid ones accepted, on odd fees only
+
Ignoring an invalid carrier spend instead of terminating the object
Objects that should be dead stay active, so object-state roots diverge
+
Emitting one event for a multiple-carrier spend
Event count and ordering differ, so the event root differs
+
Parsing agreement counters as numbers and re-serializing
Signatures stop verifying at large values or on round trips
+
Letting a mempool sighting satisfy a predecessor requirement
Constructed transactions fail confirmation provenance once mined
+
Sorting object leaves by display identifier rather than binary key
Object-state root differs whenever ordering differs between the two
+
Treating key_id as authenticated
An unsigned envelope field is trusted to select trust
+
+
+
+
+
+
+
If you are building a product
+
+ Parser conformance is one concern. Shipping something to people who hold keys is
+ another, and the second one has its own rules.
+
+
+
Label the network and the verification status wherever state is shown. Never present state from a height where authorized pipelines disagree.
+
Keep mainnet construction and broadcast unavailable. Mainnet is not active, and no repository, website, or funded address changes that.
+
Fail closed for state-changing actions during any indexer disagreement or unreadiness.
+
Complete, export, checksum, and restore-test every recovery transaction before its parent is broadcast. The exit is prepared before the move, not after.
+
Reconstruct and display inputs, outputs, amounts, scripts, fees, sequences, locktime, key order, and commitments before requesting a signature. Blind signing defeats the pairing property entirely.
+
Never request a seed phrase or a raw private key, and never accept one if offered.
+
Treat all retrieved content as untrusted. Enforce your own size and type limits, render safely, and never execute retrieved content as application code.
+
Explain permanence and correlation before a user commits personal, legal, location, or relationship data to a public chain.
+
+
+
+ There is no marketplace to integrate with.
+
+ The Bitcoin Universe capability snapshot lists 38 protocols, 29 of them with a
+ marketplace entry. Tandem has no entry at all, so no Universe surface implements a
+ buy, sell, list, or transfer action for a Tandem object. If your product needs a trade
+ path, Tandem does not provide one and this repository does not describe one.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/considerations.html b/docs/considerations.html
new file mode 100644
index 0000000..342dcea
--- /dev/null
+++ b/docs/considerations.html
@@ -0,0 +1,392 @@
+
+
+
+
+
+
+
+
+
+
+
+ Tandem security, limitations, and changelog
+
+
+
+
+ Skip to the page
+
+
+
+
+ Tandem
+
+
+ Report privately
+
+
+
+
Honesty
+
What it protects, and what it cannot.
+
+ Shared control changes the shape of the risk. It does not remove risk, and in a few specific
+ ways it adds some. This page states both directions plainly, then records what has changed.
+
+
Protocol finalized. Mainnet is not active.
+
+
+
+
+
+
+
+
1. What Tandem protects
+
+ These properties come from the rules themselves, not from anyone's good behaviour. Each
+ one is enforced either by Bitcoin script or by the deterministic parser, and each names
+ the rule that provides it.
+
+
+
+
+
Protected properties and their sources
+
Property
Enforced by
+
+
Neither participant can advance the object alone
Bitcoin script, the 2-of-2 carrier, rules PAIR-2 and PAIR-3
+
Neither participant can end the object on better terms than the other
Equal, strictly positive payouts, rule PAIR-7
+
Neither participant can silently replace the other
ROTATE requires the old pair to authorize the new one, rule OP-4
+
History cannot be reordered or backdated
Exact sequence increments and the chained root, rules PAIR-5 and ROOT-4
+
An operation cannot be replayed onto another object
Namespace, genesis outpoint, and predecessor outpoint are all inside the commitment preimages
+
A deployment cannot be quietly substituted
The binding pins network, INIT txid, and specification digest; a different INIT is a different protocol, rules ID-1 and ID-2
+
No administrator can rewrite state
No rule grants any coordinator, host, indexer, wallet, or issuer that authority
+
Two implementations cannot silently differ
Stable reason codes and three roots, invariant 12
+
A prepared exit exists before the object moves
REFUND is pre-signed by both sides and matures on a relative lock, rule OP-6
+
+
+
+
+
+
+
2. Risks that pairing adds
+
+ This is the section most protocol documentation leaves out. Requiring two signatures
+ solves one problem by creating another: now two things can go wrong instead of one.
+
+
+
+
+
SIDE A can stop everything
+
+ A participant who stops cooperating stops every cooperative operation. There is no
+ majority, no tie-break, and no arbitration. The only path that does not need live
+ cooperation is a REFUND that was already signed by both sides before it was needed.
+
+
+
+
SIDE B can stop everything too
+
+ The situation is exactly symmetric, which is the design working as intended. Neither
+ side has a lever the other lacks. Symmetric deadlock is the cost of symmetric
+ control.
+
+
+
+
+
Key loss is doubled
+
+ Two keys means two chances to lose one. A 2-of-2 has no redundancy: losing either key
+ has the same effect as losing both. If a prepared REFUND exists and its relative lock
+ has matured, the funds can still be recovered. If it does not, they cannot.
+
+
+
The recovery clock keeps restarting
+
+ The REFUND relative delay of 52,560 blocks is measured from the confirmation height of
+ the exact carrier being spent. Every confirmed MARK or ROTATE creates a new carrier and
+ restarts the delay. An active pair that adds chapters regularly is continually pushing
+ their own recovery path further into the future. A pair that intends to rely on recovery
+ should understand that using the object delays it.
+
+
+
A recovery path must be prepared before, not after
+
+ A REFUND spends a specific carrier and needs both signatures. Once cooperation has broken
+ down, it is too late to obtain the second signature. The rule is therefore operational
+ rather than protocol-level, and it is absolute: complete, export, checksum, and
+ restore-test the recovery transaction for a carrier before broadcasting the parent that
+ creates it.
+
+
+
An invalid spend is fatal, not merely rejected
+
+ If a confirmed transaction consumes an active carrier but does not match an operation
+ template, the object terminates as EXITED_NONCANONICAL. It does not fail
+ harmlessly. A wallet bug, an accidental coin selection that grabs a 20,000-satoshi
+ carrier as ordinary change, or a malformed marker all end the object permanently. Rule
+ ST-2 is not a punishment, it is an acknowledgement that the output is gone.
+
+
+
+ Protect carriers from ordinary coin selection.
+
+ A carrier is a normal-looking 20,000-satoshi P2WSH output. Wallet software that does
+ not know about Tandem will happily treat it as spendable value. Any product that holds
+ keys near a Tandem object must mark carrier outpoints as protected, and should behave
+ conservatively around outputs it cannot classify.
+
+
+
+
+
+
3. Threat notes
+
+
Blind signing defeats the whole design
+
+ Every protection on this page assumes both participants know what they signed. A signing
+ flow that shows an opaque hash converts a 2-of-2 into a single point of failure operated
+ by whoever composed the transaction. Reconstruct and display inputs, outputs, amounts,
+ scripts, fees, sequences, locktime, key order, and commitments before asking for a
+ signature, and do it for the recovery transaction too.
+
+
+
A single parser is a trusted third party
+
+ Bitcoin consensus does not enforce Tandem rules, so the answer to what state an object
+ is in comes from a parser. A user reading one indexer's answer is trusting that
+ indexer. The mitigation is the two-pipeline agreement described on
+ the verifier page, and it only helps if products actually
+ fail closed on disagreement rather than falling back to one pipeline.
+
+
+
Content is a separate trust domain
+
+ A commitment authenticates exact bytes. It says nothing about whether those bytes are
+ available, whether they are safe to render, or whether the person who committed them was
+ honest about what they are. Manifests arrive from whoever put a commitment on chain,
+ which is to say from anyone. Enforce your own size and type limits, verify bytes against
+ content_sha256 before display, and never execute retrieved content.
+
+
+
Everything is public and permanent
+
+ Bitcoin events are observable by anyone, forever. A Tandem object publishes that two
+ specific keys are jointly controlling something, at specific times, with a specific
+ cadence of activity. Even with encrypted content, the existence, timing, and pattern of a
+ relationship is on a public ledger. Plan for correlation before committing anything
+ personal, legal, locational, or relational.
+
+
+
Coercion and disputes are outside the protocol
+
+ Tandem enforces that two keys signed. It cannot tell whether a person signed freely, and
+ it defines no ownership, no identity, and no dispute resolution. A signature obtained
+ under pressure is indistinguishable from one given willingly.
+
+
+
+
+
4. Limitations
+
+ Stated as facts, not as future work.
+
+
+
Mainnet is not active. The protocol is finalized; activation remains closed until independent verification, recovery, security, signer, and operational outcomes are complete and an explicit decision is recorded. No repository, website, or funded address constitutes activation.
+
Exactly two participants. Not three, not a threshold, not a quorum. The carrier is a 2-of-2 and the payload has room for exactly one pair.
+
No partial or conditional operations. There is no escrow, no timelock other than the fixed refund delay, and no conditional transfer.
+
No transfer of an object. ROTATE replaces keys with the current pair's authorization; it is not a unilateral sale or handover, and there is no transfer primitive.
+
No marketplace. The Bitcoin Universe capability snapshot lists 38 protocols, 29 with marketplace entries. Tandem has no entry in that snapshot, so no Universe product implements a buy, sell, list, or transfer path for it.
+
Fixed constants. Carrier value 20,000 satoshis, refund delay 52,560 blocks, founding window 4,320 blocks, INIT lead 1,008 blocks. None are configurable; changing one is a different protocol.
+
The protocol is immutable. There is no version negotiation and no extension mechanism. Unknown formats, opcodes, kinds, reasons, flags, reserved values, and oversized payloads are invalid, permanently.
+
The specification prose contains one known error. See erratum 1. It cannot be corrected in place without changing the digest and therefore the protocol identity.
+
No conformance authority exists. No certification, no test service, no registry.
+
The published corpus is partial. It contains no raw transactions, no signatures, no block data, and no reorganization scenarios.
+
Neither implementation is a released product. Both public implementations describe themselves as source with unit coverage, not deployed infrastructure, and both disclaim live chain, database, and signing evidence.
+
Continuous two-pipeline agreement has not been published. The agreement design is real and runnable against the fixed corpus. Sustained agreement over a live chain is recorded as not yet performed.
+
Content availability is not provided. Tandem stores no files and guarantees no URI keeps resolving.
+
No recovery from key loss beyond a prepared REFUND. There is no social recovery, no backup signer, and no administrator path.
+
+
+
+
+
5. What Tandem is not
+
+
+
IT IS
+
+
A deterministic Bitcoin metaprotocol
+
One jointly controlled object with a continuous history
+
An exact set of transaction templates
+
A recovery path prepared in advance
+
Verifiable independently, in another language
+
+
+
+
IT IS NOT
+
+
A token, an NFT, or an issuance scheme
+
A marketplace, an investment, or a yield product
+
A governance, ranking, or reputation system
+
An identity proof or a legal ownership record
+
A guarantee against key loss, coercion, or misuse
+
A storage layer for files
+
A platform with an administrator key
+
+
+
+
+ Nothing in Tandem defines supply, price, rarity, royalties, or transferability. If a
+ product presents a Tandem object as any of those, the presentation is the product's
+ claim and not the protocol's.
+
+
+
+
+
6. Reporting a problem
+
+ Treat fund-loss risk, signing confusion, unsafe coin selection, incorrect recovery,
+ divergent interpretation between implementations, and exposed credentials as sensitive.
+ Report them privately through
+ GitHub private vulnerability reporting
+ rather than in a public issue, and begin the title with Security report: so
+ maintainers can route it. See SECURITY.md for the full
+ policy.
+
+
+ A specification ambiguity that two reasonable implementations could resolve differently
+ is a security issue for a deterministic protocol, not a documentation nit. Please report
+ it as one.
+
+
+
+
+
7. Changelog
+
+ This records changes to the public artifacts and to this documentation. The protocol
+ bytes themselves are frozen: any change to tandem.md would
+ change its digest and therefore create a different protocol, so there will never be a
+ version 2 of it under this identity.
+
+
+
+
+
Public artifact and documentation history
+
Date
Change
+
+
+
2026-09-01
+
+ Added this protocol dossier: a normative specification restated as numbered
+ rules, a worked-example guide, an in-browser pair check tool, indexer semantics,
+ a JSON Schema reference, a test-vector reference, an independent-verifier page,
+ conformance material with an ordered implementation checklist, and this page.
+ Documented erratum 1, the event-leaf preimage length. Enabled the published site.
+ No protocol artifact was modified.
+
+
+
+
2026-09-01
+
Added a repository documentation manifest describing this repository to the Bitcoin Universe documentation portal.
+
+
+
2026-08-31
+
Public verification workflow restricted so that pull requests from forks do not run on private capacity.
+
+
+
2026-08-31
+
Public verification scheduling updated to use capacity-routed runners with overflow.
+
+
+
2026-08-31
+
Workspace text policy applied to public prose.
+
+
+
2026-08-31
+
Validated release promoted to the public branch, publishing the finalized specification, the release record, the three schemas, and the golden vector corpus.
+
+
+
+
+
+
Frozen artifact digests
+
+ These have not changed since the release promotion and are pinned in three independent
+ places: this repository's verifier, the vector manifest, and the independent Rust
+ verifier's input lock.
+
+ Run node scripts/verify-public.mjs from the repository root to check all of
+ them, along with the specification byte contract and the manifest-to-fixture binding.
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/guide.html b/docs/guide.html
new file mode 100644
index 0000000..aeca698
--- /dev/null
+++ b/docs/guide.html
@@ -0,0 +1,623 @@
+
+
+
+
+
+
+
+
+
+
+
+ Tandem guide and worked examples
+
+
+
+
+ Skip to the guide
+
+
+
+
+ Tandem
+
+
+ Golden corpus
+
+
+
+
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 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.
+
One jointly controlled object with a continuous history.
What makes it distinct
Shared control is structural. Cooperation lives in the Bitcoin output, not an account permission.
-
The history follows exact rules. Independent implementations can derive the same canonical state.
+
The history follows exact rules. Independent implementations can derive the same state.
Recovery is prepared before progress. Each state-changing parent needs a completed recovery path first.
No administrator can rewrite it. No platform key can replace participants or edit object state.
@@ -201,7 +202,7 @@
Signing
Verification
-
State-changing construction and broadcast should fail closed whenever independent indexers disagree at the same canonical height.
+
State-changing construction and broadcast should fail closed whenever independent indexers disagree at the same block height.
Content
@@ -256,7 +257,7 @@
Independent agreement
Content manifests
- Canonicalize chapter and close manifests using RFC 8785 JCS before hashing. Retrieve
+ Normalize chapter and close manifests with RFC 8785 JCS before hashing. Retrieve
only the payload identified by content_sha256, verify its bytes before
display, and treat URI availability as separate from on-chain validity.
@@ -271,15 +272,33 @@
Exact artifacts, stable paths, independent inspection.
line endings before hashing.
+ The protocol dossier restates the specification as numbered
+ rules, works through examples using the published vectors, documents indexer
+ semantics and the independent verifier, and provides an in-browser
+ pair check tool.
+
divergent interpretation, exposed credentials, and private conduct concerns as
sensitive. Never place them in a public issue.
- Open a private report
+ Open a private report
Begin the title with Security report: or Conduct report: so
maintainers can route it correctly. Include only the minimum safe reproduction or
@@ -374,7 +393,7 @@
Make the public experience clearer and safer.
Keep claims accurate about Bitcoin, custody, recovery, privacy, availability, and network status.
Never submit secrets, private transaction data, internal plans, operational procedures, or sensitive implementation detail.
-
Do not edit canonical bytes, schemas, release metadata, or golden vectors as part of a product copy change.
+
Do not edit protocol bytes, schemas, release metadata, or golden vectors as part of a product copy change.
Run node scripts/verify-public.mjs and review visual changes across keyboard, motion, zoom, narrow, and wide conditions.
- Explore the canonical actions. This demonstration explains the state model and
+ Explore the defined actions. This demonstration explains the state model and
never creates, signs, or broadcasts a transaction.
@@ -230,20 +231,20 @@
It is not
The public contract is exact and inspectable.
- Read the canonical protocol bytes, validate chapter and close manifests, and test
+ Read the authoritative protocol bytes, validate chapter and close manifests, and test
against the published golden vectors. Independent implementations can conform to
the same public contract without sharing a codebase.
Some objects belong to one person. This one begins
One Bitcoin-native object, controlled together.
Documentation
+ Protocol dossierSecurityContributeRepository boundary
diff --git a/llms.txt b/llms.txt
new file mode 100644
index 0000000..1e73123
--- /dev/null
+++ b/llms.txt
@@ -0,0 +1,59 @@
+# Tandem
+
+> Tandem is an original Bitcoin Universe protocol. It defines one Bitcoin-native object controlled by a matched pair of keys, where every state change requires both halves of the pair. The pairing requirement is enforced by an exact 2-of-2 native P2WSH carrier output and by a deterministic parser with an exhaustive stable reason registry.
+
+Status: the protocol is finalized and its bytes are frozen. Mainnet is NOT active. No repository, website, or funded address constitutes activation. Do not construct or broadcast a Tandem transaction with real funds.
+
+Scope note: Tandem has no entry in the Bitcoin Universe capability snapshot, which records 38 protocols and 29 marketplace integrations. No Universe product implements a buy, sell, list, or transfer path for a Tandem object. Tandem defines shared control and a history, not a tradable asset.
+
+This site publishes the authoritative protocol bytes, three JSON Schemas, a golden vector corpus, and a documentation dossier. The bytes at /tandem.md are the source of truth; every other page is navigation and explanation.
+
+## Authoritative artifacts
+
+- [Normative specification](https://bitcoinuniverseio.github.io/tandem/tandem.md): the complete protocol. SHA256 caa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f, 47343 bytes, UTF-8, LF endings, no BOM, exactly one final LF. Hash it as raw bytes with no normalization.
+- [Specification release record](https://bitcoinuniverseio.github.io/tandem/release/spec.json): digest, byte count, line count, encoding, and finalization status.
+- [Public contract index](https://bitcoinuniverseio.github.io/tandem/protocol/README.md): the stable paths of every published artifact.
+
+## Protocol dossier
+
+- [Dossier overview](https://bitcoinuniverseio.github.io/tandem/docs/protocol.html): plain-language introduction, the five places a pair must match, and the object model.
+- [Normative specification as numbered rules](https://bitcoinuniverseio.github.io/tandem/docs/specification.html): identity and binding, record format, pairing rules, operation validity, state transitions, invalidity and rejection precedence, authoritative roots, and one documented erratum.
+- [Guide and worked examples](https://bitcoinuniverseio.github.io/tandem/docs/guide.html): derive a binding, build a carrier from two keys, decode a marker byte by byte, split a fee, follow an object, classify a failure, commit to content.
+- [Pair check tool](https://bitcoinuniverseio.github.io/tandem/docs/pair-check.html): an in-browser verifier that takes two candidate records or two candidate keys and reports which numbered rule and stable reason code each check maps to. Client-side only.
+- [Indexer semantics](https://bitcoinuniverseio.github.io/tandem/docs/indexing.html): block scope, confirmation provenance, block connection, reorganization, why the mempool never reaches protocol state, and two-pipeline agreement.
+- [JSON Schema reference](https://bitcoinuniverseio.github.io/tandem/docs/schemas.html): all three published schemas explained field by field.
+- [Test vectors](https://bitcoinuniverseio.github.io/tandem/docs/vectors.html): the golden corpus, all 31 validation cases, and every root with the formula that reproduces it.
+- [Independent verifier](https://bitcoinuniverseio.github.io/tandem/docs/verifier.html): the second implementation in Rust, what it proves, and what it does not.
+- [Conformance and implementation checklist](https://bitcoinuniverseio.github.io/tandem/docs/conformance.html): the conformance claim, available evidence, an ordered build checklist, and common divergence causes.
+- [Security, limitations, and changelog](https://bitcoinuniverseio.github.io/tandem/docs/considerations.html): protected properties, the risks pairing adds, honest limitations, and the change record.
+
+## Machine-readable contracts
+
+- [Chapter manifest schema](https://bitcoinuniverseio.github.io/tandem/schemas/chapter.schema.json): JSON Schema Draft 2020-12, urn:tandem:schema:chapter.
+- [Close manifest schema](https://bitcoinuniverseio.github.io/tandem/schemas/close.schema.json): JSON Schema Draft 2020-12, urn:tandem:schema:close.
+- [Agreement envelope schema](https://bitcoinuniverseio.github.io/tandem/schemas/agreement-envelope.schema.json): JSON Schema Draft 2020-12, urn:tandem:schema:agreement-envelope.
+- [Golden fixture corpus](https://bitcoinuniverseio.github.io/tandem/vectors/generated/golden.json): markers, reason registry, 31 validation cases, event and object leaves with full preimages, Merkle levels, and roots.
+- [Golden vector manifest](https://bitcoinuniverseio.github.io/tandem/vectors/generated/manifest.json): binds the specification digest, the fixture digest, and the vector root.
+- [CREATE marker example](https://bitcoinuniverseio.github.io/tandem/vectors/create-marker.example.json): a minimal standalone marker input.
+
+## Key protocol facts
+
+- Operations: INIT (opcode 0x00), CREATE (0x01), MARK (0x02), ROTATE (0x03), CLOSE (0x04), plus a markerless REFUND recovery path.
+- Object statuses: ACTIVE, CLOSED, REFUNDED, EXITED_NONCANONICAL. A confirmed carrier spend that does not validate as an operation terminates the object.
+- Fixed constants: carrier value 20,000 satoshis, refund delay 52,560 blocks, founding window 4,320 blocks, INIT lead 1,008 blocks, wire magic TNDM, marker format 0x01.
+- The carrier is an exact 71-byte 2-of-2 witness script wrapped in native P2WSH, with keys sorted so key0 is below key1 by unsigned bytewise order.
+- The reason registry has 26 permanent entries, from VALID (0x0000) to UNMARKED_CARRIER_SPEND (0x0030). An implementation must return the lowest failing code, not merely a true one.
+- Every required prevout must have confirmed in a strictly earlier block. Same-block chaining fails with UNCONFIRMED_OR_SAME_BLOCK_PREVOUT.
+- Mempool observations never change protocol state, counters, or roots.
+- Known erratum: tandem.md section 13.2 states the event leaf preimage is 374 bytes with a 16-byte domain tag. The published vectors and the independent Rust verifier both use 371 bytes with a 13-byte domain tag. The 358-byte field block stated in the same sentence is correct. Follow the byte layout and the vectors.
+
+## Related repositories
+
+- [index-tandem](https://github.com/bitcoinuniverseio/index-tandem): TypeScript indexer, pipeline A, with a published OpenAPI contract. Describes itself as an implementation scaffold, not deployed infrastructure.
+- [tandem-verifier-rs](https://github.com/bitcoinuniverseio/tandem-verifier-rs): Rust independent verifier, pipeline B. Shares only frozen artifacts pinned by digest and does not import pipeline A.
+
+## Optional
+
+- [Product documentation](https://bitcoinuniverseio.github.io/tandem/docs/): the non-technical explanation of the product, its safety boundary, and its readiness path.
+- [Repository boundary](https://bitcoinuniverseio.github.io/tandem/REPOSITORY_BOUNDARY.md): what belongs in this public repository and what stays private.
+- [Security policy](https://bitcoinuniverseio.github.io/tandem/SECURITY.md): report suspected vulnerabilities privately, never in a public issue.
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 0000000..2158bab
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,8 @@
+# Tandem public product and protocol site.
+# Everything published here is intended to be public and is safe to index.
+
+User-agent: *
+Allow: /
+
+Sitemap: https://bitcoinuniverseio.github.io/tandem/sitemap.xml
+Sitemap: https://bitcoinuniverseio.github.io/tandem/sitemap-index.xml
diff --git a/scripts/serve.mjs b/scripts/serve.mjs
index eb43750..0396e86 100644
--- a/scripts/serve.mjs
+++ b/scripts/serve.mjs
@@ -29,7 +29,7 @@ const server = createServer(async (request, response) => {
return;
}
- response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self'");
+ response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'");
response.setHeader("Cross-Origin-Opener-Policy", "same-origin");
response.setHeader("Referrer-Policy", "no-referrer");
response.setHeader("X-Content-Type-Options", "nosniff");
diff --git a/scripts/verify-public.mjs b/scripts/verify-public.mjs
index 0b53aa8..7b13445 100644
--- a/scripts/verify-public.mjs
+++ b/scripts/verify-public.mjs
@@ -19,24 +19,38 @@ const approvedPaths = new Set([
".github/PULL_REQUEST_TEMPLATE.md",
".github/workflows/verify-public.yml",
".gitignore",
+ "assets/dossier.css",
+ "assets/pair-check.js",
"assets/site.css",
"assets/site.js",
"CODE_OF_CONDUCT.md",
"CONTRIBUTING.md",
"docs.manifest.json",
+ "docs/conformance.html",
+ "docs/considerations.html",
"docs/faq.md",
"docs/getting-started.md",
"docs/how-it-works.md",
+ "docs/guide.html",
"docs/index.html",
+ "docs/indexing.html",
"docs/integrations.md",
+ "docs/pair-check.html",
+ "docs/protocol.html",
"docs/roadmap.md",
"docs/safety.md",
+ "docs/schemas.html",
+ "docs/specification.html",
"docs/use-cases.md",
+ "docs/vectors.html",
+ "docs/verifier.html",
"docs/what-is-tandem.md",
"index.html",
"LICENSE",
+ "llms.txt",
"protocol/README.md",
"README.md",
+ "robots.txt",
"release/spec.json",
"REPOSITORY_BOUNDARY.md",
"schemas/agreement-envelope.schema.json",
@@ -45,6 +59,8 @@ const approvedPaths = new Set([
"scripts/serve.mjs",
"scripts/verify-public.mjs",
"SECURITY.md",
+ "sitemap-index.xml",
+ "sitemap.xml",
"tandem.md",
"vectors/create-marker.example.json",
"vectors/generated/golden.json",
@@ -63,7 +79,7 @@ const approvedArtifacts = new Map([
["vectors/generated/manifest.json", { bytes: 369, sha256: "d443d9b6e178b95b707620593e471b2146c2747be0f7789dc06f54ce133c33ac" }],
]);
-const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".mjs", ".txt", ".yaml", ".yml"]);
+const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".mjs", ".txt", ".xml", ".yaml", ".yml"]);
const forbiddenTopLevel = new Set([
"backend",
"deployments",
@@ -328,6 +344,106 @@ async function verifyWorkflow() {
if (/\b(?:deploy|publish|release)\b/i.test(workflow)) fail("public workflow must not deploy, publish, or release");
}
+const dossierPages = [
+ "docs/protocol.html",
+ "docs/specification.html",
+ "docs/guide.html",
+ "docs/pair-check.html",
+ "docs/indexing.html",
+ "docs/schemas.html",
+ "docs/vectors.html",
+ "docs/verifier.html",
+ "docs/conformance.html",
+ "docs/considerations.html",
+];
+
+async function verifyDossier() {
+ for (const path of dossierPages) {
+ const html = await readFile(resolve(root, path), "utf8");
+ const essentials = [
+ [//i, "language"],
+ [/name="viewport"/i, "responsive viewport"],
+ [/name="description"\s*\n?\s*content="[^"]{80,}"/i, "a description of at least 80 characters"],
+ [/class="skip-link"/i, "skip link"],
+ [/]*id="dossier-main"/i, "main landmark"],
+ [/Content-Security-Policy/i, "content security policy"],
+ [/mainnet is not active/i, "mainnet status"],
+ [/assets\/dossier\.css/i, "dossier stylesheet"],
+ [/[^<]+<\/title>/i, "title"],
+ [/property="og:title"/i, "Open Graph title"],
+ ];
+ for (const [pattern, label] of essentials) if (!pattern.test(html)) fail(`${path} is missing ${label}`);
+
+ const headings = html.match(/
]+(?:src|href)="([^"]+)"/gi)) {
+ if (/^(?:https?:)?\/\//i.test(match[1])) fail(`external runtime dependency found in ${path}: ${match[1]}`);
+ }
+ for (const match of html.matchAll(/]*>/gi)) {
+ if (!/\balt=/i.test(match[0])) fail(`image without alt text in ${path}`);
+ }
+ for (const match of html.matchAll(/