ChainBloom Docs
Open ChainBloom

Data structures

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

Every field of WorldState, LaneState, EventState and the rest, with the exact rule behind each lane status and world status.

These are the shapes your database, your API, and your user interface will end up mirroring. Get the identifiers and the statuses right and two independent services describe the same world in the same words. Every field below is copied from src/types.ts.

Identity first#

Two identifiers carry the whole model, and both are derived rather than assigned.

A world id is the txid of its CREATE transaction. There is no counter, no registry, and no chance of a collision. Lowercase hex, 64 characters.

A lane id is <worldId>:<laneNumber>. Lane numbers start at 0 and run to laneCount - 1, matching output order in the CREATE: the rooted at vout 1 is lane 0. The helper is laneId(worldId, laneNumber) in src/validator.ts, and it lowercases the txid, so ids compare as plain strings.

An outpoint key is txid:vout. Same shape, different meaning. outpointKey builds it, and the state engine uses it to answer the one question that matters on every transaction: does this input spend a live ?

The world and its paths#

WorldState#

Everything fixed at creation, plus the status that follows from height.

FieldTypeNotes
idstringThe txid of the CREATE
networkNetworkId0 mainnet, 1 testnet4, 2 signet, 3 regtest
ruleset1Always 1 in this version
laneCountnumber1 to 8
durationBlocksnumber144 to 52,560
maxStepsnumber1 to 512, per path
seedstring16 bytes as hex
titlestringUp to 32 ASCII bytes, possibly empty
createdHeightnumberThe block that confirmed the CREATE
endHeightExclusivenumbercreatedHeight + durationBlocks
statusWorldStatusACTIVE, ENDED, or EXPIRED
laneIdsreadonly string[]One id per lane, in lane order

endHeightExclusive is exclusive, as the name says. A world created at height 812,000 with durationBlocks of 1,008 ends at 813,008: the last height at which a step can confirm is 813,007.

LaneState#

One row per . This is the record your application will read most often.

FieldTypeNotes
idstring<worldId>:<laneNumber>
worldIdstringThe world this path belongs to
laneNumbernumberFrom 0
statusLaneStatusLIVE, CLOSED, ABANDONED, or EXPIRED
currentOutpointOutpointNull once the path is no longer LIVE
currentScriptPubKeyHexstringNull once the path is no longer LIVE
stepCountnumberCompared against the world's maxSteps
createdHeightnumberThe height of the CREATE
lastEventHeightnumberHeight of the most recent event on this path
eventTxidsreadonly string[]Every event, in order, starting with the CREATE
terminalTxidstringNull while LIVE
terminalReasonstringNull while LIVE

stepCount starts at 0 for a root path even though eventTxids already holds the CREATE txid. The CREATE is not a step: it opens the path rather than moving it.

Events and what they record#

EventState#

One row per confirmed, valid ChainBloom transaction.

FieldTypeNotes
txidstringThe event id
networkNetworkIdThe network of the view that recorded it
operationOperationNameCREATE, BLOOM, GRAFT, RENDEZVOUS, or CLOSE
payloadOperationPayloadThe decoded fields of that operation
heightnumberThe block that confirmed it
txIndexnumberPosition within the block
blockHashstringThe block it belongs to
worldIdsreadonly string[]Usually one, two when a meeting crosses worlds
laneIdsreadonly string[]The paths this event touched
parentOutpointsreadonly Outpoint[]The carriers it spent, empty on a CREATE
successorOutpointsreadonly Outpoint[]The carriers it created, empty on a CLOSE
graftTargetTxidstringThe echo target, null on every other operation

height and txIndex together are the sort key for history. They are why two services that read the same blocks list the same events in the same order without talking to each other.

InvalidCarrierSpend#

A path can also end by accident, and that is recorded rather than hidden.

FieldTypeNotes
txidstringThe confirmed transaction that spent a live carrier
heightnumberWhere it confirmed
blockHashstringWhich block
laneIdsreadonly string[]Every path it ended
issueCodesreadonly string[]Why it was not a valid event

When this happens the lanes become ABANDONED with terminalReason set to INVALID_CONFIRMED_SPEND. Nothing is invented to replace them. Keeping the spend, with its issue codes, is what lets you explain to a participant exactly what happened.

SuccessorMapping#

Produced by validation, consumed when the event is applied. It says which output continues which path.

FieldTypeNotes
laneIdstringThe path being advanced
inputIndexnumberWhich input held its old carrier
outputIndexnumberWhich output holds its new one
outpointOutpointThe new carrier, as txid and vout
scriptPubKeyHexstringThe new carrier script

Reading a whole view#

StateSnapshot#

What snapshot() returns, and what a restore puts back.

FieldTypeNotes
tipHashstringNull before any block has been applied
tipHeightnumberNull before any block has been applied
worldsreadonly WorldState[]Sorted by id
lanesreadonly LaneState[]Sorted by id
eventsreadonly EventState[]Sorted by height, then by txIndex
invalidCarrierSpendsreadonly InvalidCarrierSpend[]In the order they were seen

That sorting is deliberate. Two services that applied the same blocks produce byte-identical snapshots, which makes a snapshot something you can diff, hash, or compare across machines.

MempoolProjection#

What an unconfirmed transaction looks like. A projection, never history.

FieldTypeNotes
txidstringThe waiting transaction
validbooleanWhether it would be valid at the next height
conflictsWithreadonly string[]Other waiting transactions spending the same outpoint
laneIdsreadonly string[]The paths it would touch
issueCodesreadonly string[]Empty when valid

MempoolOverlay in src/state.ts never creates lineage parents. A step whose parent is still in the does not become spendable-from just because both are waiting.

ValidationResult#

What validateProtocolTransaction hands back.

FieldTypeNotes
validbooleanTrue only when issues is empty
markerDecodedMarkerNull when the marker could not be decoded at all
issuesreadonly ValidationIssue[]Each with a code, a message, and a path
carrierLaneIdsreadonly string[]Live paths this transaction spends
successorMappingsreadonly SuccessorMapping[]Path continuations it creates

carrierLaneIds is populated even when valid is false, and that is the field that turns a bad transaction into abandoned paths rather than a silent gap.

Statuses, and the exact rule for each#

Lane statuses#

StatusThe rule
LIVEThe path holds an unspent carrier and can be advanced
CLOSEDA valid CLOSE spent the carrier. terminalReason is CLOSE_ plus the one-byte reason
ABANDONEDA confirmed transaction spent the live carrier and was not a valid event. terminalReason is INVALID_CONFIRMED_SPEND
EXPIREDA block arrived at or above endHeightExclusive while the path was LIVE. terminalReason is WORLD_DURATION_ELAPSED

Three of those four are terminal endings with different meanings, and it is worth carrying that difference into your interface. Closed is a decision. is the ending the creator chose at the start. is a mistake, and it is the only one worth warning people about in advance.

World statuses#

StatusThe rule
ACTIVEAt least one lane is still LIVE and the height has not reached endHeightExclusive
ENDEDEvery lane has stopped, but the world's own duration has not run out
EXPIREDA block arrived at or above endHeightExclusive. Every LIVE lane expired with it

Expiry wins. refreshWorldStatus skips a world that is already EXPIRED, so a world never travels back from EXPIRED to ENDED. A world can reach ENDED early, when the last path is closed or abandoned before its time runs out.

Next

See how these are produced

The rules that decide whether a transaction becomes an event at all, and which lanes it touches.

Validation rules