Error and issue codes
Every code ChainBloom can produce, the difference between a thrown error and a recorded issue, and what to do about the five you will meet most.
Every failure in ChainBloom has a short stable code, and knowing which of two kinds it is tells you who has to act. One kind says your call was wrong. The other says a Bitcoin transaction is not a ChainBloom event -- which is a fact about the chain, not a bug in your software.
Two kinds of failure#
A ChainBloomError is thrown#
ChainBloomError extends Error. Its name is ChainBloomError, and it carries a code, a human message, and sometimes a details object with the numbers involved.
import { ChainBloomError, decodeMarkerHex } from '@chainbloom/protocol';
try {
decodeMarkerHex('43424c4d01030204070302c800');
} catch (error) {
if (error instanceof ChainBloomError) {
console.log(error.code); // INVALID_PAYLOAD_LENGTH
}
}A throw means nothing happened. No was produced, no PSBT was built, no block was applied to state. Three parts of the library throw: the codec when bytes or fields are wrong, the builders when a draft transaction cannot be assembled honestly, and the state engine when blocks arrive out of order. The thing to fix is the call.
Anything unexpected is wrapped rather than leaked. asChainBloomError turns an unknown throw into UNEXPECTED_ERROR with the original message, so callers only ever have to handle one shape.
A validation issue is recorded#
validateProtocolTransaction does not throw when a transaction is bad. It returns a ValidationResult with valid, the decoded marker if there was one, and an issues array. Each issue is { code, message, path }, where path points at the part of the transaction that failed, such as an input index.
An issue is not a complaint about your code. It is the answer to a question: is this transaction a ChainBloom event? MARKER_VALUE, WORLD_ENDED and RENDEZVOUS_LANE_ORDER all mean the same thing at that level -- no.
Why the difference matters#
The two kinds lead to different behaviour, and mixing them up is how software starts inventing history.
A thrown error stops you before anything is signed or stored. Show it, fix the input, try again.
A recorded issue may describe a transaction that is already confirmed and can never change. If that transaction spent a live , the path it spent becomes ABANDONED with the terminal reason INVALID_CONFIRMED_SPEND, and the spend is kept in invalidCarrierSpends with its issue codes. The path stops there. Nothing is substituted, guessed, or rolled forward to keep a story going.
Every code#
The table below is built from the source at build time, so it cannot fall behind the implementation. The third column names the file that raises each code, which is usually the fastest way to understand one.
| Code | Message | Raised in |
|---|---|---|
CARRIER_INPUT_MAPPING | Recognized carriers must be exactly at vin … | src/validator.ts |
CREATE_SPENDS_CARRIER | CREATE may not spend a live carrier | src/validator.ts |
DUPLICATE_INPUT | Input … is listed more than once | src/builders.ts |
DUPLICATE_LANE | RENDEZVOUS requires two different lanes | src/builders.ts |
DUPLICATE_MARKER_OUTPUT | Extra outputs may not contain another ChainBloom marker | src/builders.ts |
GRAFT_NETWORK_MISMATCH | GRAFT target network differs | src/validator.ts |
INSUFFICIENT_INPUT_VALUE | Outputs exceed inputs | src/builders.ts |
INTEGER_OUT_OF_RANGE | … must be an integer from … through … | src/bytes.ts |
INVALID_BLOCK_HEIGHT | Block height must be a non-negative safe integer | src/state.ts |
INVALID_CARRIER_INPUT | Carrier must be an exact 1,000-sat P2TR UTXO | src/builders.ts |
INVALID_CARRIER_SCRIPT | Carrier successor must be standard P2TR | src/validator.ts |
INVALID_CARRIER_VALUE | Carrier output must be exactly … sats | src/validator.ts |
INVALID_CREATE | CREATE contains invalid fixed fields | src/codec.ts |
INVALID_HEX | … must be even-length hexadecimal | src/bytes.ts |
INVALID_INPUT_VALUE | Input value cannot be negative | src/builders.ts |
INVALID_MAGIC | Marker magic must be … | src/codec.ts |
INVALID_OP_RETURN_SIZE | OP_RETURN pushed data must be 1..… bytes | src/script.ts |
INVALID_OUTPUT_VALUE | Output value cannot be negative | src/builders.ts |
INVALID_PAYLOAD_LENGTH | … payload must be exactly … bytes | src/codec.ts |
INVALID_SEED_LENGTH | seed must be … bytes | src/codec.ts |
INVALID_TITLE | title must be 0..32 ASCII bytes from [A-Za-z0-9 ._:-] | src/codec.ts |
INVALID_TITLE_LENGTH | CREATE title length is not exact | src/codec.ts |
INVALID_TRANSACTION | Unable to parse Bitcoin transaction | src/transaction.ts |
INVALID_TXID | … must be 32-byte hexadecimal | src/bytes.ts |
INVALID_U16 | Value does not fit in an unsigned 16-bit integer | src/bytes.ts |
INVALID_VOUT | Input vout must be a uint32 | src/builders.ts |
INVALID_XONLY_KEY | Output key must be 32 bytes | src/builders.ts |
LANE_COUNT_MISMATCH | CREATE requires exactly lane_count root output keys | src/builders.ts |
MARKER_POSITION | Transaction must contain exactly one ChainBloom marker at vout 0 | src/validator.ts |
MARKER_TOO_LARGE | Marker exceeds … bytes | src/codec.ts |
MARKER_VALUE | vout 0 marker value must be zero | src/validator.ts |
MAX_STEPS_REACHED | Lane has reached max_steps | src/validator.ts |
MISSING_CARRIER_OUTPUT | Required successor output is missing | src/validator.ts |
MISSING_MARKER | Transaction has no vout 0 | src/validator.ts |
MISSING_PREVOUT | Fee input prevout context is required | src/validator.ts |
MISSING_ROOT_CARRIER | CREATE root carrier output is missing | src/validator.ts |
MISSING_TAPROOT_SIGNATURE | No Taproot signature found | src/validator.ts |
MISSING_WITNESS | Confirmed input must contain witness data | src/validator.ts |
MISSING_WORLD | Carrier world is missing | src/validator.ts |
NETWORK_MISMATCH | Marker network … does not match indexer network … | src/validator.ts |
NO_BLOCK_TO_ROLLBACK | State has no applied block | src/state.ts |
NON_ASCII_TEXT | … must contain ASCII only | src/bytes.ts |
NON_CANONICAL_SEQUENCE | Every input nSequence must be … | src/validator.ts |
NON_CONTIGUOUS_BLOCK | Block does not extend the current best chain tip | src/state.ts |
NON_MINIMAL_OP_RETURN | Marker must use one exact minimal direct push with no trailing opcodes | src/script.ts |
NON_NATIVE_SEGWIT_FEE_INPUT | Fee inputs must spend native SegWit outputs | src/validator.ts |
NON_NATIVE_SEGWIT_INPUT | Every input must be native SegWit | src/builders.ts |
NOT_OP_RETURN | Output is not OP_RETURN | src/script.ts |
RENDEZVOUS_LANE_ORDER | Rendezvous carrier inputs must be ordered lexicographically by lane ID | src/validator.ts |
RESERVED_NETWORK | Network byte … is reserved | src/codec.ts |
RESERVED_OPCODE | Opcode byte … is reserved | src/codec.ts |
ROLLBACK_HASH_MISMATCH | Expected hash is not the current best chain tip | src/state.ts |
TRUNCATED_HEADER | ChainBloom marker is shorter than 8 bytes | src/codec.ts |
TRUNCATED_U16 | Cannot read an unsigned 16-bit integer | src/bytes.ts |
UNCONFIRMED_GRAFT_TARGET | GRAFT target must be confirmed in a prior block | src/validator.ts |
UNCONFIRMED_LINEAGE_PARENT | Carrier parent must be confirmed in a prior block | src/validator.ts |
UNEXPECTED_ERROR | Unknown error | src/errors.ts |
UNKNOWN_GRAFT_TARGET | GRAFT target event does not exist | src/validator.ts |
UNKNOWN_NETWORK_NAME | Unknown ChainBloom network: … | src/codec.ts |
UNSAFE_SIGHASH | Taproot signatures must use SIGHASH_DEFAULT or SIGHASH_ALL | src/validator.ts |
UNSUPPORTED_RULESET | CREATE ruleset must be 1 | src/codec.ts |
UNSUPPORTED_VERSION | Unsupported version byte … | src/codec.ts |
WORLD_ENDED | World is no longer active | src/validator.ts |
The five you will meet most#
INVALID_MAGIC#
You handed the decoder an OP_RETURN that is not ChainBloom. Expected, and not a fault, if you are scanning every output of every block -- most data outputs on Bitcoin belong to something else.
What to do: call isChainBloomMagic on the bytes first and skip anything that does not match. Do not use exceptions as a filter in a hot indexing loop.
INVALID_PAYLOAD_LENGTH#
The declared payload length did not consume every remaining byte. Almost always a hand-assembled marker with a spare byte, or a payload built for a different operation. The trailing-byte test vector is this exact case.
What to do: build markers with encodeMarker rather than by hand. If you must assemble bytes yourself, decode the result before you spend anything.
INSUFFICIENT_INPUT_VALUE#
A builder refused because the inputs do not cover the outputs plus the fee. Easy to hit because the successor carriers are part of the outputs: every path a transaction continues needs another 1,000 satoshis, and a creates two of them.
What to do: add a fee input, or lower the fee rate. The builder will not silently shrink a carrier or drop the change output to make the numbers work.
UNCONFIRMED_LINEAGE_PARENT#
You tried to move a path forward while its own last event is in the same block or later. Lineage is strict: a parent must already be confirmed in an earlier block.
What to do: wait for one confirmation. This is a fact about timing, not a broken transaction -- the same bytes will be valid a block later. In an interface, say "waiting for the previous step to confirm", not "invalid".
NON_CONTIGUOUS_BLOCK#
applyBlock was given a block that does not extend the current tip. The state engine requires height plus one and a matching previous hash, every time.
What to do: feed blocks in order. If you are following a , call rollbackTip() until the tip is the common ancestor, then apply the new branch. A failure inside a block leaves state exactly as it was before that block, so you never have to reason about a half-applied block.
Codes that mean the world said no#
Three codes look like errors and are not. They are a world working correctly:
WORLD_ENDED-- the world is no longerACTIVE, or the chain has reached itsendHeightExclusive. Every live path becameEXPIREDwith the reasonWORLD_DURATION_ELAPSED.MAX_STEPS_REACHED-- the path has taken every step the world allows. ACLOSEis still accepted, so the path can still be completed on purpose.CREATE_SPENDS_CARRIER-- someone tried to open a new world with a live carrier as a fee input. The carrier is protected from being consumed by accident.
Present these as endings and limits, not as failures. A world that ran out of time did the thing it was designed to do.
How a code reaches you#
The CLI prints one JSON object per outcome. Success goes to standard output; a failure goes to standard error and sets exit code 1:
{"error":"RESERVED_OPCODE","message":"Opcode byte 9 is reserved"}That shape is stable, so a script can branch on error without parsing prose. Codes are the contract; messages may be reworded for clarity, and details may gain fields.