Chio/Docs
LOGIN · JOIN

Replication & Convergence

Follow a returning peer as it pulls missing records, repairs its baseline from a snapshot, and counts again as a budget witness.

Concept chio@5f63e6e

On this page

Pull replication brings a trust-control node back to its peers’ control state after a restart or a partition. Each store arrives as a page that must start where the node’s cursor stopped; a page that skips ahead is refused, and a node that cannot advance by pages repairs from a full snapshot.

Replication carries the control state an operator spreads across nodes: capability authority, revocations, budget mutations, tool and child receipts, and capability lineage. A background loop visits configured peers one at a time. Client writes go to the locally selected leader; these pulls repair every other replica.

A peer visit pulls two lanes, budget events first

Within one visit, budget events take the first claim on a shared allowance and revocations run afterward on an allowance of their own, so a spending backlog cannot starve revocation.

Replicated logChoose a view.
replicated on each nodeBbudget pool BnAnode AsourcenBnode BreceivernCnode Cpeerorigin A · budget mutation events55charge USD 2.0056settle USD 1.4057charge USD 0.6058charge USD 1.2059reduce USD 0.2060charge USD 0.80head 60cursor 56 → 59page 57 to 59 · appliedBpool BnAnode AnBnode BnCnode Corigin A · budget mutation events55charge56settle57charge58charge59reduce60charge6056→59head, cursorpage 57 to 59 · applied

A receiver accepts a page that starts at its cursor’s successor

Node B pulls origin A’s budget events from cursor 56. The page carries 57, 58, and 59 with no gap, so B applies the rows and advances its cursor to 59; event 60 arrives on the next page. Each visit charges its pages against an allowance of 64 pages and 200,000 records, and revocations get a fresh allowance after the shared lane. Replication & Convergence.

One origin’s log on three trust-control nodes: each row is an entry with its index, and a filled dot is a row that node holds. Indices, amounts, and capability ids are illustrative; tool receipts, child receipts, and lineage, whose sequences may contain gaps, are left out.
  1. Status. Skip a peer marked partitioned; otherwise fetch its status, mark it reachable, and read its revocation contract.
  2. Clamp. Lower or remove any recorded budget acknowledgment the fresh status no longer supports.
  3. Repair. Apply a full cluster snapshot when one is required, then merge the peer’s authority record.
  4. Shared lane. Pull budget events, then tool receipts, child receipts, and lineage while the allowance of 64 pages and 200,000 records lasts.
  5. Revocation lane. Pull revocations on a fresh allowance, unless the visit demoted the peer.
  6. Finalize. Record the peer’s advertised acknowledgment heads and wake waiting writers, if the peer is still healthy with no snapshot pending.

An exhausted shared allowance stops the rest of that lane and leaves the revocation allowance whole. Revocations also travel inside full snapshots, so a peer repaired by snapshot receives them either way.

Read more in Pull outcomes and Work allowances.

Each stream accepts only the ordering it can promise

The budget mutation stream and the revocation log number their records densely, so a page must run consecutively from the cursor’s successor. At cursor 10, a page holding only 110 and 111 is refused even though its maximum advances, because accepting it would strand 11 through 109.

Tool receipts, child receipts, and lineage allocate sequences that retention and allocation can leave with holes. Their pages must advance past the cursor without repeating a sequence, and a gap is legal. Demanding every intervening sequence there would refuse honest pages.

A page that breaks its stream’s rule is a protocol violation. The receiver marks the peer unhealthy in its own view, drops it from the eligible and witness sets, and requires a snapshot before it witnesses again; the cursor stays where it was, and pages already imported in the visit stay imported.

Read more in Page validation by stream.

A replica that cannot advance by pages repairs from a snapshot

Three conditions send a peer to snapshot repair:

  • An unpageable budget window. A budget page holds at most 400 combined mutation events, usage projections, and abandoned slots, so a requested window of 200 events can overflow it. The puller halves its event window, refetches the same cursor, and restores the normal window after a usable page; a single event that still overflows requires a snapshot.
  • Accumulated deltas. Eight delta records since the peer’s last snapshot schedule one, and a forced-recovery flag requests the same path.
  • A moved revocation stream. A changed stream identity, a head below the cursor, or an empty page short of the advertised head.

Applying a snapshot resets each cursor to the snapshot’s validated head, imports the state, and clears the cached acknowledgments in the same update that clears the pending flag. A snapshot response is bound by the same 64 MiB body limit as any peer response, and repair needs a source that holds the missing data. Cluster Recovery drills the procedure.

A budget write counts only acknowledgments for its own origin

Budget admissions and the monetary transitions on the write path wait for a replication witness; invocation increments and denial records use narrower leader-visible behavior. The writer counts itself, and a peer counts when three conditions hold:

  1. Reachable. The observing node considers the peer healthy.
  2. Available. The peer is not partitioned and has no snapshot pending.
  3. Same origin. Its acknowledged head for the write’s origin_id is at least the write’s event_seq. A higher head from another origin counts for nothing.

A head is the last event before the first gap, derived from a contiguous import frontier, so one unresolved hole can hold back acknowledgments for several origins. Quorum is peers.div_ceil(2) + 1, where the configured peer count excludes the writer. A witnessed write reports quorumCommitted; the clustered budget authority still reports guaranteeLevel: advisory_posthoc, and the partition exposure example shows what that level allows.

Read more in Acknowledgment ordering.

Replication assumes honest peers inside one operator’s domain

Peer standing belongs to the node that observes it, and no node publishes a cluster-wide verdict. The pulls build no globally ordered consensus log, and a forwarded request gains no exactly-once property from them.

The model tolerates peers that crash or fall behind, offers no Byzantine tolerance, and trusts what a configured peer serves: an abandoned budget slot is the source’s claim, so a faulty source that labels a live event abandoned makes the receiver skip it, and a missing receipt sequence can be an omission or an honest gap. Cross-operator trust uses Federation, where each kernel admits at its own door.

Streams, outcomes, and limits

Replication streams

StreamRouteCursorLane
Budget events, usage, abandoned slots
/v1/internal/budgets/delta
event_seq, dense
Shared, first
Tool receipts
/v1/internal/receipts/tools/delta
tool_seq, gaps allowed
Shared, second
Child receipts
/v1/internal/receipts/children/delta
child_seq, gaps allowed
Shared, third
Capability lineage
/v1/internal/lineage/delta
lineage_seq (rowid), gaps allowed
Shared, fourth
Revocations
/v1/internal/revocations/delta
Version 4, stream identity, dense sequence
Fresh allowance
Capability authority
/v1/internal/authority/snapshot
None: full merge
Before both lanes

Budget events must arrive in ascending order, and their union with valid abandoned slots must be contiguous from the cursor. The authority merge runs when the receiving node has an authority database and must succeed before either lane runs: if the receiver has --authority-db and the peer does not, the peer’s snapshot route returns 409 and the visit ends with no further pulls or acknowledgment increases. Acknowledgments already recorded stay as they were, apart from status clamping and any snapshot already applied. On a receiver without an authority database, sync_peer_authority does no work; Authority & Rotation separates replicated public state from signing custody.

Pull outcomes

OutcomeRemaining work and peer standingAcknowledgment finalization
RoundLimit
Stop when the page, record, or deadline allowance is exhausted. The peer stays healthy and imported cursor progress stays. Revocations still get their fresh allowance.
Proceeds if the peer is healthy with no snapshot pending.
PullError::Transient
Record a sync error without demotion. In the shared lane, skip its remaining pullers; revocations still run. Earlier pages may already have advanced cursors.
Proceeds. The success update can clear last_error in the same visit.
PullError::Protocol
Mark the peer unhealthy and set force_snapshot. Skip remaining shared work and the revocation lane if it has not run. The peer leaves the local eligible and witness sets.
Skipped. A later successful status contact and snapshot repair are required.
PullError::ForceSnapshot
Keep the peer healthy and require a snapshot. Stop the affected shared lane; revocations can still run. Earlier cursor progress stays.
Skipped while repair is pending, even though the peer is reachable.

A failed or rejected page never moves the cursor past missing data, and it never rolls back pages already imported in the visit. Read last_error as current peer state: update_peer_success clears it for an unpartitioned peer.

Work allowances

LimitValueBounds
Charged pages
64 per lane per visit
Pages charged by allowance accounting; rejected oversized pages are free
Charged records
200,000 per lane per visit
Combined record counts charged to that lane
Deadline
20 seconds from each allowance’s creation
Checked before each blocking fetch and when charging its response
Peer response body
64 MiB before JSON decode
Every peer response, full snapshots included

The deadline stops further fetches; an in-flight request finishes, so the last one can end after the 20 seconds. The authority merge and full cluster snapshot sit outside both allowances, and an oversized-page retry sends an HTTP request of its own and uses no charged-page slot. The server caps an abandoned-slot list at 401 entries, so truncation trips the receiver’s oversized-page check before its contiguity check.

Page validation by stream

  • Budget mutation stream. Received events ascend, and events plus validated abandoned slots form a gap-free sequence from the cursor’s successor. The receiver checks that reported abandoned slots are ordered, in range, and clear of live events on the page, then accepts the source’s claim that those positions are empty.
  • Revocation log. Version and stream identity match, every record carries a sequence, and the page runs consecutively from the cursor’s successor.
  • Tool receipts, child receipts, lineage. The sorted page advances beyond the cursor and repeats no sequence.
  • Tuple revocation contract. Each (revoked_at, capability_id) tuple advances and the sequence fields are absent. Contiguity cannot detect an omission here, and an empty page resets the cursor so the next pass replays from the beginning.

Budget pages also refuse usage records without mutation events, abandoned slots without anchoring events, and invalid abandoned sequences. The records-without-events check runs before the oversized-page check, so that shape is a protocol violation even on an oversized page.

Revocation stream contract

The status response selects the revocation contract once per visit. A peer serving a current revocation store advertises cursor version 4 and a stream identity, even with no records; advertising neither selects the tuple contract, and advertising half the pair is a protocol error. A peer read under the tuple contract inherits no sequence cursor: version, identity, and sequence are cleared before the pass begins.

For a sequenced stream, a cached cursor with a different identity, a missing sequence, the wrong version, or a sequence above the advertised head requires snapshot repair. Those conditions fit a replaced or rolled-back source, and the receiver resynchronizes without diagnosing which. A snapshot must name the same stream and a head at least as high as the status advertised; a changed or regressed contract is refused before the body applies. Revocation Propagation specifies page and snapshot validation.

Source definition of the peer contract
crates/platform/chio-control-plane/src/trust_control/cluster/pull_budget.rs117-150rust
pub(crate) fn revocation_peer_contract(
    replication: &ClusterReplicationHeadsView,
) -> Result<RevocationPeerContract, PeerProtocolError> {
    match (
        replication.revocation_cursor_version,
        replication.revocation_stream_id.as_deref(),
    ) {
        (None, None) => Ok(RevocationPeerContract::Legacy),
        (Some(version), Some(stream_id)) => {
            ensure_revocation_cursor_version(Some(version))?;
            if stream_id.is_empty() {
                return Err(PeerProtocolError::MissingRevocationStreamIdentity);
            }
            let head_seq = match replication.revocation_cursor.as_ref() {
                None => 0,
                Some(cursor)
                    if cursor.cursor_version == Some(version)
                        && cursor.stream_id.as_deref() == Some(stream_id)
                        && cursor.seq.is_some_and(|seq| seq > 0) =>
                {
                    cursor.seq.unwrap_or(0)
                }
                Some(_) => {
                    return Err(PeerProtocolError::IncompleteRevocationStreamContract);
                }
            };
            Ok(RevocationPeerContract::Current {
                stream_id: stream_id.to_string(),
                head_seq,
            })
        }
        _ => Err(PeerProtocolError::IncompleteRevocationStreamContract),
    }
}

Acknowledgment ordering

After the status and contract checks, clamp_down_peer_budget_acks sets each recorded head to min(recorded, advertised) and removes origins the peer no longer advertises, so a stale high acknowledgment stops counting before new pull work begins. Increases wait for finalize_peer_sync_round, which checks for demotion or a pending snapshot before it records the advertised heads, updates peer success, and wakes waiting writers. That gate reads peer standing: a transient pull error preserves eligibility and still reaches it.

Waiting writes subscribe to shared progress notifications and start no sync loop of their own. The wait grows with the configured peer count, is capped at 300 seconds, and fails early when the node detects it has lost local quorum. The timeout is the caller’s wait policy.

Shutdown between peers

The sync loop checks for shutdown before it starts the next peer. A visit in progress completes its blocking work, so at most one visit remains: status, a snapshot, the authority merge, and both lanes. The budget-write timeout models a visit as three 15-second HTTP stages and two 20-second allowances; that 85-second figure is arithmetic for the timeout, since pull deadlines leave in-flight fetches running and local import takes time of its own.

Source tests

  • Dense sequence validation. require_contiguous_page_rejects_cursor_jump_and_interior_gap, sequence_revocation_page_must_be_cursor_anchored_and_dense.
  • Allowance exhaustion. charge_page_reports_round_limits_not_protocol_errors, is_exhausted_flags_spent_budget_before_the_next_fetch.
  • Acknowledgment ordering. regressed_ack_head_is_cleared_before_validation_not_witnessed_at_old_high, with the gate in finalize_peer_sync_round.
  • Same-origin witnessing. prop_witness_never_overclaims_durability, witness_requires_same_origin_ack.
  • Oversized-page repair. oversized_budget_page_retries_smaller_before_snapshotting, unpageable_single_event_budget_page_still_force_snapshots.

The visit order lives in crates/platform/chio-control-plane/src/trust_control/cluster/deltas.rs.

Next: drill a peer back to convergence

Replication & Convergence · Chio Docs