Exact consumer identity
A relying contract wants Alice's eligible checkpoint in one chosen registry. A valid checkpoint elsewhere must not authorize its payment. The reviewed model makes that choice explicit before it examines a candidate holding.
Identity and authority
The model binds to the public Singular revision
d69ab005c3b710aeebf033d3c5cd08c56feb5643.
It imports the actual
application-held identity declarations
from that same pinned tree. This nested model coexists with Singular's reduced
economic model; importing it does not establish a production implementation.
flowchart TD
S["Seed and release<br/>state policy"] -->|derive State asset| R["Registry asset"]
R -->|add encoded AID key| A["Active preimage"]
A -->|selected codec and hash| H["Representative asset"]
stateAsset returns the release's state policy and the hash of the encoded
State preimage containing the creation seed. activeAsset uses that full
registry asset and the encoded AID key in its Active preimage. The active
asset's policy is the registry's policy. These are the imported functions,
not a second naming algorithm copied into KERI.
The consumer definitions live in CardanoKeri.ConsumerIdentity; their exact
source identities and theorem statements are recorded in the repository's
consumer bindings and theorem-story ledger. The preserved custody baseline is
KERI revision ff78b1c0d87a29d2f856cb32f1261591f84831db.
All existing lifecycle eligibility, custody and economic contracts remain in
force. The new holding predicate adds identity and observation requirements.
| Choice | Alternative excluded | Reason |
|---|---|---|
| Independently fixed context | Let the candidate select its registry or release | Self-consistency does not authenticate the intended identity |
| Imported complete State and Active identities | Treat the reduced model's raw key or registry label as the asset name | The two representations have different purposes |
| Explicit finite codec and collision premises | Assume a finite cryptographic hash is globally injective | Separation claims need stated, bounded assumptions |
| Actual economic fold plus identity fold | Replace economic routing with the identity-only fold | The identity fold does not route request money |
| Preserved state eligibility inside the holding check | Treat any retained token or active leaf as consumable | Pending holdings must refuse |
The bounded registration bridge
The connected model begins at actual genesis and executes both folds. It never starts by declaring that a final holding is valid.
flowchart TD
G["Imported genesis"] -->|then| B["Actual booking"]
B -->|then| F["Economic fold"]
F -->|then| I["Identity fold"]
I -->|compare both| J["Identity, datum<br/>and money agreement"]
J -->|receipt and origin| C["Checkpoint registration"]
C -->|initial holding| T["Treasury eligibility"]
createRegistry checks configuration correspondence and actual imported
genesis. registerJoint executes booking, the preserved economic fold, the
full-identity fold and Checkpoint registration. It checks the selected
outputs' key, reference, credential, datum, quantity, deposit and trie
mapping, then constructs the stored registration origin.
moneyAgreementB compares complete spent references, input reference/value
pairs, ordered output amounts, fee, donation and validity interval between
the two fold observations. The bridge also requires the registry's deposit to be
the checkpoint's deposit plus freeze bond, and the declared carrier to be exactly
the lovelace the registered checkpoint holds, so the initial pool is what the
carrier holds above the minimum ADA, the deposit and the freeze bond (see the
checkpoint output's value).
The proofs establish inversion, completeness under the explicit component premises, delivered-origin correspondence and preservation of the old eligibility predicate. The connected fixture continues through all four rotation branches, freeze and recovery, poison and clearing, concrete conviction, booking and matching completion. It executes a full-identity terminal fold against the same state reference and money observations. A valid foreign request cannot complete the stored pending request; a mixed fold that consumes that request can.
What the interpretation must establish
The model takes a codec, hash, AID-to-key function and release-indexed datum decoder as interpretation inputs. Its finite separation theorems expose the codec and collision premises in their types. Distinct AID labels do not imply distinct encoded keys without a corresponding mapping premise.
The executable corpus publishes its finite preimage tables and checks every pair in the declared domain. Constant codecs, constant hashes and a constant reduced-registry map fail their respective premises. These controls use a fixture codec and hash; the concrete bytes follow in the next section.
| Boundary still owed | Effect on a real consumer |
|---|---|
| Hash collision freedom | Rely on BLAKE2b-256 having no collision over the identity domain; this is assumed, not proved |
| Published scripts | Bind actual policy application and script hashes to an authenticated release manifest |
| Network and release resolution | Obtain them independently; missing or conflicting evidence refuses |
| Physical value | Read the premium pool from the output's value and require at least the minimum ADA, the deposit and the freeze bond the datum says are held (see the checkpoint output's value); the validator enforcing it on chain is the datum version 2 work (#339) |
| Current reference and holding movement | Verify current unspentness and observation fidelity; registration Origin is immutable metadata |
| Reduced registry identifier | Establish the explicit finite faithful mapping when separation relies on that representation |
| Discovery and settlement | Do not infer absence or terminality from an empty lookup or a provider failure |
realizesState checks the credential, representative quantity, decoded AID and
datum facts, and the output's lovelace: the decoded state is compared with the
intended state, both with the pool set to zero, because the pool is an amount
held in the value and not a fact in the datum; the lovelace must be exactly
what the intended state holds. It has no current-reference or unspentness check. Local fixture
observations are explicitly selected model interpretations, not a production
observer. No checkpoint expiry or renewal behavior is supplied by the model.
The concrete bytes
The model declares one concrete interpretation in
CardanoKeri.ConsumerIdentity.Bytes. The registry key of an AID is the
AID's qualified CESR text bytes. The codec is the Plutus Data serialisation of
the Preimage type in Singular's
singular_state/preimage module
at the pinned revision. The hash is BLAKE2b-256, as in that module. The datum
decoder is declared in the checkpoint datum.
The upstream model carries byte strings such as transaction ids, policies, asset names and keys as natural numbers. Each such field carries its bytes through a bijective base-256 numeral, so empty strings and leading zero bytes keep their identity. Output indices and deposits are plain integers.
| Premise of the consumer statements | Status for the concrete bytes |
|---|---|
| Finite encoding over the identity domain | Proved for every pair of contexts: reading the serialisation back recovers the seed, the registry and the key |
| Finite collision freedom over the identity domain | Reduced to BLAKE2b-256 having no collision over the same four preimages, which stays an explicit assumption |
| Distinct AIDs have distinct keys | Proved: the key is the AID's CESR text, so equal keys mean equal AIDs |
A 256-bit digest of inputs of any length cannot be injective, so no proof of collision freedom exists; the statements name the assumption in their types. With it, the theorems for different AIDs, different seeds and a rival seed's registry hold for the concrete bytes.
The vector table lists, for each state policy, seed and AID, the key, both serialised preimages, both names and the representative asset a genuine holding carries. The Lean emits the table and checks the committed copy. Each row runs the consumer predicate: the committed asset is accepted, and a changed state policy, transaction id, output index, AID or asset name is refused. Changing any committed field of the table fails the check. The same code reproduces every preimage and name of Singular's own table at the pinned revision, across its five roles.
These checks cover the listed inputs only. The registration approval role splices an already serialised holding address and is outside the consumer statements. The Aiken consumer predicate reproduces the table, as described in the concrete identity bytes in Aiken.
The checkpoint datum
A contract author holds a candidate checkpoint output with an inline datum.
The consumer must read that datum exactly as the checkpoint writes it: which
AID it belongs to, whether it is live or awaiting termination, whether it is
poisoned, whether its bond is frozen and when it was born. A datum that does
not have that shape is refused. The model declares this datum, version 2, in
CardanoKeri.CheckpointDatum, together with the decoder the consumer runs.
flowchart LR
D["CheckpointDatum<br/>V2 = constructor 1"] -->|form 0| L["Live: aid, sn, epoch,<br/>poisoned, frozen, born_at,<br/>refund_to, topped_up, keys"]
D -->|form 1| P["TerminationPending:<br/>aid, sn, epoch"]
L -->|decoder| S["present state,<br/>pool set to zero"]
P -->|decoder| T["terminationPending state"]
O["version 1 or any<br/>other shape"] -->|decoder| R["refused"]
S -->|consumer| V{"unpoisoned, unfrozen,<br/>born_at + W ≤ now?"}
V -->|yes| A["accepted"]
V -->|no| R
T -->|consumer| R
Version 1, constructor 0, is the layout the checkpoint validator writes today. It carries keys, thresholds, witnesses and counters, but no poison, no freeze, no birth slot and no pending-termination form, so a poisoned checkpoint and a clean one would be byte-identical. The decoder refuses it. Version 2 is written by the model first; the on-chain validator adopts it later.
| Position | Field | Kind | Lean state | What the consumer does with it |
|---|---|---|---|---|
| Live 0, Pending 0 | aid |
byte string: the qualified CESR text | DecodedDatum.aid |
Requires it to equal the AID the relying contract fixed |
| Live 1, Pending 1 | sn |
non-negative integer | Live.sn, Hash.sn |
Nothing |
| Live 2, Pending 2 | epoch |
non-negative integer | Live.epoch, Hash.epoch |
Nothing |
| Live 3 | poisoned |
Boolean | Live.poisoned |
Refuses when true |
| Live 4 | frozen |
Boolean | Live.frozen |
Refuses when true: the bond is not held |
| Live 5 | born_at |
non-negative integer | Live.bornAt |
Refuses until born_at + W ≤ now |
| Live 6 | refund_to |
byte string | Live.refundTo |
Nothing |
| Live 7 | topped_up |
Boolean: this key state was already topped up; every rotation clears it | Live.toppedUp |
Nothing; a second top-up of the key state is refused by the checkpoint |
| Live 8 | keys |
key material; not in the pending form | none | Checks its shape only |
| none | the premium pool | not in the datum | Live.pool, decoded as zero |
Nothing; the amount is held in the value |
The key material is constructor 0 with six fields, in the version 1 types:
current keys (a list of byte strings), current threshold, next keys, next
threshold, witnesses (a list of byte strings) and toad (an integer). A
threshold is constructor 0 over an integer (unweighted) or constructor 1 over
a list of clauses, each a list of weights; a weight is constructor 0 over a
numerator and a denominator. A Boolean is Aiken's: false is constructor 0 and
true is constructor 1, both without fields.
| Decision | Chosen | Rejected | Why |
|---|---|---|---|
| Which datum the consumer reads | Version 2, declared in the Lean first | Decoding version 1 as written today | Version 1 carries none of the facts the consumer needs |
| How the AID is carried | Its qualified CESR text, the registry key of the concrete bytes | The 32 raw digest bytes | The key is the text; rebuilding it from a digest needs a derivation code the datum does not carry |
| Where the pool and the bonds live | In the output's value | In the datum | The datum carries facts, the value carries amounts |
| What the decoder validates | Every shape in the whole datum; the values of the fields it reads | The full well-formedness of the key material | Key widths, threshold ranges, witness duplicates and toad are the checkpoint validator's obligation when it writes the datum |
| What the pending-termination form carries | aid, sn and epoch: at most 72 bytes serialised for a 44-byte AID with sn and epoch below 2^64 (pendingSizeBound, pending_size_le_bound) |
The key material as well | The output holds the fixed minimum ADA Mc, and the ledger minimum grows with the datum while the key material is unbounded; no step reads the keys in that state, and the key state at conviction stays readable in the spent live output |
The serialisation is Plutus Data's: constructors 0 to 6 as tags 121 to 127, 7 to 127 as tags 1280 to 1400, any other index as tag 102 over the index and the fields; non-empty lists indefinite; maps definite; byte strings definite up to 64 bytes, otherwise in chunks of 64; integers plain below 2^64, otherwise tagged big numbers. A raw application datum is these bytes carried through the same bijective numeral as the identity bytes. The reader also accepts the other well-formed encodings of the same Data. One layout serves every release at this model revision; a new layout is a new version constructor.
What the Lean proves about it:
- Reading back: every Data, read from its serialisation with any rest, returns
with that rest (
parse_cbor), and every serialised element is a byte, so the raw carrier is faithful (decode_raw). - Written datums decode: for every AID, key material and state with an output,
the decoder returns that AID and the state's facts (
decodeData_datum,decode_written). - Nothing else decodes: an accepted Data is exactly a written datum
(
decodeData_complete). A wrong version, version 1 included, a wrong number of fields, an unknown form and a field of the wrong kind are each refused (wrong_version_refused,version_one_refused,version_arity_refused,unknown_form_refused,live_arity_refused,pending_arity_refused,live_fields_typed,pending_fields_typed). - The decoder is a function of the raw datum alone: one datum, one result,
under every release (
decode_deterministic,decode_release_independent). - The consumer: with this decoder in the concrete interpretation, a holding
carrying the datum written for any state, funded pools included, is accepted
exactly when that state is consumable (
written_holding_iff). A funded registration passes the registration bridge's datum check (funded_registration_agrees), and a version 1 datum is refused (version_one_holding_refused).
The twenty-six consumer statements are proved for every interpretation, so they
hold with this one. Their wording is unchanged; the three that compare a
decoded state with an intended one, through realizesState and the
registration bridge, compare the datum's facts and the output's lovelace, so
they apply to funded registrations too.
The vector table lean/checkpoint-datum-vectors.json lists seven written
datums with the decoder's result and the consumer's verdict on a genuine
holding (registered and funded, juvenile, rotated with weighted thresholds and
witnesses, poisoned, frozen, pending termination, numbers beyond 2^64), and
twenty-nine datums the decoder refuses, each a named change: version 1, an
unknown version, every wrong arity, every field of the wrong kind, a trailing
byte, a truncated datum. The Lean emits the table and checks the committed
copy against the decoder and the consumer predicate. Changing any committed
field fails the check, and every node of every accepted datum, replaced by a
node of another kind, is refused.
These checks cover the listed inputs; the theorems cover all inputs. The Aiken consumer decodes version 2 (the datum decoder in Aiken); the on-chain validator does not write version 2 yet. Reading the pool and the bonds from the value is modelled in the checkpoint output's value. This code is not for mainnet.
The checkpoint output's value
A contract author holds a candidate checkpoint output and has read its datum.
The datum says the checkpoint is live and its bond is not frozen, but it
carries no amounts. The author wants to know the bonds are really there: the
output's lovelace must be what that state holds, and a holding missing part of
a bond must be refused. The model states this amount in
CardanoKeri.CheckpointValue as State.heldValue.
stateDiagram-v2
[*] --> Live: register<br/>Mc + D + B + pool0
Live --> Live: rotate, poison,<br/>top-up
Live --> Frozen: freeze<br/>B to hunter
Frozen --> Live: deposit<br/>rotation, B in
Frozen --> Frozen: rotate,<br/>poison,<br/>top-up
Live --> Pending: convict<br/>D, B, pool out
Frozen --> Pending: convict<br/>D, pool out
Pending --> [*]: booking<br/>takes Mc
The diagram reads in words: a registration delivers the minimum ADA, the deposit, the freeze bond and the initial pool the registrant chooses. Every later step moves exactly the amounts its flow names. A freeze pays the freeze bond to the hunter, a rotation with a deposit brings it back, and a conviction pays the deposit to the convictor and the bond and pool to the refund address. The pending output keeps only the minimum ADA, which the termination booking consumes.
| State | Lovelace the output holds | Floor: below it the holding is refused |
|---|---|---|
| Live, not frozen | Mc + D + B + pool |
Mc + D + B |
| Live, frozen | Mc + D + pool |
Mc + D |
| Pending termination | Mc |
Mc |
| Absent or convicted | no output | none |
Here Mc is the registry's minimum ADA, D the deposit and B the freeze bond
of the checkpoint's parameters, and pool the premium pool. The datum's facts
and the lovelace together give back the whole state: the pool is the lovelace
above the floor (withValue_iff, realizes_determines_state).
- Every step keeps the amounts: what the input holds plus what the step takes
in equals what the output holds plus what it pays out
(
held_amounts_conserved,held_value_conserved); a registration holds the minimum ADA plus everything it takes in (registration_holds_carrier). - A conviction pays out everything the live checkpoint held except the minimum
ADA, and the pending output holds exactly the minimum ADA the termination
booking requires (
conviction_pays_out_every_amount,pending_holds_minimum_ada). - A holding below the floor realizes no state with those facts, whatever its
pool (
short_holding_refused,missing_bond_lovelace_refused). - The registry books a carrier of at least
Mc + D_registry, where the registry's deposit is the checkpoint's deposit plus freeze bond; one lovelace short is refused (short_carrier_registration_refused). A successful joint registration delivers exactlyMc + D + B + pool0(registerJoint_value).
| Decision | Chosen | Rejected | Why |
|---|---|---|---|
| The minimum ADA | Separate from the deposit, and the registry's Mc |
Part of the deposit; a checkpoint parameter of its own | A conviction pays the whole deposit out, yet the pending output exists until the termination booking, which requires Mc of it |
| What registration delivers | A carrier of Mc + D + B + pool0, the pool chosen per registration |
A fixed carrier; a pool or bond paid outside the carrier | The registry's fold delivers exactly the declared carrier to the output |
| Where the amounts live | In the output's value | In the datum | The datum carries facts; the value carries amounts |
The vector table lean/checkpoint-value-vectors.json takes the seven states of
the datum vectors and gives, for each, the lovelace its output holds, its floor
and what a holding with those datum facts reads as at lovelace values around
them: the state itself only at the lovelace it holds. It also books
registration carriers with initial pools of 0, 3 and five million, refuses one
carrier short by one lovelace, and records the connected registration fixture,
whose carrier of 17 is the minimum ADA 4, the deposit 8, the freeze bond 2 and
the pool 3. The Lean emits the table and checks the committed copy; changing
any committed field fails the check.
These checks cover the listed inputs; the theorems cover all inputs. The consumer predicate itself does not read the value: a relying contract that needs the bonds checks the lovelace against the floor, and the validator that enforces the amounts on chain is the datum version 2 work (#339). How an honest owner recovers the deposit, the freeze bond or the pool is not settled by the model.
The consumer predicate in Aiken
onchain/lib/cardano_keri/consumer.ak is consumableHoldingB clause for
clause, in the Lean order: authenticated network and complete release, the
release's application credential, a total quantity of one for the
representative asset, and a decoded datum for the intended AID in a consumable
state (present, unfrozen, unpoisoned, at least W old). The registry and
representative asset names follow registryAsset and representativeAsset.
The network, release, seed and AID are arguments fixed by the relying contract.
The hash, codec, AID-to-key map and datum decoder are four function arguments
(Aiken data types cannot hold functions, so the Lean's Interpretation record
becomes those four arguments); the candidate output selects none of them.
| Lean | Aiken |
|---|---|
registryAsset |
registry_asset |
representativeAsset |
representative_asset |
decoded |
decoded (an inline datum goes to the decoder; any other datum is None) |
authenticatedContextB |
authenticated_context |
Checkpoint.consumableStateB |
consumable_state |
consumableHoldingB |
consumable_holding |
Two executable layers check it. All 32 queries of
simulator/consumer-corpus.json are generated by scripts/consumer-vectors.mjs
into consumer_corpus_vectors.ak, one test per row carrying the row's own
expected result; the four rows whose interpretation tables are incomplete or
ambiguous require the predicate to fail, never to answer. Property tests
(consumer_tests.ak) run each clause beside its refusal over generated
variations and name the theorem they mirror (CI01 to CI16). The vectors are
regenerated and compared by node scripts/consumer-vectors.mjs --check.
The connected example
consumer_identity_example is a spending validator for a relying contract. It
is parameterised by the consumer context (network, release, seed, AID) and the
checkpoint parameters, and it spends only if some reference input is a
consumable holding for that context, on the concrete identity bytes and the
datum decoder of the Lean model. The end-to-end specification
(ConsumerE2ESpec) runs it on a local test chain: one transaction that
references the eligible holding settles, and transactions that reference a
rival registry's token, a rival seed's token, a holding of quantity two, a
holding with termination pending, and a holding carrying a version 1 datum are
rejected when the consumer script evaluates false. Each refusal carries no
other script and passes the ledger rules, so the rejection is the consumer's
own verdict. The holdings carry version 2 datums written field by field from
the Lean layout, the AID as its qualified CESR text.
The Lean writes no datum for an absent or a convicted state
(CheckpointDatum.datum is none for both), so a retired holding with a
version 2 datum does not exist; those two arms of consumable_state stay
covered by the Aiken predicate tests.
One label bounds what this proves.
- Seeded holding, not a registration journey. The holdings are minted by a test-only policy at a test application credential. The genuine registration journey belongs to #324.
Declared limits of the example. A script cannot observe the network or resolve
a release, so the example's environment is the context it is parameterised with
(the trusted integration's observation). now is the lower bound of the
transaction's validity interval, in the same unit as the datum's birth time and
the maturity parameter; a transaction with no finite lower bound is refused.
A datum the decoder does not accept makes the consumer refuse the holding; it
never makes the script fail. The pool and the bond amounts are held in the
output's value and are not read from it (the datum decodes with a pool of 0;
#339). The
acceptance item that binds the predicate to the actual Singular-backed output
stays open: the registration journey is still outstanding.
The concrete identity bytes in Aiken
onchain/lib/cardano_keri/consumer_identity_bytes.ak computes, in Aiken, the
names the Lean concrete interpretation computes. It is plugged into the
predicate through the existing function arguments; the predicate is unchanged.
Lean (CardanoKeri.ConsumerIdentity.Bytes) |
Aiken (consumer_identity_bytes) |
|---|---|
hash |
hash: BLAKE2b-256 |
code of a State preimage |
code: constructor 0 holding the seed's output reference, serialised as Plutus Data |
code of an Active preimage |
code: constructor 1 holding the registry (state policy, registry name) and the key, serialised as Plutus Data |
keyOf of an AID, through the carrier unpack |
key_of: the bytes the AID's number carries, which are its qualified CESR text |
aidOfCesr, pack |
aid_of_cesr, pack |
The relying contract fixes the state policy, the seed and the AID. The AID is
the Lean aidOfCesr of its CESR text: the bijective base-256 number of the
text's UTF-8 bytes. Only the State and Active preimages are computed, because
the predicate uses only those two roles.
Three executable layers check it.
- The Lean vectors.
consumer_identity_vectors.akis generated byscripts/consumer-identity-vectors.mjsfromlean/consumer-identity-vectors.json(just gen-consumer-identity-vectors).just check-consumer-identity-vectorsrejects any drift and runs in the Onchain CI job beside the corpus vectors check. For each of the eight vectors there is one test per claim: the state preimage, the registry name, the key (and the AID back from the key), the active preimage, the active name and the representative asset, each compared with the Lean bytes. - Properties (
consumer_identity_tests.ak): the byte carrier laws, every width of the output index, the change of the active name under each committed field (seed transaction id, seed output index, state policy, AID) over a grid of seventy-two contexts, and the predicate itself accepting the holding named by the concrete bytes and refusing every rival's. - Connected.
ConsumerE2ESpecrecomputes the names from the Lean layout, checks them against the vectors, and on the local test chain settles a spend that references the holding so named and refuses, in the consumer script, a spend that references a rival seed's holding.
What stays a declared test choice: the environment (the contract's own context stands for the trusted integration's observation) and the seeded holdings (the genuine registration journey belongs to #324). These checks cover the vectors and the generated variations only: byte strings of more than 64 bytes would be serialised in chunks, which no vector reaches (a CESR text is 44 bytes). This code is not for mainnet.
The datum decoder in Aiken
onchain/lib/cardano_keri/consumer_checkpoint_datum.ak is
CardanoKeri.CheckpointDatum.decodeData and decode over the inline datum as
Plutus data, with the Lean's field readers under the same names. The consumer
validator passes the concrete identity bytes and this decoder to the predicate.
Lean (CardanoKeri.CheckpointDatum) |
Aiken (consumer_checkpoint_datum) |
|---|---|
decodeData |
decode_data |
decode (ignores the release) |
decode, the predicate's decoder argument |
bool?, nat?, byteString? |
bool, nat, byte_string |
bytesList?, weight?, clause? |
bytes_list, weight, clause |
threshold?, keyMaterial? |
threshold, key_material |
pack of the aid and refund_to bytes |
consumer_identity_bytes.pack |
| the pool decoded as 0 | pool: 0 |
parse, ofBytes?, cbor (the raw bytes layer) |
none on chain; see below |
Raw bytes are the ledger's. The Lean reads the datum's serialised bytes.
On chain the ledger delivers the inline datum to the script already as plutus
data, so the Aiken decoder starts at the data and has no counterpart of
parse. This is a representation boundary, declared here: nothing in this
repository runs the ledger's parse, and the connected test above is the only
evidence that the data the ledger delivers is the data the Lean describes. In
the Aiken tests, aiken/cbor.deserialise of a vector's datum bytes stands in
for the ledger's parse; bytes that are not one whole datum never reach the
decoder, as the Lean's ofBytes? refuses them. The one vector with tagged big
numbers (large-numbers) cannot be read that way, because that function stops
the whole test run on a tagged big number. Its tests use data the generator
builds, and require that this data serialises, with the plutus serialiser, to
the vector's bytes, so the bytes of that row are checked against the Lean's
inside the suite.
The decoder never fails. Every unwrap of plutus data follows a test of its
kind, so a datum the decoder does not accept is None, never an abort: a
contract that scans reference inputs skips an unrelated output's datum instead
of failing on it.
Two executable layers check it, beside the connected run.
- The Lean vectors.
consumer_checkpoint_datum_vectors.akis generated byscripts/checkpoint-datum-vectors.mjsfromlean/checkpoint-datum-vectors.json(just gen-checkpoint-datum-vectors). Each of the thirty-six rows has a test that the decoder returns the row's result and a test that the consumer predicate, on the concrete identity bytes and this decoder, returns the row's verdict; each test is named after the row. The Lean CI job runsnode scripts/checkpoint-datum-vectors.mjs --checkas the last step ofscripts/check-consumer-design.shand fails on any drift;just check-checkpoint-datum-vectorsruns the same check locally. - Properties (
consumer_checkpoint_datum_tests.ak): the decoder never fails on arbitrary data; every value of each field it reads decodes to the Lean's result; and every other kind of node in each position, including each component of the key material, is refused.
These checks cover the vectors and the generated variations only. This code is not for mainnet.
Evidence and implementation acceptance
The source includes twenty-six proved consumer statements with bound Given/When/Then stories. The model driver emits the complete accepted and refused queries, connected certificate events and actual fold observations. The browser and command-line simulator use the same pure consumer function; their replay gates compare those observations with freshly generated Lean output. Mutation controls change executable decision sites and retain setup failures separately from detected behavioral faults.
This establishes a local model and simulator candidate. Independent acceptance, hosted checks and publication are separate evidence. Production completion under the consumer implementation ticket requires both real implementation properties and connected ledger-boundary tests, including the observation obligations above. Neither layer is supplied by proving the model or replaying its corpus. This code is not for mainnet.
Start with the consumer checklist, then play the treasury story.