live · mainnetoc · docs
specs · api · guides
docs / state machine

State machine

Every pledge is in exactly one of these states at any given time:

StateMeaning
pendingSworn, before resolves_at.
resolvablePast resolves_at, within expires_at, no outcome published yet.
keptOutcome published, outcome = kept, dispute window passed.
brokenOutcome published, outcome = broken, dispute window passed.
disputedContradictory outcome envelopes within dispute window — pending dispute.
expired_unresolvedPast expires_at with no consistent outcome.

State transitions are pure functions of (pledge envelope, outcome envelope or absence, current time, relevant public state). Two verifiers with the same inputs produce the same classification. This is a conformance requirement — disagreement on state is a protocol bug.

Transitions

sworn ──────► pending
                │
                │ now ≥ resolves_at
                ▼
            resolvable ───── outcome envelope published & verified ──► kept | broken
                │                                                       (after dispute window)
                │ now ≥ expires_at && no outcome
                ▼
        expired_unresolved

Plus the abandonment branch:

pending  ── abandonment envelope published ──►  broken
                                                (no separate "abandoned" state)

Plus the dispute branch:

resolvable ── outcome envelope #1 ──► (within dispute window, pending)
                                       │
                                       │ outcome envelope #2 contradicts
                                       ▼
                                    disputed ── dispute mechanism resolves ──► kept | broken
                                       │
                                       │ no resolution by expires_at
                                       ▼
                              expired_unresolved

What "verified" means in resolvable → kept|broken

For deterministic mechanisms (chain_state, nostr_event_exists, stamp_published, http_get_hash, dns_record): the verifier re-computes the outcome envelope from public state. Byte-equal to the published one ⇒ verified.

For counterparty_signs and vote_resolves: BIP-322 signature checks under the resolver address; for vote_resolves, the named poll must have a finalized tally meeting the pledge's threshold.

Bond verification at every transition

A pledge with a bond reference whose underlying OC Attest UTXO is spent mid-pledge is not auto-reclassified by the protocol — that's a verifier-policy decision (see SPEC §8). The protocol surfaces E_BOND_SPENT as an error code; an integrator choosing strict policy treats a spent bond as an automatic break, while a lenient one treats it as informational. Refusing automatic reclassification preserves the no-slashing rule — the protocol never does anything to a swearer's status; it only exposes the facts.

Dispute window

For deterministic outcomes the dispute window is short (default 24 hours) because contradiction is unlikely — if two observers reach different outcomes for the same public state, at least one is buggy. For counterparty_signs it's longer (default 7 days) because contradiction is the expected disagreement mode and the pledge's dispute.mechanism needs time to resolve.

The dispute window length is a per-pledge field, not a global default. Swearers choose at swearing time.

The expired_unresolved class is intentional

A pledge with no outcome by expires_at is not automatically broken. It's a third outcome class, queryable separately. The reason: a pledge with counterparty_signs resolution where the counterparty just never shows up is informational about the counterparty's behavior, not the swearer's. Conflating it with broken would let lazy counterparties break pledges by silence.

Verifiers building gates can compose any policy they want — count expired_unresolved as broken for one address class, ignore it for another. The protocol surfaces the raw fact.