ChainBloom Docs
Open ChainBloom

Test vectors

Technical referenceChecked against @chainbloom/protocol@0.1.0Last reviewed 2026-07-317 min read

Eleven fixed byte strings that tell you whether your ChainBloom reader agrees with every other one. Five markers that must parse and six that must be refused.

If you are writing ChainBloom software, start on this page. Eleven fixed byte strings decide whether your reader agrees with every other reader, and checking them takes one command. Everything else in this section is easier to trust once these pass.

Start with the bytes, not with the prose#

Prose can be read two ways. Bytes cannot. The eleven vectors in vectors/ are the smallest complete statement of the format: five strings your decoder must accept and turn into exactly the listed fields, and six it must refuse with exactly the listed error code.

The check is a round trip, not just a parse. For each valid vector, scripts/check-vectors.ts decodes the hex and re-encodes the payload, then compares the result byte for byte with the original string. That pins the encoder as well as the decoder, and it makes one promise you can build on: for a given network and payload there is exactly one legal marker. There is no optional padding, no alternative field order, and no second way to say the same thing.

The five valid vectors#

Each vector names a network byte, a full marker in hex, and the payload it must decode to. The first 8 bytes are always the same shape: magic 43424c4d, version 1, network, , payload length.

VectorNetworkMarker hex
create-two-lanes3 regtest43424c4d0103011b010200900005000102030405060708090a0b0c0d0e0f044461776e
bloom3 regtest43424c4d01030204070302c8
graft-display-order-txid3 regtest43424c4d01030323000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f020c05
rendezvous2 signet43424c4d01020404041209dc
close0 mainnet43424c4d0100050101
vectors/valid-markers.json: every byte an encoder must produce

create-two-lanes#

The only operation whose payload changes length, so it is the one that catches lazy parsing.

create-two-lanes, field by field
43424c4d   magic
01         version
03         network: regtest
01         opcode: CREATE
1b         payload length: 27 bytes
01         ruleset
02         laneCount
0090       durationBlocks: 144, big endian
0005       maxSteps: 5, big endian
000102…0f  seed: 16 bytes
04         title length
4461776e   title: "Dawn"

It proves four things at once: the two multi-byte numbers are big endian, the seed sits at a fixed offset, the title carries its own length byte, and the declared payload length is 23 plus the title length. Decode this one wrongly and every you read will have the wrong duration.

bloom#

The most common step in any world, and the shortest fixed payload: glyph 07, palette 03, motion 02, magnitude c8. Magnitude is 200, which uses more than half the byte range on purpose -- a decoder that treats these fields as signed gets a different number. Compare this string with bloom-glyph-out-of-range below; they differ by one byte.

graft-display-order-txid#

The vector name is a warning. An echo carries the of the event it answers, and inside the marker those 32 bytes appear in display order, exactly as the id is written and searched for. They are not reversed.

That matters because the same id appears reversed elsewhere in the same transaction: a Bitcoin input stores its previous transaction hash in internal byte order. scripts/generate-fixtures.ts shows both conventions side by side. Reversing the marker bytes is the single most common implementation mistake, and it fails silently -- the marker still decodes, it just names an event nobody can find, which surfaces later as UNKNOWN_GRAFT_TARGET.

The payload is 35 bytes: 32 for the target, then relation 02, glyph 0c, palette 05.

rendezvous#

Signet, byte 02, so only the sixth byte differs from the regtest examples. The payload is bridge style 04, glyph 12 (18), palette 09, intensity dc (220) -- four bytes, the same size as a bloom.

That is the point worth noticing: a joins two paths, but its marker is no bigger than a single step. The second path is expressed by the shape of the transaction, not by the marker. Nothing in these bytes tells you which paths met.

close#

Nine bytes, and the smallest legal marker there is: header plus a single reason byte. Mainnet is network 00, so this vector also proves that the mainnet byte is a real value and not an unset default. A decoder that treats a zero network byte as missing fails here.

The six invalid vectors#

Each of these must be refused, and refused with the exact code. A reader that repairs any of them has stopped agreeing with everyone else.

VectorMarker hexError codeWhat it proves
truncated-header4342TRUNCATED_HEADERLength is checked before any byte is interpreted. Two bytes is shorter than the 8-byte header.
wrong-magic58424c4d01030204070302c8INVALID_MAGICAll four magic bytes are compared. Here only the first differs.
reserved-network43424c4d01090204070302c8RESERVED_NETWORKAn unknown network byte is refused, never treated as mainnet.
reserved-opcode43424c4d01030904070302c8RESERVED_OPCODEAn unknown opcode is refused, never skipped as a future extension.
trailing-byte43424c4d01030204070302c800INVALID_PAYLOAD_LENGTHThe declared length must consume every remaining byte. One spare byte is fatal.
bloom-glyph-out-of-range43424c4d01030204200302c8INTEGER_OUT_OF_RANGEField ranges are checked after the structure parses. Glyph 20 is 32; the highest glyph is 31.
vectors/invalid-markers.json: what a reader must refuse, and how

Four of the six are one byte away from the valid bloom vector, which is deliberate. It is easy to write a decoder that reads the fields it wants and ignores the rest; these strings catch that decoder immediately.

The shared principle is: refuse, do not repair. There is no lenient mode, no best-effort parse, and no rule that lets an unknown opcode through so a later version can define it. If two implementations disagree about whether a string is a marker, they will eventually disagree about what a world contains -- so the format admits no discretion.

Running them#

The vectors ship in the repository and run without a Bitcoin node, a network, or a key.

check every vector
npm run check:vectors

On success it prints one line and exits zero:

TEXT
Verified 5 valid and 6 invalid marker vectors.

Any failure throws and names the vector, so Valid vector failed: graft-display-order-txid tells you where to look without a debugger. The command is also part of npm run ci, which means the numbers in that sentence are checked on every change.

The CLI has a narrower version of the same idea:

the packaged command
chainbloom vectors verify

It re-encodes the five valid vectors and prints {"valid":true,"verified":5}. It does not run the invalid set -- use npm run check:vectors for that.

Whole transactions: the fixtures#

Markers are only the inner layer. fixtures/transactions.json holds five complete regtest transactions -- create, bloom, graft, rendezvous, close -- confirmed at heights 100 to 104, one per block. Each entry carries a name, a height, a block hash, the previous block hash, the txid, the raw transaction hex, and the fee prevouts that transaction spends.

That sequence is a whole small world from first to last moment: a two-path world opens, one path blooms, that path echoes its own bloom, it then meets the second root path, and one of the successors is completed. Replay it and you have something to compare a snapshot against.

The file is generated, not hand-written. scripts/generate-fixtures.ts builds the five transactions with the same encoder the library uses and prints the JSON to standard output, so the fixtures cannot drift away from the code that made them. Read it when you want a worked example of a valid transaction shape -- input order, the 1,000-satoshi carrier outputs, the change output, and the reversed input txids.

What the vectors do not cover#

They cover markers, and one happy path through the fixtures. They say nothing about:

  • carrier values, output positions, or input ordering;
  • the 0xfffffffd sequence number and version 2 rules;
  • signature types, when witness checking is switched on;
  • whether a world is still open, or a path still has steps left;
  • what happens to a path whose is spent by something that is not a ChainBloom action.

Those live in validation rules, and every code either set can produce is listed in error and issue codes.

  • All five valid vectors decode to the listed fields
  • All five re-encode to the identical hex string
  • All six invalid vectors are refused with the exact error code
  • A GRAFT target txid survives a decode and encode round trip unreversed
  • The decoder refuses a marker larger than 72 bytes
  • Nothing in the reader treats an unknown opcode as a future extension

Next

Then read the rules a marker cannot express

The vectors pin down eleven byte strings. Whole transactions are checked by a longer list of rules.

Read the validation rules