Why two implementations
A metaprotocol is not enforced by Bitcoin consensus. Bitcoin will happily confirm a transaction that Tandem considers invalid. Everything above the script layer is decided by parsers, which means a single parser is a single point of failure with no external check on it.
The usual mitigation is to publish a specification and hope. Tandem goes further: the specification's own invariant 12 requires that independent implementations produce identical events, reason codes, state, counters, and all three roots at every height, given the same binding and the same blocks. That is a testable claim, and the second implementation exists to test it.
PIPELINE A
index-tandem. TypeScript, NestJS, MySQL. Consumes ordered blocks from Bitcoin Core, records protocol observations, and exposes query and agreement surfaces over HTTP with a published OpenAPI contract.
PIPELINE B
tandem-verifier-rs. Rust, PostgreSQL. Reimplements the marker parser, the reducer, and the roots. Its README states plainly that it does not import, execute, or depend on the TypeScript parser or indexer.
Different language, different runtime, different database, different developer assumptions. A bug that survives both is far more likely to be a specification problem than an implementation slip, which is exactly the class of problem you want surfaced before a network is live.
The shared surface, pinned by digest
The two implementations do share something, and being precise about what is the whole point. The Rust repository carries a lock file listing every shared artifact and its SHA256. Nothing else crosses the boundary.
| Artifact | Pinned digest |
|---|---|
| tandem.md | caa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f |
| schemas/agreement-envelope.schema.json | 1d5493758b1cc358b02491b675b9e7cb64c51fe3ce2e3f0cde9669882717faa1 |
| schemas/chapter.schema.json | 9fa613d576b2aeb95b52140c89180f05f65ecd7f4266797f41a1e685610dfc17 |
| schemas/close.schema.json | e6645b4ec1eeb44996a37847d4318168959a905fb334b48f4f3298cc6340bc59 |
| vectors/generated/manifest.json | d443d9b6e178b95b707620593e471b2146c2747be0f7789dc06f54ce133c33ac |
| vectors/generated/golden.json | fc4bee2c20fe94a66a9849f1dc3d73bc407179474e936de29eddef85dcfb5856 |
Every one of those six digests matches the values this repository's own verifier pins in scripts/verify-public.mjs. Two independently maintained lists of hashes agreeing is a small thing, but it is the kind of small thing that stops a silent artifact swap.
Note what is not pinned. The vector root from the manifest is deliberately absent from the lock. The Rust verifier recomputes it from the fixture digest instead of trusting the published value, so an incorrect root would be caught rather than copied. The lock also refuses any path that is absolute or contains a parent-directory component, so a tampered lock cannot be used to read files outside the protocol directory it is pointed at.
What the Rust implementation verifies
| Capability | Implemented in |
|---|---|
| Marker candidate detection and strict push encoding | tandem-core, marker module |
| Payload grammar and per-opcode exact lengths | tandem-core, marker and types modules |
| The stable reason enumeration with explicit numeric values | tandem-core, types module |
| Rejection precedence, including the dispatch order in rule INV-1 | tandem-core, reducer module |
| The state machine, block application, and block disconnection | tandem-core, reducer module |
| Event leaf hashing and the event Merkle root | tandem-core, roots module |
| Object-state leaf hashing and the object Merkle root | tandem-core, roots module |
| The chained block root and the pre-INIT state root | tandem-core and the CLI |
| Namespace, protocol identifier, and object key derivation | tandem-core, types module |
| Carrier witness script and P2WSH derivation | tandem-core, types module |
| SegWit v0 signature verification for P2WPKH and the 2-of-2 carrier | The service crate, at its own Bitcoin Core boundary |
| Agreement tuple JCS normalization and Ed25519 signing | The service crate, signer module |
tandem-core is deliberately free of any database, HTTP, or network
dependency. Callers supply resolved transaction inputs including exact prevout data
and the result of independent signature verification. The reducer reads that
result as a boolean and maps a false value to
BAD_SIGNATURE_OR_SIGHASH. Signatures are actually computed in the service
crate, which verifies them against real chain data at its own Bitcoin Core boundary.
The practical consequence: a replay run through the CLI trusts the signature flag in the replay file it is given. It proves the parser, reducer, and roots agree. It does not, on its own, prove anything about signatures.
Run it yourself
This is the part that matters. You do not need our infrastructure, our database, or our permission. You need a Rust toolchain and a checkout of two public repositories.
The verifier pins Rust 1.97.1 with a toolchain file, uses edition 2024, forbids unsafe
code, and denies both the all and pedantic clippy groups. Its
three crates are at version 0.1.0 and are not published to crates.io, so build from
source.
cargo run -p tandem-cli -- verify-inputs \
--lock protocol-inputs.lock.json \
--protocol-root ../tandem
This reads every artifact named in the lock from your checkout of this repository and compares its digest. If someone has altered the specification, a schema, or the corpus, this is where it shows up.
cargo run -p tandem-cli -- verify-vectors \
--manifest ../tandem/vectors/generated/manifest.json \
--spec ../tandem/tandem.md
This one does the real work. It checks the manifest's schema identifier, confirms it
names tandem.md and golden.json, recomputes the specification
digest and the fixture digest, derives the vector root and requires it to equal the
published one, confirms the fixture's own schema identifier, and confirms the
specification digest recorded inside the fixture matches the one it just computed.
cargo run -p tandem-cli -- replay --input replay.json
A replay file supplies a binding, a list of block views, and optionally an array of expected roots per height. The CLI applies each block through the Rust reducer and fails with a root mismatch at the first height that diverges, which makes it a usable differential driver against any other implementation that can emit the same block views.
The verifier's own recorded verification run reports the same three roots this
repository publishes: event root
475b25d2…fae046ce, object-state root
67ec64ab…7e110d79, and chained root
c54cd3c6…6322e260. Those are the values on
the vectors page, arrived at by different code.
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo build --workspace --release --locked
Fuzzing and property tests
The verifier carries two cargo-fuzz targets, kept in their own workspace so
they never enter a release build.
- Marker target. Feeds arbitrary bytes to the candidate detector and, on a hit, to the parser. The property is that arbitrary script bytes, including malformed push lengths, truncation, trailing data, and non-minimal encodings, always produce a reason rather than a panic.
- Replay target. Deserializes arbitrary block views against a fixed regtest binding and drives the reducer over the first 32 blocks. The property is that adversarial but type-valid input produces errors, never aborts.
Alongside them are property tests asserting that marker detection and parsing never panic, that any successfully parsed marker has a payload length in the range 7 to 80 and exactly the declared length for a defined opcode, that distinct CREATE transaction identifiers yield distinct object keys, and that the exact payload lengths match the specification.
The verifier's own verification record explicitly disclaims a timed fuzz campaign. The harness is present and runnable; published evidence of a sustained run is not. Do not read the presence of fuzz targets as a claim about fuzzing coverage.
What agreement actually proves
Independent agreement is a strong property and a narrow one. Being precise about its edges is what makes it worth anything.
The design is a comparison of signed outputs, not a differential test rig. Neither pipeline reads the other's output. Each processes the same height independently and signs a tuple. Agreement means the two signed tuples match on the nine semantic fields. When they do, you know:
- Two codebases, written in different languages, independently derived the same event set for that height.
- They agree on every object's status, sequence, key pair, carrier, and chapter count, because those all feed the object-state root.
- They agree on the whole history up to that height, because the chained root incorporates every previous block's root.
- A parser bug that changes any classification, any reason code, or any ordering would have to exist identically in both, which is unlikely for genuinely independent work.
That is a meaningful reduction in the risk that one parser silently presents incorrect state. It is the property the whole two-pipeline arrangement exists to produce, and it is worth building for.
How to respond to a disagreement
Fail closed. Do not present state for the disputed height and do not allow a state-changing construction against it. Then compare in a fixed order: event root, object-state root, counters, chained root. The first difference localizes the problem. All three components matching while the chained root differs points at the chaining input rather than at this block. Do not designate either pipeline as correct on the basis of ownership or release order.
What it does not prove
| Not proven | Why |
|---|---|
| That the specification is correct | Both implementations follow the same document. A specification error is reproduced faithfully by both, and agreement will not surface it. The errata is an example of a discrepancy caught by measurement rather than by agreement. |
| That either pipeline is honest at runtime | A signed tuple proves what a key holder asserted, not that the software behind it behaved. Provenance fields identify a build; they do not attest that the build ran. |
| That the two pipelines have ever agreed on a real chain | The verifier's own launch gate for pipeline agreement is recorded as not performed. What exists today is a shared golden corpus that both can process, and a recorded local verification run. |
| That the Rust verifier is production ready | Its own gate list records the great majority of gates as not completed, not exercised, or not granted. Its documentation disclaims live Bitcoin Core, live database, and live message-queue exercise, plus regtest and signet transcripts, a key ceremony, and mainnet authority. |
| That signatures are correct in a CLI replay | The core library takes signature validity as an input. Only the service crate computes it from real chain data. |
| That your own key management is sound | Nothing in this arrangement protects a lost key, a coerced signature, or a compromised signing device. |
Mainnet is not active. Both repositories are public source with unit coverage, not deployed infrastructure, and both say so in their own words. Treat the verification story described on this page as a design that can be exercised today against the published corpus, and as evidence still to be produced against a live network.
The honest summary: an independent second implementation in a different language is a genuinely strong trust property, and this one is real, runnable, and pinned to the same frozen bytes. What has not yet happened is the two pipelines agreeing continuously over a live chain for a sustained period. Until that is published, the property is demonstrated on a fixed corpus rather than proven in operation.