The trust property

Two implementations,
no shared code.

The strongest claim a deterministic protocol can make is that someone who does not trust its author can rebuild it and get the same answer. Tandem is implemented twice: once in TypeScript and once in Rust. The Rust implementation refuses to import the TypeScript one. The only thing they share is a short list of frozen files, pinned by digest.

Protocol finalized. Mainnet is not active.

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.

The six pinned inputs, and where each is published in this repository
ArtifactPinned digest
tandem.mdcaa77ce0122c0b833fc5f099191b54280b0481be325bdc98f2b48b0b905b923f
schemas/agreement-envelope.schema.json1d5493758b1cc358b02491b675b9e7cb64c51fe3ce2e3f0cde9669882717faa1
schemas/chapter.schema.json9fa613d576b2aeb95b52140c89180f05f65ecd7f4266797f41a1e685610dfc17
schemas/close.schema.jsone6645b4ec1eeb44996a37847d4318168959a905fb334b48f4f3298cc6340bc59
vectors/generated/manifest.jsond443d9b6e178b95b707620593e471b2146c2747be0f7789dc06f54ce133c33ac
vectors/generated/golden.jsonfc4bee2c20fe94a66a9849f1dc3d73bc407179474e936de29eddef85dcfb5856

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

Capabilities and where they live
CapabilityImplemented in
Marker candidate detection and strict push encodingtandem-core, marker module
Payload grammar and per-opcode exact lengthstandem-core, marker and types modules
The stable reason enumeration with explicit numeric valuestandem-core, types module
Rejection precedence, including the dispatch order in rule INV-1tandem-core, reducer module
The state machine, block application, and block disconnectiontandem-core, reducer module
Event leaf hashing and the event Merkle roottandem-core, roots module
Object-state leaf hashing and the object Merkle roottandem-core, roots module
The chained block root and the pre-INIT state roottandem-core and the CLI
Namespace, protocol identifier, and object key derivationtandem-core, types module
Carrier witness script and P2WSH derivationtandem-core, types module
SegWit v0 signature verification for P2WPKH and the 2-of-2 carrierThe service crate, at its own Bitcoin Core boundary
Agreement tuple JCS normalization and Ed25519 signingThe service crate, signer module
The core library does not verify signatures, and says so.

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.

Verify the frozen inputs against this repository
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.

Verify the golden corpus independently
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.

Replay resolved blocks through the independent reducer
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.

Build and test the whole workspace
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.

Targets exist, a campaign has not been recorded.

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

Limits of the independent verification story, stated plainly
Not provenWhy
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.
Neither implementation is a released product.

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.