ChainBloom Docs
Open ChainBloom

Error and issue codes

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

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.

catching one
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.

CodeMessageRaised in
CARRIER_INPUT_MAPPINGRecognized carriers must be exactly at vin …src/validator.ts
CREATE_SPENDS_CARRIERCREATE may not spend a live carriersrc/validator.ts
DUPLICATE_INPUTInput … is listed more than oncesrc/builders.ts
DUPLICATE_LANERENDEZVOUS requires two different lanessrc/builders.ts
DUPLICATE_MARKER_OUTPUTExtra outputs may not contain another ChainBloom markersrc/builders.ts
GRAFT_NETWORK_MISMATCHGRAFT target network differssrc/validator.ts
INSUFFICIENT_INPUT_VALUEOutputs exceed inputssrc/builders.ts
INTEGER_OUT_OF_RANGE… must be an integer from … through …src/bytes.ts
INVALID_BLOCK_HEIGHTBlock height must be a non-negative safe integersrc/state.ts
INVALID_CARRIER_INPUTCarrier must be an exact 1,000-sat P2TR UTXOsrc/builders.ts
INVALID_CARRIER_SCRIPTCarrier successor must be standard P2TRsrc/validator.ts
INVALID_CARRIER_VALUECarrier output must be exactly … satssrc/validator.ts
INVALID_CREATECREATE contains invalid fixed fieldssrc/codec.ts
INVALID_HEX… must be even-length hexadecimalsrc/bytes.ts
INVALID_INPUT_VALUEInput value cannot be negativesrc/builders.ts
INVALID_MAGICMarker magic must be …src/codec.ts
INVALID_OP_RETURN_SIZEOP_RETURN pushed data must be 1..… bytessrc/script.ts
INVALID_OUTPUT_VALUEOutput value cannot be negativesrc/builders.ts
INVALID_PAYLOAD_LENGTH… payload must be exactly … bytessrc/codec.ts
INVALID_SEED_LENGTHseed must be … bytessrc/codec.ts
INVALID_TITLEtitle must be 0..32 ASCII bytes from [A-Za-z0-9 ._:-]src/codec.ts
INVALID_TITLE_LENGTHCREATE title length is not exactsrc/codec.ts
INVALID_TRANSACTIONUnable to parse Bitcoin transactionsrc/transaction.ts
INVALID_TXID… must be 32-byte hexadecimalsrc/bytes.ts
INVALID_U16Value does not fit in an unsigned 16-bit integersrc/bytes.ts
INVALID_VOUTInput vout must be a uint32src/builders.ts
INVALID_XONLY_KEYOutput key must be 32 bytessrc/builders.ts
LANE_COUNT_MISMATCHCREATE requires exactly lane_count root output keyssrc/builders.ts
MARKER_POSITIONTransaction must contain exactly one ChainBloom marker at vout 0src/validator.ts
MARKER_TOO_LARGEMarker exceeds … bytessrc/codec.ts
MARKER_VALUEvout 0 marker value must be zerosrc/validator.ts
MAX_STEPS_REACHEDLane has reached max_stepssrc/validator.ts
MISSING_CARRIER_OUTPUTRequired successor output is missingsrc/validator.ts
MISSING_MARKERTransaction has no vout 0src/validator.ts
MISSING_PREVOUTFee input prevout context is requiredsrc/validator.ts
MISSING_ROOT_CARRIERCREATE root carrier output is missingsrc/validator.ts
MISSING_TAPROOT_SIGNATURENo Taproot signature foundsrc/validator.ts
MISSING_WITNESSConfirmed input must contain witness datasrc/validator.ts
MISSING_WORLDCarrier world is missingsrc/validator.ts
NETWORK_MISMATCHMarker network … does not match indexer network …src/validator.ts
NO_BLOCK_TO_ROLLBACKState has no applied blocksrc/state.ts
NON_ASCII_TEXT… must contain ASCII onlysrc/bytes.ts
NON_CANONICAL_SEQUENCEEvery input nSequence must be …src/validator.ts
NON_CONTIGUOUS_BLOCKBlock does not extend the current best chain tipsrc/state.ts
NON_MINIMAL_OP_RETURNMarker must use one exact minimal direct push with no trailing opcodessrc/script.ts
NON_NATIVE_SEGWIT_FEE_INPUTFee inputs must spend native SegWit outputssrc/validator.ts
NON_NATIVE_SEGWIT_INPUTEvery input must be native SegWitsrc/builders.ts
NOT_OP_RETURNOutput is not OP_RETURNsrc/script.ts
RENDEZVOUS_LANE_ORDERRendezvous carrier inputs must be ordered lexicographically by lane IDsrc/validator.ts
RESERVED_NETWORKNetwork byte … is reservedsrc/codec.ts
RESERVED_OPCODEOpcode byte … is reservedsrc/codec.ts
ROLLBACK_HASH_MISMATCHExpected hash is not the current best chain tipsrc/state.ts
TRUNCATED_HEADERChainBloom marker is shorter than 8 bytessrc/codec.ts
TRUNCATED_U16Cannot read an unsigned 16-bit integersrc/bytes.ts
UNCONFIRMED_GRAFT_TARGETGRAFT target must be confirmed in a prior blocksrc/validator.ts
UNCONFIRMED_LINEAGE_PARENTCarrier parent must be confirmed in a prior blocksrc/validator.ts
UNEXPECTED_ERRORUnknown errorsrc/errors.ts
UNKNOWN_GRAFT_TARGETGRAFT target event does not existsrc/validator.ts
UNKNOWN_NETWORK_NAMEUnknown ChainBloom network: …src/codec.ts
UNSAFE_SIGHASHTaproot signatures must use SIGHASH_DEFAULT or SIGHASH_ALLsrc/validator.ts
UNSUPPORTED_RULESETCREATE ruleset must be 1src/codec.ts
UNSUPPORTED_VERSIONUnsupported version byte …src/codec.ts
WORLD_ENDEDWorld is no longer activesrc/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 longer ACTIVE, or the chain has reached its endHeightExclusive. Every live path became EXPIRED with the reason WORLD_DURATION_ELAPSED.
  • MAX_STEPS_REACHED -- the path has taken every step the world allows. A CLOSE is 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:

stderr on failure
{"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.

Next

Seeing one of these in the application?

The same codes, matched to what you were trying to do and what to try next.

Open troubleshooting