Observer architecture
What ships, and the accepted design
Unless a paragraph below is marked as preprod or main today, it
describes the accepted design (D-036 to D-040): active, parked holding
the hash, or convicted; no withdraw; the reap by the next keys; deposit
is the unfreeze; poison epoch-local and cleared by rotation. Play the
checkpoint simulation.
What ships today is the V1 checkpoint with role addresses.
The production transaction shape separates small state-machine checks from large KERI evidence checks. The checkpoint script stays thin; a separate reference script, called an observer, performs the expensive validation in the same transaction.
This split is required by Cardano's size limits, not by a trust boundary. On the protocol-11 development network used by the settled stories:
- the full transaction-size limit is 16,384 bytes; and
- an applied reference script has a 16,133-byte budget after its transaction wrapper.
Putting every operation and every KERI parser into one checkpoint program did not fit. The observer split keeps each deployed program under the limit while making both halves agree on the same transaction.
Thin checkpoint, heavy observers
The thin checkpoint owns the rules that protect the state UTxO:
- resolve the exact input being spent;
- require the expected ACTIVE or ARMED role;
- preserve or burn the quantity-one AID token as the operation requires;
- enforce the escrow and successor-output shape;
- require the correct observer to run; and
- bind the observer claim to this checkpoint policy and, for a spend, this exact input reference.
The observers own the heavy evidence predicates:
observer_lifecyclevalidates registration evidence;observer_advancevalidates ordinary rotations and ARMED responses; andobserver_enforcementvalidates Freeze evidence.
They are Plutus stake scripts delivered through reference-script UTxOs. A reference script is stored once on the ledger and referred to by later transactions, so its bytes do not have to be copied inline every time.
---
config:
htmlLabels: false
---
flowchart TD
TX["One Cardano transaction"] --> CK["Thin checkpoint<br/>token + value<br/>role + state shape"]
TX --> WD["Zero-lovelace withdrawal<br/>observer envelope"]
REF["Reference-script UTxO"] --> OBS["Heavy observer<br/>KERI evidence predicate"]
WD --> OBS
CK -->|"exact observer claim"| OBS
OBS -->|"checks the same tx and input/output"| CK
Neither half is optional. A correct observer envelope cannot bypass a bad checkpoint state transition, and a well-shaped checkpoint transition cannot bypass the KERI proof.
Why a zero-lovelace withdrawal runs code
Cardano executes a stake validator when a transaction withdraws rewards for that script credential. The transaction uses a withdrawal amount of zero, so no reward value moves, but the ledger still evaluates the validator.
The withdrawal redeemer is an ObserverEnvelope:
ObserverEnvelope {
claim: {
action: Register | Advance | Freeze | ResponseAdvance
checkpoint_policy: PolicyId
own_ref: None | Some(OutputReference)
}
payload: operation-specific KERI evidence
}
Registration has no checkpoint input yet, so own_ref is None. Advance,
Freeze, and response Advance bind own_ref to the exact checkpoint UTxO they
consume. Stable action tags keep the wire unambiguous.
The checkpoint verifies that:
- the configured observer credential appears in the withdrawals;
- its withdrawal amount is exactly zero;
- its redeemer decodes as the expected envelope;
- the action tag matches the checkpoint operation; and
- the policy and input reference match this transition.
The observer then reads that same transaction and validates the heavy payload against the named old state and unique successor.
Registration's premint fact token
This section describes the shipped V1 flow. The proposed M1 architecture moves inception admission into a repeatable attestation, leaving registry absence and checkpoint creation at the registration fold. The existing hash-proof token below is not that full inception attestation.
KERI AIDs use BLAKE3, while Plutus does not provide a native BLAKE3 builtin. The project has an Aiken implementation, but running it together with all of Register made the transaction needlessly expensive and difficult to fit.
Registration is therefore two transactions:
---
config:
sequence:
wrap: true
width: 180
---
sequenceDiagram
participant R as Registrant
participant H as BLAKE3<br/>hash-proof policy
participant C as Thin<br/>checkpoint policy
participant O as Registration<br/>observer
R->>H: Premint: inception bytes + claimed AID
H-->>R: Mint deterministic proof token
R->>C: Register with bare Register redeemer
R->>O: Zero withdrawal + evidence envelope
O->>O: Check proof-token input and burn,<br/>event projection, signatures, receipts
C->>C: Check checkpoint token, ACTIVE output,<br/>datum, and min + D_reg + B
C-->>R: Settled ACTIVE checkpoint
The premint token is a fact token: its deterministic asset name binds the inception bytes to the claimed AID. It carries no controller authority and does not choose the checkpoint keys. Register consumes an input containing the token and burns exactly that token while the lifecycle observer checks the same event bytes.
The checkpoint mint redeemer is deliberately the bare constructor:
Register
The large RegistrationEvidence no longer rides inside that mint redeemer.
It rides once in the observer envelope. This is the current wire shape from
PR #146.
Reference delivery
The live builder first publishes the required scripts in separate reference-script outputs, then spends those references from the operation transaction. Registration needs the checkpoint, lifecycle observer, and BLAKE3 proof policy; later stories also publish the Advance and enforcement observers.
This matters because three inline validators already exceed the 16,384-byte transaction cap. Reference delivery keeps the operation transaction small while still executing the exact deployed programs. The builder also uses a two-pass budget calculation: evaluate once, derive declared execution units from observed use, rebuild, and submit with the aggregate limit checked.
Measured sizes and costs
These are applied-program sizes and observed execution units from the merged story pull requests, not estimates.
| Program or operation | Measurement | Source |
|---|---|---|
| Thin checkpoint after Freeze wiring | 9,155 bytes (about 9.2 KB) | PR #150 |
| Enforcement observer | 13,548 bytes | PR #150 |
| Advance observer | 16,130 of 16,133 bytes | PR #150 |
| Register | about 1.9 million memory units, about 11% of the 16.5 million limit | PR #146 |
| Two-key Advance, thin checkpoint | 351,567 memory / 120,489,432 CPU | PR #148 |
| Two-key Advance, observer | 4,110,025 memory / 2,008,935,582 CPU | PR #148 |
| Freeze, thin checkpoint | 428,033 memory / 150,487,344 CPU | PR #150 |
| Freeze, enforcement observer | 3,287,294 memory / 1,689,999,750 CPU | PR #150 |
| ARMED response, thin checkpoint | 419,014 memory / 144,731,099 CPU | PR #150 |
| ARMED response, Advance observer | 3,707,244 memory / 1,845,793,955 CPU | PR #150 |
The Advance observer has only 3 bytes of measured applied-script headroom. Issue #149 is a required size-reduction chore before the real seven-key rotation story. A passing current transaction does not make that margin maintainable.
Security properties of the split
The split preserves four important properties:
- Same-transaction coupling. The observer validates the exact input and successor protected by the thin checkpoint.
- No evidence duplication. Large KERI evidence appears in one envelope, not in both mint and withdrawal redeemers.
- No trusted observer. “Observer” names a validator role, not an off-chain person or oracle. Its code is a reference script and its result is enforced by the ledger.
- Fail-closed extension. A new lifecycle verb is unavailable until the thin checkpoint admits its action tag and the matching observer path is deployed. Unknown constructors, roles, tags, or datum shapes reject.