Repository navigation
ciris-persist v56.0.0
[56.0.0] - 2026-10-14
Security — rustls 0.23.46 (RUSTSEC-2026-0346, RUSTSEC-2026-0347)
- Lockfile only: rustls 0.23.45 → 0.23.46. Both advisories were published
2026-10-14 and stopped this release's push at the pre-pushcargo deny
leg. Neither reaches persist's surface (no customClientSessionStore
Debuglogging; ECH is not enabled), so this is the dependency floor
moving, not a behaviour change.
Fixed — tag CI: a restored cache never carries a build-script unit with missing outputs
No library code changed.
- The failure: the v55.0.0 tag CI (run 38380545287) failed twice, the
same way each time, in one job:maturin abi3-py311 wheel (windows-x86_64), withcouldn't read target\release\build\libsqlite3-sys-…\out/bindgen.rs.- rust-cache found nothing, so the CIRISCache restore that won was the
ciris-substrate-v1-windows-x64-release-rustc1.97.0-phead-enone-v20.1.0
blob an earlier tag's wheel job saved. - That save prunes target/ to its budget by deleting the OLDEST-mtime FILES
(cc_prune_lru). It deleted a build-script run's output (out/bindgen.rs)
and kept the unit's fingerprint. Cargo then trusted the run and
include!d a file that was not there. - Main pushes do not run the wheel jobs, so nothing re-saved that blob, and
every later tag would have failed the same way.
- rust-cache found nothing, so the CIRISCache restore that won was the
- The fix, in persist's workflow (no registry deletion, no CIRISCache
change):scripts/ci_cache_units.py.- Save side: before each CIRISCache save (every
CIRISCache/save@v1.1call site, the wheel save included),seal
writes each build-script unit'sout.manifest(every file in the unit,
sorted) and touches itsout/files to now, so a file-by-file prune
drops them last. This step is continue-on-error, because it is an
optimisation. - Restore side: in
.github/actions/ciriscache, after the restore,
repairremoves a build-script unit whose manifest is missing, or names
an absent file, as a whole unit: itsbuild/<pkg>-<hash>/and its
.fingerprint/<pkg>-<hash>/. Cargo then reruns that build script. The
step is NOT continue-on-error, so a target/ that cannot be checked is not
built on. - A unit with no manifest (every blob saved before v55.0.1) is judged
by a bound, not removed outright.- Why: removing all of them cost the v55.0.1 PR run's
secretsleg 272
units and 15 compile minutes, and it timed out. - The prune is a file LRU, so a pruned file is no newer than the oldest
surviving file (cutoff). A build-script run writesout/during the
run andoutputat its end, so a pruned sibling implies
output <= cutoff + R_MAX, whereR_MAX= 2 h bounds a script's
runtime. - A manifest-less run unit whose
outputis newer than that is kept.
One at or below it is removed with its fingerprint, as is a run unit
with nooutput. A script-compile unit is judged by its newest file. - A restore that clobbered every mtime makes everything fall within the
bound, so everything manifest-less is removed, which is the safe
direction. - The repair prints three counts: removed (manifest), removed
(boundary), and kept (unverified).
- Why: removing all of them cost the v55.0.1 PR run's
- Save side: before each CIRISCache save (every
- The linux test legs' step timeout is now 75 minutes (was 50). The
secretsleg runs about 46 minutes warm, and a cold or repaired cache adds
up to 25. A hung test is still caught by name by.config/nextest.toml. - Witness:
scripts/ci_cache_units.py --self-test, run in the hygiene
job. It builds a fixture target/ with a consistent unit, a sealed
build-script unit, a unit pruned of one out file, and a unit saved before
manifests existed. The repair removes the last two with their
fingerprints and keeps the first two, and the seal touches out files to
now. Turning the repair off, or keeping the fingerprint, fails it.- The bound's own legs, which use fixed offsets:
- a manifest-less run unit far above the cutoff is kept, and one within
R_MAXof it is removed; - a run unit with no
outputis removed; - a script unit far from the cutoff is kept, and one near it is removed;
- clobbered mtimes remove everything.
- a manifest-less run unit far above the cutoff is kept, and one within
- Setting
R_MAX = 0, or inverting the boundary comparison, turns legs
red.
- The bound's own legs, which use fixed offsets:
Added — tooling: size tracking, semver-checks, coverage, machete, diff mutants, evidence-pointer gate (#934)
Six instruments beside #934's consolidation (#1076 slice T). Five are
reports: each exits 0 on a finding and every CI job running one is
continue-on-error, so none turns main red without the operator. One is
a gate, the evidence-pointer check, because a dead pointer is a false
evidence claim. docs/TOOLING.md says how to run each and which is which.
- Size tracking.
scripts/size_report.pywritesevidence/size.tsv:
code/comment/blank lines persrc/**/*.rsfile (stdlib only; comment- and
string-aware,#[cfg(test)]-blind), aBACKENDSrow
(src/store/{sqlite,postgres,memory}.rs: 137 779 lines at v55.0.0) and a
TOTALrow (649 639).checkprints the per-file delta against the
committed table or--against <ref>;--fail-if-backends-grew(exit 1) is
#934 step 1's acceptance criterion for the PR that moves a door. certify's
fast stage gainssizetest(the counter's fixture witnesses — a gate, like
the other tool self-tests, also in ci.yml's lint job) andsizereport(the
delta, always green, totals printed under the fast-gate table). - API breaks.
scripts/api_break_report.sh [<prev-tag>]runs
cargo semver-checks check-releaseon the shipped-shape feature set at
--release-type minor, so a major bump does not hide the breaks. v54.1.1 →
v55.0.0: 12 lints, 71 distinct instance lines. New workflowreports.yml
(non-gating):semveron every PR against the latestv*tag reachable
from the base (artefact + step summary), andsize(the PR's delta vs its
base). - Coverage.
scripts/coverage.sh <leg> [-- <nextest args>]:cargo llvm-cov nextestfor ONE certify feature set (derived, never spelled;
postgres sets throughpg_test_db.sh), lcov + summary + html under
target/coverage/<set>/.reports.ymlruns every leg weekly (legs derived
at run time) and on dispatch. - Unused dependencies.
cargo machete --with-metadatain ci-hygiene's new
non-gatingreportsjob. It names two dev-dependencies,hex-literaland
tracing-subscriber, that no.rsfile uses; not removed — the operator's
call. - Mutants on the diff.
scripts/mutants_diff.sh <base> [-- <cargo-mutants args>]:cargo mutants --in-diffover thesrc/lines changed since the
merge base, with a wall-clock budget;reports.ymlruns it only for a PR
labelledrun-mutants, or on dispatch. The hand mutation tables and the
nightly scoped matrix stay the gates. - Evidence pointers (a GATE).
scripts/evidence_pointers.py check:
everysrc/row ofevidence/cc_impl.tsvmust name a definition in its
cited file, with comments and literals blanked;Owner::membermust be a
member ofOwner; a mention, a string or a call site never resolves, and a
moved symbol fails until repointed. Stricter than the in-crate witness,
which let the v54 → v55 dead pointerblobs.rs#holds_bytes_attestation_envelope
live on a doc comment and a string literal. Clean on this tree (223 rows)
and at every tag v46.0.0..v55.0.0; that pointer sat in the Constitution's
register, andregister <claims.tsv>names it on CIRISConstitution
fca367d^. certify fast gateevidenceptr, ci.ymllintstep,
ci-hygieneevidence-pointersjob (daily);evidenceptrccreports the
Constitution register when a checkout is found.
Added — trace_format v2.8.0 admission (#1089, CIRISAgent#1255)
A v2.8.0 trace now admits. Before this, a v2.8.0 trace's run kind read as
absent, so trace_run_kind_mock never fired. The two new profile members
were also dropped on parse, so the re-canonicalised bytes did not match the
signature.
-
Where
run_kindis read. A v2.8.0 trace signsrun_kindinside
deployment_profile, the only signed block the agent supplies. Every
run-kind gate now reads the signed member from either place through
CompleteTrace::signed_run_kind: the top level (v2.7.x) or
deployment_profile.run_kind(v2.8.0).- The ingest mock gate on a canonical node reads it.
- The oversize mint copies it into
manifest.run_kind. steward_purge::trace_run_kindreads it, and so do
trace_attestation_run_kindand the federation-tier attestation gate.- The
trace:*inline-form admission check holds
deployment_profile.run_kindto the closed vocabulary. A top level that
disagrees with it is refused. - The canonical reconstruction is unchanged. It lifts only the top-level
member, because the profile member is signed where it sits.
-
Typed, signed profile members.
DeploymentProfilegains
run_kind: Option<String>andtrace_format_version: Option<String>. Each
is skipped when absent, so a v2.7.x profile serializes to the same bytes as
before. A v2.8.0 profile round-trips byte-identically through admission. -
The format gate keys on
deployment_profile.trace_format_version. This
is a signed member, distinct fromtrace_schema_version, which keeps
selecting canonicalization. The new module isschema::trace_format."2.8.0"validates the whole trace against the vendored
src/schema/trace_format_v2_8_0.json.- A format this node has not vendored is refused by name:
schema_unsupported_trace_format, with the format string asdetail.
The batch is refused whole and nothing is written. - A trace that does not conform is
schema_trace_format_violation, never
truncated. Thedetailis the format plus the first violation's instance
path and schema path, with no payload values. deployment_profile.run_kindwithout the format member is
schema_missing_field(deployment_profile.trace_format_version). At
2.7.0 the profile is not in the signed bytes, so an unsignedrun_kind
never reaches a gate.- A top-level
run_kindthat disagrees with the profile's is
schema_run_kind_conflict. A profilerun_kindoutside the vocabulary is
schema_unknown_run_kind. - All four are
schema::Errorvariants. They reach Python asValueError,
the type of the schema arm they join.
-
The run_kind vocabulary has six values (operator amendment on #1089).
schema::trace::RUN_KINDSis nowproduction | prod-personal | prod-managed | qa | battery | mock.prod-personalis a production run on
a personal install.prod-managedis a production run on a managed
deployment, set by CIRISManager. The three production values admit alike,
onlymockis refused (trace_run_kind_mock), andqaandbatteryare
admitted and labelled as before.productionstays valid, so v2.7.x traces
admit unchanged. -
What the vendored schema still refuses. Under the vendored v2.8.0
schema (8c51028c…), a v2.8.0 trace withdeployment_profile.run_kind
prod-personalorprod-managedis refused by schema validation, by name
(schema_trace_format_violationat/deployment_profile/run_kind), until
the re-cut schema is vendored. The follow-up is re-vendor + pin swap. -
The vendored schema. It was copied byte-exact from
CIRISAI/CIRISAgent@e7d9115180e8910999901f4805c31b617b34e8d5, the head of
CIRISAgent#1255, at
ciris_adapters/ciris_accord_metrics/schemas/trace_format_v2_8_0.json. Its
file name, SHA-256 and source commit are ONE constant,
schema::trace_format::TRACE_FORMAT_V2_8_0_PIN, whose bytes are
include_str!of the same file name. A re-vendor is that one constant plus
the file. The SHA-256 is:8c51028c8983741f0e14c89c32988df6aacc0981db5a173814e31d9f461c9f81 -
Witnesses I1030–I1037. I1030 to I1034 run on memory, sqlite and
postgres. I1035 to I1037 are pure tests.- I1030: a v2.8.0 mock run is refused from the profile, and a production
run admits and verifies. - I1031: the signed bytes equal the re-canonicalised bytes.
- I1032: a v2.7.x trace is unchanged.
- I1033: an unvendored format is refused by name, along with the
neighbouring refusals. - I1034: the oversize mint copies the profile's
run_kind, and the tier
crossing refuses the mock row. - I1035: the schema hash is pinned.
- I1036: the run_kind gate alone admits
prod-personalandprod-managed
exactly as it admitsproduction, at the parse gate and thetrace:*
shape gate. I670's store-level admission loop now covers both values too. - I1037: under the pre-amendment cut, a v2.8.0 trace carrying either new
value is refused by schema validation at the member. The leg flips when
the pin moves, and then both values must admit.
- I1030: a v2.8.0 mock run is refused from the profile, and a production
CIRISAgent adoption.
- Emit
deployment_profile.trace_format_version: "2.8.0"and
deployment_profile.run_kindtogether, withtrace_schema_version
"3.0.0"(JCS). - A v2.8.0 trace must validate against the pinned schema. Until persist
re-vendors the re-cut schema, emitprod-personalandprod-managedonly
in a v2.7.x trace's top-levelrun_kind, or hold them back. A v2.8.0 trace
carrying either one is refused at v56.0.0. Post the re-cut hash on #1089. - Do not also emit a top-level
run_kindthat differs from the profile's. - v2.8.0 component data carries no
attempt_index. At the"3.0.0"epoch,
persist stores an absent one as 0, so retries of one event share a dedup
key.
Added — transit KEX pair on the TransportDestination binding (#1077, CC 3.3.6.2 rc8 2a7ed97)
- The member. A signed
transport_destinationenvelope may carry
transit_kex_pubkeys {x25519_base64, ml_kem_768_base64}: a relay's static
hybrid KEX pair for the CC 5.3.3.5 E1 hop exchange. Both values are standard
base64. The x25519 key decodes to exactly 32 bytes and the ML-KEM-768 key to
exactly 1184. The member is optional, and a row without it is the v55 shape,
unchanged. Persist stores, admits and serves the pair. It does not implement
the key exchange, whose pattern, labels, KDF and KATs belong to
CIRISVerify#309. - Admission rules.
admission::verify_signed_transport_destination
refuses each violation asError::MalformedRowon plane
transport_destination, before the signature check. A pair object with one
member or neither istransit_kex_half_pair. A member of the wrong decoded
length istransit_kex_wrong_length. A transit x25519 equal to the row's
own Reticulum link key (transport_x25519_pubkey_base64) is
transit_kex_key_reuse. That is CC 3.3.6.2 key separation as far as the
row's own bytes reach. Signing keys are a different algorithm and are not
byte-comparable (CC 3.3.6.1). This in-row rule is the whole admission duty
(CC 3.3.6.2 rc8, CIRISPersist#1078). A pair that is not an object (includingnull), a
non-string member or bad base64 isenvelope_member_invalid. Unknown
members inside the pair object are tolerated. The three tokens join
federation::malformed_row::ALL. - The accessor.
SignedTransportDestination::transit_kex_pair(&self) -> Result<Option<TransitKexPair>, Error>reads the pair fromsigned_envelope
throughself_at_login::transit_kex_pair_from_envelope, the same reading
admission applies.TransitKexPair { x25519: [u8; 32], ml_kem_768: Box<[u8; 1184]> }carries the lengths in its types.rooted_transit_kex_pair()
returnsNonefor anAdvisorybinding, andbinding_provenance()returns
the binding's provenance. - The per-peer resolver (CIRISPersist#1078, CC 3.3.6.2 rc8).
self_at_login::transit_kex_pair_for(dir, occurrence_key_id) -> Result<Option<TransitKexPair>, Error>is the one place a consumer gets a
relay's hop key. It takes the pair only from a liveRootedsigned binding.
It returnsNonewhen the node has no such binding, when two live Rooted
bindings carry different pairs, or when the transit x25519 or ML-KEM-768
bytes equal any key on the node's current occurrence. Those keys are the
encryption_pubkeyspair, the occurrence transport binding's Reticulum
keys, the federation signing keys of the occurrence key and of each identity
it is bound under, and the transport keys of its live bindings.None
means the node cannot relay (CC 3.3.6.2). The resolver only reads, and it
judges the rows as held with no clock. This closes the cross-row reuse case
at use, deterministically. Admission cannot check it without depending on
arrival order, which CC 3.2 T8 vii forbids, so there is no deferred check
and no retraction. Generating a fresh pair, separate from every other
key, stays the producer's duty; the resolver is the consumer's backstop. - The content hash is identical across versions. The pair is NOT a field
of the typedTransportDestinationprojection. CIRISEdge's route plane and
persist's wire index hash the wholeSignedTransportDestination(the
projection plus the verbatim envelope). A v55 node and a v56 node therefore
produce byte-identical wrapper bytes and the same content hash for a
pair-bearing row, so a mixed fleet does not re-advertise and re-pull forever.
The pair is still hash-covered, because the envelope is. Witness I1010 pins
the hash of a fixed row as14da66a0…65e2and parses the same bytes through
a replica of the v55 types. - Rotation rides the existing
(epoch, asserted_at)supersession, with no
new field. Because the projection no longer captures everything that
differs, all three backends now answerUnchangedonly when the stored
envelope carries the same pair. A re-sign at the same(epoch, asserted_at)that changes or drops the pair isRefusedas a same-clock
fork. Before, it would have been kept as the old pair without a word. - No migration. The pair lives inside the stored
signed_envelopetext.
v55 nodes admit, store and re-serve pair-bearing envelopes unchanged, so this
needs no fleet floor. - CIRISEdge adoption. Build the pair into the envelope only: insert
transit_kex_pubkeys {x25519_base64, ml_kem_768_base64}into the envelope
JSON before signing, and leave the typed row as it is. Generate the pair
fresh. A transit x25519 equal to the row's link key is refused at admission,
and one equal to any other key of the node resolves toNoneat use. Publish the same transit pair on every transport kind and rotate them
together; a node whose bindings disagree cannot relay until they match. For per-peer resolution call
transit_kex_pair_for, not the rawtransit_kex_pair()accessor, which
reads one row's envelope and applies neither the Rooted rule nor the
occurrence comparison. Admission refusals to map aretransit_kex_half_pair,
transit_kex_wrong_length,transit_kex_key_reuse, and
envelope_member_invalidfor shape and base64, each as
Error::MalformedRowon planetransport_destination.content_hash_of
and its three call sites need no change. - Witnesses I1010–I1022. The memory, sqlite and postgres backends share
run_transit_kex_matrix(I1011–I1016) andrun_transit_kex_resolver_matrix
(I1017–I1022).
Changed — the attestation plane's write doors share one plan (#934 step 1)
Each door below is now a shared plan plus a per-backend executor. The plan
holds every admission decision and is pure over reads. The executor in each
backend does only SQL, row mapping and transactions. The inventory, the gate
sequences and the open items are in docs/ATTESTATION_WRITE_DOORS_934.md.
-
put_attestation_with_origin(underput_attestation,
put_attestation_authoredand the replicated apply's fresh-id store) is
decided once infederation::attestation_admit::plan_put_attestation. Its
59 gates were written three times. Thestore::parityscan now reports the
door asshared. -
attestation_upsert_local/attestation_insert_localare decided once
inplan_write_local_attestation. Their 17 gates were written three times,
and the v55 scan read both doors asgateless. -
attach_attestation_pqc_signatureis decided once in
plan_attach_attestation_pqc_signature: unknown id, already complete, and
the completed row's hash. Memory's executor gains the SQL backends'
"concurrently completed" guard. -
reseal_attestation_v31(door 4) is decided once in
plan_reseal_attestation: the shape gate (check_reseal_admission) and
the seal gate over the row as read. Each executor'sstore_planned_reseal
then re-reads the row INSIDE its write step (memory's lock, sqlite's
transaction, postgres'sSELECT … FOR UPDATE) and hands it to the shared
PlannedReseal::sealed_over, which re-asks the shape gate on that row. The
scan reports the door and its re-ask asshared; the memory
DeclaredDivergenceis gone. -
purge_attestation_v31andpurge_genesis_delegation_row_v31(doors 5
and 6) are decided once inpurge_attestation_planned(the exclusion gate,
migration::check_purge_admission, over the row as read) and
purge_genesis_delegation_row_planned(the re-bake gate, then a
compare-and-delete on the expectedpersist_row_hash). Both execute through
one newAttestationStore::delete_attestation(id, only_if_hash). No
behaviour change: the purge's read and delete stay two steps on every
backend, as before. I954 is new: nothing had offered the STORE's genesis
door an id the bundle does not carry, so removing its gate left every test
green. I954 does, on memory, sqlite and postgres. -
A plan takes two named instants,
PlanInstants { fold_at, received_at }
(the Constitution's condition 1 on #934).fold_atis the row's signed
instant, for folds over other rows.received_atis the receiver's clock at
the door, for the receipt bounds. Nothing inside a plan reads the clock.
Each gate reads the instant it read before. Four folds still read receipt
and are listed as #1049 candidates.store::parity'sPLAN_INSTANTSpins
which gate reads which, and I950 is the witness on all three backends.plan call instant before #934 put check_instant_bindingreceived_atclock (future-skew bound) put check_attach_freshnessreceived_atclock (CC 2.6.7 window W) put check_capacity_consent_admissionreceived_atclock (#1049 candidate) put check_delegation_cycle_admissionreceived_atclock (#1049 candidate) put check_trust_charter_admissionreceived_atclock (#1049 candidate) put check_peer_deadmissionreceived_atclock (#1049 candidate) put check_licensure_delegator_is_authorityboth clock and asserted_atput check_community_trust_grant_admissionfold_atasserted_at(CC 3.3.1)put executor re-ask check_delegation_cycle_admissionreceived_atclock, under the write lock PQC completion pqc_completed_atstampreceived_atclock local check_trust_charter_admission,check_attach_freshness,local_row_instant,check_instant_bindingreceived_atclock re-seal plan check_reseal_admissionreceived_atclock (its skew bound) re-seal re-ask sealed_over→check_reseal_admissionreceived_atclock The two purge plans read neither instant: both gates read only the row or
the compiled-in bundle.
Fixed
- Local upsert with an occupied
attestation_id. Memory kept the old
row, and sqlite and postgres could delete without storing. One plan now
replaces in-dimension and refuses cross-dimension with the new typed
Error::LocalAttestationIdOccupied. An insert of a held id still writes
nothing. This is the first divergence the one-door extraction made visible;
I951 asserts one outcome on all three backends. - A re-seal racing a crossing overwrote the crossed row on sqlite and
postgres. Both read the row before their write transaction and asked
nothing inside it, so a row that crossed to the federation tier between
the read and the write took the local re-seal's envelope. Memory re-asked
under its lock and refused. Every backend now re-asks the shape gate on the
row it is about to overwrite, inside its write step, and refuses ("diverges
from the stored"). I953 was red on sqlite and postgres before the fix and is
green on all three after. Behaviour changes only under that race, in the
refusing direction.
Adopter-visible.
- New
Error::LocalAttestationIdOccupied(kind
federation_local_attestation_id_occupied, PythonValueError). An
exhaustivematchonfederation::Errormust name it. - Gate signatures now take explicit instants.
received_at: DateTime<Utc>is
added to:admission::check_capacity_consent_admissionadmission::check_delegation_cycle_admissionadmission::check_peer_deadmissiontrust_root::check_trust_charter_admission
admission::check_licensure_delegator_is_authoritytakes
(received_at, fold_at).community_trust_consent::check_community_trust_grant_admissiontakes
fold_at.- To keep the previous behaviour, pass
chrono::Utc::now()for
received_at, and the row'sasserted_atforfold_at. - A substrate failure while reading the row in
attach_attestation_pqc_signaturenow readsget_attestation: …(it was
attach lookup: …). The read is now recorded by the read probe.
Known gap
- Non-row reads still in the put plan. CC condition 2 asks that no
admission decision read a projection. These reads in
plan_put_attestationare not signed rows, and none was re-derived in this
step:- the AV-76 per-peer quota and its cohort-affinity memo (node-local,
receipt-time rate state); settle_if_held's wire-index lookup (an accelerator that settles only on
a byte-identical held row);- the blob store, read by
check_rendition_placementand
check_holding_claim_scope; - the purge tombstones table, read by
refuse_if_tombstoned_attestation.
- the AV-76 per-peer quota and its cohort-affinity memo (node-local,
- The purge's read and delete are two steps on every backend, as before
#934. It is a candidate for the re-seal's in-write re-ask; reported, not
changed.
Changed — typed bounded reads (#1001, #1071)
One bounded attestation read replaces the wide ones. Every attestation
read the directory makes on a key's slice is now an AttestationQuery
(federation::attestation_query) executed through
AttestationReads::query_attestations, which returns ONE page. There is no
default page size and no "every row" form. The statement is planned once
(render_sql, one parameterised SELECT … LIMIT n + 1 on sqlite and
postgres; the memory backend evaluates the same predicate,
AttestationQuery::admits), so the three backends agree by construction.
Every caller in persist moved onto it, plane by plane (delegation/admission,
trust/ownership, consent/audience, identity facts, custody/config/index,
the trait's default bodies, the Python surface). No migration: the existing
indexes serve every shape (I903 reads the sqlite plan of each; on postgres
each plan tops out in a Limit).
The query (all fields public; builders in brackets):
| field | meaning |
|---|---|
by / to [AttestationQuery::by(k, limit), ::to(k, limit), .authored_by, .attested_to] |
the pinned attester / attested key; at least one is required |
dimension: DimensionFilter [.dimension_prefix, .dimension, .dimension_any_of] |
Any, Prefix (byte range, never LIKE), Exact, or AnyOf(Vec<String>): exact dimensions OR'd in one statement (dimension IN (…), one seek per member on the (key, dimension) index). An empty AnyOf is refused. |
types, type_prefix [.types, .type_prefix] |
attestation_type is one of / starts with |
excluded_dimensions [.excluding_dimensions] |
dimension is none of these (absent kept) |
scope: Option<ScopeFilter> [.scope(cohort_scope, Option<cohort_target>)] |
stored cohort_scope, and V150's cohort_target when given. A to query seeks V060's (attested_key_id, cohort_scope, asserted_at DESC); a by query filters the attester's ordered walk. |
tier: QueryTier [.tier] |
Federation (default) or Local |
order: QueryOrder [.oldest_first, .order] |
ByAssertedAt(Direction): the SIGNED instant, ties broken by attestation_id; newest first by default |
limit: NonZeroU32 |
required page size, at most MAX_PAGE (10,000) |
after: Option<AttestationCursor> [.after] |
resume after this row |
as_of: Option<DateTime<Utc>> [.as_of] |
only rows whose signed asserted_at <= t: a fold over an as_of(t) query replays identically on every node and at every later moment |
The whole set is a loop. A Page holds at most limit rows and next: Option<AttestationCursor>, which is Some exactly when another row exists.
A caller that needs every matching row passes next back as after until it
is None, or walks AttestationQuery::pages() in a while let. The loop is
visible at the call site; FOLD_PAGE (500) is the page persist's own folds
use.
Not served is never empty. query_attestations returns
ReadOutcome::Served(Page) (possibly zero rows: the store answered and
nothing matched) or ReadOutcome::NotServed(NotServedReason), which is
Refused (no pinned key, a page above MAX_PAGE, an empty AnyOf, an
unknown cursor version, a proxy's Unsupported) or Fault (the backend
failed, or returned rows past limit + 1). Those are the only two reasons.
Pressure is not one: a read under load is served or faulted, never shortened
or emptied. ReadOutcome::served() is the ? form; a fold sees a not-served
read as an error.
Retractions are a second query. The structural composers (withdraws,
supersedes, recants) carry NO envelope dimension (a withdraws
envelope is {kind, references_attestation_id, references_attestation_type}).
A dimension-filtered page (Prefix, Exact, AnyOf) therefore never
carries its own rows' retractions. The bounded second read is
AttestationQuery::by(k, limit).types(&["withdraws", "supersedes", "recants"]), paged on next, joined on the envelope's
references_attestation_id.
#1071, the re-ack pass. One pass signs at most
HOLDING_CLAIM_REACK_MAX_PER_PASS (256) claims, renewals before upgrade
conversions, the OLDEST due claim first (the nearest expiry), and counts the
rest in ReackReport::deferred for the next hourly pass. A claim is due at
24 h and lapses at 72 h, so 48 passes lie between. Both of its walks page an
AttestationQuery (the node's custody:ack: reports; its retired
holds_bytes: rows by type prefix), and the held-blob set is deduped as the
pages arrive instead of growing with the claim history.
API break.
FederationDirectory::list_attestations_by/list_attestations_forare
demoted:#[cfg(any(test, feature = "test-anchor"))]provided methods
that page the bounded query. No production build has them. They are deleted
in v57.- Removed from the trait (and every backend):
list_attestations_by_dimension_prefix,
list_attestations_for_dimension_prefix,list_attestations_by_type,
list_attestations_for_type,list_attestations_by_types,
list_attestations_for_types. Removed: the free fn
federation::types_and_dimension_admitandadmission::DelegationGraphReads
(its role isattestation_query::AttestationQueryExec). - New required trait method:
execute_attestation_query(&AttestationQuery) -> Result<QueryRows, Error>(storage only; a wrapper delegates, a proxy that
cannot answer returnsUnsupported, which surfaces asNotServed(Refused)).
list_attestations_by_dimension_citingloses its default body (it composed
over a removed read) and is now required. - Telemetry: the eight per-method
doorlabel values of the attestation read
counters collapse into one,attestation_query, recorded once per page by
the shared wrapper. Itsrowsare the rows the page returns; itsbytes
are every envelope the statement decoded, which includes the one lookahead
row fetched past the page (the decode cost, as for every door). A page of
nrows with another page behind it reportsnrows andn + 1rows'
bytes. - Python:
Engine.list_attestations_by(attesting_key_id, limit, cursor_json=None, dimension_prefix=None)returns ONE page asAttestationListPageJSON
({"items": […], "next_cursor": …}) instead of a JSON list of every row;
limitis required. A read that is not served raises. - New public API:
federation::attestation_query::{AttestationQuery, DimensionFilter, ScopeFilter, QueryTier, QueryOrder, Direction, Page, Pages, ReadOutcome, NotServedReason, AttestationReads, AttestationQueryExec, QueryRows, MAX_PAGE, FOLD_PAGE, page_size, render_sql, Dialect, Bind, Statement}.
Gates. I914 classifies every production list_* method on the trait as
paged, bounded by the schema (with the reason), or open (with what it grows
with) in federation/bounded_reads_table.rs, and fails on an unclassified
method or a classification its signature contradicts. The open rows are
named, not fixed, here. I915 walks src/ with comments, strings and test-only
items stripped and finds no production call of the demoted reads.
Adopting (CIRISEdge).
- Replace each
dir.list_attestations_by(k)/_for(k)with the narrowest
query that answers the fold:AttestationQuery::by(k, FOLD_PAGE)plus
.types,.dimension_prefix,.dimension_any_ofor.scope, and loop
onnext(or.pages()) when the fold needs every match. - Several exact dimensions in one read:
.dimension_any_of(&[…]). Rows of
one community:.scope("community", Some(community_id)). - A fold that honours retractions pairs its dimension-filtered page with the
bounded composer query above; the dimension filter never returns them. - Handle
ReadOutcome::NotServedas an error (.served()?), never as "no
rows". - Tests may keep calling
list_attestations_by/_forthrough the
test-anchordev-dependency until v57. A productionimpl FederationDirectorymust drop its ownlist_attestations_by/_for(and
the six removed methods) and implementexecute_attestation_query(a
wrapper delegates to its inner directory).