Skip to content

Conformance — v1 test vectors

This document points at the canonical fixtures a v1 implementation SHOULD validate itself against. The fixtures live in tests/fixtures/ on GitHub and are committed to source so external implementations can pull just that directory without cloning the full repository.

The reference implementations in piwallet/ (Python) and companion/ (TypeScript) both regenerate and check against these files on every run of their respective test suites, so they cannot silently drift.

1. Canonical wallet

File: tests/fixtures/addresses_canonical.json

Anchors the seed → addresses pipeline. The fixture is a JSON object:

{
  "mnemonic":     "abandon abandon ... about",
  "path":         "m/44'/236'/0'",
  "fingerprint":  "<8 lowercase hex chars>",
  "xpub":         "<Base58Check xpub at m/44'/236'/0'>",
  "addresses": {
    "receive": [ { "index": 0, "address": "<base58>" }, ... ],
    "change":  [ { "index": 0, "address": "<base58>" }, ... ]
  }
}

A conformant implementation MUST be able to:

  1. Run BIP39 mnemonic → 64-byte seed with empty passphrase producing the seed that derives xpub byte-for-byte at m/44'/236'/0'.
  2. Compute the 4-byte self-fingerprint of xpub (see derivation.md §4) and produce the same value as fingerprint.
  3. Derive m/0/i and m/1/i from xpub for every listed index and produce the same Base58Check P2PKH addresses.

The reference implementations cross-check this in both directions — Python's tests/test_derivation.py and TypeScript's companion/tests/derive.test.ts both load this file and assert their output equals every entry.

2. Canonical proposal

Files:

  • tests/fixtures/proposal_01.cbor — a v1 unsigned_proposal envelope, gzip-compressed, 511 bytes.
  • tests/fixtures/proposal_01.json — companion metadata (mnemonic, funding tx, addresses, amounts, block height, Merkle root).
  • tests/fixtures/proposal_01_decoded.json — a field-by-field decoded form of the envelope, suitable as a structural reference for a third-party decoder. Generated by scripts/dump_decoded_envelope.py.

The proposal pays 30 000 sats from m/44'/236'/0'/0/0 to an external address, with 19 500 sats of change back to m/44'/236'/0'/1/0 and 500 sats of implicit fee. The funding tx sits inside a synthetic 2-leaf Merkle tree whose root is anchored at the (synthetic) block height 812 345. None of this is on the live chain — it is a self-consistent synthetic vector for offline testing.

2.1 Bytes-level check

A v1 envelope decoder fed the 511 bytes of proposal_01.cbor MUST:

  • gunzip the bytes to ~650 bytes of CBOR;
  • decode the CBOR to a single map with v == 1 and kind == "tx";
  • expose the same fields, in the same types, that proposal_01_decoded.json lists.

A simple way to verify your implementation is to print your own structural dump and compare against proposal_01_decoded.json. The Python reference dump is regenerated by:

python scripts/dump_decoded_envelope.py \
    tests/fixtures/proposal_01.cbor \
    tests/fixtures/proposal_01_decoded.json

proposal_01_decoded.json is regenerated as part of the repo's test suite, so the file in git always matches the current CBOR encoding.

2.2 Cryptographic check

The bytes are not just structurally valid — they pass every check in spv.md §1. A v1 signer fed this envelope (with the mnemonic from §1 as its loaded wallet) MUST:

  • Parse the envelope without errors.
  • Recover the same funding txid as proposal_01.json.funding_txid from the BEEF.
  • Compute the same Merkle root as proposal_01.json.merkle_root_hex from the path attached to the funding tx.
  • Re-derive the input's P2PKH script from m/44'/236'/0'/0/0 and match it against the funding tx's output at vout=0.
  • Re-derive the change output's P2PKH script from m/44'/236'/0'/1/0 and match it against the proposal's outputs[1].script.
  • Compute a fee_sats of 500.

Implementations that get this far on the fixture have a viable v1 signer; implementations that fail any of the above are not conformant and SHOULD NOT advertise compatibility.

3. Cross-implementation decode

This is optional but useful as a sanity check. The reference TypeScript decoder reads the Python-produced proposal_01.cbor and asserts every logical field matches the metadata (tests/test_decoded_envelope_fixture.py and companion/tests/envelope.test.ts).

A third-party encoder is free to produce different bytes than the Python reference (e.g., a different gzip compression level, canonical CBOR map ordering, or mtime value in the gzip header) as long as the bytes round-trip through both reference decoders to the same logical envelope. Use the structural dump from §2.1 as the comparison target rather than a raw byte-level diff.

4. PW1 multipart round-trip

There is no dedicated PW1 fixture file. Instead, build one on-the-fly:

  1. Take any envelope blob (e.g., proposal_01.cbor).
  2. Run your encoder to produce a list of PW1|... lines.
  3. Feed them into your assembler in arbitrary order.
  4. Compare the assembled bytes against the original.

The reference implementations exercise this in both directions:

  • Python — tests/test_qr_multipart.py.
  • TypeScript — companion/tests/pw1.test.ts.

If you can do this for one envelope, you can do it for all of them — the transport is content-agnostic.

5. Versioning the fixtures

When v2 of the protocol arrives, this directory will be cloned to docs/protocol/v2/ with a new tests/fixtures/v2/ directory. v1 fixtures will be kept in their current location forever so older implementations can keep validating themselves.

A change that would alter proposal_01.cbor bytes is, by definition, a protocol change and a new fixture file SHOULD be added rather than mutating an existing one.