wai: stage receipts — chains per key and track, verified by inclusion, and the stage and delivery-gap claims #171
31 changed files with 10485 additions and 147 deletions
12
.github/workflows/wai-conformance.yml
vendored
12
.github/workflows/wai-conformance.yml
vendored
|
|
@ -543,7 +543,13 @@ jobs:
|
|||
working-directory: wai
|
||||
shell: bash
|
||||
run: python3 staged-measure-conformance/sim_verify.py staged-measure-conformance
|
||||
- name: claim_verify.py, differential.py, jwp_verify.py, signer_verify.py
|
||||
# chain_differential.py signs receipt chains around jwp-receipts §3 step 3 and §6.1
|
||||
# (two keys, two tracks, both payloads, replays, forks, gaps, the guard and the
|
||||
# freshness bound), and chain_gap_differential.py genuine chains up to 90 groups
|
||||
# delivered shuffled, withheld and walked back with rewritten links and forks; both
|
||||
# require wai_jwp_vectors and jwp_verify.py to give every chain the same verdict. Two
|
||||
# fixed seeds each, so a run is reproducible.
|
||||
- name: claim_verify.py, differential.py, jwp_verify.py, chain_differential.py, chain_gap_differential.py, signer_verify.py
|
||||
working-directory: wai
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
@ -551,6 +557,10 @@ jobs:
|
|||
python3 claim-conformance/claim_verify.py claim-conformance/vectors.json
|
||||
python3 claim-conformance/differential.py "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_claim_vectors" --random 2000 --seed 1
|
||||
python3 jwp-conformance/jwp_verify.py jwp-conformance/vectors.json
|
||||
for seed in 1 2; do
|
||||
python3 jwp-conformance/chain_differential.py "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_jwp_vectors" --n 3000 --seed "$seed"
|
||||
python3 jwp-conformance/chain_gap_differential.py "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_jwp_vectors" --n 400 --seed "$seed"
|
||||
done
|
||||
python3 signer-conformance/signer_verify.py signer-conformance/vectors.json
|
||||
python3 signer-conformance/signer_verify.py verdicts signer-conformance/vectors.json > "$RUNNER_TEMP/signer-py.tsv"
|
||||
"$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_signer_vectors" verdicts signer-conformance/vectors.json > "$RUNNER_TEMP/signer-rs.tsv"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,105 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### Stage receipts: chains per key and track, objects verified by inclusion, `stage` and `delivery-gap` claims
|
||||
|
||||
- **One receipt chain per publisher key and track** ([jwp-receipts](extensions/jwp-receipts.md)
|
||||
§2.4.1, §3 step 3, §6.1). `group_id` and `parent_receipt_hash` run per
|
||||
(`signer_pubkey`, `track_alias`). The text disagreed with itself: the signing
|
||||
payload's comment and §6.1's last bullet said per track, §2.4.1 and §6.1's first
|
||||
bullets per key; all now say per key and track. Receipts issued with `track_alias`
|
||||
`0`, or chained per track, verify as before. Receipts a publisher chained per key
|
||||
across non-zero tracks, as the earlier §2.4.1 allowed, now fail at §3 step 3: each
|
||||
group whose parent is on another track names nothing its own chain holds
|
||||
(`parent_not_held`). A stage set carries each layer on a
|
||||
track of its own, and a subscriber verifies the tracks it subscribes to without the
|
||||
receipts of the ones it does not. A group that passes §3 steps 1–3 can head its chain
|
||||
whether or not its objects arrived, so a dropped object does not break its track's
|
||||
next link. §6.1's checks are now real in the reference verifier
|
||||
(`jwp_delivery::ReceiptChains`): the monotonic `group_id` guard per key and track
|
||||
(`group_id_not_increasing`) and the freshness bound on `group_end`
|
||||
(`group_not_fresh`), off by default for labelled groups.
|
||||
- **A legacy link authenticates nothing** (§3 step 3, §6.1). The legacy payload signs
|
||||
neither `parent_receipt_hash` nor, through the link hash, anything that covers it, so
|
||||
a relay that rewrote one link — the first group's, to name the head — made the
|
||||
chain's whole history link again as new. The legacy payload does sign `group_id`, so a
|
||||
verifier now refuses a legacy group whose `group_id` does not exceed its chain head's,
|
||||
whatever its opt-in setting. A labelled replay fails its signature.
|
||||
- **Forks are told from gaps** (§3 step 3). With a chain's head held, a `null` parent or a
|
||||
parent that is another group the verifier holds is `chain_break`; a parent naming
|
||||
nothing it holds is `parent_not_held`. A chain's live edge is the highest `group_id`
|
||||
of a group that verified and arrived inside the freshness window, linked or not; a
|
||||
group whose signed `group_id` is below it is history, exempt from the bound, so a
|
||||
group a relay delayed past the window no longer wedges the chain behind it, and a gap
|
||||
of any length fills in any order. Both payloads sign `group_id`, so no rewritten link
|
||||
earns the exemption. A previous group of another key or track
|
||||
is not held: a `null` parent beside one starts the chain, any other parent is
|
||||
`parent_not_held`. `group_chain_break` in `jwp-conformance/` now holds a second chain
|
||||
head; its earlier input, a parent that is not the previous group held, is
|
||||
`group_parent_not_the_previous` and is `parent_not_held`.
|
||||
- **`inclusion-verified`** (§3, *Verified by inclusion*; §4.1, §4.4): an object receipt
|
||||
checked without the object's bytes — steps 1, 2, 3 and 5 — establishes that the
|
||||
publisher signed an object with that content hash at that position, and is never
|
||||
acceptance or delivery. A relay forwards the receipts of every object it does not
|
||||
forward, whatever the reason, and **MAY** say why in a `delivery-gap` claim (§4.2).
|
||||
`ObjectReceipt::verify_against(group, envelope: Option<&[u8]>)` returns
|
||||
`Evidence::{DeliveredVerified, InclusionVerified}`, which converts into staged
|
||||
delivery's `ReceiptEvidence`.
|
||||
- **The `stage` class** (§2.4.10): what a party observed of a stage set, about its base —
|
||||
the stage requested, committed, delivered and presented, as points of staged
|
||||
delivery's vocabulary `urn:wai:staged-delivery:1` (registered in
|
||||
[energy-binding](extensions/energy-binding.md) §7.2; §2.4.9's vocabulary row names
|
||||
it); units by stage; the tier and stage hash of what was presented; every layer
|
||||
object with its units, bytes, status, evidence and staged-delivery §4.2 reason;
|
||||
conflicts, deadline misses and envelope bytes. Six rules at §3.1 step 4, in order:
|
||||
the points; the stage hash; the units; the objects (in order and within the set,
|
||||
except an object refused before its span was checked; evidence `delivered-verified`
|
||||
for an applied layer wherever a delivered stage is stated; bytes for every object
|
||||
that arrived and none for a dropped one, fewer in all than the set's; no layer applied
|
||||
twice to a unit; a partial object over at least two units); the units against the
|
||||
objects, per unit (an applied object of layer *s* lies within the spans of layer
|
||||
*s* − 1, a partial one meets them, and the units at stage *s* or above are no more
|
||||
than layer *s* can have reached, a partial object counting at most its span less
|
||||
one); and the delivered stage, which must be the
|
||||
one the objects give. Delivered and presented are separate measures: a set whose
|
||||
layers were all refused meets its commitment with stage 0 presented. A delivered stage
|
||||
may be stated with no commitment beside it, unlike §2.4.9's rule 3: the observer
|
||||
states what arrived. Whether a commitment was met is derived, never stated.
|
||||
- **The `delivery-gap` class** (§2.4.11): a relay's statement that it did not forward
|
||||
the object at (`track_alias`, `group_id`, `object_id`) — `delivery-timeout`,
|
||||
`discretion` or `not-received` — checked at step 6 against the object receipt at
|
||||
those coordinates (`gap_subject_mismatch`).
|
||||
- **Authorities** (§7.5): a `stage` claim needs `stage`, a `delivery-gap` claim
|
||||
`delivery-gap`; neither is pinned-only, and `delivery` grants neither.
|
||||
- **staged-delivery §10** is split: §10.1 attestation of layers, which now names
|
||||
`delivered-verified` and `inclusion-verified` and the per-track chains, and §10.2
|
||||
claims. `staged::claim::StageObserver` states a sink's `stage` claim from what it
|
||||
observed: the layers that arrived, with the layer and span their own `refines`
|
||||
states, those known only by their receipts, and every report the sink gave, the
|
||||
latest deciding. Every event carries the sink's epoch for the base
|
||||
(`StageSink::base_epoch`, which counts the times the sink has forgotten it): a later
|
||||
epoch starts the observation over, keeping only the envelopes the sink still holds for
|
||||
the base, and a claim never spans two epochs. Every case of `staged-conformance/` is replayed through a
|
||||
sink and its observers, and each claim seals and states what the sink did. [http-delivery](extensions/http-delivery.md) §10.5 says what an
|
||||
abandoned refinement is: `inclusion-verified`, and `dropped` or `late` in a `stage`
|
||||
claim; [staged-measurement](extensions/staged-measurement.md) §10.2 says the same of
|
||||
an object its simulator drops.
|
||||
- **Corpora.** `jwp-conformance/` gains the `chain` procedure — vector
|
||||
`v6_per_track_chain` (§5.9) and its neighbours: interleaved tracks, a track joined
|
||||
mid-chain, a link across tracks, forks, the guard and the freshness bound — and the
|
||||
`inclusion` procedure. `claim-conformance/` gains both classes (§5.10), every reason
|
||||
they add, and a context for a gap's object receipt; `signer-conformance/` gains the
|
||||
authorities they need. `jwp_verify.py`, `claim_verify.py` and `signer_verify.py`
|
||||
report every new outcome; `differential.py` generates `stage` bodies around every
|
||||
rule and gaps against receipts at and away from their coordinates, and
|
||||
`chain_differential.py` signs chains around §3 step 3 and §6.1 for both verifiers.
|
||||
Every vector already in the claim and key-discovery corpora is unchanged, and the
|
||||
claim corpus pins the ones before the new classes by a digest computed before they
|
||||
were added. In `jwp-conformance/`, `group_chain_break` alone changes, as the item on
|
||||
forks and gaps says.
|
||||
- No receipt or claim byte of existing material changes; one existing vector,
|
||||
`group_chain_break`, is given a genuine chain break, as above.
|
||||
|
||||
### HTTP delivery (Draft)
|
||||
|
||||
- **The extension** [http-delivery](extensions/http-delivery.md) binds envelopes to
|
||||
|
|
|
|||
|
|
@ -649,8 +649,9 @@ Merkle roots, Ed25519 group signatures, and cross-hop preservation; an
|
|||
object's figure, acquisition class and origin bound into its Merkle leaf,
|
||||
and a group total's class signed under a labelled group payload; the
|
||||
typed receipt classes beside delivery (`energy`, `lawful-basis`,
|
||||
`log-inclusion`, `measurement`, `operating-point`) carried as `claim`
|
||||
receipts; and a second, weaker binding class (a registry-named soft
|
||||
`log-inclusion`, `measurement`, `operating-point`, `stage`, `delivery-gap`)
|
||||
carried as `claim` receipts; one receipt chain per publisher key and track, and
|
||||
an object not delivered accounted for by its receipt, verified by inclusion; and a second, weaker binding class (a registry-named soft
|
||||
binding with a resolution endpoint) whose recovered receipts attest the
|
||||
ancestor, never the object in hand.
|
||||
See [`extensions/energy-measurement.md`](extensions/energy-measurement.md) for
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# claim-conformance
|
||||
|
||||
Conformance for [typed receipt classes and the soft binding class](../extensions/jwp-receipts.md)
|
||||
(§2.4.3–§2.4.9, §2.6, §3.1–§3.2): `claim` receipts of class `energy`,
|
||||
`lawful-basis`, `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement` and
|
||||
`operating-point`, and recovery through a soft binding under the ancestor rule.
|
||||
(§2.4.3–§2.4.11, §2.6, §3.1–§3.2): `claim` receipts of class `energy`,
|
||||
`lawful-basis`, `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`,
|
||||
`operating-point`, `stage` and `delivery-gap`, and recovery through a soft binding under
|
||||
the ancestor rule.
|
||||
|
||||
Each vector declares **one outcome**: `accept` — with the claim's `link_hash`, which
|
||||
pins the canonical form, or for a recovery the ancestor it attests — or `reject` at a
|
||||
|
|
@ -27,7 +28,9 @@ declaration's subject — §2.6.3 makes the declaration context only together wi
|
|||
a vector that gives a recovery record one without the other is malformed), and
|
||||
`exact_match_algs` (the soft-binding algorithms the verifier has a matcher for; the
|
||||
corpus's matcher is byte equality), and `figure` (the receipt that carries the figure a
|
||||
`measurement` or `operating-point` claim names). An `object` context may also carry the
|
||||
`measurement` or `operating-point` claim names), and `gap_object` (the object receipt at
|
||||
a `delivery-gap` claim's coordinates, as the verifier holds it once verified, by
|
||||
inclusion at least: `{track_alias, group_id, object_id, content_hash}`). An `object` context may also carry the
|
||||
class its leaf is labelled with (jwp-receipts §2.2), in energy-measurement §2.7's JSON
|
||||
members, read as that section reads them. A `figure` context is one of two forms:
|
||||
`claim`, an `energy` claim's wire form, viewed under the `moq-jwp:claim-link` profile;
|
||||
|
|
@ -41,7 +44,7 @@ object in hand's `observed_hash` (or `null`). Both procedures report
|
|||
defect, in a recovery performed and in one recorded.
|
||||
|
||||
Everything is synthetic. Keys are Ed25519 from fixed seeds (0x11 publisher, 0x22 meter,
|
||||
0x33 operator, 0x44 sink, 0x55 resolver) and attest nothing; figures, uncertainties and
|
||||
0x33 operator, 0x44 sink, 0x55 resolver, 0x66 relay) and attest nothing; figures, uncertainties and
|
||||
calibration references are invented; algorithm identifiers, registries, endpoints, logs
|
||||
and calibration certificates use reserved example domains; vocabulary terms are
|
||||
illustrative and no verifier here checks them against a vocabulary.
|
||||
|
|
@ -116,6 +119,52 @@ the figure with its class. The vectors cover:
|
|||
a class, agreeing, and disagreeing in figure, class or evidence; an `OnChipCounter`
|
||||
label with no filtering state reads as `Unknown`.
|
||||
|
||||
## What a party observed of a stage set: `stage` and `delivery-gap`
|
||||
|
||||
A `stage` claim (§2.4.10) is signed by the party that observed a stage set
|
||||
([staged-delivery](../extensions/staged-delivery.md) §10.2) about its base. A
|
||||
`delivery-gap` claim (§2.4.11) is signed by a relay about an object it did not forward.
|
||||
The vectors cover:
|
||||
|
||||
- **Accepted:** a set requested, committed, delivered and presented at stage 2, four
|
||||
per-frame first layers and a second layer over the four frames; the second layer
|
||||
dropped at a relay, its receipt verified by inclusion, so the stage delivered is 1 and
|
||||
a reader derives that the commitment was not met; a base whose receipt was not
|
||||
verified first, whose layers applied with no evidence and which states no delivered
|
||||
stage, so rule 6 does not apply, and a layer over such a base applied on inclusion
|
||||
evidence; a second layer delivered and late for one frame (`partial`,
|
||||
`deadline-missed`); two first layers for one frame, both refused and the frame frozen;
|
||||
the base alone at `entropy-consistency`, with no stage hash; both layers delivered and
|
||||
refused, which meets a commitment to stage 2 with stage 0 presented; a span past the
|
||||
set refused `refines-malformed`, which lists its units as stated; and such a layer
|
||||
delivered over its bytes, which delivers nothing to the set.
|
||||
- **Rejected at step 4,** each rule with its reason: the vocabulary, a point with a
|
||||
second member, above 255 or empty, and a request with no delivered stage; a stage hash
|
||||
without `decode-equivalence` and the converse; no unit, a last stage with no unit, a
|
||||
presented stage other than the lowest with units, more misses than units; objects out
|
||||
of order or listed twice, outside the units (and a span past the set refused for a
|
||||
reason decided after it was checked), at layer 0, with a reason where none belongs or
|
||||
none where one does, a reason staged delivery does not define, an object applied on
|
||||
inclusion alone or with no receipt beside a stated delivered stage, a dropped object
|
||||
said to be delivered or counted with bytes, objects that arrived with no bytes, more
|
||||
object bytes than the set's or all of them, and one layer applied twice to a unit;
|
||||
units at a stage no applied object of that layer reaches, a partial first layer
|
||||
counted as though applied to all four frames (with all four at stage 1 or at stage 2),
|
||||
and a second layer applied over frames the first never reached
|
||||
(`stage_units_unsupported`), with the honest partial layer, late for one frame,
|
||||
accepted beside them; a partial object over one unit;
|
||||
and a delivered stage the objects do not give, among them one over objects that leave
|
||||
a unit uncovered.
|
||||
- **Rejected at steps 1–3:** a subject that is a receipt; a status, an evidence or a tier
|
||||
outside its enumeration; `presented` null; an object without `reason`; units with a
|
||||
member they do not define; a count written as a string; two defects, read in the
|
||||
table's order; and `presented` rewritten after signing.
|
||||
- **`delivery-gap`:** accepted against the object receipt at its coordinates, without
|
||||
it, and chained to the relay's previous gap; refused when that receipt names another
|
||||
object (`gap_subject_mismatch`), when the receipt given is at other coordinates or is
|
||||
given to a claim that is not a gap (`context_mismatch`), for a reason outside its
|
||||
enumeration, a date that does not exist, or a subject that is a receipt.
|
||||
|
||||
## Steps 1 and 2: types, and the order a body is read in
|
||||
|
||||
§3.1 step 2 checks exactly the type a member table gives, member by member in the
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Second verifier for claim-conformance/ — typed claim receipts, the soft binding class,
|
||||
and recovery under the ancestor rule (extensions/jwp-receipts.md §2.4.3-§2.4.9, §2.6,
|
||||
and recovery under the ancestor rule (extensions/jwp-receipts.md §2.4.3-§2.4.11, §2.6,
|
||||
§3.1-§3.2), and the arithmetic of a measurement report (energy-measurement.md §3.1, §6.2),
|
||||
computed with Python's integers.
|
||||
|
||||
|
|
@ -217,7 +217,8 @@ ENVELOPE = ["kind", "class", "subject_type", "subject", "seq", "issued_at", "bod
|
|||
"signer_pubkey", "signer_id", "parent_receipt_hash", "sig"]
|
||||
ADMITS = {"energy": {"object"}, "lawful-basis": {"object", "receipt"}, "log-inclusion": {"receipt"},
|
||||
"soft-binding": {"object"}, "binding-recovery": {"object"},
|
||||
"measurement": {"object", "receipt"}, "operating-point": {"object", "receipt"}}
|
||||
"measurement": {"object", "receipt"}, "operating-point": {"object", "receipt"},
|
||||
"stage": {"object"}, "delivery-gap": {"object"}}
|
||||
FIGURE = ["figure_profile", "figure_index", "group", "joules_micro", "energy_provenance", "energy_uncertainty",
|
||||
"energy_counter_filtering", "energy_calibration_ref"]
|
||||
BODY = {
|
||||
|
|
@ -228,7 +229,23 @@ BODY = {
|
|||
"binding-recovery": ["alg", "binding_type", "value", "declaration", "resolution_endpoint", "observed", "attests"],
|
||||
"measurement": FIGURE + ["bracket", "build", "power_reader"],
|
||||
"operating-point": FIGURE + ["vocabulary", "op_requested", "op_committed", "op_delivered", "prediction"],
|
||||
"stage": ["vocabulary", "op_requested", "op_committed", "op_delivered", "presented", "units_by_stage", "tier",
|
||||
"stage_hash", "objects", "conflicts", "deadline_misses", "bytes_envelopes"],
|
||||
"delivery-gap": ["track_alias", "group_id", "object_id", "reason", "observed_at"],
|
||||
}
|
||||
# §2.4.10: one reported layer object, its units, the enumerations this extension defines
|
||||
# for it, the tiers of staged-delivery §5, and the reasons of staged-delivery §4.2.
|
||||
STAGE_OBJECT = ["layer", "object", "units", "bytes", "status", "evidence", "reason"]
|
||||
STAGE_UNITS = ["from", "count"]
|
||||
STAGE_STATUS = ("applied", "partial", "late", "dropped", "refused")
|
||||
STAGE_EVIDENCE = ("delivered-verified", "inclusion-verified", "none")
|
||||
STAGE_TIERS = ("decode-equivalence", "entropy-consistency", "none")
|
||||
STAGE_REASONS = ("refines-malformed", "capability-not-refinement", "base-absent", "base-via-fallback",
|
||||
"base-not-admitted", "refinement-unattested", "payload-malformed", "cost-over-limit",
|
||||
"chain-over-limit", "layer-conflict", "prev-mismatch", "layer-gap", "geometry-mismatch",
|
||||
"deadline-missed")
|
||||
STAGE_VOCABULARY = "urn:wai:staged-delivery:1"
|
||||
GAP_REASONS = ("delivery-timeout", "discretion", "not-received") # §2.4.11
|
||||
BRACKET = ["idle", "busy", "window_us", "completions", "in_flight", "opening"]
|
||||
SUMMARY = ["n", "mean_uw", "sd_uw", "live_intervals", "longest_live_run"]
|
||||
BUILD = ["profile", "toolchain", "target"]
|
||||
|
|
@ -381,6 +398,35 @@ def check_body(cls, b):
|
|||
raise Reject(2, "field_type")
|
||||
return
|
||||
members(b, BODY[cls], 2)
|
||||
if cls == "stage":
|
||||
# In the member table's order: `vocabulary` before the objects, whose members are
|
||||
# checked where the table reaches them.
|
||||
need(is_uri(b["vocabulary"]), 2)
|
||||
for k in ("op_requested", "op_committed", "op_delivered", "presented"):
|
||||
pt = b[k]
|
||||
need(pt is not None or k != "presented", 2)
|
||||
if pt is not None:
|
||||
need(isinstance(pt, dict) and all(is_token(n) and is_int(x) for n, x in pt.items()), 2)
|
||||
need(isinstance(b["units_by_stage"], list) and all(is_int(n) for n in b["units_by_stage"]), 2)
|
||||
need(b["tier"] in STAGE_TIERS, 2)
|
||||
need(b["stage_hash"] is None or is_hex(b["stage_hash"], 32), 2)
|
||||
need(isinstance(b["objects"], list), 2)
|
||||
for o in b["objects"]:
|
||||
need(isinstance(o, dict), 2)
|
||||
members(o, STAGE_OBJECT, 2)
|
||||
need(is_int(o["layer"]) and is_hex(o["object"], 32), 2)
|
||||
need(isinstance(o["units"], dict), 2)
|
||||
members(o["units"], STAGE_UNITS, 2)
|
||||
need(is_int(o["units"]["from"]) and is_int(o["units"]["count"]), 2)
|
||||
need(is_int(o["bytes"]) and o["status"] in STAGE_STATUS and o["evidence"] in STAGE_EVIDENCE, 2)
|
||||
need(o["reason"] is None or isinstance(o["reason"], str), 2)
|
||||
need(all(is_int(b[k]) for k in ("conflicts", "deadline_misses", "bytes_envelopes")), 2)
|
||||
return
|
||||
if cls == "delivery-gap":
|
||||
need(all(is_int(b[k]) for k in ("track_alias", "group_id", "object_id")), 2)
|
||||
need(b["reason"] in GAP_REASONS, 2)
|
||||
need(isinstance(b["observed_at"], str) and is_rfc3339(b["observed_at"]), 2)
|
||||
return
|
||||
if cls in ("measurement", "operating-point"):
|
||||
check_figure_header(b)
|
||||
if cls == "measurement":
|
||||
|
|
@ -607,6 +653,8 @@ def class_rules(c):
|
|||
need(not (dlv is not None and com is None), 4, "operating_point_delivered_alone")
|
||||
need(not ((req is not None or com is not None) and dlv is None), 4, "operating_point_incomplete")
|
||||
need(dlv is not None or b["prediction"] is not None, 4, "operating_point_empty")
|
||||
elif cls == "stage":
|
||||
stage_rules(b)
|
||||
elif cls == "log-inclusion":
|
||||
need(b["log_receipt"] != "", 4, "log_inclusion_no_evidence")
|
||||
elif cls in ("soft-binding", "binding-recovery"):
|
||||
|
|
@ -618,6 +666,150 @@ def class_rules(c):
|
|||
need(b["observed"] != c["subject"], 4, "recovery_of_hard_bound_object")
|
||||
|
||||
|
||||
def stage_layers(pt):
|
||||
"""The layers a stage point names: exactly the member `wai-stage:layers`, 0 to 255."""
|
||||
if len(pt) != 1 or "wai-stage:layers" not in pt or pt["wai-stage:layers"] > 255:
|
||||
return None
|
||||
return pt["wai-stage:layers"]
|
||||
|
||||
|
||||
STAGE_REFUSED_UNCHECKED = ("refines-malformed", "capability-not-refinement", "base-absent") # §4.1 steps 1-2
|
||||
|
||||
|
||||
def refused_unchecked(o):
|
||||
"""An object the sink refused before it checked its refines against the set: its
|
||||
units are as its refines states them, and it is no layer of the set's."""
|
||||
return o["status"] == "refused" and o["reason"] in STAGE_REFUSED_UNCHECKED
|
||||
|
||||
|
||||
def merge(spans):
|
||||
"""Half-open intervals, sorted, merged, none empty."""
|
||||
out = []
|
||||
for a, e in sorted(x for x in spans if x[0] < x[1]):
|
||||
if out and a <= out[-1][1]:
|
||||
out[-1] = (out[-1][0], max(out[-1][1], e))
|
||||
else:
|
||||
out.append((a, e))
|
||||
return out
|
||||
|
||||
|
||||
def length(v):
|
||||
return sum(e - a for a, e in v)
|
||||
|
||||
|
||||
def intersect(v, span):
|
||||
a, e = span
|
||||
return [(max(x, a), min(y, e)) for x, y in v if max(x, a) < min(y, e)]
|
||||
|
||||
|
||||
def subtract(v, w):
|
||||
out = []
|
||||
for a, e in v:
|
||||
at = a
|
||||
for x, y in w:
|
||||
if y <= at or x >= e:
|
||||
continue
|
||||
if x > at:
|
||||
out.append((at, x))
|
||||
at = max(at, y)
|
||||
if at < e:
|
||||
out.append((at, e))
|
||||
return out
|
||||
|
||||
|
||||
def covered(objs, n):
|
||||
"""How many of the units 0..n the objects' spans cover between them."""
|
||||
spans = sorted((min(o["units"]["from"], n), min(o["units"]["from"] + o["units"]["count"], n)) for o in objs)
|
||||
total, reach = 0, 0
|
||||
for a, e in spans:
|
||||
a = max(a, reach)
|
||||
if e > a:
|
||||
total += e - a
|
||||
reach = e
|
||||
return total
|
||||
|
||||
|
||||
def delivered_stage(b, n):
|
||||
"""§2.4.10: the highest k such that, at every layer from 1 to k, the objects listed
|
||||
with delivered-verified evidence — an object refused before its refines was checked
|
||||
excepted — cover every one of the n units."""
|
||||
k = 0
|
||||
for layer in range(1, 256):
|
||||
objs = [o for o in b["objects"] if o["layer"] == layer and o["evidence"] == "delivered-verified"
|
||||
and not refused_unchecked(o)]
|
||||
if covered(objs, n) < n:
|
||||
break
|
||||
k = layer
|
||||
return k
|
||||
|
||||
|
||||
def stage_rules(b):
|
||||
"""§2.4.10's rules, in order: the points, the stage hash, the units, the objects, the
|
||||
units against the objects, the delivered stage."""
|
||||
pts = [b[k] for k in ("op_requested", "op_committed", "op_delivered")]
|
||||
need(b["vocabulary"] == STAGE_VOCABULARY, 4, "stage_point_malformed")
|
||||
need(all(stage_layers(p) is not None for p in pts + [b["presented"]] if p is not None), 4, "stage_point_malformed")
|
||||
req, com, dlv = pts
|
||||
need(not ((req is not None or com is not None) and dlv is None), 4, "stage_point_incomplete")
|
||||
need((b["stage_hash"] is not None) == (b["tier"] == "decode-equivalence"), 4, "stage_hash_without_tier")
|
||||
ubs = b["units_by_stage"]
|
||||
n = sum(ubs)
|
||||
need(0 < len(ubs) <= 256 and n > 0 and ubs[-1] != 0, 4, "stage_units_malformed")
|
||||
lowest = next(i for i, c in enumerate(ubs) if c != 0)
|
||||
need(stage_layers(b["presented"]) == lowest and b["conflicts"] <= n and b["deadline_misses"] <= n,
|
||||
4, "stage_units_malformed")
|
||||
for o in b["objects"]:
|
||||
u, st, ev, r = o["units"], o["status"], o["evidence"], o["reason"]
|
||||
in_set = refused_unchecked(o) or u["from"] + u["count"] <= n
|
||||
least = 2 if st == "partial" else 1 # a partial object applied to some units of its span, not all
|
||||
need(1 <= o["layer"] <= 255 and u["count"] >= least and in_set, 4, "stage_objects_malformed")
|
||||
if st in ("applied", "late", "dropped"):
|
||||
need(r is None, 4, "stage_objects_malformed")
|
||||
else:
|
||||
need(r in STAGE_REASONS, 4, "stage_objects_malformed")
|
||||
if st in ("applied", "partial") and dlv is not None:
|
||||
need(ev == "delivered-verified", 4, "stage_objects_malformed") # §10.1 over a base verified first
|
||||
if st == "dropped":
|
||||
need(ev != "delivered-verified", 4, "stage_objects_malformed")
|
||||
need((st == "dropped") == (o["bytes"] == 0), 4, "stage_objects_malformed")
|
||||
keys = [(o["layer"], o["units"]["from"], o["object"]) for o in b["objects"]]
|
||||
need(all(x < y for x, y in zip(keys, keys[1:])), 4, "stage_objects_malformed")
|
||||
need(sum(o["bytes"] for o in b["objects"]) < b["bytes_envelopes"], 4, "stage_objects_malformed")
|
||||
# Two objects of one layer are never both applied to a unit.
|
||||
for layer in {o["layer"] for o in b["objects"]}:
|
||||
spans = sorted((o["units"]["from"], o["units"]["from"] + o["units"]["count"]) for o in b["objects"]
|
||||
if o["layer"] == layer and o["status"] == "applied")
|
||||
need(all(spans[i + 1][0] >= max(e for _, e in spans[:i + 1]) for i in range(len(spans) - 1)),
|
||||
4, "stage_objects_malformed")
|
||||
# The units against the objects, per unit: an applied object of layer s lies within the
|
||||
# spans of layer s - 1's applied and partial objects (every unit for layer 1), a partial
|
||||
# one meets them, and the units at stage s or above are no more than the units layer s's
|
||||
# applied objects cover and, of the rest below, at most each partial object's span less
|
||||
# one.
|
||||
reaching = [o for o in b["objects"] if o["status"] in ("applied", "partial")]
|
||||
top = max([o["layer"] for o in reaching] + [len(ubs) - 1])
|
||||
below = [(0, n)]
|
||||
for st in range(1, top + 1):
|
||||
layer = [o for o in reaching if o["layer"] == st]
|
||||
span = lambda o: (min(o["units"]["from"], n), min(o["units"]["from"] + o["units"]["count"], n))
|
||||
applied = merge([span(o) for o in layer if o["status"] == "applied"])
|
||||
free, partial_most = [], 0
|
||||
for o in layer:
|
||||
inside = intersect(below, span(o))
|
||||
if o["status"] == "applied":
|
||||
need(length(inside) >= o["units"]["count"], 4, "stage_units_unsupported")
|
||||
else:
|
||||
need(len(inside) > 0, 4, "stage_units_unsupported")
|
||||
open_ = subtract(inside, applied)
|
||||
partial_most += min(length(open_), o["units"]["count"] - 1)
|
||||
free += open_
|
||||
reach = length(applied) + min(length(merge(free)), partial_most)
|
||||
need(sum(ubs[st:]) <= reach, 4, "stage_units_unsupported")
|
||||
below = merge([span(o) for o in layer])
|
||||
if dlv is not None:
|
||||
need(stage_layers(dlv) == delivered_stage(b, n), 4, "stage_delivered_disagrees")
|
||||
|
||||
|
||||
def verify_claim(text):
|
||||
"""§3.1 steps 1-4. Returns the claim; raises Reject."""
|
||||
try:
|
||||
|
|
@ -718,6 +910,13 @@ def check_context(c, ctx):
|
|||
need(dig(r, "subject", "output") == c["subject"], 6, "context_mismatch")
|
||||
need(dig(r, "provenance", "how") == b["grade"], 6, "energy_disagrees_with_oer")
|
||||
need(figure_rounds_to_micro(dig(r, "energy", "joules_total"), b["joules_micro"]), 6, "energy_disagrees_with_oer")
|
||||
if "gap_object" in ctx:
|
||||
# §2.4.11: the object receipt at the claim's coordinates, as the verifier holds it
|
||||
# once verified (by inclusion at least).
|
||||
g = ctx["gap_object"]
|
||||
need(c["class"] == "delivery-gap", 6, "context_mismatch")
|
||||
need(all(g[k] == b[k] for k in ("track_alias", "group_id", "object_id")), 6, "context_mismatch")
|
||||
need(g["content_hash"] == c["subject"], 6, "gap_subject_mismatch")
|
||||
if "declaration" in ctx:
|
||||
# In §3.1 step 6's order: the claim must be one that names a declaration; the
|
||||
# declaration must itself pass steps 1-4 (context_invalid) — its signer_pubkey is
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ measurement reports whose brackets sit at the least declaration and one ppm belo
|
|||
half microjoule, at the ratio thresholds, with a flat idle bracket, with products far past
|
||||
128 bits and with every pattern of liveness facts; operating-point claims with every
|
||||
presence combination of their points and prediction; reports checked against figure views
|
||||
that agree and disagree in each member; and energy claims against labelled objects —
|
||||
that agree and disagree in each member; energy claims against labelled objects; stage
|
||||
claims around every rule of §2.4.10 — points, the stage hash, the units, the objects'
|
||||
order, statuses, evidence, reasons and bytes, and the delivered stage they give — and
|
||||
delivery-gap claims against object receipts at their coordinates and elsewhere —
|
||||
writes them in the corpus's format with no declared outcome, runs both verifiers'
|
||||
`verdicts` mode, and reports every input on which they differ.
|
||||
|
||||
|
|
@ -60,7 +63,7 @@ def pk(k):
|
|||
return k.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw).hex()
|
||||
|
||||
|
||||
PUBLISHER, METER, OPERATOR, VIEWER, RESOLVER = (key(s) for s in (0x11, 0x22, 0x33, 0x44, 0x55))
|
||||
PUBLISHER, METER, OPERATOR, VIEWER, RESOLVER, RELAY = (key(s) for s in (0x11, 0x22, 0x33, 0x44, 0x55, 0x66))
|
||||
OER_SIGNER = "ed25519:claim-conformance-meter"
|
||||
WM_ALG = "org.example.watermark.1"
|
||||
|
||||
|
|
@ -389,6 +392,35 @@ BODY_DEFECTS = {
|
|||
(("log_receipt",), [5, "zz", "", DELETE]),
|
||||
((EXTRA,), [1]),
|
||||
]),
|
||||
"stage_accept": ("VIEWER", [
|
||||
(("vocabulary",), [5, "staged-delivery", "urn:example:stages:1", DELETE]),
|
||||
(("op_requested",), ["x", {}, {"wai-stage:layers": "2"}, {"a b": 1}, {"wai-stage:layers": 256}, None, DELETE]),
|
||||
(("op_delivered",), [None, {"wai-stage:layers": 1}, {"wai-stage:layers": 3}, DELETE]),
|
||||
(("presented",), [None, 5, {}, {"wai-stage:layers": 1}, DELETE]),
|
||||
(("units_by_stage",), ["x", [], [0, 0, 4, 0], [4], [0, "4"], [1, 1, 2], DELETE]),
|
||||
(("tier",), [5, "none", "exact", DELETE]),
|
||||
(("stage_hash",), [None, "zz", "AB" * 32, DELETE]),
|
||||
(("objects",), ["x", [], [5], DELETE]),
|
||||
(("objects", 0, "layer"), ["1", 0, 256, DELETE]),
|
||||
(("objects", 0, "units"), ["x", {}, DELETE]),
|
||||
(("objects", 0, "units", "count"), [0, 4, DELETE]),
|
||||
(("objects", 0, "units", EXTRA), [1]),
|
||||
(("objects", 0, "status"), [5, "skipped", "late", "partial", "dropped", DELETE]),
|
||||
(("objects", 0, "evidence"), ["verified", "none", "inclusion-verified", DELETE]),
|
||||
(("objects", 0, "reason"), [5, "too-late", "deadline-missed", DELETE]),
|
||||
(("objects", 0, EXTRA), [1]),
|
||||
(("objects", 4, "status"), ["dropped", "refused"]),
|
||||
(("deadline_misses",), [5, "1", DELETE]),
|
||||
(("bytes_envelopes",), [0, DELETE]),
|
||||
((EXTRA,), [1]),
|
||||
]),
|
||||
"gap_accept": ("RELAY", [
|
||||
(("track_alias",), ["8", None, DELETE]),
|
||||
(("group_id",), [9, DELETE]),
|
||||
(("reason",), [5, "timeout", "discretion", DELETE]),
|
||||
(("observed_at",), [5, "2026-02-29T00:00:00Z", "now", DELETE]),
|
||||
((EXTRA,), [1]),
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -416,7 +448,8 @@ def edit_at(body, path, value):
|
|||
|
||||
|
||||
def bodies(ins):
|
||||
keys = {"OPERATOR": OPERATOR, "VIEWER": VIEWER, "METER": METER, "PUBLISHER": PUBLISHER, "RESOLVER": RESOLVER}
|
||||
keys = {"OPERATOR": OPERATOR, "VIEWER": VIEWER, "METER": METER, "PUBLISHER": PUBLISHER, "RESOLVER": RESOLVER,
|
||||
"RELAY": RELAY}
|
||||
for name, (signer, positions) in BODY_DEFECTS.items():
|
||||
base = ins.receipt(name)
|
||||
singles = [(path, i, value) for path, values in positions for i, value in enumerate(values)]
|
||||
|
|
@ -434,6 +467,67 @@ def bodies(ins):
|
|||
ins.add(f"body_{name[:-len('_accept')]}_{label}", CV.jcs(sign(dict(base, body=body), keys[signer])))
|
||||
|
||||
|
||||
def stages(ins, rng, n):
|
||||
"""§2.4.10 and §2.4.11 around their rules: stage bodies built at random — a set of
|
||||
one to five units, points that agree or not, objects of random layers, spans (some
|
||||
past the set, refused before their span was checked or not), statuses, evidence,
|
||||
reasons and bytes, in order or not, a tier with or without a hash — signed by the
|
||||
sink, so each is decided at step 2, 4 or later; and the corpus's gap
|
||||
claim against object receipts at its coordinates and elsewhere, naming its object or
|
||||
another."""
|
||||
base = ins.receipt("stage_accept")
|
||||
hashes = [CV.blake3(b"differential: layer object %d" % i).hex() for i in range(8)]
|
||||
for i in range(n):
|
||||
units = rng.randint(1, 5)
|
||||
top = rng.randint(0, 3)
|
||||
ubs = [0] * (top + 1)
|
||||
for _ in range(units):
|
||||
ubs[rng.randint(0, top)] += 1
|
||||
if rng.random() < 0.1:
|
||||
ubs.append(rng.choice([0, 1]))
|
||||
objects = []
|
||||
for _ in range(rng.randint(0, 6)):
|
||||
frm = rng.randint(0, units)
|
||||
st = rng.choice(["applied", "applied", "partial", "late", "dropped", "refused"])
|
||||
objects.append({
|
||||
"layer": rng.choice([1, 1, 2, 2, 3, 0, 256]) if rng.random() < 0.2 else rng.randint(1, max(1, top)),
|
||||
"object": rng.choice(hashes),
|
||||
"units": {"from": frm, "count": rng.choice([1, units - frm, 0, units + 1, units + 7]) if units > frm else rng.choice([0, 1])},
|
||||
"bytes": (0 if st == "dropped" else rng.choice([700, 2400])) if rng.random() < 0.8 else rng.choice([0, 700]),
|
||||
"status": st,
|
||||
"evidence": rng.choice(["delivered-verified", "delivered-verified", "inclusion-verified", "none"]),
|
||||
"reason": (None if st in ("applied", "late", "dropped") else "deadline-missed")
|
||||
if rng.random() < 0.8 else rng.choice([None, "layer-conflict", "too-late", "refines-malformed", "base-absent"]),
|
||||
})
|
||||
if rng.random() < 0.8:
|
||||
objects.sort(key=lambda o: (o["layer"], o["units"]["from"], o["object"]))
|
||||
lowest = next((j for j, c in enumerate(ubs) if c), 0)
|
||||
pt = lambda k: {"wai-stage:layers": k}
|
||||
delivered = rng.choice([None, pt(rng.randint(0, 3)), pt(lowest)])
|
||||
committed = rng.choice([None, pt(2)]) if delivered is not None else rng.choice([None, None, pt(2)])
|
||||
tier = rng.choice(["decode-equivalence", "decode-equivalence", "entropy-consistency", "none"])
|
||||
has_hash = (tier == "decode-equivalence") != (rng.random() < 0.1)
|
||||
body = dict(base["body"], op_requested=rng.choice([None, committed]), op_committed=committed,
|
||||
op_delivered=delivered, presented=pt(lowest if rng.random() < 0.9 else rng.randint(0, 3)),
|
||||
units_by_stage=ubs, tier=tier, stage_hash=base["body"]["stage_hash"] if has_hash else None,
|
||||
objects=objects, conflicts=rng.choice([0, 0, 1, units + 1]),
|
||||
deadline_misses=rng.choice([0, 0, 1, units + 1]),
|
||||
bytes_envelopes=rng.choice([5000, 5000, 5000, 0]) + max(0, sum(o["bytes"] for o in objects) - rng.choice([0, 1])))
|
||||
ins.add(f"stage_random_{i}", CV.jcs(sign(dict(base, body=body), VIEWER)))
|
||||
gap = ins.wire("gap_accept")
|
||||
g = json.loads(gap)
|
||||
for name, ctx in [
|
||||
("at", {"track_alias": 8, "group_id": 3, "object_id": 0, "content_hash": g["subject"]}),
|
||||
("other_object", {"track_alias": 8, "group_id": 3, "object_id": 0, "content_hash": hashes[0]}),
|
||||
("other_track", {"track_alias": 7, "group_id": 3, "object_id": 0, "content_hash": g["subject"]}),
|
||||
("other_group", {"track_alias": 8, "group_id": 4, "object_id": 0, "content_hash": g["subject"]}),
|
||||
("other_index", {"track_alias": 8, "group_id": 3, "object_id": 1, "content_hash": hashes[1]}),
|
||||
]:
|
||||
ins.add("gap_context_" + name, gap, {"gap_object": ctx})
|
||||
ins.add("gap_context_to_a_stage_claim", ins.wire("stage_accept"),
|
||||
{"gap_object": {"track_alias": 8, "group_id": 3, "object_id": 0, "content_hash": g["subject"]}})
|
||||
|
||||
|
||||
def figures(ins, rng, n):
|
||||
oer_t = ins.by_name["energy_matches_oer_accept"]["context"]["oer"]["receipt"]
|
||||
claim_t = ins.receipt("energy_matches_oer_accept")
|
||||
|
|
@ -912,6 +1006,7 @@ def main(argv):
|
|||
contexts(ins)
|
||||
bodies(ins)
|
||||
reports(ins, random.Random(f"reports {a.seed}"), a.random) # its own stream, as classes()
|
||||
stages(ins, random.Random(f"stages {a.seed}"), a.random) # its own stream, as classes()
|
||||
doc = text_of({"about": "differential inputs; no outcome is declared", "vectors": ins.out})
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
|
||||
f.write(doc)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"about": "jwp-receipts section 2.4.3-2.4.9, 2.6 and 3.1-3.2: typed claim receipts and the soft binding class. Every vector is synthetic: keys are Ed25519 from fixed 32-byte seeds (0x11 publisher, 0x22 meter, 0x33 operator, 0x44 sink, 0x55 resolver) and attest nothing; figures are invented; algorithm names, endpoints, logs and registries use reserved example domains; vocabulary terms are illustrative and are not checked against the vocabulary. Expected outcomes are declared per vector in the generator, not copied from a verifier. Regenerate with `wai_claim_vectors gen`.",
|
||||
"about": "jwp-receipts section 2.4.3-2.4.11, 2.6 and 3.1-3.2: typed claim receipts and the soft binding class. Every vector is synthetic: keys are Ed25519 from fixed 32-byte seeds (0x11 publisher, 0x22 meter, 0x33 operator, 0x44 sink, 0x55 resolver, 0x66 relay) and attest nothing; figures are invented; algorithm names, endpoints, logs and registries use reserved example domains; vocabulary terms are illustrative and are not checked against the vocabulary. Expected outcomes are declared per vector in the generator, not copied from a verifier. Regenerate with `wai_claim_vectors gen`.",
|
||||
"vectors": [
|
||||
{
|
||||
"name": "energy_accept",
|
||||
|
|
@ -4290,6 +4290,748 @@
|
|||
"step": 6,
|
||||
"reason": "context_mismatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_accept",
|
||||
"about": "a sink's account of a stage set: requested, committed, delivered and presented at stage 2, four per-frame first layers and a second layer over the four frames, each delivered over its bytes; a reader derives that the commitment was met",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"3bd5dea72e15792d996cfd1425effe215738a407d29092aa53ad00fda85fadb9a797c43f4b8a2ba5c13afb971daa02baa275e95ad40b013a9caf6160555f3c0d\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "26b6f9265097e6782a49d4b03e238294390ba8fd1dd4f809aab5156cb3fc5bae"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_not_met_accept",
|
||||
"about": "the second layer dropped at a relay: its receipt verified by inclusion only, it is dropped, the stage delivered is 1 and the commitment of 2 was not met",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":8606,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":0,\"evidence\":\"inclusion-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"dropped\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"f0ef87500f21c79ea1a7c20f5a46c4bd10a0b32a67a12c343f4b8351d821b4feb1b1918f5ada6d712a13b50b4749e2954a4132f1498314e89ba123272d380809\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "ae271039814c1721d58d6821b14d7e84115cddd23b054bbf14976ef7c7e720de"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_null_delivered_states_no_delivery",
|
||||
"about": "a base whose receipt was not verified first: layers applied with no evidence, no stage is stated as delivered, and the delivered-stage rule does not apply",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"none\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"none\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"none\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"none\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"none\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":null,\"op_delivered\":null,\"op_requested\":null,\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"28be15f5152949572c6f913580367e7dfb933c6786047106faf6ad03f866c635e660a11227f5d1607ca8a0a4de8fab270b00cbaeff72ed3b47a862fd3854e10a\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "1bc2d6cd17c52b78807335128aa68eceee07c9c45adc6493759c6e76d9f7498c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_late_partial_accept",
|
||||
"about": "the second layer arrived delivered and missed one frame's deadline: partial, deadline-missed, one unit presented at stage 1, delivered at 2",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":1,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":\"deadline-missed\",\"status\":\"partial\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,1,3],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"ab470cd2443db1109add31d9a068599598d3493ef7a2b2d133d652b6a14e402944730752317becc44868af5b72d8acc195faa7f9cf81e2c5e6cdea512c806c0c\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "c9c57f421653bc589ec97aa97eaa453e196173439346b1021f404fe9b323b957"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_conflict_accept",
|
||||
"about": "two different first layers for frame 0, both refused and the frame frozen at stage 0: one conflict",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":9507,\"conflicts\":1,\"deadline_misses\":0,\"objects\":[{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"524bc57fb0c1a123426495b9aa2c70820c30a1550dff2c2ce9530477b5e72843\",\"reason\":\"layer-conflict\",\"status\":\"refused\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"8d5de99d66331c8fbbda62049b135d0492dc19e134410d0f3c4ad4610230d367\",\"reason\":\"layer-conflict\",\"status\":\"refused\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[1,3],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"a1b5846e3c1eba2f861d48daa3660338a054fcc6bc7350b71be665cee0cd27f5cc6a0bdc869751592c340ef427352d5331be5b4def7eb2e2623cfff0f7b24d08\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "86e14bef8d9583c009b7fee9adee8f9fd1f3a962ec74e4d23a4bfe210d462df5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_entropy_consistency_accept",
|
||||
"about": "the base alone at entropy-consistency: no stage hash",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5000,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":0},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":null,\"tier\":\"entropy-consistency\",\"units_by_stage\":[4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"65f107bca29ad6c2ff5823f81d15399611d64e91b3abb186b8249cf338cb586519f14d88ae559ebccc76692a4f710ddf1d2dbfb8fe528f46a165fa6e918c0b0f\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "2f09c3c61c582849e8bfe9dabd418c87dd28cfad9a754570357db020a660fb81"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_vocabulary_other",
|
||||
"about": "a vocabulary other than staged delivery's: its points are not stages",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:example:stages:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"f9ed3f19d2354c4083878b974035d2e352540b3404c558c5ed3ccc115d249895808f21904b4fff0dfd6ea1dbc767ee246056cea050cdf3dee56cb0ac0bdcdd0d\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_point_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_point_two_members",
|
||||
"about": "a presented point with a second parameter",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2,\"x-frames\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"bbc3b9fb3f307b52feae70d044a931600bad5a45232da38049df9a36a4c31e51280de37be4c4228f932b85944c1f0b2738c5d701fec31c40cfa9bbe8bcf36c0a\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_point_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_point_above_255",
|
||||
"about": "a requested stage of 256 layers",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":256},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"6d53711842d1684e3b06700c3a7ae93c33dcf82a39e43063917c7b881554d2c9f9591b2ffac9b3b66c4f4fa10ff1bd5aa491787eb2e4b947d09948c3f1a9f30a\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_point_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_point_empty",
|
||||
"about": "a committed point with no member",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"0e5e9b84cbfa36929bae2a33ab0ed7b4a998b19f5ccd0898edbade4c01a9bcb47b8a665c28809c049aed85c4dffd754b750b11cc736413d6b7886ab9a37e5c0e\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_point_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_request_without_delivery",
|
||||
"about": "a request and a commitment with no delivered stage",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"none\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"none\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"none\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"none\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"none\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":null,\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"b41fc89d5273cb34cd9dc78943837871c51256fa5f806a0192c572f5f8aeadaf74b68e44d9ac1ec5bcb11b846a0f8d7183d713c2b12658a38784dada0385d300\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_point_incomplete"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_hash_without_tier",
|
||||
"about": "a stage hash beside the tier none",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"none\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"8c02fda375e146ad764f8881de0b336c707c5f1a7c04b06f9e12274e7be5006944a9af1c1972e82ee87e3e3b7269a9905d7e985355b596d310b4706630680e00\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_hash_without_tier"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_tier_without_hash",
|
||||
"about": "decode-equivalence with no stage hash",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":null,\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"ffd983fc4ea467fffceadf55bf53e1d294ebec0fcf5531dd6f6214d46b9253514f85429a3913232cad90f45ddc303942b935446cd4c85340690d2239992e1804\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_hash_without_tier"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_units_empty",
|
||||
"about": "no unit at any stage",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"dec8c20b1972dfb4491bd7080d95436605649594acbcc0c7d74201a24323910181fd3dd6a680434550713903c9b3dba0e18856277eef0b4d229da2b7930fee07\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_units_trailing_zero",
|
||||
"about": "a last stage with no unit",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4,0],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"3b6f1b7af282316e0501f04a5e7f561b71e65e3a6ca9a2945499be80002ec080b3190770ab2039d0861fdb7ac1d22c7364dfb07eff80f050746df3cbfd2a4c00\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_presented_not_lowest",
|
||||
"about": "one unit at stage 1 and three at 2, presented stated as 2",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,1,3],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"c8f1b9eab29b64d27418d12e59ea2fb88d60a08525733236fef206e823829505b15f1e56128fa591eef5794ebb993e39bf477a49544efd5488cf63f9345ae400\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_more_misses_than_units",
|
||||
"about": "five deadline misses over four units",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":5,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"f1837987b08addec7a796779aa4d405397a48bd70a24fe47bd8ab8ca0798fd00678faa8c5abf8244dd3a6d850459aed58ac5455b0061d7bd4f7a1a68a00d2601\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_objects_out_of_order",
|
||||
"about": "the second layer listed before a first layer",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"e3aec920485fc0aff3aaef4086fdec7b9cac0412b7ed42c26b584a54c537e56acafb8244e78b8b60c68ec373fbdb557e43a81e17b7b959fb50e5a8c3e2a70c0e\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_object_listed_twice",
|
||||
"about": "one object listed twice",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"d9df89304ba39159fb18591c4a145029e1fbd39d37e092f34d412f11e4019ff711a6e240869b91df65e322c021ec382aa3367aae8937255c337e0af4e548e901\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_object_outside_units",
|
||||
"about": "a second layer covering five units of four",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":5,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"be0540a63e76647d38444dec63297ab938d519048adcdda360641ac6abf0bf15d12b28f75d45bbdb26e3912bd0901fa5c9f698c2925e5117e37e9e349e5f5801\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_object_layer_zero",
|
||||
"about": "an object at layer 0: the base is not a layer object",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":0,\"object\":\"88d762ea8255025b3f4266c93a86f047c2f3e4607f85e65d5f5276564cbb95b0\",\"reason\":null,\"status\":\"late\",\"units\":{\"count\":4,\"from\":0}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"cce567319b53e0065bc0a386c820198751c625ab5801c2f5aeba72a3903edb7aa1d4727d9f1b317770c9633f226cc29f824764938a60d65490fcf0250df7bd04\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_applied_with_reason",
|
||||
"about": "an applied object naming a reason",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":\"deadline-missed\",\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"0b494c63248f0657552df36c4411ded69a061a2baffc194439eb0a61b86e50b4d3c2ca5d7f9dc045d43ce6c47e015a7c24d46e6b79c806850887c64a5ab1930b\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_refused_without_reason",
|
||||
"about": "a refused object naming no reason",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"refused\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"37664ef5e5eb0d751a2c0fbe27e300e26b3de59a8de52ea740076c2010c8d4ab95836a16d7bff22658d93ad29266e74ebcc84f04ee474f76eacdc3327e05900a\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_reason_not_staged",
|
||||
"about": "a refused object naming a reason staged delivery does not define",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":\"too-late\",\"status\":\"refused\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"2aa9e5534492d6ea740e99613e8e9c6da57de4242b3f8fab5429240235573dd107d15884c34b408e64d063a39bb822caf12db2d510535d3734d4919122a73b06\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_applied_by_inclusion",
|
||||
"about": "an object applied on a receipt verified by inclusion alone, which never authorises an application",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"inclusion-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"67858126c13a8245622b4c51e8dc4b83f7abb382660b0e26c9e5125bb9bd83cbe58782f655b4aae57d0d1e66353f80481972b5dc38dec86f982ad2368171350f\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_dropped_delivered",
|
||||
"about": "a dropped object whose receipt is said to be verified over its bytes",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":8606,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"dropped\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"904649b08ba6a5f5f948b8a205eb086743befce85fa4fe97f7c488062363e3ef3c0d90771f3fb0848bc443c7b6a02ef85c89fb4dafb9fa69e3fd23fa8a59d208\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_dropped_with_bytes",
|
||||
"about": "a dropped object counted with bytes",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":8616,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":10,\"evidence\":\"inclusion-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"dropped\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"6e419aedec388c2a3d9c449514e8d07cd7df8538bc6a83179cc1fbab7be7661c9ff76a3bf9c4f61d1eaca225dcac7b7bd8cb1aee5947fcd19e0df22a037d2806\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_objects_above_set_bytes",
|
||||
"about": "objects counting more bytes than the set",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":100,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"e8252796e0f15c08f717975750adc1b87ca0ad720f83bca81fc13c564c1c361a76b2f132b23b8a25e21e5b510ff0faefcf57c5624db3a9d308bade80dd3e1705\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_delivered_disagrees",
|
||||
"about": "a delivered stage of 2 while the second layer verified by inclusion only",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":8606,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":0,\"evidence\":\"inclusion-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"dropped\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"dd00286c35e70a7048f51acbc18e29928e1024cde80931bf89eed8037c1b1eb64abcaa04ed88a852c0eaeaa7b5804e36cde3b01f2e55aa368fe26682904d6c0a\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_delivered_disagrees"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_delivered_yet_applied_without_receipt",
|
||||
"about": "a delivered stage stated, so the base's receipt was verified first, and a second layer applied with no receipt verified: over such a base a layer applies only on a receipt verified over its bytes",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":6600,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}},{\"bytes\":700,\"evidence\":\"none\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":2,\"from\":0}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,2,2],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"e5373791189d9a0f06988cadfcefff6a364e2f29bc75895557c25c5ba1666c3f00ff5f1d85af7cfb2624c0680c2e8db85bb2249a0051403ddc3a3c09db51fa0a\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_unattested_inclusion_evidence_accept",
|
||||
"about": "over a base whose receipt was not verified first, a layer applies whatever was verified of its receipt: applied with inclusion-verified evidence, and no delivered stage stated",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5900,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"inclusion-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":null,\"op_delivered\":null,\"op_requested\":null,\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"58d61b198fbaab6da98f0e459550f2e12ab9b9c9d52ed68aa1b711db587ab644d3c73d324a981e104a8cd7e095db9555ef909ac172f7c54101e7060f7582cf0c\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "1e934c21bacb7b446dbb0e60ad9e3a71fa0d6af5405e2b55e3e7b5a669255745"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_units_without_objects",
|
||||
"about": "four units presented at stage 3 and no layer object observed",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5000,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[],\"op_committed\":null,\"op_delivered\":null,\"op_requested\":null,\"presented\":{\"wai-stage:layers\":3},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"fff794194094575ea16817f98fdffc17a125d72cb53c29a3103a4a66177e3e412b933afceedc715f5fb32a44697c631b5e3bfb9aeeecbedc814545f6b666470d\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_unsupported"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_units_above_their_layers",
|
||||
"about": "two units presented at stage 5 with a first layer alone observed",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5900,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,2,0,0,0,2],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"d9208d366027054217a1a511704450341162ceeb8927236d9229deddbcf62e3cdfe54ed711dc5b83e7995f96603ee5eeb590c1e265eabea0dff2e1c48ee2cf01\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_unsupported"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_arrived_with_no_bytes",
|
||||
"about": "objects applied, so they arrived, counted with no bytes",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":0,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"9abef7a5a61af14b75263d21412f678b2a1f398b293fd261f22a7de5cff0ecb7d8bf52120c208602b4b0de483b680ca80e9d6d9efce8a14a2bd494c46d92fa01\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_objects_as_many_bytes_as_the_set",
|
||||
"about": "the objects count every byte of the set, leaving none for its base",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":6006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"baa0228aadebbc17dfdb0c8bffc4b167ad865093457b2f951878438db188496d98d8567ee6104c33d45ec843f1c1b71289a6d8a63cba8eae6277c9e378f9b90b\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_one_layer_applied_twice",
|
||||
"about": "two different first layers both applied to frames 1 and 2: the second would conflict with the first or find them past its stage",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":6800,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"433bae8417245e600317f17e9ac65a2965fc7d2ee17fb8dc5a0d812262031444\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":3,\"from\":0}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"be48d71848d4f202e844b2dbf4339f25091b8f58d97482fd3f3438d6b9111373\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":3,\"from\":1}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"5ad069ab8ab501c1a55774b4baf2fbdd2fef3ef74812a9a03452711515578e54988ede1900c1be080e0df922243c7e6014f711799fd2a7790220833de2b51505\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_partial_counted_as_full",
|
||||
"about": "a first layer partial over four frames and all four at stage 2: a partial layer was not applied to every unit of its span",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":6800,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":\"layer-gap\",\"status\":\"partial\",\"units\":{\"count\":4,\"from\":0}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"2362dc8b305deaf131e1ef1821b4487fd8c384509927926bf0a2c2ed9effa46c093dea168684d928c52e7b53a90bab378cb5c72d56d17851f0c66cbc5f8c2909\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_unsupported"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_layer_over_units_never_reached",
|
||||
"about": "a first layer applied to frames 0-1 and a second applied to frames 2-3, which never had the first",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":6800,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":2,\"from\":0}},{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":2,\"from\":2}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":0},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[2,0,2],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"882b620c7057ffedd2a2c355200f12844578f2edf148fa0ea55033b8e5d09f989700cf2c67a3b92ccf61c187485e506d2985a2f9aef259027ab4a1aa6deb2a0f\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_unsupported"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_partial_layer_brings_every_unit",
|
||||
"about": "one first layer, partial over four frames, and all four stated at stage 1",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5900,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":\"deadline-missed\",\"status\":\"partial\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"886fa4051653a1427c1bd61c4f07c365df93a7e96fce05a1ece7a53d5aa74f5abcf68c098b793d10dff888b472666dd6ccb46bbbb6e3906993f3071f4f161601\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_units_unsupported"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_partial_layer_accept",
|
||||
"about": "the same first layer, partial over four frames and late for one: three frames at stage 1 and one at 0",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5900,\"conflicts\":0,\"deadline_misses\":1,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":\"deadline-missed\",\"status\":\"partial\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[1,3],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"8aa8afdb67a746d7f4931fd7fefff344dec7bca938fbf5c2b08271edde9a68d31286543f8f108cbb9c75dacf46a32ff8850f42fb371f87e71cf3da532400ad01\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "fc29beeb6515a01934bca2376ac55126de3b3b6bb3d311630fe2415769dc88c1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_partial_over_one_unit",
|
||||
"about": "a partial object over one unit: partial means applied to some units of its span and not to others",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":\"layer-gap\",\"status\":\"partial\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"3f56cb514ed9924cc439f8cde2531cf4e6aaf0add7c50af1ec00e2998bc658fbef9a474b4cf63925942ecde25e1ed2e4d6b8186b89c18a46f6c92f79217e3703\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_refused_unchecked_span_accept",
|
||||
"about": "a first layer whose span runs past the base's units, refused refines-malformed: refused before its span was checked against the set, it lists its units as its refines states them, and the claim stands",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11506,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":500,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"1f6b29a0fc6c612cf8e9c85070d7767888347bae655bc07386b4399d6cc81351\",\"reason\":\"refines-malformed\",\"status\":\"refused\",\"units\":{\"count\":10,\"from\":2}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"e72628e2e7d5790a3480ecb68ef4ac5493b8d41aa6b53f5355b8d42cda99304e142bd31e78036620cdd0987b4ed6edabfff3bed03f226937d00976d79cd3c307\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "5a1fbae3e038e23425a7ab3257ca862a2f61570e32f3404f4104f792fa0ad837"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_unchecked_layer_delivers_nothing_accept",
|
||||
"about": "the second layer dropped, and another second layer over units 0-9 refused refines-malformed, delivered over its bytes: refused before its span was checked, it is no layer of the set's, so the stage delivered stays 1",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":9106,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":500,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"1f6b29a0fc6c612cf8e9c85070d7767888347bae655bc07386b4399d6cc81351\",\"reason\":\"refines-malformed\",\"status\":\"refused\",\"units\":{\"count\":10,\"from\":0}},{\"bytes\":0,\"evidence\":\"inclusion-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"dropped\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"00090a20f9a798bc0813dd0c2932b9f5a1f4085cc295ccf3599e6491398db6df484a2f0dc8704ac4e66343426c4d767f9e65a38252bb43fdc98ea7c10f5afa0e\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "59fa3e6fa40b080e7cfbf6a2b8d159ed81bcce5b985c8f14315c287f685d7bc3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_checked_span_outside_units",
|
||||
"about": "the same span refused for a reason decided after the span was checked: its units must lie in the set",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11506,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":500,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"1f6b29a0fc6c612cf8e9c85070d7767888347bae655bc07386b4399d6cc81351\",\"reason\":\"prev-mismatch\",\"status\":\"refused\",\"units\":{\"count\":10,\"from\":2}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"b243b9eac9a1dc3ca929951a218ca341781dcb88040e1de6d3d60e3e7f6be7dc1e80b0ba7ea012899e3824e004f94d853bcb0f872ac2f91426dce63514094502\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_objects_malformed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_all_refused_meets_commitment_accept",
|
||||
"about": "both layers arrived delivered and were refused: delivered at stage 2, as committed, and presented at stage 0. Delivered and presented are separate measures",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":8300,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"d373fb250b03d721e1ac9de9f0225c60b5e543b416acce0f09d5dca8cc0fe92d\",\"reason\":\"prev-mismatch\",\"status\":\"refused\",\"units\":{\"count\":4,\"from\":0}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":\"prev-mismatch\",\"status\":\"refused\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"5f40d092d39396b2eac8539a95d01f86908c7fd1f0ec0828ab18f2ac59e4b29801cefad0150fbdb4013164ad2bc626833bc682092a94dc3654daa49f6afc7e06\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "e24e1ab7fc6944465b67d8aee9f8a21f2066fe82f64f3f5c4dd74aeecdbcdf71"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_delivered_over_a_hole",
|
||||
"about": "frame 2's first layer dropped, the others delivered: the first layer's delivered objects leave a hole, so the stage delivered is 0, not 1",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":0,\"evidence\":\"inclusion-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"dropped\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}}],\"op_committed\":null,\"op_delivered\":{\"wai-stage:layers\":1},\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[1,3],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"85993e27b4be376337695053265f76d50f1b2c63a9ff1e50feb0eb188ca5e7f3ab8ec884b74205b0602e0b1b4b7d05a2cae6d533f7809bfd4c9ef79692cbc203\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 4,
|
||||
"reason": "stage_delivered_disagrees"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_about_a_receipt",
|
||||
"about": "a stage claim's subject is the base, an object",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"c73a0a0e06721f582c62515f5a35799019cd2b7027338c225cd64e38698dec4253edce9f639b7977e8b4a7a4daf0b0734e5d40c823019e3d3e4781bd56b3fa0f\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"receipt\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 1,
|
||||
"reason": "subject_type_not_admitted"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_status_outside_enumeration",
|
||||
"about": "a status this extension does not define",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"skipped\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"d3ce9138c7d9d4b733d77aa0ef3a7e1d449b936816ed1cd89ca43499796f2a23fa4dfe59a45a6139cde6f6e2cffcd0a4f56a7d9418614788e22820f89af1280e\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_evidence_outside_enumeration",
|
||||
"about": "an evidence this extension does not define",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"096b0984c47c7fbef1b0095fa97bb41166978a5e66e5aef2c8feb87cd55d6eb407dafc6ed18d3bcb31e0495ee196982617239487f8428cf4546282aad4cae408\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_tier_outside_enumeration",
|
||||
"about": "a tier staged delivery does not define",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"exact\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"8cdac0695a2c6f031eed92bcd703eb3e7f2f6ce506392ac259626d002a2982dca0431351abb5b0bdb2da0ce54d2864dd916052c00b4ad837743dfdb2ccdc5e01\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_presented_null",
|
||||
"about": "presented is a point, never null",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":null,\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"7894c9047e4c4dbedf3c1a09588fcfcabaff6a47ae40ed99aeb7b0c94adf845fee5a970770a1c0ec93ad176244657a2f4305170eb03ca0de6ac2cce799d12704\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_object_missing_reason",
|
||||
"about": "an object without reason: a nullable member is present and null",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"b0e9638de0fb3cf48c56ec077b6517c13fada1424cdccc6c175d6f460108c524d5500f33d3f4b8914e8ac757e794c5f688b0b713d1401b7634610bc61712a705\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "missing_required_field"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_units_unknown_member",
|
||||
"about": "an object's units carrying a member they do not define",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0,\"to\":1}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"7aaf724f30b1f7b1e3cdd9e5ab07514a6f2dcdde315058d5b96872feacdd2cdd04c174c7d954a37ceb0645373c4e2449810c0d9ed8ad4c329bcb7c36f13de200\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "unknown_member"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_units_by_stage_string",
|
||||
"about": "a count written as a string",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,\"4\"],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"029cfd0412b7f48261123fb9a447461ad619e5d886043ab54907fdb8844ab981332d60600f15b0a2a78a35a53b415ab3cbeb4ecd6f6c7f8dd2a6c519d7ad2908\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_two_defects_vocabulary_first",
|
||||
"about": "a vocabulary that is not an absolute URI and an object without reason: vocabulary is read first",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"staged-delivery\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"5c62be3a343838fca3b6f317d687180c7d675acda2744975f24a3c063f0ca46fbb43ce1a5fad8186b3a213af32b7ed05fd4a9926271c33ce14c13212f9564d02\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stage_presented_rewritten",
|
||||
"about": "presented rewritten after signing",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":1},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"3bd5dea72e15792d996cfd1425effe215738a407d29092aa53ad00fda85fadb9a797c43f4b8a2ba5c13afb971daa02baa275e95ad40b013a9caf6160555f3c0d\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 3,
|
||||
"reason": "signature_verify_failed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_accept",
|
||||
"about": "a relay did not forward a layer object whose delivery timeout passed, and says so; its subject is the object receipt's content hash",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"00b8d56dbdac0715c3fccd686d1114ad6134793f825ffdf3850434a5f62e937208cb0eabd5d99af8c8377ede43278c4b2bbf71435eb46f9d9e720d4459c3d70d\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"gap_object": {
|
||||
"track_alias": 8,
|
||||
"group_id": 3,
|
||||
"object_id": 0,
|
||||
"content_hash": "78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "7debd010d851bd5da595099a0e5d6e99f74265501ec41dc556fdd05f23472e69"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_accept_without_the_receipt",
|
||||
"about": "the same claim checked without the object receipt: steps 1-5 only",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"00b8d56dbdac0715c3fccd686d1114ad6134793f825ffdf3850434a5f62e937208cb0eabd5d99af8c8377ede43278c4b2bbf71435eb46f9d9e720d4459c3d70d\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "7debd010d851bd5da595099a0e5d6e99f74265501ec41dc556fdd05f23472e69"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_chained_accept",
|
||||
"about": "the relay's second gap claim, chained to its first",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":1,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"not-received\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":\"7debd010d851bd5da595099a0e5d6e99f74265501ec41dc556fdd05f23472e69\",\"seq\":1,\"sig\":\"2905c38fefd7c67726dd04c84c3367df3ff6bb8bfc76fdf23103ae7b63df7e3ef6fd1d9dbbd7e606c6a7de20a7f9e4552bb32a4a9f69873251ba2b73b319f800\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"previous": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"00b8d56dbdac0715c3fccd686d1114ad6134793f825ffdf3850434a5f62e937208cb0eabd5d99af8c8377ede43278c4b2bbf71435eb46f9d9e720d4459c3d70d\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"gap_object": {
|
||||
"track_alias": 8,
|
||||
"group_id": 3,
|
||||
"object_id": 1,
|
||||
"content_hash": "78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "accept",
|
||||
"link_hash": "233111219dfc59572affc63974bfa8d50f72bbd99ab9306947c40c54346a9f04"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_subject_mismatch",
|
||||
"about": "the object receipt at the claim's coordinates names another object",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"00b8d56dbdac0715c3fccd686d1114ad6134793f825ffdf3850434a5f62e937208cb0eabd5d99af8c8377ede43278c4b2bbf71435eb46f9d9e720d4459c3d70d\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"gap_object": {
|
||||
"track_alias": 8,
|
||||
"group_id": 3,
|
||||
"object_id": 0,
|
||||
"content_hash": "11960518bdcb35cd116fefa960a378f636fca122c98d09dccc6ecbbcc642fbaf"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 6,
|
||||
"reason": "gap_subject_mismatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_other_coordinates",
|
||||
"about": "an object receipt at other coordinates is not the claim's context",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"00b8d56dbdac0715c3fccd686d1114ad6134793f825ffdf3850434a5f62e937208cb0eabd5d99af8c8377ede43278c4b2bbf71435eb46f9d9e720d4459c3d70d\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"gap_object": {
|
||||
"track_alias": 8,
|
||||
"group_id": 3,
|
||||
"object_id": 1,
|
||||
"content_hash": "78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 6,
|
||||
"reason": "context_mismatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_object_given_to_a_stage_claim",
|
||||
"about": "an object receipt given to a claim that is not a gap",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":11006,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[{\"bytes\":900,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3b44c8434c6a0f5f04371d3d80c647316fa9410015f1228ee65f48592fcf66f4\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":0}},{\"bytes\":901,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"5580da24f9b5743d209857fdec1470e8da201e9f492d848a18e0f1bc50dd9e57\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":1}},{\"bytes\":902,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"3f6fe18c07d03478fe29766bed3654beff4e5b17bcd630a5dbc1dc684736ded6\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":2}},{\"bytes\":903,\"evidence\":\"delivered-verified\",\"layer\":1,\"object\":\"73debde9130401eb354ab7208f4c709850f5a107e991ce7d3001ba2c1924ecd1\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":1,\"from\":3}},{\"bytes\":2400,\"evidence\":\"delivered-verified\",\"layer\":2,\"object\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"reason\":null,\"status\":\"applied\",\"units\":{\"count\":4,\"from\":0}}],\"op_committed\":{\"wai-stage:layers\":2},\"op_delivered\":{\"wai-stage:layers\":2},\"op_requested\":{\"wai-stage:layers\":2},\"presented\":{\"wai-stage:layers\":2},\"stage_hash\":\"082bc00210e6d32f18f39ee723d863fd7b99f206a415ac2f44a8687793b2e068\",\"tier\":\"decode-equivalence\",\"units_by_stage\":[0,0,4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"3bd5dea72e15792d996cfd1425effe215738a407d29092aa53ad00fda85fadb9a797c43f4b8a2ba5c13afb971daa02baa275e95ad40b013a9caf6160555f3c0d\",\"signer_id\":\"did:example:sink\",\"signer_pubkey\":\"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48\",\"subject\":\"3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"gap_object": {
|
||||
"track_alias": 8,
|
||||
"group_id": 3,
|
||||
"object_id": 0,
|
||||
"content_hash": "3844a5152c86c7892dc6c942553da2496c949edd7e32dbbc1a013c4c5d41f54f"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 6,
|
||||
"reason": "context_mismatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_reason_outside_enumeration",
|
||||
"about": "a reason this extension does not define",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"ca3fa98361b5a7c5fbc8488fd14d34777cbd0c1147b0d0a35d12a3b9ae9893ccffd579b627d778906cff098ec8207acc62d6d264160a6e8a55b359fbd4568a0b\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_observed_at_not_a_date_time",
|
||||
"about": "2026-02-29 is not a date",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-02-29T00:00:00Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"ca73ba0acad45b40c68c4cb6332ee20cf3327ee8e2cc097f30ad5772adf33d16e5e5d6f2c16d908465a3e89e1716cdb7da9704824fa25123eaf06546cad4370f\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"object\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "field_type"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gap_about_a_receipt",
|
||||
"about": "a gap's subject is the object not forwarded",
|
||||
"procedure": "claim",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-27T12:00:02Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-27T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"67c61afe297a0b7f3cca42c07431e0c32f3ed536986a975f742c3fabc33c852c35cd8cf55de4b876bcbe1f71fb64526090579d1e5391e0e61537dcb6693dc002\",\"signer_id\":\"did:example:relay\",\"signer_pubkey\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"subject\":\"78f06a454eba49e220acd99a6c871657bc8f230bc3102a81b7e1241f70824db9\",\"subject_type\":\"receipt\"}",
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 1,
|
||||
"reason": "subject_type_not_admitted"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,6 +295,13 @@ the prescriptive standards nor the measurement standards provide that pair: one
|
|||
says what should be spent, the other what was, and nobody scores the second
|
||||
against the first.
|
||||
|
||||
Staged delivery registers the vocabulary `urn:wai:staged-delivery:1`, whose one
|
||||
parameter is `wai-stage:layers`, the number of layers a prefix of a stage set holds
|
||||
([staged-delivery](staged-delivery.md)). A `stage` claim (jwp-receipts §2.4.10) records
|
||||
the stage requested, committed, delivered and presented as points of it. Its signer
|
||||
observes what arrived rather than scoring an operation against a request, so it may
|
||||
record a delivered stage with no commitment beside it.
|
||||
|
||||
### 7.3 The residual
|
||||
|
||||
When a declared complexity or operating-point descriptor and a measured energy
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ A component's exploded envelope ([SPEC](../SPEC.md) §7.1, *Exploding a componen
|
|||
a function of the verified head and the component's bytes, so a client that holds them
|
||||
builds it. A receipt about that exploded envelope is checked under step 4 over the
|
||||
envelope the client built, as for any object: this is how a refinement component
|
||||
fetched by range meets [staged-delivery](staged-delivery.md) §10, which applies a layer
|
||||
fetched by range meets [staged-delivery](staged-delivery.md) §10.1, which applies a layer
|
||||
only on a receipt checked over its envelope bytes.
|
||||
|
||||
### 8.4 Keys, statuses and grants
|
||||
|
|
@ -587,7 +587,11 @@ layer under §10.1 and §10.2.
|
|||
A refinement a client abandoned is not delivered to it. Its receipts stay listed in
|
||||
the index; the client neither presents it nor reports its delivery, and the group
|
||||
receipt stays valid for the objects it did receive ([jwp-receipts](jwp-receipts.md)
|
||||
§4.4).
|
||||
§4.4). A client that checks the abandoned refinement's object receipt from the index
|
||||
without its bytes finds it `inclusion-verified` (jwp-receipts §3, *Verified by
|
||||
inclusion*): signed and not delivered, never applied (staged-delivery §10.1). A `stage`
|
||||
claim the client signs lists it as `dropped` with that evidence, or as `late` if it
|
||||
arrived after its units were due (staged-delivery §10.2).
|
||||
|
||||
## 11. HLS and DASH adjacency
|
||||
|
||||
|
|
|
|||
|
|
@ -28,16 +28,22 @@ In scope:
|
|||
signature.
|
||||
- A per-group Ed25519 signature over canonical group metadata.
|
||||
- A typed receipt envelope (JSON or equivalent CBOR) with explicit
|
||||
parent-receipt linkage for cross-hop preservation.
|
||||
parent-receipt linkage for cross-hop preservation: one chain for each
|
||||
publisher key and track (§3 step 3, §6.1).
|
||||
- Verifier behaviour, relay preservation requirements, and conformance
|
||||
test vectors.
|
||||
- **Typed receipt classes** (§2.4): every receipt belongs to exactly one
|
||||
class. `group` and `object` receipts are the **delivery** class; a
|
||||
`claim` receipt (§2.4.3) carries one of the others — `energy`,
|
||||
`lawful-basis`, `log-inclusion`, `measurement`, `operating-point`
|
||||
(§2.4.5–§2.4.9) — on one envelope, one canonical form and one signature
|
||||
rule, so a statement other than delivery does not need a field set of
|
||||
its own.
|
||||
`lawful-basis`, `log-inclusion`, `measurement`, `operating-point`,
|
||||
`stage`, `delivery-gap` (§2.4.5–§2.4.11) — on one envelope, one canonical
|
||||
form and one signature rule, so a statement other than delivery does not
|
||||
need a field set of its own.
|
||||
- **Objects not delivered** (§3, §4.4): an object a subscriber did not
|
||||
receive is accounted for by its receipt, checked without the object's bytes
|
||||
and reported as `inclusion-verified`, never as delivered; a relay forwards
|
||||
the receipts of the objects it does not forward, and may say why in a
|
||||
`delivery-gap` claim (§2.4.11).
|
||||
- An optional acquisition-class label on the object receipt, bound with the
|
||||
object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an
|
||||
optional class for the group total, signed under a labelled group payload
|
||||
|
|
@ -159,7 +165,7 @@ payload is the big-endian concatenation:
|
|||
signing_payload =
|
||||
"moq-jwp:group-sign\x01" // domain
|
||||
|| u64_be(track_alias) // optional carrier-scoped id (0 if unused)
|
||||
|| u64_be(group_id) // monotonic per-track group counter
|
||||
|| u64_be(group_id) // monotonic per (publisher_key, track_alias)
|
||||
|| merkle_root[32] // §2.2
|
||||
|| u64_be(leaf_count) // number of objects in the group
|
||||
|| u64_be(total_joules_micro) // sum of microjoules across the group
|
||||
|
|
@ -209,7 +215,7 @@ Required fields:
|
|||
|---|---|---|
|
||||
| `kind` | string | MUST equal `"group"`. |
|
||||
| `track_alias` | u64 | Carrier-scoped track identifier; `0` if unused. |
|
||||
| `group_id` | u64 | Monotonic group counter under `publisher_key`. |
|
||||
| `group_id` | u64 | Monotonic group counter under (`publisher_key`, `track_alias`). |
|
||||
| `root_hash` | 32 B hex | §2.2 Merkle root over object content hashes. |
|
||||
| `leaf_count` | u64 | Number of objects covered by `root_hash`. |
|
||||
| `joules_total` | u64 | Sum of microjoules across the group's objects; `0` is the unmetered marker ([energy-measurement](energy-measurement.md) §2). Its class, where it carries one, is in the labelled members below; a verifier holding every object checks it, or, for a group without one, reads the total from the objects' labels (§3, *The group total*). |
|
||||
|
|
@ -218,7 +224,7 @@ Required fields:
|
|||
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
|
||||
| `signer_id` | string | Bound identity string (DID, URN, or display). In §7.1's origin-bound form it names the web origin whose key log lists `signer_pubkey`; an Ed25519 `did:key` names one key; any other form is a label, and a verifier reads nothing from it. |
|
||||
| `sig` | 64 B hex | Ed25519 signature over §2.3 payload. |
|
||||
| `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key; `null` for the first group. |
|
||||
| `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key and `track_alias` (its link hash, §3 step 3); `null` for the first group of that track. |
|
||||
|
||||
**The total's class (labelled form).** A group whose total carries a class of
|
||||
its own adds these members and is signed over the labelled payload (§2.3):
|
||||
|
|
@ -378,6 +384,8 @@ key.
|
|||
| `binding-recovery` | `claim` | `object` (the ancestor) | §2.6.3 | the party that performed the recovery |
|
||||
| `measurement` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.8 | the key that signed the figure: the group's publisher for `moq-jwp:object`, the receipt's signer for every other profile |
|
||||
| `operating-point` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.9 | the party that records the request, the commitment and the delivery |
|
||||
| `stage` | `claim` | `object` (the stage set's base) | §2.4.10 | the party that observed the stage set |
|
||||
| `delivery-gap` | `claim` | `object` (the object not forwarded) | §2.4.11 | the relay that did not forward it |
|
||||
|
||||
Rules:
|
||||
|
||||
|
|
@ -746,7 +754,7 @@ figure. It is signed by the party that records them.
|
|||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `figure_profile`, `figure_index`, `group`, `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | | As §2.4.8. |
|
||||
| `vocabulary` | absolute URI | The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none. |
|
||||
| `vocabulary` | absolute URI | The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none except [staged-delivery](staged-delivery.md)'s `urn:wai:staged-delivery:1` ([energy-binding](energy-binding.md) §7.2), whose points are a `stage` claim's (§2.4.10). |
|
||||
| `op_requested` | point or `null` | What the requesting party asked for; `null` when nothing was requested. |
|
||||
| `op_committed` | point or `null` | What the acting party said it would use. |
|
||||
| `op_delivered` | point or `null` | What was used. |
|
||||
|
|
@ -783,6 +791,130 @@ was. An accepted claim establishes what its signer recorded about the request,
|
|||
the commitment, the delivery and the prediction. An `op_requested` in a claim
|
||||
the acting party signed is that party's account of the request.
|
||||
|
||||
#### 2.4.10. `class: "stage"`
|
||||
|
||||
A stage set ([staged-delivery](staged-delivery.md)) is a base and the layers that
|
||||
refine it, each its own object with its own delivery receipt. A `stage` claim
|
||||
records what one party observed of a stage set: what it asked for and was promised,
|
||||
which layer objects arrived or did not and what it verified of their receipts, what
|
||||
it presented on time, and how the presented output is reproduced. Its subject is the
|
||||
base's §2.1 content hash. It is signed by the party that observed the set: a sink, or
|
||||
another party that applies the set as a sink does (staged-delivery §10.2).
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `vocabulary` | absolute URI | `urn:wai:staged-delivery:1` ([energy-binding](energy-binding.md) §7.2), whose points have one parameter, `wai-stage:layers`: the number of layers a prefix of the set holds. |
|
||||
| `op_requested` | point or `null` | The stage asked for. |
|
||||
| `op_committed` | point or `null` | The stage the serving party said it would deliver. |
|
||||
| `op_delivered` | point or `null` | The stage the delivered objects reach: the highest *k* such that, at every layer from 1 to *k*, the objects listed with `delivered-verified` evidence cover every unit of the set, whatever became of them, an object refused before its `refines` was checked against the set excepted. `null` when the observer did not verify the base's delivery receipt over its bytes before it offered the base's first layer. |
|
||||
| `presented` | point | The lowest stage at which a unit was presented on time. |
|
||||
| `units_by_stage` | array of integers | Element *i*: the number of units presented at stage *i*. The set's units, numbered from 0, are the units it counts. |
|
||||
| `tier` | string | `decode-equivalence`, `entropy-consistency` or `none`: the weakest tier of the presented units (staged-delivery §5). |
|
||||
| `stage_hash` | 32 B hex or `null` | The stage hash of the presented output over every unit (staged-delivery §5). |
|
||||
| `objects` | array | Every layer object observed, each an object of the table below, ordered by `layer`, then `units.from`, then `object`. |
|
||||
| `conflicts` | integer | Units frozen by a conflict (staged-delivery §4.1 step 7). |
|
||||
| `deadline_misses` | integer | Units presented below the stage their layers would have reached on time (staged-delivery §8). |
|
||||
| `bytes_envelopes` | integer | Envelope bytes received for the set, the base's included (staged-delivery §11). |
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `layer` | integer | The object's `refines.layer`. |
|
||||
| `object` | 32 B hex | Its §2.1 content hash. |
|
||||
| `units` | object | `{ "from": integer, "count": integer }`: the units it covers, its `refines.span` or every unit of the set. For an object refused before its `refines` was checked against the set — a reason of staged-delivery §4.1 steps 1 and 2, `refines-malformed`, `capability-not-refinement` or `base-absent` — the units its `refines` states, which need not lie in the set. |
|
||||
| `bytes` | integer | Its envelope bytes as received: more than `0` for an object that arrived, `0` for one that did not. |
|
||||
| `status` | string | `applied` (to every unit it covers), `partial` (to some of them), `late` (it arrived, and missed the deadline of every unit it covers), `dropped` (it never arrived: not forwarded, or abandoned at a deadline) or `refused` (it arrived, and applied to no unit for another reason). |
|
||||
| `evidence` | string | What the observer verified of its delivery receipt: `delivered-verified` (§3 step 6, over its envelope bytes), `inclusion-verified` (§3, *Verified by inclusion*) or `none`. |
|
||||
| `reason` | string or `null` | For `partial`, the staged-delivery §4.2 reason of the lowest unit it was not applied to; for `refused`, the envelope's reason, or else that of its lowest unit. `null` for `applied`, `late` and `dropped`. A unit the object is still held for when the claim is made counts as not applied, for the reason it is held for. |
|
||||
|
||||
Rules, checked at §3.1 step 4 in this order:
|
||||
1. `vocabulary` **MUST** be `urn:wai:staged-delivery:1`, and every point present,
|
||||
`presented` among them, **MUST** be exactly the member `wai-stage:layers`, from 0
|
||||
to 255 (`stage_point_malformed`). `op_requested` and `op_committed` **MUST NOT**
|
||||
be present without `op_delivered` (`stage_point_incomplete`, as §2.4.9 rule 4).
|
||||
§2.4.9 rule 3 does not apply: an observer states what was delivered whether or
|
||||
not anyone committed to it.
|
||||
2. `stage_hash` **MUST** be present exactly when `tier` is `decode-equivalence`
|
||||
(`stage_hash_without_tier`).
|
||||
3. `units_by_stage` **MUST** have from 1 to 256 elements summing to at least 1, its
|
||||
last element not 0; `presented` **MUST** name the lowest stage with a unit; and
|
||||
neither `conflicts` nor `deadline_misses` may exceed the number of units
|
||||
(`stage_units_malformed`).
|
||||
4. Each object's `layer` **MUST** be from 1 to 255 and its `units.count` at least 1
|
||||
(at least 2 for a `partial` object, which applied to some units of its span and
|
||||
not to others), and its units **MUST** lie within the set's unless it is `refused` before its
|
||||
`refines` was checked against the set (the table above); its `reason` **MUST** be
|
||||
`null` exactly where the table says so and otherwise a reason of staged-delivery
|
||||
§4.2. Where `op_delivered` is not `null` — the base's receipt was verified first —
|
||||
an `applied` or `partial` object's `evidence` **MUST** be `delivered-verified`,
|
||||
since over such a base a layer applies only on a receipt verified over its bytes
|
||||
(staged-delivery §10.1); where it is `null`, a layer applied whatever was verified
|
||||
of its receipt, and any evidence is stated. A `dropped` object's `evidence` **MUST
|
||||
NOT** be `delivered-verified`. An object that arrived — `applied`, `partial`,
|
||||
`late` or `refused` — **MUST** have `bytes` above 0, and a `dropped` one 0. The
|
||||
objects **MUST** be in strictly increasing order of (`layer`, `units.from`,
|
||||
`object`); their `bytes` **MUST** sum to less than `bytes_envelopes`, which counts
|
||||
the base's bytes too; and no unit **MAY** be covered by two `applied` objects of
|
||||
one layer, since the second would conflict with the first or find the unit past
|
||||
its stage (`stage_objects_malformed`).
|
||||
5. A unit reaches stage *s* only through an object of each layer up to *s* applied to
|
||||
it, and an object of layer *s* applies only to a unit an object of layer *s* − 1
|
||||
applied to. So, for every layer *s* from 1 up (`stage_units_unsupported`):
|
||||
- an `applied` object of layer *s* **MUST** lie within the spans of the `applied`
|
||||
and `partial` objects of layer *s* − 1 (for layer 1, within the set), and a
|
||||
`partial` one **MUST** meet them;
|
||||
- the units presented at stage *s* or above **MUST NOT** outnumber the units layer
|
||||
*s* can have reached: the units its `applied` objects cover, and, of the units
|
||||
inside the spans of layer *s* − 1 that those do not cover, those its `partial`
|
||||
objects cover, counting no more than its span less one for each `partial` object,
|
||||
which was not applied to every unit of its span. Two objects of one layer never
|
||||
both apply to a unit (rule 4), so their applied units do not overlap.
|
||||
6. Where `op_delivered` is not `null`, it **MUST** be the stage the objects give
|
||||
(`stage_delivered_disagrees`). Where it is `null`, this rule does not apply. A
|
||||
stated `op_delivered` is then never below `presented`: by rules 4 and 5, at every
|
||||
layer up to the stage presented, the spans of the `applied` and `partial` objects,
|
||||
each verified over its bytes, hold every unit, though a `partial` object was not
|
||||
applied to all of its own.
|
||||
|
||||
`op_delivered` and `presented` are separate measures. `op_delivered` counts what
|
||||
arrived under a receipt verified over its bytes, whether or not the sink applied it;
|
||||
`presented` and `units_by_stage` count what the sink presented on time. A set whose
|
||||
layers all arrived and were all refused is delivered at their stage, and so meets a
|
||||
commitment to it, with stage 0 presented; a reader that asks what was presented reads
|
||||
`presented`, never `op_delivered`.
|
||||
|
||||
A reader derives whether the commitment was met as §2.4.9 derives it: it was when
|
||||
`op_delivered` equals `op_committed`, and there was none when `op_committed` is
|
||||
`null`. An accepted claim establishes what its signer observed, not that content was
|
||||
delivered: only `group` and `object` receipts state delivery, and an object's
|
||||
`evidence` is the signer's account of what it verified, which a reader holding the
|
||||
receipt checks under §3 itself.
|
||||
|
||||
#### 2.4.11. `class: "delivery-gap"`
|
||||
|
||||
A relay that does not forward an object still forwards the object's receipts (§4.1).
|
||||
A `delivery-gap` claim adds the relay's own statement that it did not forward it, and
|
||||
why. Its subject is the object's §2.1 content hash, and the relay signs it under its
|
||||
own key, as the parallel receipt §4.3 permits.
|
||||
|
||||
| member | type | meaning |
|
||||
|---|---|---|
|
||||
| `track_alias` | integer | The object's `track_alias`. |
|
||||
| `group_id` | integer | Its `group_id`. |
|
||||
| `object_id` | integer | Its `object_id`. |
|
||||
| `reason` | string | `delivery-timeout` (its delivery timeout passed at this relay), `discretion` (the relay chose not to forward it) or `not-received` (the relay never received it). |
|
||||
| `observed_at` | string | A date-time (§2.4.3): when the relay observed the gap. |
|
||||
|
||||
The coordinates are integers of §2.4.3, so an object at a `track_alias`, `group_id` or
|
||||
`object_id` above 2^53 − 1 has no gap claim.
|
||||
|
||||
**Against the object receipt** (§3.1 step 6): the object receipt at the claim's
|
||||
coordinates, verified by inclusion at least. In order: it is at the claim's
|
||||
`track_alias`, `group_id` and `object_id` (`context_mismatch`), and its `content_hash`
|
||||
is the claim's `subject` (`gap_subject_mismatch`). A claim checked without that receipt
|
||||
stops at step 5: it establishes that the relay says it did not forward an object with
|
||||
this content hash, not that the publisher sent one. An accepted claim says nothing
|
||||
about whether the object reached anyone by another path.
|
||||
|
||||
### 2.5. Worked example
|
||||
|
||||
Manifest JSON (group receipt, formatted for readability — wire form
|
||||
|
|
@ -1073,15 +1205,46 @@ match the algorithm style of WAI SPEC.md §4 (numbered, terminating).
|
|||
`signer_pubkey` under signature, and for a labelled group the total's
|
||||
class.
|
||||
|
||||
3. **Check parent linkage.** If the verifier maintains receipt-chain
|
||||
state for `signer_pubkey`, compute `BLAKE3("moq-jwp:receipt-link\x01"
|
||||
|| canonical_bytes(previous_group_receipt))` and compare to
|
||||
`parent_receipt_hash`. If they differ, reject (`chain_break`). If
|
||||
no previous receipt is known and `parent_receipt_hash` is `null`,
|
||||
accept as the chain head. A non-null `parent_receipt_hash` without
|
||||
a corresponding previously-verified receipt is a recoverable
|
||||
condition (`parent_not_held`): the verifier MAY request the missing
|
||||
receipt before accepting; it MUST NOT silently accept.
|
||||
3. **Check parent linkage.** Receipt chains are kept per
|
||||
(`signer_pubkey`, `track_alias`): one chain for each publisher key and
|
||||
track, so a group on another track, or under another key, is never a
|
||||
group's predecessor, and is not held as one. Against the chain of the
|
||||
group's key and track, with the **link hash** of a group defined below:
|
||||
- the chain's head offered again passes: another of its objects is
|
||||
being verified;
|
||||
- with no group of the chain held, a `null` parent starts the chain, and
|
||||
any other parent is `parent_not_held`;
|
||||
- with the chain's head held, a parent that is the head's link hash
|
||||
links the group; a `null` parent is `chain_break` (a second chain
|
||||
head); a parent that is the link hash of another group of the chain
|
||||
the verifier holds is `chain_break` (a fork); any other parent names a
|
||||
group the verifier does not hold, and is `parent_not_held`.
|
||||
|
||||
`parent_not_held` is recoverable: the verifier MAY request the missing
|
||||
receipt and offer it before the group again; it MUST NOT silently accept.
|
||||
A verifier that keeps only the head cannot tell a fork from a gap and
|
||||
reports both `parent_not_held`; one that keeps the link hashes of
|
||||
earlier groups of the chain tells them apart, and refuses both either
|
||||
way.
|
||||
|
||||
**A legacy link authenticates nothing.** The legacy payload (§2.3) does
|
||||
not sign `parent_receipt_hash`, and a legacy group's link hash does not
|
||||
cover it, so a relay can rewrite it at will: a legacy group linked to the
|
||||
head shows only that someone wrote the head's link hash beside it. The
|
||||
legacy payload does sign `group_id`. A verifier therefore **MUST** refuse
|
||||
a legacy group whose `group_id` does not exceed its chain head's
|
||||
(`group_id_not_increasing`), whatever it does for labelled groups:
|
||||
`group_id` is the only order a legacy chain signs, and without this rule
|
||||
a rewritten link replays the chain's history as new. The labelled
|
||||
payload signs the link, and its link hash covers it.
|
||||
|
||||
A verifier that applies §6.1's opt-in checks also refuses here a
|
||||
labelled group whose `group_id` does not exceed its chain head's
|
||||
(`group_id_not_increasing`), and a group whose `group_end` is outside
|
||||
its freshness window (`group_not_fresh`), unless its signed `group_id` is
|
||||
below the chain's live edge (§6.1). A group that passes steps 1–3 MAY become
|
||||
the head of its chain whether or not any of its objects arrives (§4.4),
|
||||
so a dropped object does not break the link of its track's next group.
|
||||
`canonical_bytes(G)` is G's §2.3 signing payload, in the form G was
|
||||
signed in, followed by its 64-byte `sig`. The resulting hash is G's
|
||||
**link hash**: the identity a `claim` names G by (§2.4.3), and a
|
||||
|
|
@ -1134,8 +1297,8 @@ match the algorithm style of WAI SPEC.md §4 (numbered, terminating).
|
|||
labelled, its `joules_micro`, its class, the class's evidence and its
|
||||
`origin` are bound with them. Where it is not, `joules_micro` and
|
||||
`origin` are the publisher's unsigned statements. The verifier MAY now
|
||||
persist the group receipt as the new chain head for `signer_pubkey`
|
||||
and dispatch the WAI envelope per WAI SPEC.md §4. Whose key
|
||||
persist the group receipt as the new chain head for (`signer_pubkey`,
|
||||
`track_alias`) and dispatch the WAI envelope per WAI SPEC.md §4. Whose key
|
||||
`signer_pubkey` is, is §7's to confirm; a group receipt needs the
|
||||
authority `delivery`, and the energy authority of each class its
|
||||
labelled leaves and its labelled total carry (§7.5).
|
||||
|
|
@ -1146,6 +1309,17 @@ verification (e.g. valid Merkle proof against an unverified root): a
|
|||
reading of an object's figure, a group total and a `WAI_RECEIPT`
|
||||
property are taken only from objects accepted under steps 1–6.
|
||||
|
||||
**Verified by inclusion.** An object receipt checked without the object's
|
||||
envelope bytes — steps 1, 2, 3 and 5, step 4 not run because there are no
|
||||
bytes to hash — is **`inclusion-verified`**: the publisher signed an object
|
||||
with this content hash at this position under a root the group signature
|
||||
binds. It is not step 6 acceptance. A verifier **MUST** report it as
|
||||
`inclusion-verified`, **MUST NOT** report it, or anything read from it, as
|
||||
delivered, and **MUST NOT** dispatch an envelope on it. It is how a subscriber
|
||||
accounts for an object it did not receive (§4.4), and how a `stage` claim names
|
||||
the evidence for such an object (§2.4.10). An object receipt accepted under
|
||||
steps 1–6 over the object's bytes is **`delivered-verified`**.
|
||||
|
||||
**The group total.** A verifier that holds, for every `leaf_index` below
|
||||
the group's `leaf_count`, exactly one object receipt it has accepted under
|
||||
steps 1–6 **MUST** check that their `joules_micro` sum to `joules_total`;
|
||||
|
|
@ -1204,7 +1378,9 @@ order listed.
|
|||
string that is not lowercase hex, two digits a byte, or a hash that
|
||||
is not 64 of them; a value outside an enumeration this extension
|
||||
defines here (`record`, `binding_type`, a bracket's `opening`, a build's
|
||||
`profile`); a URI that is not absolute;
|
||||
`profile`, a stage's `tier`, a stage object's `status` and `evidence`, a
|
||||
gap's `reason`); a date-time that is not one of §2.4.3 (a gap's
|
||||
`observed_at`); a URI that is not absolute;
|
||||
a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or
|
||||
holding a White_Space or control character); or a malformed
|
||||
`oer_receipt`; or an operating point whose member names are not terms
|
||||
|
|
@ -1228,7 +1404,10 @@ order listed.
|
|||
samples, liveness facts that do not show a live rail, a zero window,
|
||||
completion or thread count, or a busy mean not above its idle mean;
|
||||
the figure and least uncertainty a bracket gives; an empty point; and
|
||||
which points and predictions are present together.
|
||||
which points and predictions are present together; and, in a `stage`
|
||||
claim, the vocabulary, a point's parameter and range, a reason of
|
||||
staged-delivery's, and how the units, the objects and the delivered stage
|
||||
agree.
|
||||
3. **Signature.** Ed25519 (RFC 8032). This step restates RFC 8032's
|
||||
own decoding and range rules, and fixes the two choices on which
|
||||
RFC 8032 verifiers differ — whether a key or `R` of small order is
|
||||
|
|
@ -1278,7 +1457,9 @@ order listed.
|
|||
declaration (§2.4.6);
|
||||
a recovery record against its declaration together with the
|
||||
publisher key of a verified delivery receipt for the declaration's
|
||||
subject (§2.6.3), and never against the declaration alone. A
|
||||
subject (§2.6.3), and never against the declaration alone; a
|
||||
`delivery-gap` claim against the object receipt at its coordinates,
|
||||
verified by inclusion at least (§2.4.11). A
|
||||
declaration is verified here under steps 1–4, and one that fails
|
||||
them — or is not JSON — is `context_invalid`: its members,
|
||||
`signer_pubkey` among them, are its signer's only once its signature
|
||||
|
|
@ -1340,6 +1521,13 @@ and never folds it in: confirmation changes the outcome of no step here.
|
|||
| 4 | `operating_point_delivered_alone` | `op_delivered` is present without `op_committed` |
|
||||
| 4 | `operating_point_incomplete` | `op_requested` or `op_committed` is present without `op_delivered` |
|
||||
| 4 | `operating_point_empty` | neither `op_delivered` nor `prediction` is present |
|
||||
| 4 | `stage_point_malformed` | a `stage` claim's vocabulary is not `urn:wai:staged-delivery:1`, or one of its points is not exactly `wai-stage:layers` from 0 to 255 |
|
||||
| 4 | `stage_point_incomplete` | `op_requested` or `op_committed` is present in a `stage` claim without `op_delivered` |
|
||||
| 4 | `stage_hash_without_tier` | `stage_hash` is present without `decode-equivalence`, or absent with it |
|
||||
| 4 | `stage_units_malformed` | no unit, more than 256 stages, a last stage with no unit, a `presented` other than the lowest stage with a unit, or more conflicts or deadline misses than units |
|
||||
| 4 | `stage_objects_malformed` | a reported object out of order, at layer 0 or above 255, over no unit (a `partial` one over fewer than two), outside the units when its span was checked, with a reason where none belongs or none where one does; applied or partial with evidence other than `delivered-verified` beside a stated `op_delivered`; dropped with `delivered-verified` evidence; arrived with no bytes or dropped with some; objects counting no fewer bytes than the set; or two `applied` objects of one layer over one unit |
|
||||
| 4 | `stage_units_unsupported` | an `applied` object of layer *s* outside the spans of layer *s* − 1's `applied` and `partial` objects, or a `partial` one that does not meet them; or more units at stage *s* or above than layer *s* can have reached, a `partial` object counting no more than its span less one |
|
||||
| 4 | `stage_delivered_disagrees` | `op_delivered` is not the stage the reported objects give |
|
||||
| 6 | `energy_disagrees_with_delivery` | the figure differs from the `object` receipt's |
|
||||
| 6 | `energy_class_disagrees_with_delivery` | the object's leaf is labelled, and the `energy` claim's class or evidence differs from the label |
|
||||
| 6 | `figure_disagrees` | the receipt a `measurement` or `operating-point` claim names carries another figure at `figure_index` |
|
||||
|
|
@ -1357,6 +1545,7 @@ and never folds it in: confirmation changes the outcome of no step here.
|
|||
| 6 | `recovery_signer_mismatch` | the declaration a recovery record names (§2.6.3), or a recovery runs through (§3.2), is not signed by the publisher of a verified delivery receipt for its subject |
|
||||
| 6 | `recovery_value_mismatch` | the value does not match under the algorithm's rule |
|
||||
| 6 | `recovery_no_matcher` | the verifier has no matcher for the algorithm |
|
||||
| 6 | `gap_subject_mismatch` | the object receipt at a `delivery-gap` claim's coordinates names another object |
|
||||
|
||||
### 3.2. Recovery through a soft binding, and the ancestor rule
|
||||
|
||||
|
|
@ -1425,7 +1614,10 @@ hand.
|
|||
| §3 step 1 | `bad_signer_key` | `signer_pubkey` does not decode under RFC 8032 §5.1.3, or is of small order |
|
||||
| §3 step 1, 2 | `group_label_invalid` | a group's labelled members appear without `"sign_version": 2`, its `sign_version` is not `2`, or its labelled members cannot be read (step 1), or its class breaks §2.4.1 with the group alone (step 2) |
|
||||
| §3 step 2 | `signature_verify_failed` | the group signature fails §3.1 step 3's rule over the payload of the group's form |
|
||||
| §3 step 3 | `chain_break`, `parent_not_held` | as §3.1 step 5, for groups |
|
||||
| §3 step 3 | `chain_break` | within the chain of the group's (`signer_pubkey`, `track_alias`), whose head the verifier holds: a `null` parent (a second chain head), or a parent that is another group of the chain the verifier holds (a fork) |
|
||||
| §3 step 3 | `parent_not_held` | a non-null parent that names no group of the chain the verifier holds: requested, never accepted silently |
|
||||
| §3 step 3 | `group_id_not_increasing` | a `group_id` no greater than its chain head's: for a legacy group always, for a labelled one under §6.1's opt-in guard |
|
||||
| §3 step 3 | `group_not_fresh` | under §6.1's opt-in freshness bound, a `group_end` older than the window, further ahead of the verifier's clock than the skew it allows, or not a date-time, in a group whose `group_id` is not below the chain's live edge |
|
||||
| §3 step 4 | `content_hash_mismatch` | the envelope does not hash to `content_hash` |
|
||||
| §3 step 5 | `object_label_invalid` | an object receipt's class label cannot be read or breaks §2.4.2: legacy `HwShunt`, a class beside a zero figure, the unmetered marker beside a non-zero one, a refined class without its evidence or below the resolution floor, or evidence without a class |
|
||||
| §3 step 5 | `proof_position_mismatch` | the object is not at the position its proof walks: `leaf_index` outside the group, a proof of the wrong length, direction bits other than the index's, an `object_id` other than the index, or another group's coordinates |
|
||||
|
|
@ -1468,6 +1660,14 @@ chain across hops, a conforming relay:
|
|||
know its class: rejecting an unknown class is a verifier's rule
|
||||
(§2.4.4), and a relay that filters claims decides for every
|
||||
downstream verifier what it may see.
|
||||
- Forward, unaltered, every object receipt it receives for an object of a
|
||||
requested track that it does not forward, whatever the reason — a delivery
|
||||
timeout that passed at this hop, its own discretion, or never having
|
||||
received the object itself — with the object's group receipt. A subscriber can then check the
|
||||
receipt by inclusion (§3, *Verified by inclusion*): the publisher signed an
|
||||
object with this content hash at this position, and it was not delivered.
|
||||
`inclusion-verified` is never §3 step 6 acceptance, and a verifier **MUST
|
||||
NOT** treat it as delivery.
|
||||
|
||||
### 4.2. MAY
|
||||
|
||||
|
|
@ -1483,6 +1683,8 @@ chain across hops, a conforming relay:
|
|||
permits — Merkle inclusion proofs are position-bound by
|
||||
`leaf_index`, so reordering does not invalidate verification at
|
||||
the subscriber. The relay MUST NOT renumber `object_id`.
|
||||
- Sign a `delivery-gap` claim (§2.4.11) for an object it did not forward,
|
||||
under its own key, as the parallel receipt §4.3 permits.
|
||||
|
||||
### 4.3. MUST NOT
|
||||
|
||||
|
|
@ -1510,7 +1712,19 @@ An object a subscriber did not receive is not delivered to it: it
|
|||
neither presents the object nor reports its delivery, and the group
|
||||
receipt stays valid for every object it did receive, each by its own
|
||||
inclusion proof (§3 step 5). A relay that drops an object MUST still
|
||||
forward the group receipt.
|
||||
forward the group receipt and the object's receipt (§4.1), and the group
|
||||
can still head its chain (§3 step 3).
|
||||
|
||||
A subscriber that holds the receipt of an object it did not receive MAY
|
||||
check it by inclusion (§3, *Verified by inclusion*). The object is then
|
||||
`inclusion-verified`: accounted for as signed and not delivered. A `stage`
|
||||
claim lists it as `dropped` with that evidence (§2.4.10), and staged
|
||||
delivery never applies a layer on it (staged-delivery §10.1). A receipt
|
||||
that rides on the object itself, such as `WAI_RECEIPT`
|
||||
([moq-streaming-format](moq-streaming-format.md) §5), is lost with the
|
||||
object, so a stream that relies on this accounting also carries its
|
||||
receipts where a dropped object does not take them with it (a sidecar, or
|
||||
a track of its own).
|
||||
|
||||
### 4.5. HTTP caches
|
||||
|
||||
|
|
@ -1529,9 +1743,10 @@ that case states: accept, or reject at the stated step with the stated reason.
|
|||
The cases are normative as written here. Vectors that instantiate them are
|
||||
generated outside this tree, by a companion implementation, and are not yet
|
||||
published in this repository; once a revision publishes them in the tree, a
|
||||
conforming verifier MUST also verify against them. (The corpora of §5.6, §5.7
|
||||
and §5.8 — claim receipts, labelled JWP receipts and key discovery — are
|
||||
published in this tree.) Each
|
||||
conforming verifier MUST also verify against them. (The corpora of §5.6–§5.10 —
|
||||
claim receipts, labelled JWP receipts, key discovery, the receipt chain per key
|
||||
and track, and the `stage` and `delivery-gap` classes — are published in this
|
||||
tree.) Each
|
||||
vector consists of: the WAI envelope bytes for every object in the
|
||||
group, the canonical group-receipt JSON, the canonical object-receipt
|
||||
JSON for every object, and a `result.json` declaring the expected
|
||||
|
|
@ -1700,7 +1915,8 @@ pinned parameter set it may give no grant, and it adds the refusal SPEC
|
|||
covers each rule of §7.2, the head walk with revisions that are not
|
||||
held, unattached, injected, forked and superseded, each move of an
|
||||
anchor, each step of §7.4 including the pinned-only authorities, the
|
||||
authorities a `measurement` or `operating-point` claim needs, a family
|
||||
authorities a `measurement`, `operating-point`, `stage` or `delivery-gap`
|
||||
claim needs, a family
|
||||
receipt whose figures carry two classes,
|
||||
instants compared across offsets, fractions and a leap second, and each
|
||||
check of §7.9, with grants from an origin that is not one of the sink's
|
||||
|
|
@ -1724,6 +1940,59 @@ signer it names writes a label (§7.1), and every vector keeps its
|
|||
declared outcome, those of the `measurement` and `operating-point`
|
||||
classes included.
|
||||
|
||||
### 5.9. Receipt chains per key and track (vector `v6_per_track_chain`, `jwp-conformance/`)
|
||||
|
||||
One key publishes a base on track 7 and a layer on track 8, each with three
|
||||
chained groups signed over the labelled payload, which signs the link. A
|
||||
verifier given only track 7's receipts accepts all three
|
||||
(`v6_per_track_chain_base`); given both tracks' receipts interleaved, it accepts
|
||||
all six, which a verifier keeping one chain per key would not
|
||||
(`v6_per_track_chain_interleaved`); given track 8's second group without its
|
||||
first, it reports `parent_not_held` and does not accept it
|
||||
(`v6_per_track_chain_layer_without_its_first`); and a track 8 group whose parent
|
||||
is a track 7 group names nothing the layer's chain holds, and is `parent_not_held`
|
||||
(`v6_per_track_chain_cross_track_link`). Beside them the corpus of §5.7 holds a
|
||||
second chain head offered beside a held head, a fork from a held group and a gap
|
||||
beside the head, a head offered again, another key on the same track, §6.1's guard
|
||||
off, on, and compared within one track; the guard holding a legacy group with it
|
||||
off, a legacy history replayed as new after one rewritten link and its loop, and a
|
||||
labelled replay whose rewritten link fails its signature; receipts chained per key
|
||||
across tracks; its freshness bound at the clock, with an offset, at the window's
|
||||
edge, stale, ahead of the clock, stale with no bound, a stale group behind the live
|
||||
edge, which fills a gap, a gap of seventy groups walked back and forward, and a stale
|
||||
group whose rewritten legacy link earns it nothing; a group whose signature fails and so heads nothing; a
|
||||
previous group of another track or key, which is not held; and object receipts
|
||||
checked without their envelopes, which are `inclusion-verified` or refused at step 2
|
||||
or 5. `chain_differential.py` signs chains around these rules and requires both
|
||||
verifiers to agree on each.
|
||||
`wai_jwp_vectors` and `jwp_verify.py` give every vector's outcome (§5.7).
|
||||
|
||||
### 5.10. Stage and delivery-gap claims (`claim-conformance/`)
|
||||
|
||||
The corpus of §5.6 holds the classes of §2.4.10 and §2.4.11: a set delivered and
|
||||
presented at its committed stage (`stage_accept`); its second layer dropped and
|
||||
verified by inclusion, so the commitment was not met (`stage_not_met_accept`); a
|
||||
base whose receipt was not verified first, which states no delivered stage, so rule 6
|
||||
does not apply (`stage_null_delivered_states_no_delivery`), and a layer over such a
|
||||
base applied on inclusion evidence; a layer delivered and late for one unit; a
|
||||
conflict; a base presented alone; layers all refused, delivered as committed with
|
||||
stage 0 presented; a span past the set refused before it was checked, which stands
|
||||
and delivers nothing; every reason of §2.4.10's rules, among them
|
||||
`stage_hash_without_tier`, `stage_objects_malformed` and `stage_units_unsupported`:
|
||||
units at a stage no object of that layer reaches, a partial layer counted as though
|
||||
applied to its whole span, a layer applied over units the layer below never reached,
|
||||
and a partial object over one unit, with an honest partial layer accepted beside
|
||||
them; objects with no bytes or with all
|
||||
the set's, one layer applied twice to a unit, a layer applied with no receipt beside
|
||||
a stated delivered stage, and a delivered stage over objects that leave a unit
|
||||
uncovered; step 2's enumerations and order; a gap accepted against the object
|
||||
receipt at its coordinates and without it (`gap_accept`), and refused against a
|
||||
receipt naming another object (`gap_subject_mismatch`) or at other coordinates.
|
||||
The corpus of §5.8 checks the authorities both classes need (§7.5).
|
||||
`claim_verify.py` gives every outcome, and `differential.py` generates `stage`
|
||||
bodies around every rule and gaps against receipts at and away from their
|
||||
coordinates.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security considerations
|
||||
|
|
@ -1736,14 +2005,33 @@ verifier and bind unrelated bytes to the publisher's identity at a
|
|||
later time. This extension mitigates replay with two mechanisms:
|
||||
|
||||
- `parent_receipt_hash` (§2.4.1) links each group receipt to its
|
||||
predecessor under the same `signer_pubkey`, so a chain replay
|
||||
requires forging every intermediate hash. The labelled payload (§2.3)
|
||||
signs it; the legacy payload does not, so in a legacy group the link
|
||||
is the publisher's unsigned statement and a relay can rewrite it.
|
||||
- `group_id` is monotonic per publisher key. Verifiers SHOULD track
|
||||
the highest `group_id` seen per `signer_pubkey` and reject any
|
||||
receipt with a non-increasing `group_id`. This binds the chain
|
||||
to forward progress.
|
||||
predecessor under the same `signer_pubkey` and `track_alias`. The labelled
|
||||
payload (§2.3) signs it, and a labelled group's link hash covers it, so a
|
||||
replay of a labelled chain requires forging every intermediate hash. The
|
||||
legacy payload signs neither, so in a legacy group the link is an unsigned
|
||||
statement anyone on the path can rewrite: rewriting one link, the first
|
||||
group's, to name the head would make the whole history link again.
|
||||
- `group_id` is monotonic per (`signer_pubkey`, `track_alias`), and both
|
||||
payloads sign it. A verifier **MUST** refuse a legacy group whose
|
||||
`group_id` does not exceed its chain head's (`group_id_not_increasing`,
|
||||
§3 step 3): it is the only signed order a legacy chain has, and it stops
|
||||
the replay above. For a labelled group the check is OPTIONAL: verifiers
|
||||
SHOULD track the highest `group_id` seen per (`signer_pubkey`,
|
||||
`track_alias`) and reject a non-increasing one. This binds the chain to
|
||||
forward progress. In a legacy chain a relay can still drop a group and
|
||||
rewrite the next one's link to hide the gap; only the labelled payload
|
||||
rules that out.
|
||||
- Both are per track. A publisher that leaves `track_alias` at `0` has one
|
||||
chain per key, as before. A publisher that chained its groups per key
|
||||
across non-zero tracks, as an earlier reading of §2.4.1 allowed, made
|
||||
receipts that now fail at step 3: each group whose parent is on another
|
||||
track names nothing its own chain holds (`parent_not_held`). Separate
|
||||
chains per track let a subscriber verify the tracks it subscribes to
|
||||
without the receipts of tracks it does not — a stage set carries each
|
||||
layer on a track of its own ([staged-delivery](staged-delivery.md)). They
|
||||
also let a relay withhold a whole track without breaking another track's
|
||||
chain; which tracks exist is the carrier's catalog's to say, and a
|
||||
subscriber that requested a track notices its absence.
|
||||
- `group_end` (§2.3) carries a signed RFC 3339 timestamp. A verifier
|
||||
MAY additionally bound it against its own clock for the group it
|
||||
treats as the current tip — rejecting a receipt whose `group_end`
|
||||
|
|
@ -1751,10 +2039,27 @@ later time. This extension mitigates replay with two mechanisms:
|
|||
content back) or implausibly far in the future (skew). This
|
||||
wall-clock check is OPTIONAL and MUST NOT be applied to historical
|
||||
receipts replayed for late-joiner resync, whose timestamps are
|
||||
legitimately old — apply it only to the live edge. The reference
|
||||
verifier exposes both the monotonic `group_id` guard (per track)
|
||||
and this freshness bound as opt-in checks, disabled by default so
|
||||
durable re-verification of an old receipt stays a feature.
|
||||
legitimately old — apply it only to the live edge (`group_not_fresh`).
|
||||
The **live edge** of a chain is the highest `group_id` of a group of it
|
||||
that verified (§3 steps 1–2) and was inside the window when it arrived,
|
||||
whether or not it linked. A group whose `group_id` is below the live edge
|
||||
is history, and the bound does not apply to it: so a group a relay
|
||||
delayed past the window, refused once as stale, can be fetched again and
|
||||
fill the gap behind a later group that arrived fresh, in any order and
|
||||
however long the gap, and the chain does not wedge behind it. This is
|
||||
safe because both payloads sign `group_id`: only a group the publisher
|
||||
signed, and that arrived fresh, moves the edge, and no rewrite of a link —
|
||||
signed in the labelled payload, unsigned in the legacy one — moves it or
|
||||
earns a group the exemption. A stale group at or past the edge is
|
||||
refused. Below the edge a stale group still has to link (§3 step 3) and,
|
||||
if legacy, to exceed the head's `group_id`. The reference verifier
|
||||
(`jwp_delivery::ReceiptChains`) holds every legacy group to the
|
||||
`group_id` guard and exposes it for labelled groups, per
|
||||
(`signer_pubkey`, `track_alias`), and this freshness bound, read to the
|
||||
second with its offset applied, as opt-in checks, disabled by default so
|
||||
durable re-verification of an old receipt stays a feature. It keeps the
|
||||
link hashes of the groups it accepted on each chain, so it tells a fork
|
||||
from a gap, and each chain's live edge.
|
||||
|
||||
### 6.2. Forgery
|
||||
|
||||
|
|
@ -2231,7 +2536,7 @@ A confirmation needs every one of them:
|
|||
| item | authorities |
|
||||
|---|---|
|
||||
| a `group` receipt, and the `object` receipts under it | `delivery`; and `energy/<class>` for each acquisition class an `object` receipt under it labels, where its leaf binds a label (§2.2), and for the class a labelled group payload signs for the group's total (§2.3) |
|
||||
| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement` and `operating-point`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class |
|
||||
| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`, `operating-point`, `stage` and `delivery-gap`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class |
|
||||
| a receipt of another WAI family that signs its own `signer_pubkey` and `signer_id` | `receipt`; and `energy/<class>` for each class its figures carry a label for |
|
||||
| a module grant (§7.9) | `module/decoder` or `module/reconstruct`, by its `form` |
|
||||
|
||||
|
|
@ -2296,6 +2601,11 @@ its keys is that party is the origin's decision, and its log records it:
|
|||
that the figure's origin gave that key the authority to report on it.
|
||||
An origin can give `operating-point` to the party that records
|
||||
requests and commitments without letting it state figures of its own.
|
||||
- **Stage and delivery gap.** `stage` lets a key sign what its holder observed
|
||||
of a stage set (§2.4.10), and `delivery-gap` what a relay did not forward
|
||||
(§2.4.11). Neither is pinned-only, and neither is `delivery`: an origin gives
|
||||
`stage` to the sinks it speaks for and `delivery-gap` to its relays, and a key
|
||||
that holds `delivery` alone signs neither.
|
||||
- **Log inclusion.** A confirmation says under whose authority the log's
|
||||
evidence was attached. It does not check `log_receipt`: §2.4.7 is
|
||||
unchanged.
|
||||
|
|
@ -2309,8 +2619,8 @@ its keys is that party is the origin's decision, and its log records it:
|
|||
Neither is pinned-only; §7.8 says what a module authorised under a
|
||||
fetched anchor is.
|
||||
- **Pre-emption** ([unidirectional-delivery](unidirectional-delivery.md)
|
||||
§3), relay attestation (§4.3) and reconciliation have no class and so
|
||||
no authority. When one is registered for pre-emption it is pinned-only,
|
||||
§3), relay attestation (§4.3) other than a `delivery-gap` claim, and
|
||||
reconciliation have no class and so no authority. When one is registered for pre-emption it is pinned-only,
|
||||
and the pinned revision itself must list the key with it (§7.4
|
||||
step 6a): a fetched anchor is authority taken on first use, which that
|
||||
section forbids for an instruction that displaces what a person chose.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
> it refines by content hash. Every stage is presentable, every prefix of a stage set
|
||||
> states its determinism tier, and a refinement that never arrives leaves the stages
|
||||
> below it intact. Reference impl: `wai-rs` — `staged::{StageChain, StageSink}`,
|
||||
> `staged::{wir1, predict, wir1_arith, tier, schedule}`, `container::Refines`,
|
||||
> `codecs::BASE_ADMISSION`; the `wai stage` tool; the corpus
|
||||
> `staged::{wir1, predict, wir1_arith, tier, schedule}`, `staged::claim::StageObserver`,
|
||||
> `container::Refines`, `codecs::BASE_ADMISSION`; the `wai stage` tool; the corpus
|
||||
> [`staged-conformance/`](../staged-conformance/). Keywords **MUST**, **MUST NOT**,
|
||||
> **SHOULD**, **MAY** are RFC 2119/8174.
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ A sink holds, for every **unit** of the base's output, a **stage** (0 after the
|
|||
decodes) and a **producer**: the content hash of the envelope that produced the
|
||||
unit's current output, the base at stage 0. Layers apply per unit, and every stage is
|
||||
presentable. A base never names its layers, so a layer MAY be produced after its
|
||||
base, by the base's publisher or by a party the sink accepts (§10).
|
||||
base, by the base's publisher or by a party the sink accepts (§10.1).
|
||||
|
||||
## 2. Terms
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ fails is the reason for the envelope (1–6) or for the unit (7–10).
|
|||
4. The capability the base was decoded through is admitted by *R*'s capability (SPEC
|
||||
§5, *Base admission*) (`base-not-admitted`).
|
||||
5. Where the sink verified a delivery receipt for the base, it has verified one for
|
||||
*R* as §10 requires (`refinement-unattested`).
|
||||
*R* as §10.1 requires (`refinement-unattested`).
|
||||
6. The payload is well formed for *R*'s capability, in this order: it parses
|
||||
(Appendix A) (`payload-malformed`); its unit count is the number of units the
|
||||
layer covers (`payload-malformed`); its declared cost (Appendix A.5) is within the
|
||||
|
|
@ -165,7 +165,7 @@ released within the bound, and that the chain budget admits, reaches the same ou
|
|||
as one refined in order.
|
||||
|
||||
An envelope held for its base and received again is held once. If it comes with
|
||||
stronger evidence of its delivery receipt (§10: a receipt verified over its bytes is
|
||||
stronger evidence of its delivery receipt (§10.1: a receipt verified over its bytes is
|
||||
stronger than one verified by inclusion alone, which is stronger than none), the sink
|
||||
keeps the stronger evidence for steps 5 and 10; its hold still runs from its first
|
||||
arrival.
|
||||
|
|
@ -187,7 +187,7 @@ batch can release another, which is evaluated the same way.
|
|||
A sink that decodes again a base whose chain it holds keeps the chain: re-decoding a
|
||||
base does not reset its units' stages, producers or frozen state, and releases and
|
||||
reports nothing. If the sink has verified the base's delivery receipt since, the base
|
||||
is attested from then on (§10). Attestation belongs to the base's content hash: a base
|
||||
is attested from then on (§10.1). Attestation belongs to the base's content hash: a base
|
||||
once attested stays attested, held or dropped, and a decode that carries no receipt
|
||||
does not undo it; a receipt the sink verifies before it decodes the base makes the base
|
||||
attested when it is decoded. Only forgetting the base (§14) ends it. A sink **MAY** drop a base's chain when it no longer presents the
|
||||
|
|
@ -209,7 +209,7 @@ other.
|
|||
|
||||
A sink **MUST** declare a **record budget**, in bytes, for what it keeps of the bases it
|
||||
does not hold: their records, and the attestation of a base dropped while attested or
|
||||
attested before it was decoded (§10). A kept base counts 32 bytes, and each record it
|
||||
attested before it was decoded (§10.1). A kept base counts 32 bytes, and each record it
|
||||
keeps 8 bytes, 32 for each recorded stage and 32 for an envelope that passed step 8
|
||||
without being applied. When what it keeps passes the budget, the sink forgets whole
|
||||
bases, the one whose record or attestation it wrote longest ago first, until it is
|
||||
|
|
@ -252,7 +252,7 @@ the records the sink keeps of it hold (§4.4), passes the budget is not added: t
|
|||
reports `chain-over-limit` for the base's envelope, keeps no chain for it, leaves the
|
||||
records it keeps of it as they were, and holds the layers that name it as for a base it
|
||||
has not decoded (§4.3). A receipt verified with the base is recorded all the same
|
||||
(§10). The base's own decode is bounded before it starts: where its capability declares
|
||||
(§10.1). The base's own decode is bounded before it starts: where its capability declares
|
||||
a cost before decoding (`extensions/integer-payloads.md` §7), a sink applies its cost
|
||||
limit to that declared cost first, and SPEC §4 treats a base past it as one this sink
|
||||
does not decode (`cost_over_limit`), so dispatch moves on to the fallback.
|
||||
|
|
@ -336,13 +336,13 @@ envelopes of the same `base`, `layer` and producer, and conflict (§4.1 step 7).
|
|||
Under a live schedule, every unit has a presentation time, given by the base's timing
|
||||
and the target latency the sink applies, and every layer has a lead, `lead_ms`: the
|
||||
time before a unit's presentation by which the layer must have arrived, and been
|
||||
verified under §10, to be applied to it. A sink **MUST NOT** delay presenting a unit's
|
||||
verified under §10.1, to be applied to it. A sink **MUST NOT** delay presenting a unit's
|
||||
current stage to wait for any layer. A layer that arrived less than `lead_ms` before a
|
||||
unit's presentation time is not applied to that unit (`deadline-missed`). It **MAY**
|
||||
be applied to units of its span that are not yet due. A still image **MAY** be
|
||||
refined after it is presented. A sink **MUST** count every unit presented below the
|
||||
stage its expected layers would have reached on time. Verifying a layer's receipt
|
||||
(§10) adds the time to receive and verify that receipt to the layer's arrival, which
|
||||
(§10.1) adds the time to receive and verify that receipt to the layer's arrival, which
|
||||
sets the least `lead_ms` that can be met: over a base whose receipt the sink verified,
|
||||
a layer counts as arrived when the sink verified its receipt (§4.3).
|
||||
|
||||
|
|
@ -358,22 +358,48 @@ and its exploded envelope (SPEC §7.1) are the same object for every rule that n
|
|||
an object by content hash, so a layer reaches the same stages whichever way it
|
||||
travelled. This revision binds no other carriage.
|
||||
|
||||
## 10. Attestation of layers
|
||||
## 10. Receipts and claims
|
||||
|
||||
### 10.1 Attestation of layers
|
||||
|
||||
Each layer is an object under [jwp-receipts](jwp-receipts.md). A sink that verified a
|
||||
delivery receipt for a base **MUST NOT** apply a layer unless it has verified a
|
||||
delivery receipt for the layer, through that extension's §3 step 6, over the layer's
|
||||
envelope bytes as received. That receipt must be signed by the base's signer, or by a
|
||||
key the sink holds with the authority `delivery` under the same origin
|
||||
(jwp-receipts §7.5). A receipt verified without the layer's envelope bytes (§3 steps 1,
|
||||
2, 3 and 5, step 4 not run) shows that a publisher signed an object with that content
|
||||
hash; it never authorizes an application. A content hash in `refines` binds a layer
|
||||
to a base, not to its publisher. A sink that verifies a base's delivery receipt after
|
||||
envelope bytes as received (`delivered-verified`). That receipt must be signed by the
|
||||
base's signer, or by a key the sink holds with the authority `delivery` under the same
|
||||
origin (jwp-receipts §7.5). A receipt verified without the layer's envelope bytes is
|
||||
`inclusion-verified` (jwp-receipts §3, *Verified by inclusion*): it shows that a
|
||||
publisher signed an object with that content hash, and it never authorizes an
|
||||
application. A layer on a track of its own has its receipts on a chain of their own,
|
||||
one per publisher key and track (jwp-receipts §3 step 3), so a sink verifies the layers
|
||||
it subscribes to without the receipts of the ones it does not. A content hash in
|
||||
`refines` binds a layer to a base, not to its publisher. A sink that verifies a base's delivery receipt after
|
||||
it decoded the base applies step 5 to every envelope that arrives after it; envelopes
|
||||
it admitted before keep the decision they had. The verification belongs to the base's
|
||||
content hash and holds across the base's chain being dropped and the base decoded
|
||||
again (§4.4).
|
||||
|
||||
### 10.2 Claims
|
||||
|
||||
A party that observed a stage set **MAY** sign a `stage` claim about its base
|
||||
(jwp-receipts §2.4.10): the stage it asked for, the stage it was promised, the stage the
|
||||
objects it verified over their bytes reach, the stages it presented on time, the tier
|
||||
and stage hash of what it presented, and every layer object with what became of it and
|
||||
what it verified of the object's receipt. A layer object it never received but whose
|
||||
receipt it verified by inclusion is listed as `dropped` with the evidence
|
||||
`inclusion-verified`; one that arrived after its units were due is `late`. A claim
|
||||
covers what its signer observed since it last forgot the base: a base the sink forgot
|
||||
(§4.4, §14) is, decoded again, one it never decoded and never attested, so its
|
||||
observation starts over, and nothing applied to it before is part of the claim. An
|
||||
envelope the sink still holds for the base when it forgets it stays observed, with no
|
||||
decision, and is stated as the sink then decides it. A claim never spans two such
|
||||
epochs. The claim records what its signer observed, not that content
|
||||
was delivered.
|
||||
|
||||
A relay that did not forward a layer object **MAY** sign a `delivery-gap` claim about it
|
||||
(jwp-receipts §2.4.11), beside the object's receipts, which it forwards all the same
|
||||
(jwp-receipts §4.1).
|
||||
|
||||
## 11. Counting bytes
|
||||
|
||||
A figure of delivered bytes for a stage set:
|
||||
|
|
@ -389,7 +415,7 @@ A figure over a payload in any other form is not a staged-delivery byte figure.
|
|||
|
||||
## 12. Conformance
|
||||
|
||||
A conforming sink of this extension implements §4, §5, §8 and §10, and every staged
|
||||
A conforming sink of this extension implements §4, §5, §8 and §10.1, and every staged
|
||||
form whose capabilities it advertises. The corpus
|
||||
[`staged-conformance/`](../staged-conformance/) holds the cases: for each, a
|
||||
conforming sink reports the stated reason per envelope and unit, and reaches the
|
||||
|
|
@ -407,10 +433,11 @@ ignores the header.
|
|||
|
||||
## 14. Security considerations
|
||||
|
||||
- A layer is not authenticated by its `refines`; §10 authenticates it.
|
||||
- Deferral lets anyone mint a chain for a public base hash. §10 and §4.1 step 7 bound
|
||||
- A layer is not authenticated by its `refines`; §10.1 authenticates it.
|
||||
- Deferral lets anyone mint a chain for a public base hash. §10.1 and §4.1 step 7 bound
|
||||
what a sink presents. A conflict between two attested chains is evidence that an
|
||||
authorized signer signed both, and the sink reports it.
|
||||
authorized signer signed both, and the sink reports it; a `stage` claim (§10.2)
|
||||
records it as `conflicts`.
|
||||
- A hold bound (§4.3), a cost limit (Appendix A.5) and a chain budget (§4.5) are
|
||||
resource limits. A layer's header declares its output, its symbols and its working
|
||||
bytes before any symbol is decoded, so a sink refuses a layer past its limit or its
|
||||
|
|
@ -436,7 +463,7 @@ ignores the header.
|
|||
treats it as never decoded and never attested: a layer that conflicted with the
|
||||
forgotten history can apply, and step 5 no longer requires a receipt for it. A sink
|
||||
sizes its record budget for the bases it may decode again.
|
||||
- Where the base is attested (§10), a frozen unit presents only its base's output or
|
||||
- Where the base is attested (§10.1), a frozen unit presents only its base's output or
|
||||
the output of one of the producers it recorded, replayed in order (§4.4): a party
|
||||
that can sign conflicting layers can stop a unit's refinement, and cannot make it
|
||||
present anything else. Over a base whose receipt the sink did not verify, anyone who learns the
|
||||
|
|
|
|||
|
|
@ -269,7 +269,11 @@ is never a measurement of a network, and is never presented as one.
|
|||
deadlines.
|
||||
|
||||
Dropped and abandoned bytes are never delivered, and every offered byte is delivered,
|
||||
dropped or abandoned.
|
||||
dropped or abandoned. The simulator models no receipt. Over a network, a relay that
|
||||
drops an object still forwards its receipts ([jwp-receipts](jwp-receipts.md) §4.1); a
|
||||
sink that holds them accounts for the object as `inclusion-verified`, never as
|
||||
delivered (jwp-receipts §4.4), and a `stage` claim lists it as `dropped`
|
||||
(jwp-receipts §2.4.10).
|
||||
|
||||
§10.3 **Layer cap.** An object whose layer exceeds `layer_cap` is never sent and never
|
||||
counted as offered.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
# jwp-conformance
|
||||
|
||||
Conformance for the labelled forms of the [JWP delivery receipts](../extensions/jwp-receipts.md)
|
||||
(§2.2–§2.4.2, §3, §3.3 and §5.7) and for the `WAI_RECEIPT` property's readings
|
||||
([moq-streaming-format](../extensions/moq-streaming-format.md) §5): an object's figure,
|
||||
class and origin bound into its Merkle leaf; a group total's class signed under the
|
||||
labelled group payload; the position an object's inclusion proof must walk; the group
|
||||
total read from its objects; and a property read as signed only where a signed statement
|
||||
restates it.
|
||||
(§2.2–§2.4.2, §3, §3.3 and §5.7), for the `WAI_RECEIPT` property's readings
|
||||
([moq-streaming-format](../extensions/moq-streaming-format.md) §5), and for the receipt
|
||||
chain per (`signer_pubkey`, `track_alias`) and an object checked by inclusion alone (§3
|
||||
step 3, §4.4, §5.9 and §6.1): an object's figure, class and origin bound into its Merkle
|
||||
leaf; a group total's class signed under the labelled group payload; the position an
|
||||
object's inclusion proof must walk; the group total read from its objects; a property
|
||||
read as signed only where a signed statement restates it; one chain for each key and
|
||||
track; and an object whose receipt is checked without its envelope reported as
|
||||
`inclusion-verified`, never as delivered.
|
||||
|
||||
## Files
|
||||
|
||||
|
|
@ -14,6 +17,8 @@ restates it.
|
|||
|---|---|
|
||||
| `vectors.json` | The corpus, generated by `wai_jwp_vectors gen` in `wai-rs` (feature `receipt_claims`). `wai_jwp_vectors verify` requires a byte-identical regeneration and every declared outcome. |
|
||||
| `jwp_verify.py` | A second verifier, written from the extension text, that shares no code with `wai-rs`. It takes BLAKE3, jwp-receipts §3.1 step 3's Ed25519 rule and the claim verifier from `../claim-conformance/claim_verify.py`, itself independent of `wai-rs`. |
|
||||
| `chain_gap_differential.py` | Signs genuine chains of up to 90 groups and delivers them shuffled, withheld and released stale, walked back and forward, with rewritten legacy links and forks the key signs, and requires the two verifiers to agree on every one. |
|
||||
| `chain_differential.py` | Signs receipt chains the corpus does not hold — two keys, two tracks, both payloads, genuine links, replays with and without a rewritten link, forks, gaps, links across tracks and keys, bad signatures, `group_id` up, equal and down, `group_end` around the clock, guard and bound on and off — and requires `wai_jwp_vectors verdicts` and `jwp_verify.py verdicts` to agree on every one. |
|
||||
|
||||
## Procedures
|
||||
|
||||
|
|
@ -26,6 +31,8 @@ Each vector names one procedure and declares one outcome.
|
|||
| `object` | a group receipt, an object receipt and the envelope delivered | `accept` (§3 steps 1–5), or a rejection at a §3 step |
|
||||
| `total` | a group receipt and the objects delivered with their envelopes | the total's reading — `unmetered`, `unlabelled`, `weakest` (a legacy group whose every non-zero part is labelled: a class name only), or `signed` (a labelled group's own class, `checked` against every object or `unchecked`) — or a rejection |
|
||||
| `property` | a `WAI_RECEIPT` property, with the object, its group and its envelope, or with an `energy` claim | the property's reading — `unmetered`, `unsigned`, or `signed` with the class and the key that signed it (the group's for a leaf, the claim's for a claim) — or a rejection |
|
||||
| `chain` | group receipts in the order a verifier receives them, whether §6.1's `group_id` guard is on for labelled groups (it is always on for legacy ones), and §6.1's freshness bound (`now`, `max_age_s`, `max_skew_s`) or `null` | for each group in order, `accept` (§3 steps 1–3 against the chain of its key and track, whose head it then becomes) or a rejection at its §3 step; the verifier holds every group it accepted, so a fork is `chain_break` and a gap `parent_not_held`; a group whose signed `group_id` is below the chain's live edge (the highest of a group that verified and arrived fresh) is exempt from the freshness bound |
|
||||
| `inclusion` | a group receipt and an object receipt, with no envelope | `accept` with the evidence `inclusion-verified` (§3 steps 1, 2 and 5, step 4 not run; §4.4), or a rejection at a §3 step |
|
||||
|
||||
A rejection names its reason (§3.3), and its §3 step where it has one; the group total
|
||||
and a property are not steps of §3.
|
||||
|
|
@ -44,7 +51,20 @@ Ed25519 rule refuses (`S` ≥ L, a key of small order, a key whose `y` is not re
|
|||
does not match its parts, and labelled totals whose class, coverage, declared uncertainty
|
||||
or window disagree with what their parts compose to. Properties that agree with, and
|
||||
disagree with, a labelled leaf or an `energy` claim, including a claim signed by a key
|
||||
other than the publisher's, which reads as signed by that key.
|
||||
other than the publisher's, which reads as signed by that key. Chains (vector `v6_per_track_chain`
|
||||
and its neighbours): one key's base track and layer track, three chained labelled groups
|
||||
each, offered alone and interleaved; a track's second group without its first; a parent
|
||||
on another track; a chain head offered beside a held head, a fork from a held group, a
|
||||
gap beside the head, and the head offered again; another key on the same track; the
|
||||
`group_id` guard off, on, and compared within a track, and always on for a legacy group;
|
||||
a legacy history replayed after one rewritten link, once and in a loop, and a labelled
|
||||
replay; receipts chained per key across tracks; `group_end` at the clock, with an
|
||||
offset, at the window's edge, stale, ahead of the clock, stale with no bound, a stale
|
||||
group behind the live edge, which fills a gap, a gap of seventy groups, and a stale
|
||||
group whose rewritten link earns it nothing; a previous group of another track or key;
|
||||
and a group whose signature fails, which heads nothing. Objects checked without their envelopes: a dropped layer object and a labelled
|
||||
one, which verify by inclusion, and a forged group, a rewritten content hash and a moved
|
||||
position, which do not.
|
||||
|
||||
## Synthetic, and what it attests
|
||||
|
||||
|
|
@ -56,6 +76,8 @@ calibration references use a reserved example domain.
|
|||
|
||||
cargo run --no-default-features --features receipt_claims --bin wai_jwp_vectors -- verify ../jwp-conformance
|
||||
python3 jwp_verify.py vectors.json
|
||||
python3 chain_differential.py ../wai-rs/target/debug/wai_jwp_vectors --n 3000 --seed 1
|
||||
python3 chain_gap_differential.py ../wai-rs/target/debug/wai_jwp_vectors --n 400 --seed 1
|
||||
|
||||
Both run in CI: the Rust verifier on every platform of the conformance matrix, the Python
|
||||
one on the linux runner (`.github/workflows/wai-conformance.yml`). The Python verifier
|
||||
|
|
|
|||
143
wai/jwp-conformance/chain_differential.py
Normal file
143
wai/jwp-conformance/chain_differential.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Adversarial differential for jwp-receipts §3 step 3 / §6.1 receipt chains per
|
||||
(signer_pubkey, track_alias): wai-rs (`wai_jwp_vectors verdicts`) against
|
||||
jwp_verify.py (`verdicts`). Generates `chain` vectors with no declared outcome:
|
||||
two keys, two tracks, legacy and labelled payloads, genuine links, replays, replays
|
||||
with a rewritten (unsigned, in legacy) parent, forks, cross-track and cross-key
|
||||
parents, fresh genesis beside a held head, bad signatures, group_id up/equal/down,
|
||||
group_end at, before and after the clock, offsets, fractions, leap second, year 0000
|
||||
and non-dates; guard on/off; freshness on/off.
|
||||
|
||||
Every chain is signed with the corpus's published seeds, so a run is reproducible from
|
||||
its seed. Exit status 0 only if the two verifiers give the same verdict on every chain.
|
||||
|
||||
usage: chain_differential.py <wai_jwp_vectors binary> [--n N] [--seed S]
|
||||
"""
|
||||
import json, os, random, subprocess, sys, tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
||||
import jwp_verify as J # noqa: E402
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey # noqa: E402
|
||||
from cryptography.hazmat.primitives import serialization # noqa: E402
|
||||
|
||||
KEYS = {s: Ed25519PrivateKey.from_private_bytes(bytes([s]) * 32) for s in (0x11, 0x22)}
|
||||
|
||||
|
||||
def pk(k):
|
||||
return k.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw).hex()
|
||||
|
||||
|
||||
NOW = "2026-09-30T12:00:01Z"
|
||||
ENDS = [NOW, "2026-09-30T12:00:01Z", "2026-09-30T11:59:01Z", "2026-09-30T11:59:00Z", "2026-09-30T11:00:00Z",
|
||||
"2026-09-30T12:00:06Z", "2026-09-30T12:00:07Z", "2026-09-30T14:00:01+02:00", "2026-09-30T11:59:01.999Z",
|
||||
"2026-09-30T11:59:60Z", "0000-03-01T00:00:00Z", "9999-12-31T23:59:59Z", "2026-02-29T00:00:00Z", "now",
|
||||
"2026-09-30T12:00:01-00:00"]
|
||||
|
||||
|
||||
def make(seed, track, gid, root_tag, parent, end, labelled, bad_sig=False):
|
||||
g = {"kind": "group", "track_alias": track, "group_id": gid,
|
||||
"root_hash": J.blake3(b"root %d" % root_tag).hex(), "leaf_count": 1, "joules_total": 0,
|
||||
"group_start": "2026-09-30T12:00:00Z", "group_end": end, "signer_pubkey": pk(KEYS[seed]),
|
||||
"signer_id": "did:example:publisher", "sig": "00" * 64,
|
||||
"parent_receipt_hash": parent.hex() if parent is not None else None}
|
||||
if labelled:
|
||||
g["sign_version"] = 2
|
||||
r = J.read_group(g)
|
||||
sig = KEYS[seed].sign(J.payload(r))
|
||||
if bad_sig:
|
||||
sig = bytes([sig[0] ^ 1]) + sig[1:]
|
||||
g["sig"] = sig.hex()
|
||||
return g
|
||||
|
||||
|
||||
def link(g):
|
||||
return J.link(J.read_group(g))
|
||||
|
||||
|
||||
def gen(rng, i):
|
||||
guard = rng.random() < 0.5
|
||||
fresh = rng.choice([None, None, {"now": NOW, "max_age_s": 60, "max_skew_s": 5}])
|
||||
genuine = {} # (seed, track) -> [groups]
|
||||
emitted = []
|
||||
out = []
|
||||
tag = 1000 * i
|
||||
for _ in range(rng.randint(1, 9)):
|
||||
tag += 1
|
||||
seed = rng.choice([0x11, 0x11, 0x22])
|
||||
track = rng.choice([7, 7, 8])
|
||||
labelled = rng.random() < 0.5
|
||||
chain = genuine.setdefault((seed, track), [])
|
||||
end = rng.choice([NOW] * 6 + ENDS)
|
||||
kind = rng.choice(["next"] * 6 + ["replay", "replay_rewritten", "fork", "cross_track", "cross_key",
|
||||
"genesis", "bad_sig", "random_parent"])
|
||||
gid_next = (chain[-1]["group_id"] + 1) if chain else rng.choice([0, 0, 5])
|
||||
gid = rng.choice([gid_next] * 5 + [max(0, gid_next - 1), max(0, gid_next - 2), gid_next + 3])
|
||||
if kind == "replay" and emitted:
|
||||
g = rng.choice(emitted)
|
||||
elif kind == "replay_rewritten" and emitted:
|
||||
g = dict(rng.choice(emitted))
|
||||
tgt = rng.choice(emitted)
|
||||
g["parent_receipt_hash"] = link(tgt).hex() # unsigned in a legacy group
|
||||
elif kind == "fork" and len(chain) >= 2:
|
||||
g = make(seed, track, gid, tag, link(rng.choice(chain[:-1])), end, labelled)
|
||||
elif kind == "cross_track":
|
||||
other = genuine.get((seed, 15 - track)) or []
|
||||
g = make(seed, track, gid, tag, link(other[-1]) if other else None, end, labelled)
|
||||
elif kind == "cross_key":
|
||||
other = genuine.get((0x33 - seed, track)) or []
|
||||
g = make(seed, track, gid, tag, link(other[-1]) if other else None, end, labelled)
|
||||
elif kind == "genesis":
|
||||
g = make(seed, track, gid, tag, None, end, labelled)
|
||||
elif kind == "bad_sig":
|
||||
g = make(seed, track, gid, tag, link(chain[-1]) if chain else None, end, labelled, bad_sig=True)
|
||||
elif kind == "random_parent":
|
||||
g = make(seed, track, gid, tag, J.blake3(b"nothing %d" % tag), end, labelled)
|
||||
else:
|
||||
g = make(seed, track, gid, tag, link(chain[-1]) if chain else None, end, labelled)
|
||||
chain.append(g)
|
||||
emitted.append(g)
|
||||
out.append(g)
|
||||
return {"name": f"chain_diff_{i}", "procedure": "chain",
|
||||
"input": {"groups": out, "guard": guard, "freshness": fresh},
|
||||
"expected": {"outcome": "chain", "results": []}}
|
||||
|
||||
|
||||
def main(argv):
|
||||
rust_bin = argv[1]
|
||||
n = int(argv[argv.index("--n") + 1]) if "--n" in argv else 3000
|
||||
seed = argv[argv.index("--seed") + 1] if "--seed" in argv else "1"
|
||||
rng = random.Random("chains " + seed)
|
||||
vs = [gen(rng, i) for i in range(n)]
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
|
||||
json.dump({"vectors": vs}, f)
|
||||
path = f.name
|
||||
try:
|
||||
rs = subprocess.run([rust_bin, "verdicts", path], capture_output=True, text=True, check=True).stdout
|
||||
py = subprocess.run([sys.executable, os.path.join(HERE, "jwp_verify.py"), "verdicts", path],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
finally:
|
||||
os.unlink(path)
|
||||
r = dict(l.split("\t", 1) for l in rs.splitlines())
|
||||
p = dict(l.split("\t", 1) for l in py.splitlines())
|
||||
if len(r) != n or len(p) != n:
|
||||
print(f"verdict counts differ: {n} chains, wai-rs {len(r)}, python {len(p)}")
|
||||
return 1
|
||||
differ = [k for k in r if r[k] != p.get(k)]
|
||||
crashes = [k for k in r if r[k] == "error" or p.get(k) == "error"]
|
||||
tally = {}
|
||||
for v in r.values():
|
||||
for one in v.split(": ", 1)[-1].split(" / "):
|
||||
tally[one] = tally.get(one, 0) + 1
|
||||
print(f"{n} chains (seed {seed}); per group: " + ", ".join(f"{k} x{c}" for k, c in sorted(tally.items())))
|
||||
print(f"{len(differ)} differ, {len(crashes)} error")
|
||||
for k in differ[:10]:
|
||||
v = next(x for x in vs if x["name"] == k)
|
||||
ends = [g["group_end"] for g in v["input"]["groups"]]
|
||||
print(f" {k}: rust={r[k]!r}\n python={p.get(k)!r}\n freshness={v['input']['freshness']} ends={ends}")
|
||||
return 1 if differ or crashes else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
98
wai/jwp-conformance/chain_gap_differential.py
Normal file
98
wai/jwp-conformance/chain_gap_differential.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Gap and fork differential for receipt chains (jwp-receipts §3 step 3, §6.1): wai-rs
|
||||
(`wai_jwp_vectors verdicts`) against jwp_verify.py (`verdicts`). A genuine chain per
|
||||
(key, track), delivered shuffled, with re-sends, walk-backs, groups withheld and then
|
||||
released stale, legacy parents rewritten (to the group itself, to a random hash, to a
|
||||
held group), forks the key signs from a held group, and chains long enough (up to 90
|
||||
groups) that no count could bound a gap; freshness bound and guard on and off. Keys are
|
||||
the corpus's published seeds, so a run is reproducible from its seed. Exit status 0
|
||||
only if the two verifiers give the same verdict on every chain.
|
||||
|
||||
usage: chain_gap_differential.py <wai_jwp_vectors binary> [--n N] [--seed S]"""
|
||||
import json, os, random, subprocess, sys, tempfile
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import chain_differential as D # noqa: E402
|
||||
|
||||
NOW = "2026-09-30T12:00:01Z"
|
||||
STALE = "2026-09-30T11:00:00Z"
|
||||
|
||||
|
||||
def gen(rng, i):
|
||||
labelled = rng.random() < 0.5
|
||||
track, seed = rng.choice([7, 8]), rng.choice([0x11, 0x22])
|
||||
n = rng.choice([3, 5, 8, 12, 20]) if rng.random() < 0.85 else rng.choice([66, 70, 90])
|
||||
stale = set(rng.sample(range(n), k=rng.randint(0, n)))
|
||||
chain = [D.make(seed, track, 0, 10_000 * i, None, STALE if 0 in stale else NOW, labelled)]
|
||||
for k in range(1, n):
|
||||
chain.append(D.make(seed, track, k, 10_000 * i + k, D.link(chain[-1]), STALE if k in stale else NOW, labelled))
|
||||
order = []
|
||||
mode = rng.choice(["shuffle", "walkback", "inorder_gaps", "mixed"])
|
||||
if mode == "shuffle":
|
||||
order = chain[:] + rng.sample(chain, k=min(len(chain), rng.randint(0, 10)))
|
||||
rng.shuffle(order)
|
||||
elif mode == "walkback":
|
||||
cut = rng.randint(0, n - 1)
|
||||
order = chain[:cut + 1] + list(reversed(chain[cut + 1:])) + chain[cut + 1:]
|
||||
elif mode == "inorder_gaps":
|
||||
withheld = [g for g in chain if rng.random() < 0.3]
|
||||
order = [g for g in chain if g not in withheld] + withheld + chain
|
||||
else:
|
||||
order = chain[:]
|
||||
for _ in range(rng.randint(1, 15)):
|
||||
order.insert(rng.randint(0, len(order)), rng.choice(chain))
|
||||
# adversarial insertions
|
||||
out = []
|
||||
for g in order:
|
||||
out.append(g)
|
||||
r = rng.random()
|
||||
if r < 0.05:
|
||||
out.append(dict(g, parent_receipt_hash=D.link(g).hex())) # self-referential rewrite
|
||||
elif r < 0.08:
|
||||
out.append(dict(g, parent_receipt_hash=D.J.blake3(b"r%d" % rng.randint(0, 1 << 30)).hex()))
|
||||
elif r < 0.10:
|
||||
out.append(dict(g, parent_receipt_hash=D.link(rng.choice(chain)).hex()))
|
||||
elif r < 0.12:
|
||||
src = rng.choice(chain)
|
||||
out.append(D.make(seed, track, rng.randint(0, n + 2), 10_000 * i + 9000 + len(out), D.link(src), NOW, labelled)) # fork
|
||||
fresh = {"now": NOW, "max_age_s": 60, "max_skew_s": 5} if rng.random() < 0.7 else None
|
||||
return {"name": f"gap_{i}", "procedure": "chain", "input": {"groups": out, "guard": rng.random() < 0.5, "freshness": fresh},
|
||||
"expected": {"outcome": "chain", "results": []}}
|
||||
|
||||
|
||||
def main(argv):
|
||||
rust_bin = argv[1]
|
||||
n = int(argv[argv.index("--n") + 1]) if "--n" in argv else 400
|
||||
seed = argv[argv.index("--seed") + 1] if "--seed" in argv else "1"
|
||||
rng = random.Random("gaps " + seed)
|
||||
vs = [gen(rng, i) for i in range(n)]
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
|
||||
json.dump({"vectors": vs}, f)
|
||||
path = f.name
|
||||
try:
|
||||
rs = subprocess.run([rust_bin, "verdicts", path], capture_output=True, text=True, check=True).stdout
|
||||
py = subprocess.run([sys.executable, os.path.join(HERE, "jwp_verify.py"), "verdicts", path],
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
finally:
|
||||
os.unlink(path)
|
||||
r = dict(l.split("\t", 1) for l in rs.splitlines())
|
||||
p = dict(l.split("\t", 1) for l in py.splitlines())
|
||||
if len(r) != n or len(p) != n:
|
||||
print(f"verdict counts differ: {n} chains, wai-rs {len(r)}, python {len(p)}")
|
||||
return 1
|
||||
differ = [k for k in r if r[k] != p.get(k)]
|
||||
errors = [k for k in r if r[k] == "error" or p.get(k) == "error"]
|
||||
tally = {}
|
||||
for v in r.values():
|
||||
for one in v.split(": ", 1)[-1].split(" / "):
|
||||
tally[one] = tally.get(one, 0) + 1
|
||||
print(f"{len(vs)} chains, {sum(len(v['input']['groups']) for v in vs)} groups (seed {seed}); per group: "
|
||||
+ ", ".join(f"{k} x{c}" for k, c in sorted(tally.items())))
|
||||
print(f"{len(differ)} differ, {len(errors)} error")
|
||||
for k in differ[:3]:
|
||||
print(" ", k, "\n rust", r[k][:300], "\n py ", p.get(k, "")[:300])
|
||||
return 1 if differ or errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Second verifier for jwp-conformance/ — labelled leaves, labelled group receipts, the
|
||||
group total and the WAI_RECEIPT property (extensions/jwp-receipts.md §2.2-§2.4.2, §3,
|
||||
§3.3 and §5.7; moq-streaming-format.md §5).
|
||||
group total, the WAI_RECEIPT property, receipt chains per (key, track) and objects
|
||||
checked by inclusion alone (extensions/jwp-receipts.md §2.2-§2.4.2, §3, §3.3, §4.4,
|
||||
§5.7, §5.9 and §6.1; moq-streaming-format.md §5).
|
||||
|
||||
Written from the extension text. It shares no code with wai-rs (`crate::jwp_delivery`),
|
||||
which generates the corpus. It takes from ../claim-conformance/claim_verify.py — itself
|
||||
|
|
@ -227,19 +228,29 @@ def verify_group(g):
|
|||
|
||||
|
||||
def check_parent(g, previous):
|
||||
"""§3 step 3. A previous group that does not verify is not held."""
|
||||
"""§3 step 3 against a verifier that holds one group of the chain, `previous`, as its
|
||||
head. A previous group that does not verify is not held, and neither is one under
|
||||
another key or on another track. Nothing held: a null parent starts the chain, any
|
||||
other is parent_not_held. The head held: its link hash links, a null parent is
|
||||
chain_break (a second chain head), any other parent names a group not held:
|
||||
parent_not_held."""
|
||||
if previous is not None:
|
||||
try:
|
||||
verify_group(previous)
|
||||
except Refuse:
|
||||
if previous["signer_pubkey"] != g["signer_pubkey"] or previous["track_alias"] != g["track_alias"]:
|
||||
previous = None
|
||||
else:
|
||||
try:
|
||||
verify_group(previous)
|
||||
except Refuse:
|
||||
previous = None
|
||||
parent = g["parent_receipt_hash"]
|
||||
if previous is None:
|
||||
if parent is not None:
|
||||
raise Refuse(3, "parent_not_held")
|
||||
return
|
||||
if parent is None or previous["signer_pubkey"] != g["signer_pubkey"] or bytes.fromhex(parent) != link(previous):
|
||||
if parent is None:
|
||||
raise Refuse(3, "chain_break")
|
||||
if bytes.fromhex(parent) != link(previous):
|
||||
raise Refuse(3, "parent_not_held")
|
||||
|
||||
|
||||
# ------------------------------------------------------------ object receipts (§2.4.2, §3 steps 4-5)
|
||||
|
|
@ -473,6 +484,92 @@ def prop(inp):
|
|||
raise Refuse(None, "property_disagrees")
|
||||
|
||||
|
||||
# ------------------------------------------------------------ chains per (key, track) (§3 step 3, §6.1)
|
||||
|
||||
def days_from_civil(y, m, d):
|
||||
"""Days since 1970-01-01 of a date of the proleptic Gregorian calendar, year 0000
|
||||
included, in integers (the days-from-civil algorithm)."""
|
||||
y -= m <= 2
|
||||
era = y // 400
|
||||
yoe = y - era * 400
|
||||
doy = (153 * ((m + 9) % 12) + 2) // 5 + d - 1
|
||||
return era * 146097 + yoe * 365 + yoe // 4 - yoe // 100 + doy - 719468
|
||||
|
||||
|
||||
def seconds(t):
|
||||
"""An RFC 3339 date-time as seconds since 1970-01-01T00:00:00Z, its offset applied
|
||||
and its fraction dropped; a second written as 60 is the next minute's first. None
|
||||
for a text that is not one."""
|
||||
if not (isinstance(t, str) and CV.is_rfc3339(t)):
|
||||
return None
|
||||
days = days_from_civil(int(t[0:4]), int(t[5:7]), int(t[8:10]))
|
||||
v = days * 86400 + int(t[11:13]) * 3600 + int(t[14:16]) * 60 + int(t[17:19])
|
||||
tail = t[19:].lstrip(".0123456789")
|
||||
if tail[:1] in ("+", "-"):
|
||||
off = int(tail[1:3]) * 3600 + int(tail[4:6]) * 60
|
||||
v += -off if tail[0] == "+" else off
|
||||
return v
|
||||
|
||||
|
||||
def chain(inp):
|
||||
"""Each group in order through §3 steps 1-3 against its own chain, the one for its
|
||||
(signer_pubkey, track_alias). The head offered again passes. With no head held, a
|
||||
null parent starts the chain and any other is parent_not_held. With a head held, its
|
||||
link hash links the group; a null parent is chain_break; a parent that is another
|
||||
group accepted on the chain is chain_break (a fork); any other is parent_not_held.
|
||||
A legacy group, whose parent no signature covers, needs a group_id above the head's
|
||||
whatever the guard says; with the guard on, every group does. With the bound on, a
|
||||
group_end outside [now - max_age, now + max_skew] is group_not_fresh, unless the
|
||||
group's signed group_id is below the chain's live edge: the highest group_id of a
|
||||
group that verified and was inside the window (or arrived with no bound), linked or
|
||||
not. A group that passes becomes the head."""
|
||||
chains, out = {}, []
|
||||
guard, fresh = inp["guard"], inp.get("freshness")
|
||||
|
||||
def in_window(g):
|
||||
if fresh is None:
|
||||
return True
|
||||
now, t = seconds(fresh["now"]), seconds(g["group_end"])
|
||||
return t is not None and now - fresh["max_age_s"] <= t <= now + fresh["max_skew_s"]
|
||||
|
||||
for raw in inp["groups"]:
|
||||
c, g = None, None
|
||||
try:
|
||||
g = read_group(raw)
|
||||
verify_group(g)
|
||||
c = chains.setdefault((g["signer_pubkey"], g["track_alias"]), {"head": None, "held": set(), "edge": None})
|
||||
head, lk, parent = c["head"], link(g), g["parent_receipt_hash"]
|
||||
if head is None or link(head) != lk:
|
||||
if head is None:
|
||||
if parent is not None:
|
||||
raise Refuse(3, "parent_not_held")
|
||||
elif parent is None:
|
||||
raise Refuse(3, "chain_break")
|
||||
elif bytes.fromhex(parent) != link(head):
|
||||
raise Refuse(3, "chain_break" if bytes.fromhex(parent) in c["held"] else "parent_not_held")
|
||||
if (guard or not g["labelled"]) and head is not None and g["group_id"] <= head["group_id"]:
|
||||
raise Refuse(3, "group_id_not_increasing")
|
||||
behind_edge = c["edge"] is not None and g["group_id"] < c["edge"]
|
||||
if not behind_edge and not in_window(g):
|
||||
raise Refuse(3, "group_not_fresh")
|
||||
c["head"] = g
|
||||
c["held"].add(lk)
|
||||
out.append("accept")
|
||||
except Refuse as r:
|
||||
out.append(r.label())
|
||||
if c is not None and in_window(g): # verified: c is set only after the signature
|
||||
c["edge"] = g["group_id"] if c["edge"] is None else max(c["edge"], g["group_id"])
|
||||
return "chain: " + " / ".join(out)
|
||||
|
||||
|
||||
def inclusion_only(inp):
|
||||
"""§4.4: §3 steps 1, 2 and 5 without the envelope; step 4 is not run."""
|
||||
g = read_group(inp["group"])
|
||||
verify_group(g)
|
||||
inclusion(read_object(inp["object"]), g)
|
||||
return "accept evidence=inclusion-verified"
|
||||
|
||||
|
||||
# ------------------------------------------------------------ vectors
|
||||
|
||||
def verdict(v):
|
||||
|
|
@ -500,6 +597,10 @@ def verdict(v):
|
|||
return total(inp)
|
||||
if v["procedure"] == "property":
|
||||
return prop(inp)
|
||||
if v["procedure"] == "chain":
|
||||
return chain(inp)
|
||||
if v["procedure"] == "inclusion":
|
||||
return inclusion_only(inp)
|
||||
except Refuse as r:
|
||||
return r.label()
|
||||
raise ValueError("unknown procedure " + str(v["procedure"]))
|
||||
|
|
@ -508,6 +609,10 @@ def verdict(v):
|
|||
def expected_of(e):
|
||||
if "leaf" in e:
|
||||
return "leaf=" + e["leaf"]
|
||||
if e["outcome"] == "chain":
|
||||
return "chain: " + " / ".join(expected_of(r) for r in e["results"])
|
||||
if e["outcome"] == "accept" and "evidence" in e:
|
||||
return "accept evidence=" + e["evidence"]
|
||||
if e["outcome"] == "accept":
|
||||
return f"accept link_hash={e['link_hash']} signing_payload={e['signing_payload']}" if "link_hash" in e else "accept"
|
||||
if e["outcome"] == "read":
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -64,7 +64,8 @@ to, 0x63 and 0x66 delivery keys (0x66 also meters), 0x64 and 0x69 metering keys,
|
|||
and 0x6e lawful-basis keys, 0x6b and 0x6c module-granting keys, 0x60 a key added later,
|
||||
0x67, 0x6a and 0x6f keys an attacker holds, 0x70, 0x71 and 0x72 the root, committed key
|
||||
and module-granting key of a second origin, 0x73 and 0x74 a measurement key and an
|
||||
operating-point key, and 0x75 a key that signs family receipts with a ModelBased figure. jwp-receipts §7.8 says such a key must
|
||||
operating-point key, 0x75 a key that signs family receipts with a ModelBased figure, and
|
||||
0x76 and 0x77 a sink's key for `stage` and a relay's for `delivery-gap`. jwp-receipts §7.8 says such a key must
|
||||
never be listed in a real key log: every anchored verdict here carries
|
||||
`note=published_test_key`, which is what a verifier reports on finding one. Origins are
|
||||
reserved example names (`publisher.example`, with and without port 8443,
|
||||
|
|
|
|||
|
|
@ -4145,6 +4145,98 @@
|
|||
]
|
||||
},
|
||||
"expected": "fallback step=4 reason=module_status_stale"
|
||||
},
|
||||
{
|
||||
"name": "stage_needs_its_class",
|
||||
"about": "a stage claim by a key holding delivery and receipt but not stage: delivering objects is not observing a stage set",
|
||||
"procedure": "confirm",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5000,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[],\"op_committed\":null,\"op_delivered\":null,\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":null,\"tier\":\"none\",\"units_by_stage\":[4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"66f540e0a106eae2604127be24b772c4b15fb5540fc29ba849b08b91913b368256fe3b095f9f35eff1bc8a19e8c085dcdf2513d6b15671667b9dd747a1991e0f\",\"signer_id\":\"did:web:publisher.example#d1\",\"signer_pubkey\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"revisions": [
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"sk\",\"key\":\"b5f90095318f3f044cd8e40b584253d86777be62bf07221334e5fd2199cfa7f7\",\"may_sign\":[\"stage\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"rl\",\"key\":\"c853ad0f0cd2b619aea92ceec4fd56a24d6499d584ce79257e45cfd8139b60a7\",\"may_sign\":[\"delivery-gap\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"6f925073ff5c6c3e1ef985f52362a11f4da0de4628efd07ea0d613a8c1f8728934046608d9a2491839efb4a5f5944865d2704f70bbebb3ecbd64211b8a80cf0a\",\"status_max_age_s\":3600}"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"origin": "https://publisher.example",
|
||||
"revision": "9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z",
|
||||
"pinned_revision": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": "refused step=6 reason=authority_not_granted"
|
||||
},
|
||||
{
|
||||
"name": "stage_confirmed",
|
||||
"about": "a stage claim by a key holding stage, under a fetched anchor: stage is not pinned-only",
|
||||
"procedure": "confirm",
|
||||
"receipt": "{\"body\":{\"bytes_envelopes\":5000,\"conflicts\":0,\"deadline_misses\":0,\"objects\":[],\"op_committed\":null,\"op_delivered\":null,\"op_requested\":null,\"presented\":{\"wai-stage:layers\":0},\"stage_hash\":null,\"tier\":\"none\",\"units_by_stage\":[4],\"vocabulary\":\"urn:wai:staged-delivery:1\"},\"class\":\"stage\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"f0bed61c43fa0ce8e0bb5b911bf4c58039de260b72859d64018247617dfbbc263f0ebb11aeceac8d2a9a37108e2c8cffa9435e8af32605184dbd77a4f21a8a07\",\"signer_id\":\"did:web:publisher.example#sk\",\"signer_pubkey\":\"b5f90095318f3f044cd8e40b584253d86777be62bf07221334e5fd2199cfa7f7\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"revisions": [
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"sk\",\"key\":\"b5f90095318f3f044cd8e40b584253d86777be62bf07221334e5fd2199cfa7f7\",\"may_sign\":[\"stage\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"rl\",\"key\":\"c853ad0f0cd2b619aea92ceec4fd56a24d6499d584ce79257e45cfd8139b60a7\",\"may_sign\":[\"delivery-gap\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"6f925073ff5c6c3e1ef985f52362a11f4da0de4628efd07ea0d613a8c1f8728934046608d9a2491839efb4a5f5944865d2704f70bbebb3ecbd64211b8a80cf0a\",\"status_max_age_s\":3600}"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"origin": "https://publisher.example",
|
||||
"revision": "9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z",
|
||||
"pinned_revision": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": "confirmed origin=https://publisher.example key_id=sk head=9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5 anchor=fetched"
|
||||
},
|
||||
{
|
||||
"name": "gap_needs_its_class",
|
||||
"about": "a delivery-gap claim by a key holding stage but not delivery-gap",
|
||||
"procedure": "confirm",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-18T12:00:00Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"eda93778c0eec439b94b8cd2a3b82536578518e7c3df0f1afdd47aacb33360750f17968d41d4ecffc0c2c7634db952ad6b2bbe897f16a7bd12cc56cfc5c4590e\",\"signer_id\":\"did:web:publisher.example#sk\",\"signer_pubkey\":\"b5f90095318f3f044cd8e40b584253d86777be62bf07221334e5fd2199cfa7f7\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"revisions": [
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"sk\",\"key\":\"b5f90095318f3f044cd8e40b584253d86777be62bf07221334e5fd2199cfa7f7\",\"may_sign\":[\"stage\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"rl\",\"key\":\"c853ad0f0cd2b619aea92ceec4fd56a24d6499d584ce79257e45cfd8139b60a7\",\"may_sign\":[\"delivery-gap\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"6f925073ff5c6c3e1ef985f52362a11f4da0de4628efd07ea0d613a8c1f8728934046608d9a2491839efb4a5f5944865d2704f70bbebb3ecbd64211b8a80cf0a\",\"status_max_age_s\":3600}"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"origin": "https://publisher.example",
|
||||
"revision": "9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z",
|
||||
"pinned_revision": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": "refused step=6 reason=authority_not_granted"
|
||||
},
|
||||
{
|
||||
"name": "gap_confirmed",
|
||||
"about": "a delivery-gap claim by a relay's key holding delivery-gap, under a fetched anchor",
|
||||
"procedure": "confirm",
|
||||
"receipt": "{\"body\":{\"group_id\":3,\"object_id\":0,\"observed_at\":\"2026-09-18T12:00:00Z\",\"reason\":\"delivery-timeout\",\"track_alias\":8},\"class\":\"delivery-gap\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"960bf566e9fdddf3202e1fd4929a26be1bb890100282c3ccaa8739fb0e2a594ace53fc6d740049d94919c4cba7dabaa992338276753c6b8e0d85124a64744501\",\"signer_id\":\"did:web:publisher.example#rl\",\"signer_pubkey\":\"c853ad0f0cd2b619aea92ceec4fd56a24d6499d584ce79257e45cfd8139b60a7\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
|
||||
"context": {
|
||||
"revisions": [
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
|
||||
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"sk\",\"key\":\"b5f90095318f3f044cd8e40b584253d86777be62bf07221334e5fd2199cfa7f7\",\"may_sign\":[\"stage\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"rl\",\"key\":\"c853ad0f0cd2b619aea92ceec4fd56a24d6499d584ce79257e45cfd8139b60a7\",\"may_sign\":[\"delivery-gap\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"6f925073ff5c6c3e1ef985f52362a11f4da0de4628efd07ea0d613a8c1f8728934046608d9a2491839efb4a5f5944865d2704f70bbebb3ecbd64211b8a80cf0a\",\"status_max_age_s\":3600}"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"origin": "https://publisher.example",
|
||||
"revision": "9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z",
|
||||
"pinned_revision": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": "confirmed origin=https://publisher.example key_id=rl head=9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5 anchor=fetched"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ its decision); `held_for_base` marks an envelope held for a base the sink has no
|
|||
decoded. The order of a step's reports is not part of the outcome.
|
||||
|
||||
**Evidence.** Each offer states what the sink verified of the layer's delivery receipt
|
||||
(§10), as a token: `delivered-verified`, a receipt verified through jwp-receipts §3
|
||||
(§10.1), as a token: `delivered-verified`, a receipt verified through jwp-receipts §3
|
||||
step 6 over the envelope's bytes as received; `inclusion-verified`, a receipt verified
|
||||
without those bytes (§3 steps 1, 2, 3 and 5), which never authorizes an application;
|
||||
`none`, no receipt verified. A base's `attested` states whether the sink verified a
|
||||
without those bytes (jwp-receipts §3, *Verified by inclusion*: steps 1, 2, 3 and 5),
|
||||
which never authorizes an application; `none`, no receipt verified. The same tokens are
|
||||
a `stage` claim's `evidence` (jwp-receipts §2.4.10), and `jwp_delivery::Evidence`
|
||||
converts into them. A base's `attested` states whether the sink verified a
|
||||
receipt for it, so that step 5 requires `delivered-verified` of its layers.
|
||||
|
||||
- `cases/<name>/` — one case: `case.json`, each base's envelope (`base*.wai`) and its
|
||||
|
|
|
|||
|
|
@ -326,6 +326,20 @@
|
|||
*/
|
||||
#define HeadProof_FIXED_LEN 53
|
||||
|
||||
/**
|
||||
* How many earlier groups of one chain [`ReceiptChains`] keeps the link hashes of, to
|
||||
* tell a fork from a gap (§3 step 3). Past it, the oldest are forgotten, and a fork from
|
||||
* before them reads as a gap: refused all the same, for another reason.
|
||||
*/
|
||||
#define CHAIN_HELD_LINKS 65536
|
||||
|
||||
/**
|
||||
* How many parents one chain keeps as requested (§3 step 3, §6.1): the parents named by
|
||||
* groups it refused `parent_not_held`. A group whose link hash is one of them fills a
|
||||
* gap, and is exempt from the freshness bound. Past it, the oldest request is dropped.
|
||||
*/
|
||||
#define CHAIN_REQUESTED_LINKS 64
|
||||
|
||||
/**
|
||||
* `CausalOp::mutation_kind` for an action input (move/look/buttons + free payload).
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -88,6 +88,13 @@ pub enum Reason {
|
|||
ChainBreak,
|
||||
/// A non-null parent with no previous group receipt held.
|
||||
ParentNotHeld,
|
||||
/// Under the opt-in guard of §6.1, a `group_id` no greater than that of the head
|
||||
/// of its chain, the one for its (`signer_pubkey`, `track_alias`).
|
||||
GroupIdNotIncreasing,
|
||||
/// Under the opt-in freshness bound of §6.1, a `group_end` older than the window
|
||||
/// or further ahead of the verifier's clock than the skew it allows, or not a
|
||||
/// date-time.
|
||||
GroupNotFresh,
|
||||
/// The envelope does not hash to the object's `content_hash` (§3 step 4).
|
||||
ContentHashMismatch,
|
||||
/// An object's label is unreadable or breaks §2.4.2.
|
||||
|
|
@ -124,6 +131,8 @@ impl Reason {
|
|||
Reason::GroupLabelInvalid => "group_label_invalid",
|
||||
Reason::ChainBreak => "chain_break",
|
||||
Reason::ParentNotHeld => "parent_not_held",
|
||||
Reason::GroupIdNotIncreasing => "group_id_not_increasing",
|
||||
Reason::GroupNotFresh => "group_not_fresh",
|
||||
Reason::ContentHashMismatch => "content_hash_mismatch",
|
||||
Reason::ObjectLabelInvalid => "object_label_invalid",
|
||||
Reason::ProofPositionMismatch => "proof_position_mismatch",
|
||||
|
|
@ -445,22 +454,24 @@ impl GroupReceipt {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// §3 step 3: the parent link against the previous group receipt the
|
||||
/// verifier holds for this signer. A previous receipt that does not verify
|
||||
/// is not held.
|
||||
/// §3 step 3 against a verifier that holds one group of this group's chain:
|
||||
/// `previous`, its head. A previous receipt that does not verify is not held, and
|
||||
/// neither is one under another key or on another track, which is never this
|
||||
/// group's predecessor. With nothing held, a null parent starts the chain and any
|
||||
/// other is `parent_not_held`. With the head held, a parent that is its link hash
|
||||
/// links the group, a null parent is `chain_break` (a second chain head), and any
|
||||
/// other parent names a group the verifier does not hold: `parent_not_held`.
|
||||
/// [`ReceiptChains`], which holds earlier groups too, also tells a fork from a gap.
|
||||
pub fn check_parent(&self, previous: Option<&GroupReceipt>) -> Result<(), DeliveryError> {
|
||||
let previous = previous.filter(|p| p.verify().is_ok());
|
||||
let previous = previous.filter(|p| {
|
||||
p.signer_pubkey == self.signer_pubkey && p.track_alias == self.track_alias && p.verify().is_ok()
|
||||
});
|
||||
match (previous, self.parent_receipt_hash) {
|
||||
(None, None) => Ok(()),
|
||||
(None, Some(_)) => Err(DeliveryError::at(3, Reason::ParentNotHeld)),
|
||||
(Some(_), None) => Err(DeliveryError::at(3, Reason::ChainBreak)),
|
||||
(Some(p), Some(h)) => {
|
||||
if p.signer_pubkey != self.signer_pubkey || h != p.link_hash() {
|
||||
Err(DeliveryError::at(3, Reason::ChainBreak))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
(Some(p), Some(h)) if h == p.link_hash() => Ok(()),
|
||||
(Some(_), Some(_)) => Err(DeliveryError::at(3, Reason::ParentNotHeld)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -669,6 +680,25 @@ impl ObjectReceipt {
|
|||
self.verify_inclusion(group)
|
||||
}
|
||||
|
||||
/// What the receipt establishes about its object, with or without the object's
|
||||
/// envelope: §3 steps 1, 2 and 5 always, and step 4 over `envelope` where it is
|
||||
/// given. With the envelope, the object was delivered: [`Evidence::DeliveredVerified`],
|
||||
/// which is [`ObjectReceipt::accept`]. Without it, the publisher signed an object
|
||||
/// with this content hash at this position and nothing was delivered:
|
||||
/// [`Evidence::InclusionVerified`] (§4.4), which is never §3 step 6 acceptance.
|
||||
///
|
||||
/// Step 3 is the caller's: [`ReceiptChains::verify_object`] runs it against the
|
||||
/// chain of the group's (`signer_pubkey`, `track_alias`).
|
||||
pub fn verify_against(&self, group: &GroupReceipt, envelope: Option<&[u8]>) -> Result<Evidence, DeliveryError> {
|
||||
match envelope {
|
||||
Some(e) => self.accept(group, e).map(|()| Evidence::DeliveredVerified),
|
||||
None => {
|
||||
group.verify()?;
|
||||
self.verify_inclusion(group).map(|()| Evidence::InclusionVerified)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The §2.4.2 JSON form: the table's members in its order, then, for a
|
||||
/// labelled leaf, `leaf_version: 2` and the class's members (§2.7) — none
|
||||
/// for the unmetered marker.
|
||||
|
|
@ -761,6 +791,233 @@ impl ObjectReceipt {
|
|||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── evidence and chains ─────────────────────────────
|
||||
|
||||
/// What a verifier established about one object of a group (§3, §4.4).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Evidence {
|
||||
/// §3 steps 1–5 over the object's envelope bytes as received: the object was
|
||||
/// delivered, and §3 step 6 accepts it.
|
||||
DeliveredVerified,
|
||||
/// §3 steps 1, 2 and 5, with step 4 not run because the verifier holds no envelope
|
||||
/// bytes: the publisher signed an object with this content hash at this position.
|
||||
/// It is never §3 step 6 acceptance, and never delivery.
|
||||
InclusionVerified,
|
||||
}
|
||||
|
||||
impl Evidence {
|
||||
/// `delivered-verified` or `inclusion-verified`.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Evidence::DeliveredVerified => "delivered-verified",
|
||||
Evidence::InclusionVerified => "inclusion-verified",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// §6.1's opt-in freshness bound on a group's signed `group_end`, against the
|
||||
/// verifier's own clock. A verifier applies it to the live edge only, never to
|
||||
/// historical receipts replayed to resynchronise, whose times are legitimately old.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Freshness {
|
||||
/// The verifier's clock, in seconds since 1970-01-01T00:00:00Z.
|
||||
pub now_s: i64,
|
||||
/// How old a `group_end` may be, in seconds before `now_s`.
|
||||
pub max_age_s: u64,
|
||||
/// How far a `group_end` may be ahead of `now_s`, in seconds: the clock skew allowed.
|
||||
pub max_skew_s: u64,
|
||||
}
|
||||
|
||||
impl Freshness {
|
||||
/// Whether `group_end` lies in `[now_s − max_age_s, now_s + max_skew_s]`, read to
|
||||
/// the second with its offset applied. A `group_end` that is not an RFC 3339
|
||||
/// date-time is never fresh.
|
||||
pub fn admits(&self, group_end: &str) -> bool {
|
||||
let Some(t) = crate::claim_receipt::rfc3339_seconds(group_end) else {
|
||||
return false;
|
||||
};
|
||||
let oldest = self.now_s.saturating_sub_unsigned(self.max_age_s);
|
||||
let newest = self.now_s.saturating_add_unsigned(self.max_skew_s);
|
||||
oldest <= t && t <= newest
|
||||
}
|
||||
}
|
||||
|
||||
/// How many earlier groups of one chain [`ReceiptChains`] keeps the link hashes of, to
|
||||
/// tell a fork from a gap (§3 step 3). Past it, the oldest are forgotten, and a fork from
|
||||
/// before them reads as a gap: refused all the same, for another reason.
|
||||
pub const CHAIN_HELD_LINKS: usize = 65_536;
|
||||
|
||||
/// One chain of [`ReceiptChains`]: its head, the link hashes of the groups accepted on
|
||||
/// it, and its live edge.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Chain {
|
||||
head: Option<GroupReceipt>,
|
||||
held: std::collections::BTreeSet<[u8; 32]>,
|
||||
held_order: std::collections::VecDeque<[u8; 32]>,
|
||||
/// The highest `group_id` of the chain's groups that verified (§3 steps 1–2) and were
|
||||
/// inside the freshness window when they arrived, or arrived with no bound applied,
|
||||
/// whether or not they linked: the live edge (§6.1). `group_id` is signed in both
|
||||
/// payloads, so no rewrite of a link moves it.
|
||||
edge: Option<u64>,
|
||||
}
|
||||
|
||||
impl Chain {
|
||||
fn hold(&mut self, link: [u8; 32]) {
|
||||
if self.held.insert(link) {
|
||||
self.held_order.push_back(link);
|
||||
while self.held_order.len() > CHAIN_HELD_LINKS {
|
||||
if let Some(old) = self.held_order.pop_front() {
|
||||
self.held.remove(&old);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The receipt-chain state of §3 step 3 and §6.1: one chain for each
|
||||
/// (`signer_pubkey`, `track_alias`), whose head is the last group receipt accepted on
|
||||
/// it. A group on one track neither breaks nor links to the chain of another, so a
|
||||
/// subscriber verifies the tracks it subscribes to without the receipts of tracks it
|
||||
/// does not, and a publisher that leaves `track_alias` at `0` has one chain per key.
|
||||
///
|
||||
/// Step 3, against the chain of the group's key and track:
|
||||
/// - the head offered again passes (another of its objects is being verified);
|
||||
/// - no head held: a null parent starts the chain; any other is `parent_not_held`;
|
||||
/// - a head held: a parent that is the head's link hash links the group; a null parent
|
||||
/// is `chain_break` (a second chain head); a parent that is the link hash of another
|
||||
/// group accepted on the chain is `chain_break` (a fork); any other parent names a group
|
||||
/// the verifier does not hold, and is `parent_not_held`;
|
||||
/// - a group in the legacy payload, whose parent link no signature covers, needs a
|
||||
/// `group_id` above the head's (`group_id_not_increasing`) whatever the opt-in guard
|
||||
/// says, since `group_id` is the only order it signs; with the guard on, every group
|
||||
/// does;
|
||||
/// - with the freshness bound on, a `group_end` outside it is `group_not_fresh`, unless
|
||||
/// the group's signed `group_id` is below the chain's live edge: the highest `group_id`
|
||||
/// of a group that verified and was inside the window when it arrived. Such a group is
|
||||
/// history, filling a gap behind the edge, in any order and however long the gap.
|
||||
///
|
||||
/// A group becomes the head of its chain once it passes, whether or not any of its
|
||||
/// objects arrived: a group whose objects were all dropped (§4.4) still links the next
|
||||
/// one. Its objects are then verified with or without their envelopes
|
||||
/// ([`ReceiptChains::verify_object`]).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ReceiptChains {
|
||||
chains: std::collections::BTreeMap<([u8; 32], u64), Chain>,
|
||||
group_id_guard: bool,
|
||||
freshness: Option<Freshness>,
|
||||
}
|
||||
|
||||
impl ReceiptChains {
|
||||
/// No chain held, and neither opt-in check of §6.1.
|
||||
pub fn new() -> ReceiptChains {
|
||||
ReceiptChains::default()
|
||||
}
|
||||
|
||||
/// With §6.1's monotonic guard for every group: one whose `group_id` is no greater
|
||||
/// than its chain head's is refused (`group_id_not_increasing`). A legacy group is
|
||||
/// held to it whether or not it is on.
|
||||
pub fn with_group_id_guard(mut self) -> ReceiptChains {
|
||||
self.group_id_guard = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// With §6.1's freshness bound (`group_not_fresh`).
|
||||
pub fn with_freshness(mut self, f: Freshness) -> ReceiptChains {
|
||||
self.freshness = Some(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set or clear the freshness bound: a verifier that resynchronises from history
|
||||
/// sets it once it reaches the live edge.
|
||||
pub fn set_freshness(&mut self, f: Option<Freshness>) {
|
||||
self.freshness = f;
|
||||
}
|
||||
|
||||
/// The head of the chain of (`signer_pubkey`, `track_alias`), if one is held.
|
||||
pub fn head(&self, signer_pubkey: &[u8; 32], track_alias: u64) -> Option<&GroupReceipt> {
|
||||
self.chains.get(&(*signer_pubkey, track_alias)).and_then(|c| c.head.as_ref())
|
||||
}
|
||||
|
||||
/// How many chains have a head.
|
||||
pub fn chains(&self) -> usize {
|
||||
self.chains.values().filter(|c| c.head.is_some()).count()
|
||||
}
|
||||
|
||||
/// The live edge of the chain of (`signer_pubkey`, `track_alias`): the highest
|
||||
/// `group_id` of a group of it that verified and was inside the freshness window when
|
||||
/// it arrived, or arrived with no bound applied.
|
||||
pub fn live_edge(&self, signer_pubkey: &[u8; 32], track_alias: u64) -> Option<u64> {
|
||||
self.chains.get(&(*signer_pubkey, track_alias)).and_then(|c| c.edge)
|
||||
}
|
||||
|
||||
/// §3 steps 1–3 for `group` against the chain of its (`signer_pubkey`,
|
||||
/// `track_alias`), the state unchanged (see the type's documentation for the order).
|
||||
pub fn check(&self, group: &GroupReceipt) -> Result<(), DeliveryError> {
|
||||
group.verify()?;
|
||||
let link = group.link_hash();
|
||||
let chain = self.chains.get(&(group.signer_pubkey, group.track_alias));
|
||||
let head = chain.and_then(|c| c.head.as_ref());
|
||||
if head.is_some_and(|h| h.link_hash() == link) {
|
||||
return Ok(());
|
||||
}
|
||||
match (head, group.parent_receipt_hash) {
|
||||
(None, None) => {}
|
||||
(None, Some(_)) => return Err(DeliveryError::at(3, Reason::ParentNotHeld)),
|
||||
(Some(_), None) => return Err(DeliveryError::at(3, Reason::ChainBreak)),
|
||||
(Some(h), Some(p)) if p == h.link_hash() => {}
|
||||
(Some(_), Some(p)) => {
|
||||
let fork = chain.is_some_and(|c| c.held.contains(&p));
|
||||
return Err(DeliveryError::at(3, if fork { Reason::ChainBreak } else { Reason::ParentNotHeld }));
|
||||
}
|
||||
}
|
||||
let guarded = self.group_id_guard || group.label.is_none();
|
||||
if guarded && head.is_some_and(|h| group.group_id <= h.group_id) {
|
||||
return Err(DeliveryError::at(3, Reason::GroupIdNotIncreasing));
|
||||
}
|
||||
let behind_edge = chain.and_then(|c| c.edge).is_some_and(|e| group.group_id < e);
|
||||
if let Some(f) = &self.freshness
|
||||
&& !behind_edge
|
||||
&& !f.admits(&group.group_end)
|
||||
{
|
||||
return Err(DeliveryError::at(3, Reason::GroupNotFresh));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`ReceiptChains::check`], then the group becomes the head of its chain. Whether
|
||||
/// or not it links, a group that verified and is inside the freshness window (or
|
||||
/// arrived with no bound applied) moves the chain's live edge up to its `group_id`.
|
||||
pub fn accept_group(&mut self, group: &GroupReceipt) -> Result<(), DeliveryError> {
|
||||
let r = self.check(group);
|
||||
let key = (group.signer_pubkey, group.track_alias);
|
||||
let live = group.verify().is_ok() && self.freshness.is_none_or(|f| f.admits(&group.group_end));
|
||||
if r.is_ok() || live {
|
||||
let c = self.chains.entry(key).or_default();
|
||||
if r.is_ok() {
|
||||
c.hold(group.link_hash());
|
||||
c.head = Some(group.clone());
|
||||
}
|
||||
if live {
|
||||
c.edge = Some(c.edge.map_or(group.group_id, |e| e.max(group.group_id)));
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// One object: its group through [`ReceiptChains::accept_group`], then
|
||||
/// [`ObjectReceipt::verify_against`], over `envelope` where it is given. A group
|
||||
/// that passes becomes its chain's head even when the object then fails.
|
||||
pub fn verify_object(
|
||||
&mut self,
|
||||
group: &GroupReceipt,
|
||||
object: &ObjectReceipt,
|
||||
envelope: Option<&[u8]>,
|
||||
) -> Result<Evidence, DeliveryError> {
|
||||
self.accept_group(group)?;
|
||||
object.verify_against(group, envelope)
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── the group total ─────────────────────────────
|
||||
|
||||
/// The class a total's parts compose to (§3, *The group total*), or why it has
|
||||
|
|
|
|||
|
|
@ -276,8 +276,8 @@ fn vectors() -> Vec<V> {
|
|||
v("group_labelled_below_resolution", "a 2,000 uJ total declared at 144 ppm: below the resolution floor, correctly signed", "group",
|
||||
json!({ "group": sealed(gl.clone(), &|g| g.label.as_mut().unwrap().class = Some(chip(144, 2_000_000))) }),
|
||||
reject(Some(2), Reason::GroupLabelInvalid)),
|
||||
v("group_chain_break", "the parent named is not the previous group held", "group",
|
||||
json!({ "group": gl_json, "previous": labelled_other_previous() }), reject(Some(3), Reason::ChainBreak)),
|
||||
v("group_chain_break", "a chain head (a null parent) beside a previous group held: a second chain head", "group",
|
||||
json!({ "group": gm_json, "previous": labelled_other_previous() }), reject(Some(3), Reason::ChainBreak)),
|
||||
v("group_parent_not_held", "a parent the verifier does not hold is fetched, never accepted silently", "group",
|
||||
json!({ "group": gl_json }), reject(Some(3), Reason::ParentNotHeld)),
|
||||
]);
|
||||
|
|
@ -499,9 +499,247 @@ fn vectors() -> Vec<V> {
|
|||
by_claim(prop(&om[2], 500, Some(&model), &gm), claim(500, &model).replacen("\"joules_micro\":500", "\"joules_micro\":501", 1)),
|
||||
read("unsigned")),
|
||||
]);
|
||||
out.extend(chain_vectors());
|
||||
out.extend(inclusion_vectors(&gm, &om, &flipped));
|
||||
// Last, so the vectors that predate the per-track chain stay a prefix of the corpus:
|
||||
// `group_chain_break`'s earlier input, whose outcome §3 step 3 now gives otherwise.
|
||||
out.push(v("group_parent_not_the_previous", "the parent named is not the previous group held, and no other group is held: requested, never accepted", "group",
|
||||
json!({ "group": gl_json, "previous": labelled_other_previous() }), reject(Some(3), Reason::ParentNotHeld)));
|
||||
out
|
||||
}
|
||||
|
||||
/// A group of one object on `track`, as a live staged track carries them (one object
|
||||
/// per group), signed by key `signer` and chained to `parent`; envelope `n`. Track 8's
|
||||
/// groups are in the labelled payload, which signs the parent link; track 7's in the
|
||||
/// legacy one.
|
||||
fn on_track(signer: u8, track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>, end: &str) -> GroupReceipt {
|
||||
on_track_with_object(signer, track, group_id, n, parent, end).0
|
||||
}
|
||||
|
||||
/// A group of one object in the labelled payload, which signs its parent link.
|
||||
fn on_track_with_object(signer: u8, track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>, end: &str) -> (GroupReceipt, ObjectReceipt) {
|
||||
one_object(signer, track, group_id, n, parent, end, true)
|
||||
}
|
||||
|
||||
/// The same group in the legacy payload, whose parent link no signature covers.
|
||||
fn legacy_on_track(track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>) -> GroupReceipt {
|
||||
one_object(0x11, track, group_id, n, parent, T1, false).0
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn one_object(signer: u8, track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>, end: &str, labelled: bool) -> (GroupReceipt, ObjectReceipt) {
|
||||
let mut b = GroupBuilder::default();
|
||||
b.add(crate::jwp_hash::content_hash(&envelope(n)), 0, &origin(n), None);
|
||||
let (g, o) = b.finish(&key(signer), PUBLISHER_ID, track, group_id, T0, end, parent, labelled).expect("one unmetered object composes");
|
||||
(g, o.into_iter().next().expect("one object"))
|
||||
}
|
||||
|
||||
/// Three chained groups of `track` in the labelled payload, from group `first_id`,
|
||||
/// envelopes from `first`.
|
||||
fn track_chain(track: u64, first_id: u64, first: u8) -> Vec<GroupReceipt> {
|
||||
let mut out: Vec<GroupReceipt> = Vec::new();
|
||||
for i in 0..3u8 {
|
||||
let parent = out.last().map(GroupReceipt::link_hash);
|
||||
out.push(on_track(0x11, track, first_id + u64::from(i), first + i, parent, T1));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Three chained groups of track 7 in the legacy payload, envelopes from `first`.
|
||||
fn legacy_chain(first: u8) -> Vec<GroupReceipt> {
|
||||
let mut out: Vec<GroupReceipt> = Vec::new();
|
||||
for i in 0..3u8 {
|
||||
let parent = out.last().map(GroupReceipt::link_hash);
|
||||
out.push(legacy_on_track(7, u64::from(i), first + i, parent));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `g` with its parent link rewritten and its signature untouched: in the legacy
|
||||
/// payload the signature does not cover the link, and the link hash does not either.
|
||||
fn reparented(g: &GroupReceipt, parent: [u8; 32]) -> GroupReceipt {
|
||||
GroupReceipt { parent_receipt_hash: Some(parent), ..g.clone() }
|
||||
}
|
||||
|
||||
fn ok() -> Value {
|
||||
json!({ "outcome": "accept" })
|
||||
}
|
||||
|
||||
fn chain(groups: &[&GroupReceipt], guard: bool, freshness: Option<Value>) -> Value {
|
||||
json!({ "groups": groups.iter().map(|g| g.to_json()).collect::<Vec<_>>(), "guard": guard, "freshness": freshness })
|
||||
}
|
||||
|
||||
fn results(rs: Vec<Value>) -> Value {
|
||||
json!({ "outcome": "chain", "results": rs })
|
||||
}
|
||||
|
||||
/// The receipt chain per (`signer_pubkey`, `track_alias`) — jwp-receipts §3 step 3,
|
||||
/// §5.9 and §6.1 — and the two opt-in checks of §6.1. One key publishes a base on track
|
||||
/// 7 and a layer on track 8, each with three chained groups in the labelled payload.
|
||||
fn chain_vectors() -> Vec<V> {
|
||||
let base = track_chain(7, 0, 100);
|
||||
let layer = track_chain(8, 0, 110);
|
||||
let (b0, b1, b2, l0, l1, l2) = (&base[0], &base[1], &base[2], &layer[0], &layer[1], &layer[2]);
|
||||
let cross = on_track(0x11, 8, 1, 111, Some(b1.link_hash()), T1);
|
||||
let fork = on_track(0x11, 7, 3, 112, Some(b0.link_hash()), T1);
|
||||
let nowhere = on_track(0x11, 7, 3, 113, Some(crate::jwp_hash::content_hash(b"a group nobody holds")), T1);
|
||||
let same5 = on_track(0x11, 7, 5, 120, None, T1);
|
||||
let again5 = on_track(0x11, 7, 5, 121, Some(same5.link_hash()), T1);
|
||||
let other_key = on_track(0x22, 7, 0, 130, None, T1);
|
||||
let fresh = |now: &str| Some(json!({ "now": now, "max_age_s": 60, "max_skew_s": 5 }));
|
||||
let at = |end: &str, n: u8| on_track(0x11, 7, 0, n, None, end);
|
||||
let p3 = |r: Reason| reject(Some(3), r);
|
||||
// The legacy payload: a link no signature covers, and a group_id the signature does.
|
||||
let lg = legacy_chain(160);
|
||||
let (g0, g1, g2) = (&lg[0], &lg[1], &lg[2]);
|
||||
let g0_again = reparented(g0, g2.link_hash());
|
||||
let g3 = legacy_on_track(7, 3, 163, Some(g2.link_hash()));
|
||||
let lsame5 = legacy_on_track(7, 5, 164, None);
|
||||
let lagain5 = legacy_on_track(7, 5, 165, Some(lsame5.link_hash()));
|
||||
let b0_again = reparented(b0, b2.link_hash());
|
||||
let b3 = on_track(0x11, 7, 3, 103, Some(b2.link_hash()), T1);
|
||||
// A relay holds one group of a live track past the window, then later groups name it.
|
||||
let w0 = on_track(0x11, 8, 0, 170, None, T1);
|
||||
let w1 = on_track(0x11, 8, 1, 171, Some(w0.link_hash()), "2026-09-30T11:58:00Z");
|
||||
let w2 = on_track(0x11, 8, 2, 172, Some(w1.link_hash()), T1);
|
||||
let w3 = on_track(0x11, 8, 3, 173, Some(w2.link_hash()), T1);
|
||||
// A relay holds a group past the window and rewrites a link to earn an exemption.
|
||||
let r0 = legacy_on_track(7, 0, 190, None);
|
||||
let r1 = one_object(0x11, 7, 1, 191, Some(r0.link_hash()), "2026-09-30T11:00:00Z", false).0;
|
||||
let r1_self = reparented(&r1, r1.link_hash());
|
||||
let q0 = on_track(0x11, 7, 0, 192, None, T1);
|
||||
let q1 = on_track(0x11, 7, 1, 193, Some(q0.link_hash()), "2026-09-30T11:00:00Z");
|
||||
let q1_self = reparented(&q1, q1.link_hash());
|
||||
// Stale groups never move the live edge.
|
||||
let s0 = on_track(0x11, 7, 0, 196, None, T1);
|
||||
let s1 = on_track(0x11, 7, 1, 197, Some(s0.link_hash()), "2026-09-30T11:00:00Z");
|
||||
let s2 = on_track(0x11, 7, 2, 198, Some(s1.link_hash()), "2026-09-30T11:00:00Z");
|
||||
// Seventy groups held past the window, and a fresh seventy-first.
|
||||
let mut long = vec![on_track(0x11, 8, 0, 194, None, T1)];
|
||||
for i in 1..=71u64 {
|
||||
let parent = long.last().map(GroupReceipt::link_hash);
|
||||
let end = if i <= 70 { "2026-09-30T11:00:00Z" } else { T1 };
|
||||
long.push(on_track(0x11, 8, i, (194 + i) as u8, parent, end));
|
||||
}
|
||||
let mut long_order: Vec<GroupReceipt> = vec![long[0].clone(), long[71].clone()];
|
||||
long_order.extend((1..=70).rev().map(|i| long[i].clone()));
|
||||
long_order.extend((1..=71).map(|i| long[i].clone()));
|
||||
let mut long_results = vec![ok(), p3(Reason::ParentNotHeld)];
|
||||
long_results.extend((2..=70).map(|_| p3(Reason::ParentNotHeld)));
|
||||
long_results.push(ok());
|
||||
long_results.extend((1..=71).map(|_| ok()));
|
||||
// A publisher that chained per key across two tracks, as §2.4.1 once read.
|
||||
let k7 = on_track(0x11, 7, 0, 180, None, T1);
|
||||
let k8 = on_track(0x11, 8, 1, 181, Some(k7.link_hash()), T1);
|
||||
let k7b = on_track(0x11, 7, 2, 182, Some(k8.link_hash()), T1);
|
||||
vec![
|
||||
v("v6_per_track_chain_base", "one key's base track (7): three chained groups, each accepted", "chain",
|
||||
chain(&[b0, b1, b2], false, None), results(vec![ok(), ok(), ok()])),
|
||||
v("v6_per_track_chain_interleaved", "the base track (7) and a layer track (8) under one key, offered interleaved: two chains, every group accepted; a verifier keyed per key alone would break at the layer's first group", "chain",
|
||||
chain(&[b0, l0, b1, l1, b2, l2], false, None), results(vec![ok(), ok(), ok(), ok(), ok(), ok()])),
|
||||
v("v6_per_track_chain_layer_without_its_first", "the layer track's second group without its first: requested, never accepted, whatever is held of the base track", "chain",
|
||||
chain(&[b0, b1, l1], false, None), results(vec![ok(), ok(), p3(Reason::ParentNotHeld)])),
|
||||
v("v6_per_track_chain_cross_track_link", "a layer group whose parent is a base group: a link across tracks is no link, and names nothing the layer's chain holds", "chain",
|
||||
chain(&[b0, b1, l0, &cross], false, None), results(vec![ok(), ok(), ok(), p3(Reason::ParentNotHeld)])),
|
||||
v("chain_head_predecessor_replayed", "the base track's first group offered again after its second: a chain head beside a held head is a fork", "chain",
|
||||
chain(&[b0, b1, b0], false, None), results(vec![ok(), ok(), p3(Reason::ChainBreak)])),
|
||||
v("chain_fork_from_a_held_group", "a group whose parent is a group the chain holds that is not its head: a fork", "chain",
|
||||
chain(&[b0, b1, b2, &fork], false, None), results(vec![ok(), ok(), ok(), p3(Reason::ChainBreak)])),
|
||||
v("chain_parent_not_held_beside_head", "a group whose parent is no group the chain holds: a gap, requested, not a fork", "chain",
|
||||
chain(&[b0, b1, b2, &nowhere], false, None), results(vec![ok(), ok(), ok(), p3(Reason::ParentNotHeld)])),
|
||||
v("chain_head_offered_again", "the head offered again, for another of its objects: it passes", "chain",
|
||||
chain(&[b0, b0], false, None), results(vec![ok(), ok()])),
|
||||
v("chain_another_key_same_track", "another key's group on the same track: another chain", "chain",
|
||||
chain(&[b0, &other_key], false, None), results(vec![ok(), ok()])),
|
||||
v("chain_group_id_guard_off", "a correctly chained labelled group whose group_id does not increase: the guard is opt-in for labelled groups, and off it is accepted", "chain",
|
||||
chain(&[&same5, &again5], false, None), results(vec![ok(), ok()])),
|
||||
v("chain_group_id_guard_on", "the same with the guard of section 6.1 on", "chain",
|
||||
chain(&[&same5, &again5], true, None), results(vec![ok(), p3(Reason::GroupIdNotIncreasing)])),
|
||||
v("chain_legacy_group_id_always_guarded", "the same in the legacy payload with the guard off: a legacy group's parent link is unsigned and its group_id is not, so the guard always applies to it", "chain",
|
||||
chain(&[&lsame5, &lagain5], false, None), results(vec![ok(), p3(Reason::GroupIdNotIncreasing)])),
|
||||
v("chain_group_id_guard_per_track", "with the guard on, the layer track starts at group 0 while the base track stands at 2: ids are compared within a chain", "chain",
|
||||
chain(&[b0, b1, b2, l0], true, None), results(vec![ok(), ok(), ok(), ok()])),
|
||||
v("chain_legacy_replayed_as_new", "a legacy chain g0, g1, g2; then g0 with its unsigned parent rewritten to g2's link hash, which leaves its link hash unchanged; then the publisher's genuine g3. The replay is refused by its signed group_id, guard off, and g3 still links", "chain",
|
||||
chain(&[g0, g1, g2, &g0_again, &g3], false, None), results(vec![ok(), ok(), ok(), p3(Reason::GroupIdNotIncreasing), ok()])),
|
||||
v("chain_legacy_replay_loop", "the same rewritten g0, then the whole history replayed after it, twice: no replayed group advances the chain; g1 names a group the chain holds behind its head, and g2 is the head offered again", "chain",
|
||||
chain(&[g0, g1, g2, &g0_again, g1, g2, &g0_again, g1], false, None),
|
||||
results(vec![ok(), ok(), ok(), p3(Reason::GroupIdNotIncreasing), p3(Reason::ChainBreak), ok(), p3(Reason::GroupIdNotIncreasing), p3(Reason::ChainBreak)])),
|
||||
v("chain_labelled_replay_rewritten", "the labelled chain's first group with its parent rewritten to the head's link: the labelled payload signs the link, so the replay fails its signature", "chain",
|
||||
chain(&[b0, b1, b2, &b0_again, &b3], false, None), results(vec![ok(), ok(), ok(), reject(Some(2), Reason::SignatureVerifyFailed), ok()])),
|
||||
v("chain_issued_per_key_across_tracks", "a key that chained its groups per key across tracks 7 and 8, as an earlier reading of section 2.4.1 allowed: each group that names another track's group names nothing its own chain holds", "chain",
|
||||
chain(&[&k7, &k8, &k7b], false, None), results(vec![ok(), p3(Reason::ParentNotHeld), p3(Reason::ParentNotHeld)])),
|
||||
v("chain_fresh", "the freshness bound on, a group_end at the verifier's clock", "chain",
|
||||
chain(&[&at(T1, 140)], false, fresh(T1)), results(vec![ok()])),
|
||||
v("chain_fresh_offset", "a group_end written with an offset, the same instant as the clock", "chain",
|
||||
chain(&[&at("2026-09-30T14:00:01+02:00", 141)], false, fresh(T1)), results(vec![ok()])),
|
||||
v("chain_fresh_at_window", "a group_end exactly 60 s old, its fraction dropped: at the window", "chain",
|
||||
chain(&[&at("2026-09-30T11:59:01.900Z", 142)], false, fresh(T1)), results(vec![ok()])),
|
||||
v("chain_stale", "a group_end older than the window", "chain",
|
||||
chain(&[&at("2026-09-30T11:00:00Z", 143)], false, fresh(T1)), results(vec![p3(Reason::GroupNotFresh)])),
|
||||
v("chain_ahead_of_clock", "a group_end further ahead of the clock than the skew allowed", "chain",
|
||||
chain(&[&at("2026-09-30T12:00:07Z", 144)], false, fresh(T1)), results(vec![p3(Reason::GroupNotFresh)])),
|
||||
v("chain_stale_without_bound", "the same stale group with no bound: historical receipts replayed for resync are accepted", "chain",
|
||||
chain(&[&at("2026-09-30T11:00:00Z", 143)], false, None), results(vec![ok()])),
|
||||
v("chain_freshness_gap_refilled", "a relay holds group 1 of a live track past the window: it is refused as stale; groups 2 and 3, fresh, name groups the chain does not hold and are gaps, and move the live edge to 3; group 1, offered again, has a signed group_id behind the edge, is history, exempt from the bound, and fills the gap; then 2 and 3 link", "chain",
|
||||
chain(&[&w0, &w1, &w2, &w3, &w1, &w2, &w3], true, fresh(T1)),
|
||||
results(vec![ok(), p3(Reason::GroupNotFresh), p3(Reason::ParentNotHeld), p3(Reason::ParentNotHeld), ok(), ok(), ok()])),
|
||||
v("chain_freshness_rewritten_legacy_link", "a relay holds a legacy chain's newest group past the window, then offers it with its unsigned parent rewritten to its own link hash, a group the chain does not hold: a gap, but a stale one, which moves no edge; offered once more with its genuine link, it is still stale and refused", "chain",
|
||||
chain(&[&r0, &r1, &r1_self, &r1], false, fresh(T1)),
|
||||
results(vec![ok(), p3(Reason::GroupNotFresh), p3(Reason::ParentNotHeld), p3(Reason::GroupNotFresh)])),
|
||||
v("chain_freshness_rewritten_labelled_link", "the same with a labelled group: the rewritten link fails its signature", "chain",
|
||||
chain(&[&q0, &q1, &q1_self, &q1], false, fresh(T1)),
|
||||
results(vec![ok(), p3(Reason::GroupNotFresh), reject(Some(2), Reason::SignatureVerifyFailed), p3(Reason::GroupNotFresh)])),
|
||||
v("chain_stale_group_moves_no_edge", "a stale group 2 that names a group not held, then a stale group 1 that links to the head: a stale group moves no live edge, so group 1 is not behind one and is refused as stale", "chain",
|
||||
chain(&[&s0, &s2, &s1], false, fresh(T1)),
|
||||
results(vec![ok(), p3(Reason::ParentNotHeld), p3(Reason::GroupNotFresh)])),
|
||||
v("chain_freshness_long_gap", "seventy groups of a live track held past the window, then a fresh seventy-first that names the seventieth: walked back, each names a group not held, until the first links; walked forward, every one is behind the live edge and is accepted. No count bounds a gap", "chain",
|
||||
chain(&long_order.iter().collect::<Vec<_>>(), true, fresh(T1)),
|
||||
results(long_results)),
|
||||
v("group_parent_on_another_track", "a base group whose parent link names a layer group, given that group as the previous one: a group on another track is never held as this one's predecessor, so the parent names nothing held", "group",
|
||||
json!({ "group": on_track(0x11, 7, 1, 151, Some(l0.link_hash()), T1).to_json(), "previous": l0.to_json() }),
|
||||
reject(Some(3), Reason::ParentNotHeld)),
|
||||
v("group_null_parent_previous_on_another_track", "a chain head given a previous group on another track: nothing of its own chain is held, so it starts the chain", "group",
|
||||
json!({ "group": on_track(0x11, 7, 0, 152, None, T1).to_json(), "previous": l0.to_json() }),
|
||||
group_accept(&on_track(0x11, 7, 0, 152, None, T1))),
|
||||
v("group_null_parent_previous_of_another_key", "a chain head given another key's group as the previous one: it starts the chain", "group",
|
||||
json!({ "group": on_track(0x11, 7, 0, 153, None, T1).to_json(), "previous": other_key.to_json() }),
|
||||
group_accept(&on_track(0x11, 7, 0, 153, None, T1))),
|
||||
v("chain_bad_signature_heads_nothing", "a group whose signature fails is refused at step 2, and the next group of its track finds no head", "chain",
|
||||
chain(&[&resigned_wrong(b0), b1], false, None), results(vec![reject(Some(2), Reason::SignatureVerifyFailed), p3(Reason::ParentNotHeld)])),
|
||||
]
|
||||
}
|
||||
|
||||
/// `g` with one bit of its signature flipped.
|
||||
fn resigned_wrong(g: &GroupReceipt) -> GroupReceipt {
|
||||
let mut t = g.clone();
|
||||
t.sig[0] ^= 1;
|
||||
t
|
||||
}
|
||||
|
||||
/// §4.4: an object receipt checked without the object's envelope — steps 1, 2 and 5,
|
||||
/// step 4 not run — is `inclusion-verified`, never delivery.
|
||||
fn inclusion_vectors(gm: &GroupReceipt, om: &[ObjectReceipt], flipped: &GroupReceipt) -> Vec<V> {
|
||||
let (lg, lo) = on_track_with_object(0x11, 8, 0, 110, None, T1);
|
||||
let inc = |g: &GroupReceipt, o: Value| json!({ "group": g.to_json(), "object": o });
|
||||
let verified = json!({ "outcome": "accept", "evidence": "inclusion-verified" });
|
||||
let mut moved = om[2].to_json();
|
||||
moved["content_hash"] = hexs(&crate::jwp_hash::content_hash(b"WAI1 another object")).into();
|
||||
let mut position = om[2].to_json();
|
||||
position["object_id"] = 0.into();
|
||||
vec![
|
||||
v("inclusion_dropped_layer_object", "a live layer object a relay did not forward: its receipt, checked without the envelope, is inclusion-verified", "inclusion",
|
||||
inc(&lg, lo.to_json()), verified.clone()),
|
||||
v("inclusion_labelled_object", "a labelled object checked without its envelope: the label, the position and the walk still run", "inclusion",
|
||||
inc(gm, om[0].to_json()), verified),
|
||||
v("inclusion_bad_group_signature", "without the envelope, the group signature still runs", "inclusion",
|
||||
inc(flipped, om[0].to_json()), reject(Some(2), Reason::SignatureVerifyFailed)),
|
||||
v("inclusion_content_hash_rewritten", "a receipt whose content hash was rewritten: step 4 cannot run without bytes, and the walk misses the root", "inclusion",
|
||||
inc(gm, moved), reject(Some(5), Reason::InclusionProofFailed)),
|
||||
v("inclusion_wrong_position", "a receipt presented at another object's position", "inclusion",
|
||||
inc(gm, position), reject(Some(5), Reason::ProofPositionMismatch)),
|
||||
]
|
||||
}
|
||||
|
||||
/// A group receipt, valid, that is not the labelled group's parent.
|
||||
fn labelled_other_previous() -> Value {
|
||||
build(80, &[(1, None)], 2, None, false).0.to_json()
|
||||
|
|
@ -635,6 +873,45 @@ pub fn verdict(v: &Value) -> Result<String, String> {
|
|||
Err(e) => err_label(&e),
|
||||
})
|
||||
}
|
||||
Some("chain") => {
|
||||
let mut chains = ReceiptChains::new();
|
||||
if i.get("guard").and_then(Value::as_bool).ok_or("no guard")? {
|
||||
chains = chains.with_group_id_guard();
|
||||
}
|
||||
match i.get("freshness") {
|
||||
None | Some(Value::Null) => {}
|
||||
Some(f) => {
|
||||
let now = f.get("now").and_then(Value::as_str).ok_or("freshness.now")?;
|
||||
chains = chains.with_freshness(Freshness {
|
||||
now_s: crate::claim_receipt::rfc3339_seconds(now).ok_or("freshness.now")?,
|
||||
max_age_s: f.get("max_age_s").and_then(Value::as_u64).ok_or("freshness.max_age_s")?,
|
||||
max_skew_s: f.get("max_skew_s").and_then(Value::as_u64).ok_or("freshness.max_skew_s")?,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for g in i.get("groups").and_then(Value::as_array).ok_or("no groups")? {
|
||||
out.push(match GroupReceipt::from_json(g).and_then(|g| chains.accept_group(&g)) {
|
||||
Ok(()) => "accept".to_owned(),
|
||||
Err(e) => err_label(&e),
|
||||
});
|
||||
}
|
||||
Ok(format!("chain: {}", out.join(" / ")))
|
||||
}
|
||||
Some("inclusion") => {
|
||||
let g = match group("group")? {
|
||||
Ok(g) => g,
|
||||
Err(e) => return Ok(err_label(&e)),
|
||||
};
|
||||
let o = match ObjectReceipt::from_json(i.get("object").ok_or("no object")?) {
|
||||
Ok(o) => o,
|
||||
Err(e) => return Ok(err_label(&e)),
|
||||
};
|
||||
Ok(match o.verify_against(&g, None) {
|
||||
Ok(e) => format!("accept evidence={}", e.label()),
|
||||
Err(e) => err_label(&e),
|
||||
})
|
||||
}
|
||||
other => Err(format!("unknown procedure {other:?}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -642,11 +919,23 @@ pub fn verdict(v: &Value) -> Result<String, String> {
|
|||
/// The verdict a vector declares.
|
||||
pub fn expected(v: &Value) -> Result<String, String> {
|
||||
let e = v.get("expected").ok_or("no expected")?;
|
||||
let s = |k: &str| e.get(k).and_then(Value::as_str).ok_or(format!("expected.{k}"));
|
||||
if let Some(l) = e.get("leaf").and_then(Value::as_str) {
|
||||
return Ok(format!("leaf={l}"));
|
||||
}
|
||||
one(e)
|
||||
}
|
||||
|
||||
/// One declared outcome: an accept (with a link hash and signing payload for a group, or
|
||||
/// with the evidence for an object checked by inclusion), a reading, a rejection, or a
|
||||
/// chain's outcomes in order.
|
||||
fn one(e: &Value) -> Result<String, String> {
|
||||
let s = |k: &str| e.get(k).and_then(Value::as_str).ok_or(format!("expected.{k}"));
|
||||
match s("outcome")? {
|
||||
"chain" => {
|
||||
let rs = e.get("results").and_then(Value::as_array).ok_or("expected.results")?;
|
||||
Ok(format!("chain: {}", rs.iter().map(one).collect::<Result<Vec<_>, _>>()?.join(" / ")))
|
||||
}
|
||||
"accept" if e.get("evidence").is_some() => Ok(format!("accept evidence={}", s("evidence")?)),
|
||||
"accept" => match (e.get("link_hash"), e.get("signing_payload")) {
|
||||
(Some(l), Some(p)) => Ok(format!(
|
||||
"accept link_hash={} signing_payload={}",
|
||||
|
|
|
|||
|
|
@ -434,8 +434,10 @@ fn the_parent_link() {
|
|||
assert_eq!(g1.check_parent(Some(&g0)), Ok(()));
|
||||
assert_eq!(g1.check_parent(None), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert_eq!(g0.check_parent(Some(&g1)), Err(DeliveryError::at(3, Reason::ChainBreak)));
|
||||
// A parent that is not the one group held names a group the verifier does not
|
||||
// hold: requested, never accepted.
|
||||
let (other, _) = group(&[(2, lab(EnergyProvenance::ModelBased))], true);
|
||||
assert_eq!(g1.check_parent(Some(&other)), Err(DeliveryError::at(3, Reason::ChainBreak)));
|
||||
assert_eq!(g1.check_parent(Some(&other)), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -523,3 +525,314 @@ fn a_property_reads_as_signed_only_where_a_signed_statement_restates_it() {
|
|||
forged.sig[0] ^= 1;
|
||||
assert_eq!(prop(1, 500, Some(&m)).check_against_claim(&forged), Ok(PropertyReading::Unsigned));
|
||||
}
|
||||
|
||||
// ───────────────────────────── evidence and chains ─────────────────────────────
|
||||
|
||||
/// One object a group, as a live staged track carries them: the object `n` of `track`,
|
||||
/// at `group_id`, chained to `parent`, signed by key `signer`.
|
||||
fn on_track(signer: u8, track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>, end: &str) -> (GroupReceipt, ObjectReceipt) {
|
||||
in_payload(signer, track, group_id, n, parent, end, false)
|
||||
}
|
||||
|
||||
/// The same group in the labelled payload, which signs its parent link.
|
||||
fn on_track_labelled(signer: u8, track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>, end: &str) -> GroupReceipt {
|
||||
in_payload(signer, track, group_id, n, parent, end, true).0
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn in_payload(signer: u8, track: u64, group_id: u64, n: u8, parent: Option<[u8; 32]>, end: &str, labelled: bool) -> (GroupReceipt, ObjectReceipt) {
|
||||
let mut b = GroupBuilder::default();
|
||||
b.add(crate::jwp_hash::content_hash(&env(n)), 0, "origin.example", None);
|
||||
let (g, o) = b.finish(&key(signer), "did:example:publisher", track, group_id, T0, end, parent, labelled).expect("composes");
|
||||
(g, o.into_iter().next().expect("one object"))
|
||||
}
|
||||
|
||||
/// Three chained groups on one track, objects numbered from `first`.
|
||||
fn track_of(track: u64, first: u8) -> Vec<(GroupReceipt, ObjectReceipt)> {
|
||||
let mut out: Vec<(GroupReceipt, ObjectReceipt)> = Vec::new();
|
||||
for i in 0..3u8 {
|
||||
let parent = out.last().map(|(g, _)| g.link_hash());
|
||||
out.push(on_track(0x11, track, u64::from(i), first + i, parent, T1));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// With the envelope, an object is delivered; without it, only its inclusion under the
|
||||
/// signed root is established, and nothing about it is delivery (§4.4).
|
||||
#[test]
|
||||
fn an_object_without_its_envelope_is_inclusion_verified_never_delivered() {
|
||||
let (g, objs) = group(&[(0, None), (0, None), (0, None)], false);
|
||||
assert_eq!(objs[1].verify_against(&g, Some(&env(1))), Ok(Evidence::DeliveredVerified));
|
||||
assert_eq!(objs[1].verify_against(&g, None), Ok(Evidence::InclusionVerified));
|
||||
assert_eq!(Evidence::InclusionVerified.label(), "inclusion-verified");
|
||||
// Step 4 runs only over bytes held.
|
||||
assert_eq!(objs[1].verify_against(&g, Some(&env(2))), Err(DeliveryError::at(4, Reason::ContentHashMismatch)));
|
||||
// Steps 2 and 5 still run without them.
|
||||
let mut forged = g.clone();
|
||||
forged.sig[0] ^= 1;
|
||||
assert_eq!(objs[1].verify_against(&forged, None), Err(DeliveryError::at(2, Reason::SignatureVerifyFailed)));
|
||||
let mut moved = objs[1].clone();
|
||||
moved.content_hash = crate::jwp_hash::content_hash(b"another object");
|
||||
assert_eq!(moved.verify_against(&g, None), Err(DeliveryError::at(5, Reason::InclusionProofFailed)));
|
||||
}
|
||||
|
||||
/// One key, two tracks, three chained groups each, offered interleaved: every group is
|
||||
/// accepted, and each track keeps its own head. Keyed per key alone, the second track's
|
||||
/// first group (a null parent beside a held head) would break the chain.
|
||||
#[test]
|
||||
fn chains_are_per_key_and_track() {
|
||||
let (base, layer) = (track_of(7, 0), track_of(8, 10));
|
||||
let mut v = ReceiptChains::new().with_group_id_guard();
|
||||
for i in 0..3 {
|
||||
for (g, o) in [&base[i], &layer[i]] {
|
||||
let n = if g.track_alias == 7 { i as u8 } else { 10 + i as u8 };
|
||||
assert_eq!(v.verify_object(g, o, Some(&env(n))), Ok(Evidence::DeliveredVerified), "track {} group {i}", g.track_alias);
|
||||
}
|
||||
}
|
||||
assert_eq!(v.chains(), 2);
|
||||
assert_eq!(v.head(&base[2].0.signer_pubkey, 7), Some(&base[2].0));
|
||||
assert_eq!(v.head(&layer[2].0.signer_pubkey, 8), Some(&layer[2].0));
|
||||
// A subscriber to the base track alone verifies it without the layer's receipts.
|
||||
let mut base_only = ReceiptChains::new();
|
||||
for (g, _) in &base {
|
||||
assert_eq!(base_only.accept_group(g), Ok(()));
|
||||
}
|
||||
// A link across tracks is no link: track 8's chain does not continue track 7's.
|
||||
// It names nothing the layer's chain holds.
|
||||
let (cross, _) = on_track(0x11, 8, 3, 13, Some(base[2].0.link_hash()), T1);
|
||||
assert_eq!(v.check(&cross), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
// Handed the base track's group as its predecessor, it still does not link: a group
|
||||
// of another track is not held as this one's.
|
||||
assert_eq!(cross.check_parent(Some(&base[2].0)), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
let (head, _) = on_track(0x11, 8, 0, 14, None, T1);
|
||||
assert_eq!(head.check_parent(Some(&base[2].0)), Ok(()), "a chain head beside another track's group starts its chain");
|
||||
}
|
||||
|
||||
/// A track's second group without its first is requested, never accepted (§3 step 3),
|
||||
/// whatever the verifier holds of other tracks.
|
||||
#[test]
|
||||
fn a_track_joined_mid_chain_is_parent_not_held() {
|
||||
let (base, layer) = (track_of(7, 0), track_of(8, 10));
|
||||
let mut v = ReceiptChains::new();
|
||||
v.accept_group(&base[0].0).unwrap();
|
||||
assert_eq!(v.accept_group(&layer[1].0), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert!(v.head(&layer[1].0.signer_pubkey, 8).is_none(), "a refused group heads nothing");
|
||||
v.accept_group(&layer[0].0).unwrap();
|
||||
assert_eq!(v.accept_group(&layer[1].0), Ok(()));
|
||||
// Another key's track 8 is another chain.
|
||||
let (other, _) = on_track(0x12, 8, 0, 20, None, T1);
|
||||
assert_eq!(v.accept_group(&other), Ok(()));
|
||||
assert_eq!(v.chains(), 3);
|
||||
}
|
||||
|
||||
/// A group whose one object was dropped still heads its chain, so the next group of
|
||||
/// the track links; the dropped object verifies by inclusion only.
|
||||
#[test]
|
||||
fn a_group_whose_object_was_dropped_still_links_the_next() {
|
||||
let layer = track_of(8, 10);
|
||||
let mut v = ReceiptChains::new();
|
||||
assert_eq!(v.verify_object(&layer[0].0, &layer[0].1, Some(&env(10))), Ok(Evidence::DeliveredVerified));
|
||||
assert_eq!(v.verify_object(&layer[1].0, &layer[1].1, None), Ok(Evidence::InclusionVerified));
|
||||
assert_eq!(v.verify_object(&layer[2].0, &layer[2].1, Some(&env(12))), Ok(Evidence::DeliveredVerified));
|
||||
// The same group offered again, for another of its objects, passes as the head.
|
||||
assert_eq!(v.verify_object(&layer[2].0, &layer[2].1, None), Ok(Evidence::InclusionVerified));
|
||||
}
|
||||
|
||||
/// §6.1's guard is opt-in and per chain: a correctly chained group whose `group_id`
|
||||
/// does not increase is refused only with the guard on, and a track's ids are compared
|
||||
/// with that track's alone.
|
||||
#[test]
|
||||
fn the_group_id_guard_is_opt_in_and_per_track() {
|
||||
let g0 = on_track_labelled(0x11, 7, 5, 0, None, T1);
|
||||
let g1 = on_track_labelled(0x11, 7, 5, 1, Some(g0.link_hash()), T1);
|
||||
let mut off = ReceiptChains::new();
|
||||
off.accept_group(&g0).unwrap();
|
||||
assert_eq!(off.accept_group(&g1), Ok(()));
|
||||
let mut on = ReceiptChains::new().with_group_id_guard();
|
||||
on.accept_group(&g0).unwrap();
|
||||
assert_eq!(on.check(&g1), Err(DeliveryError::at(3, Reason::GroupIdNotIncreasing)));
|
||||
// Track 8 starts at 0 while track 7 stands at 5.
|
||||
assert_eq!(on.accept_group(&on_track(0x11, 8, 0, 2, None, T1).0), Ok(()));
|
||||
}
|
||||
|
||||
/// A legacy group's parent link is outside its signature, and its link hash does not
|
||||
/// cover the link either; its `group_id` is inside. So the guard holds every legacy
|
||||
/// group, on or off: rewriting the first group's link to name the head makes it link,
|
||||
/// and its signed `group_id` refuses it; no replay of the history advances the chain, and
|
||||
/// the publisher's next group still links.
|
||||
#[test]
|
||||
fn a_legacy_history_cannot_be_replayed_as_new() {
|
||||
let g: Vec<GroupReceipt> = {
|
||||
let mut out: Vec<GroupReceipt> = Vec::new();
|
||||
for i in 0..3u8 {
|
||||
let parent = out.last().map(GroupReceipt::link_hash);
|
||||
out.push(on_track(0x11, 7, u64::from(i), 20 + i, parent, T1).0);
|
||||
}
|
||||
out
|
||||
};
|
||||
let replay = GroupReceipt { parent_receipt_hash: Some(g[2].link_hash()), ..g[0].clone() };
|
||||
assert_eq!(replay.verify(), Ok(()), "the rewritten link is outside the legacy signature");
|
||||
assert_eq!(replay.link_hash(), g[0].link_hash(), "and outside the legacy link hash");
|
||||
let next = on_track(0x11, 7, 3, 23, Some(g[2].link_hash()), T1).0;
|
||||
let mut v = ReceiptChains::new();
|
||||
for x in &g {
|
||||
v.accept_group(x).unwrap();
|
||||
}
|
||||
assert_eq!(v.accept_group(&replay), Err(DeliveryError::at(3, Reason::GroupIdNotIncreasing)));
|
||||
assert_eq!(v.accept_group(&g[1]), Err(DeliveryError::at(3, Reason::ChainBreak)), "g1 names a group behind the head");
|
||||
assert_eq!(v.accept_group(&g[2]), Ok(()), "the head offered again");
|
||||
assert_eq!(v.head(&g[2].signer_pubkey, 7), Some(&g[2]));
|
||||
assert_eq!(v.accept_group(&next), Ok(()));
|
||||
// Labelled, the same rewrite breaks the signature.
|
||||
let b0 = on_track_labelled(0x11, 8, 0, 30, None, T1);
|
||||
let b1 = on_track_labelled(0x11, 8, 1, 31, Some(b0.link_hash()), T1);
|
||||
let rewritten = GroupReceipt { parent_receipt_hash: Some(b1.link_hash()), ..b0.clone() };
|
||||
assert_eq!(rewritten.verify(), Err(DeliveryError::at(2, Reason::SignatureVerifyFailed)));
|
||||
}
|
||||
|
||||
/// A parent the chain holds behind its head is a fork; one it does not hold is a gap,
|
||||
/// reported `parent_not_held`.
|
||||
#[test]
|
||||
fn a_fork_is_told_from_a_gap() {
|
||||
let b0 = on_track_labelled(0x11, 7, 0, 40, None, T1);
|
||||
let b1 = on_track_labelled(0x11, 7, 1, 41, Some(b0.link_hash()), T1);
|
||||
let fork = on_track_labelled(0x11, 7, 2, 42, Some(b0.link_hash()), T1);
|
||||
let gap = on_track_labelled(0x11, 7, 2, 43, Some(crate::jwp_hash::content_hash(b"unseen")), T1);
|
||||
let mut v = ReceiptChains::new();
|
||||
v.accept_group(&b0).unwrap();
|
||||
v.accept_group(&b1).unwrap();
|
||||
assert_eq!(v.accept_group(&fork), Err(DeliveryError::at(3, Reason::ChainBreak)));
|
||||
assert_eq!(v.accept_group(&gap), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert_eq!(v.live_edge(&b0.signer_pubkey, 7), Some(2), "a verified group moves the edge, linked or not");
|
||||
}
|
||||
|
||||
/// With the freshness bound on, a group a relay held past the window is refused; the
|
||||
/// later, fresh groups that name it are gaps, and move the live edge past it; it is then
|
||||
/// history, exempt from the bound, and fills the gap: the chain does not wedge.
|
||||
#[test]
|
||||
fn a_stale_group_behind_the_live_edge_is_exempt() {
|
||||
let now = crate::claim_receipt::rfc3339_seconds(T1).unwrap();
|
||||
let f = Freshness { now_s: now, max_age_s: 60, max_skew_s: 5 };
|
||||
let w0 = on_track_labelled(0x11, 8, 0, 50, None, T1);
|
||||
let w1 = on_track_labelled(0x11, 8, 1, 51, Some(w0.link_hash()), "2026-09-30T11:58:00Z");
|
||||
let w2 = on_track_labelled(0x11, 8, 2, 52, Some(w1.link_hash()), T1);
|
||||
let w3 = on_track_labelled(0x11, 8, 3, 53, Some(w2.link_hash()), T1);
|
||||
let mut v = ReceiptChains::new().with_group_id_guard().with_freshness(f);
|
||||
v.accept_group(&w0).unwrap();
|
||||
assert_eq!(v.accept_group(&w1), Err(DeliveryError::at(3, Reason::GroupNotFresh)));
|
||||
assert_eq!(v.live_edge(&w0.signer_pubkey, 8), Some(0), "a stale group does not move the edge");
|
||||
assert_eq!(v.accept_group(&w2), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert_eq!(v.accept_group(&w3), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert_eq!(v.live_edge(&w0.signer_pubkey, 8), Some(3));
|
||||
assert_eq!(v.accept_group(&w1), Ok(()), "behind the edge: history, not the live edge");
|
||||
assert_eq!(v.accept_group(&w2), Ok(()));
|
||||
assert_eq!(v.accept_group(&w3), Ok(()));
|
||||
// A stale group at or past the edge is still refused.
|
||||
let stale = on_track_labelled(0x11, 8, 4, 54, Some(w3.link_hash()), "2026-09-30T11:58:00Z");
|
||||
assert_eq!(v.accept_group(&stale), Err(DeliveryError::at(3, Reason::GroupNotFresh)));
|
||||
// Exactly at the edge: a fresh group 5 names a group not held and sets the edge to 5;
|
||||
// another, stale group 5 that links to the head is not below the edge, and is refused.
|
||||
let edge5 = on_track_labelled(0x11, 8, 5, 55, Some(crate::jwp_hash::content_hash(b"not held")), T1);
|
||||
assert_eq!(v.accept_group(&edge5), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert_eq!(v.live_edge(&w0.signer_pubkey, 8), Some(5));
|
||||
let at_edge = on_track_labelled(0x11, 8, 5, 56, Some(w3.link_hash()), "2026-09-30T11:58:00Z");
|
||||
assert_eq!(v.accept_group(&at_edge), Err(DeliveryError::at(3, Reason::GroupNotFresh)));
|
||||
}
|
||||
|
||||
/// The exemption cannot be forged with an unsigned link. A relay holds a legacy chain's
|
||||
/// newest group past the window, then offers it again with its parent rewritten to name
|
||||
/// a group the chain does not hold: that is a gap, but the group is stale and moves no
|
||||
/// edge, so offered once more with its genuine link it is still refused.
|
||||
#[test]
|
||||
fn a_rewritten_legacy_link_earns_no_exemption() {
|
||||
let now = crate::claim_receipt::rfc3339_seconds(T1).unwrap();
|
||||
let f = Freshness { now_s: now, max_age_s: 60, max_skew_s: 5 };
|
||||
let g0 = on_track(0x11, 7, 0, 60, None, T1).0;
|
||||
let g1 = on_track(0x11, 7, 1, 61, Some(g0.link_hash()), "2026-09-30T11:00:00Z").0;
|
||||
let forged = GroupReceipt { parent_receipt_hash: Some(g1.link_hash()), ..g1.clone() };
|
||||
let mut v = ReceiptChains::new().with_freshness(f);
|
||||
v.accept_group(&g0).unwrap();
|
||||
assert_eq!(v.accept_group(&g1), Err(DeliveryError::at(3, Reason::GroupNotFresh)));
|
||||
assert_eq!(v.accept_group(&forged), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
assert_eq!(v.accept_group(&g1), Err(DeliveryError::at(3, Reason::GroupNotFresh)));
|
||||
assert_eq!(v.live_edge(&g0.signer_pubkey, 7), Some(0));
|
||||
}
|
||||
|
||||
/// A gap of any length fills, in any order: seventy groups held past the window, then a
|
||||
/// fresh one names the last; walked back and then forward, every one is accepted.
|
||||
#[test]
|
||||
fn a_gap_of_any_length_fills() {
|
||||
let now = crate::claim_receipt::rfc3339_seconds(T1).unwrap();
|
||||
let f = Freshness { now_s: now, max_age_s: 60, max_skew_s: 5 };
|
||||
let mut g = vec![on_track_labelled(0x11, 8, 0, 0, None, T1)];
|
||||
for i in 1..=71u64 {
|
||||
let end = if i <= 70 { "2026-09-30T11:00:00Z" } else { T1 };
|
||||
let parent = g.last().map(GroupReceipt::link_hash);
|
||||
g.push(on_track_labelled(0x11, 8, i, (i % 200) as u8, parent, end));
|
||||
}
|
||||
let mut v = ReceiptChains::new().with_group_id_guard().with_freshness(f);
|
||||
v.accept_group(&g[0]).unwrap();
|
||||
assert_eq!(v.accept_group(&g[71]), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
|
||||
for i in (2..=70).rev() {
|
||||
assert_eq!(v.accept_group(&g[i]), Err(DeliveryError::at(3, Reason::ParentNotHeld)), "walking back at {i}");
|
||||
}
|
||||
for (i, x) in g.iter().enumerate().skip(1) {
|
||||
assert_eq!(v.accept_group(x), Ok(()), "forward at {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The vectors the corpus held before the per-track chain, less `group_chain_break`
|
||||
/// (whose input §3 step 3 now decides otherwise, and which now holds a genuine second
|
||||
/// chain head), as one BLAKE3 over their compact JSON.
|
||||
fn pre_branch_digest(doc: &str) -> (Vec<String>, String) {
|
||||
let d: Value = serde_json::from_str(doc).expect("JSON");
|
||||
let vs = d["vectors"].as_array().expect("vectors");
|
||||
let first: Vec<Value> = vs[..83].to_vec();
|
||||
let names = first.iter().map(|v| v["name"].as_str().expect("name").to_owned()).collect();
|
||||
let kept: Vec<Value> = first.into_iter().filter(|v| v["name"] != "group_chain_break").collect();
|
||||
(names, hex(blake3::hash(serde_json::to_string(&kept).expect("serialises").as_bytes()).as_bytes()))
|
||||
}
|
||||
|
||||
/// The vectors that predate the per-track chain stay the corpus's prefix, in their order,
|
||||
/// and every one but `group_chain_break` is unchanged. The digest was computed from the
|
||||
/// corpus as it stood before this branch, and must never be edited to make the test pass.
|
||||
#[test]
|
||||
fn the_pre_branch_vectors_are_an_unchanged_prefix() {
|
||||
let (names, digest) = pre_branch_digest(&super::corpus::generate());
|
||||
assert_eq!(names.len(), 83);
|
||||
assert_eq!(names.iter().filter(|n| *n == "group_chain_break").count(), 1);
|
||||
assert_eq!(digest, "539d169f7196986b035c825d05a7ccfa16a2b72ea1b702423b3e1614d9f69b3d");
|
||||
}
|
||||
|
||||
/// The pinned corpus (`jwp-conformance/vectors.json`) is exactly what the generator
|
||||
/// writes, and every vector gets its declared outcome.
|
||||
#[test]
|
||||
fn the_pinned_jwp_corpus_reproduces_and_passes() {
|
||||
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../jwp-conformance/vectors.json");
|
||||
let pinned = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())).replace("\r\n", "\n");
|
||||
assert_eq!(super::corpus::generate(), pinned, "regenerate with `wai_jwp_vectors gen`");
|
||||
let results = super::corpus::run(&pinned).unwrap();
|
||||
assert_eq!(results.len(), super::corpus::vector_count());
|
||||
for (name, want, got) in results {
|
||||
assert_eq!(got, want, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// §6.1's freshness bound reads `group_end` to the second with its offset applied, and
|
||||
/// refuses a time outside the window or not a date-time; off, nothing is refused.
|
||||
#[test]
|
||||
fn the_freshness_bound_is_opt_in_and_reads_offsets() {
|
||||
// 2026-09-30T12:00:01Z.
|
||||
let now = crate::claim_receipt::rfc3339_seconds(T1).unwrap();
|
||||
assert_eq!(now, 1_790_769_601);
|
||||
let f = Freshness { now_s: now, max_age_s: 60, max_skew_s: 5 };
|
||||
assert!(f.admits("2026-09-30T12:00:01Z"));
|
||||
assert!(f.admits("2026-09-30T14:00:06+02:00"), "the offset is applied: 12:00:06Z, at the skew");
|
||||
assert!(!f.admits("2026-09-30T12:00:07Z"), "past the skew");
|
||||
assert!(f.admits("2026-09-30T11:59:01.999Z"), "at the window, the fraction dropped");
|
||||
assert!(!f.admits("2026-09-30T11:59:00Z"), "older than the window");
|
||||
assert!(!f.admits("yesterday"));
|
||||
let stale = on_track(0x11, 7, 0, 0, None, "2026-09-30T11:00:00Z").0;
|
||||
assert_eq!(ReceiptChains::new().check(&stale), Ok(()));
|
||||
assert_eq!(ReceiptChains::new().with_freshness(f).check(&stale), Err(DeliveryError::at(3, Reason::GroupNotFresh)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
use super::*;
|
||||
use crate::claim_receipt::{
|
||||
verify_claim_json, BindingType, ClaimBody, ClaimReceipt, DeclaredUncertainty, EnergyClaim,
|
||||
FigureHeader, LawfulBasisClaim, MeasurementClaim, OperatingPointClaim, Purpose, SoftBindingClaim, SubjectType,
|
||||
VectorResult,
|
||||
stage_point, verify_claim_json, BindingType, ClaimBody, ClaimReceipt, DeclaredUncertainty, DeliveryGapClaim,
|
||||
EnergyClaim, FigureHeader, GapReason, LawfulBasisClaim, MeasurementClaim, OperatingPointClaim, Purpose,
|
||||
SoftBindingClaim, StageClaim, SubjectType, VectorResult, STAGE_VOCABULARY,
|
||||
};
|
||||
use crate::energy_report::{BuildProfile, BuildRecord};
|
||||
use serde_json::json;
|
||||
|
|
@ -39,6 +39,8 @@ const QMOD: u8 = 0x72; // module/decoder at another origin
|
|||
const MM: u8 = 0x73; // measurement, energy/OnChipCounter
|
||||
const OPM: u8 = 0x74; // operating-point, energy/ModelBased
|
||||
const FR: u8 = 0x75; // receipt, energy/ModelBased
|
||||
const SK: u8 = 0x76; // stage
|
||||
const RL: u8 = 0x77; // delivery-gap
|
||||
|
||||
const GRANT_MAX: u64 = 43_200;
|
||||
const STATUS_MAX: u64 = 3_600;
|
||||
|
|
@ -108,6 +110,8 @@ struct Logs {
|
|||
r2_module_key_revoked: Revision,
|
||||
rm: Revision,
|
||||
rf: Revision,
|
||||
/// r1 with a sink's key for `stage` and a relay's for `delivery-gap`.
|
||||
rs: Revision,
|
||||
q0: Revision,
|
||||
u0: Revision,
|
||||
}
|
||||
|
|
@ -188,9 +192,13 @@ fn logs() -> Logs {
|
|||
R0,
|
||||
unsigned(P, Some(&r1), "2026-09-16T18:00:00Z", R1, with_keys(&r1, vec![e("fr", FR, &["receipt", "energy/ModelBased"])])),
|
||||
);
|
||||
let rs = sealed(
|
||||
R0,
|
||||
unsigned(P, Some(&r1), "2026-09-17T00:00:00Z", R1, with_keys(&r1, vec![e("sk", SK, &["stage"]), e("rl", RL, &["delivery-gap"])])),
|
||||
);
|
||||
let q0 = sealed(R0, unsigned(P8443, None, "2026-09-01T00:00:00Z", R1, vec![e("d1", D1, &["delivery", "receipt"])]));
|
||||
let u0 = sealed(STRANGER, unsigned(P, None, "2026-09-02T00:00:00Z", R2, vec![e("d1", ATT, &["delivery", "receipt"])]));
|
||||
Logs { r0, r1, r2, t2, o2, o3, r2_revoked, r2_retired, r2_module_revoked, r2_module_key_revoked, rm, rf, q0, u0 }
|
||||
Logs { r0, r1, r2, t2, o2, o3, r2_revoked, r2_retired, r2_module_revoked, r2_module_key_revoked, rm, rf, rs, q0, u0 }
|
||||
}
|
||||
|
||||
// ───────────────────────────── items and claims ─────────────────────────────
|
||||
|
|
@ -248,6 +256,36 @@ fn operating_point_body(class: &str) -> ClaimBody {
|
|||
})
|
||||
}
|
||||
|
||||
/// A `stage` claim about a base presented alone: no layer object, no delivered stage
|
||||
/// stated.
|
||||
fn stage_body() -> ClaimBody {
|
||||
ClaimBody::Stage(StageClaim {
|
||||
vocabulary: STAGE_VOCABULARY.into(),
|
||||
op_requested: None,
|
||||
op_committed: None,
|
||||
op_delivered: None,
|
||||
presented: stage_point(0),
|
||||
units_by_stage: vec![4],
|
||||
tier: "none".into(),
|
||||
stage_hash: None,
|
||||
objects: vec![],
|
||||
conflicts: 0,
|
||||
deadline_misses: 0,
|
||||
bytes_envelopes: 5_000,
|
||||
})
|
||||
}
|
||||
|
||||
/// A `delivery-gap` claim: an object not forwarded once its delivery timeout passed.
|
||||
fn gap_body() -> ClaimBody {
|
||||
ClaimBody::DeliveryGap(DeliveryGapClaim {
|
||||
track_alias: 8,
|
||||
group_id: 3,
|
||||
object_id: 0,
|
||||
reason: GapReason::DeliveryTimeout,
|
||||
observed_at: "2026-09-18T12:00:00Z".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn declaration_body() -> ClaimBody {
|
||||
ClaimBody::LawfulBasis(LawfulBasisClaim::Declaration {
|
||||
vocabulary: DPV.into(),
|
||||
|
|
@ -1652,6 +1690,43 @@ fn vectors() -> Vec<V> {
|
|||
Some(NOW),
|
||||
"fallback step=4 reason=module_status_stale".into(),
|
||||
));
|
||||
|
||||
// ── §7.5: a `stage` claim needs `stage`, a `delivery-gap` claim `delivery-gap` ──
|
||||
let with_rs: &[&Revision] = &[&l.r0, &l.r1, &l.rs];
|
||||
let fetch_rs = || vec![anchor(P, &l.rs, "fetched")];
|
||||
let confirmed_rs = |key_id: &str| format!("confirmed origin={P} key_id={key_id} head={} anchor=fetched", h(&l.rs));
|
||||
v.push(confirm_claim(
|
||||
"stage_needs_its_class",
|
||||
"a stage claim by a key holding delivery and receipt but not stage: delivering objects is not observing a stage set",
|
||||
claim(D1, &id(PID, "d1"), stage_body()),
|
||||
with_rs,
|
||||
fetch_rs(),
|
||||
refused("6", "authority_not_granted"),
|
||||
));
|
||||
v.push(confirm_claim(
|
||||
"stage_confirmed",
|
||||
"a stage claim by a key holding stage, under a fetched anchor: stage is not pinned-only",
|
||||
claim(SK, &id(PID, "sk"), stage_body()),
|
||||
with_rs,
|
||||
fetch_rs(),
|
||||
confirmed_rs("sk"),
|
||||
));
|
||||
v.push(confirm_claim(
|
||||
"gap_needs_its_class",
|
||||
"a delivery-gap claim by a key holding stage but not delivery-gap",
|
||||
claim(SK, &id(PID, "sk"), gap_body()),
|
||||
with_rs,
|
||||
fetch_rs(),
|
||||
refused("6", "authority_not_granted"),
|
||||
));
|
||||
v.push(confirm_claim(
|
||||
"gap_confirmed",
|
||||
"a delivery-gap claim by a relay's key holding delivery-gap, under a fetched anchor",
|
||||
claim(RL, &id(PID, "rl"), gap_body()),
|
||||
with_rs,
|
||||
fetch_rs(),
|
||||
confirmed_rs("rl"),
|
||||
));
|
||||
v
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ fn claim_authorities_follow_the_table() {
|
|||
assert_eq!(a, want);
|
||||
seen.insert(a[0].split('/').next().unwrap().to_owned());
|
||||
}
|
||||
for class in ["energy", "lawful-basis", "measurement", "operating-point"] {
|
||||
for class in ["energy", "lawful-basis", "measurement", "operating-point", "stage", "delivery-gap"] {
|
||||
assert!(seen.contains(class), "the claim corpus holds no confirmable {class} claim: {seen:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
339
wai/wai-rs/src/staged/claim.rs
Normal file
339
wai/wai-rs/src/staged/claim.rs
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
//! A `stage` claim from what a sink observed (`extensions/staged-delivery.md` §10.2,
|
||||
//! jwp-receipts §2.4.10).
|
||||
//!
|
||||
//! A [`StageObserver`] watches one stage set at a sink: the layer envelopes that arrived,
|
||||
//! with the evidence of their delivery receipts; the layer objects that never arrived and
|
||||
//! are known only by their receipts (dropped at a relay, abandoned at a deadline,
|
||||
//! jwp-receipts §4.4); and every [`OfferReport`] the sink gave for them. With the base's
|
||||
//! chain it states the claim's body: each object's status, its first reason and its
|
||||
//! evidence; the units by stage, the conflicts and the deadline misses; the tier and
|
||||
//! stage hash of what is presented; and the stage the delivered objects reach. The body
|
||||
//! is signed with [`ClaimReceipt::seal`](crate::claim_receipt::ClaimReceipt::seal), whose
|
||||
//! checks are §2.4.10's rules, so a body this module states and a verifier refuses never
|
||||
//! leaves the sink signed.
|
||||
|
||||
use super::{OfferReport, ReceiptEvidence, StageChain, StageRefusal};
|
||||
use crate::claim_receipt::{stage_point, StageClaim, StageEvidence, StageObject, StageStatus, STAGE_VOCABULARY};
|
||||
use crate::codecs::DeterminismTier;
|
||||
use crate::container::{RefForm, Refines, Wai};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Why an envelope could not be observed as a layer of the set.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ObserveError {
|
||||
/// Not a `WAI1` envelope, or one with no `refines` that reads.
|
||||
NotALayer,
|
||||
/// A layer of another base.
|
||||
OtherBase,
|
||||
/// An event of an epoch earlier than one the observer has already seen.
|
||||
StaleEpoch,
|
||||
}
|
||||
|
||||
/// What became of one layer object, as the sink saw it.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Seen {
|
||||
layer: u8,
|
||||
/// The units it covers; `None` for every unit of the base (no `span`).
|
||||
span: Option<(u32, u32)>,
|
||||
/// Its envelope bytes; 0 for an object that never arrived.
|
||||
bytes: u64,
|
||||
arrived: bool,
|
||||
evidence: ReceiptEvidence,
|
||||
envelope: Option<StageRefusal>,
|
||||
held_for_base: bool,
|
||||
applied: BTreeSet<u32>,
|
||||
refused: BTreeMap<u32, StageRefusal>,
|
||||
held: BTreeSet<u32>,
|
||||
}
|
||||
|
||||
impl Seen {
|
||||
fn report(&mut self, r: &OfferReport) {
|
||||
// A report decides for the envelope as of now: a refusal replaces what was held,
|
||||
// and a report that refuses nothing for the envelope, or applies a unit, clears an
|
||||
// earlier refusal (a layer refused, then offered again and applied).
|
||||
match r.envelope {
|
||||
Some(why) => (self.envelope, self.held_for_base) = (Some(why), false),
|
||||
None => (self.envelope, self.held_for_base) = (None, r.held_for_base),
|
||||
}
|
||||
if !r.applied.is_empty() {
|
||||
(self.envelope, self.held_for_base) = (None, false);
|
||||
}
|
||||
for u in &r.applied {
|
||||
self.applied.insert(*u);
|
||||
self.refused.remove(u);
|
||||
self.held.remove(u);
|
||||
}
|
||||
for (u, why) in &r.refused {
|
||||
if !self.applied.contains(u) {
|
||||
self.refused.insert(*u, *why);
|
||||
self.held.remove(u);
|
||||
}
|
||||
}
|
||||
for u in &r.held {
|
||||
if !self.applied.contains(u) && !self.refused.contains_key(u) {
|
||||
self.held.insert(*u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The status, and its reason, over the `count` units the object covers. A unit
|
||||
/// still held when the claim is made counts as not applied, for the reason it is
|
||||
/// held for (`layer-gap`, or `base-absent` for the whole envelope).
|
||||
fn status(&self, from: u32, count: u32) -> (StageStatus, Option<String>) {
|
||||
if !self.arrived {
|
||||
return (StageStatus::Dropped, None);
|
||||
}
|
||||
// What the sink applied decides first: a unit applied stays applied, whatever a
|
||||
// later offer of the same envelope was told.
|
||||
if self.applied.is_empty() {
|
||||
if let Some(r) = self.envelope {
|
||||
return (StageStatus::Refused, Some(r.code().into()));
|
||||
}
|
||||
if self.held_for_base {
|
||||
return (StageStatus::Refused, Some(StageRefusal::BaseAbsent.code().into()));
|
||||
}
|
||||
}
|
||||
let units = from..from.saturating_add(count);
|
||||
let not_applied: Vec<(u32, StageRefusal)> = units
|
||||
.filter(|u| !self.applied.contains(u))
|
||||
.map(|u| (u, self.refused.get(&u).copied().unwrap_or(StageRefusal::LayerGap)))
|
||||
.collect();
|
||||
match (not_applied.first(), not_applied.len() as u64 == u64::from(count)) {
|
||||
(None, _) => (StageStatus::Applied, None),
|
||||
(Some(_), true) if not_applied.iter().all(|(_, r)| *r == StageRefusal::DeadlineMissed) => (StageStatus::Late, None),
|
||||
(Some((_, r)), true) => (StageStatus::Refused, Some(r.code().into())),
|
||||
(Some((_, r)), false) => (StageStatus::Partial, Some(r.code().into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evidence_of(e: ReceiptEvidence) -> StageEvidence {
|
||||
match e {
|
||||
ReceiptEvidence::DeliveredVerified => StageEvidence::DeliveredVerified,
|
||||
ReceiptEvidence::InclusionVerified => StageEvidence::InclusionVerified,
|
||||
ReceiptEvidence::None => StageEvidence::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// One stage set at a sink, observed for a `stage` claim.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StageObserver {
|
||||
base: [u8; 32],
|
||||
base_bytes: u64,
|
||||
base_delivered: bool,
|
||||
requested: Option<u8>,
|
||||
committed: Option<u8>,
|
||||
objects: BTreeMap<[u8; 32], Seen>,
|
||||
/// The sink's epoch for the base ([`super::StageSink::base_epoch`]) at the last event;
|
||||
/// `None` before any.
|
||||
epoch: Option<u64>,
|
||||
}
|
||||
|
||||
impl StageObserver {
|
||||
/// A set whose base has content hash `base` and `base_bytes` envelope bytes.
|
||||
/// `base_delivered` is whether the sink verified the base's delivery receipt over its
|
||||
/// bytes before it offered the base's first layer: only then is a stage stated as
|
||||
/// delivered (jwp-receipts §2.4.10, `op_delivered`).
|
||||
pub fn new(base: [u8; 32], base_bytes: u64, base_delivered: bool) -> StageObserver {
|
||||
StageObserver { base, base_bytes, base_delivered, requested: None, committed: None, objects: BTreeMap::new(), epoch: None }
|
||||
}
|
||||
|
||||
/// Whether the sink verified the base's delivery receipt over its bytes before it
|
||||
/// offered the base's first layer in the current epoch. A new epoch clears it.
|
||||
pub fn set_base_delivered(&mut self, delivered: bool) {
|
||||
self.base_delivered = delivered;
|
||||
}
|
||||
|
||||
/// Every event carries the sink's epoch for the base ([`super::StageSink::base_epoch`]).
|
||||
/// A later epoch means the sink forgot the base (staged-delivery §4.4, §14): decoded
|
||||
/// again, it is a base the sink never decoded and never attested, and nothing it did
|
||||
/// before is part of what it presents. The observation starts over: an envelope the
|
||||
/// sink still holds for the base keeps its arrival, its bytes and its evidence, and is
|
||||
/// held for the base again with no decision; every other object is dropped; and no
|
||||
/// delivered stage is stated until the caller says otherwise. An event of an earlier
|
||||
/// epoch than one already seen is stale, and is ignored (`false`).
|
||||
fn at(&mut self, epoch: u64) -> bool {
|
||||
match self.epoch {
|
||||
Some(e) if epoch < e => return false,
|
||||
Some(e) if epoch > e => self.restart(),
|
||||
_ => {}
|
||||
}
|
||||
self.epoch = Some(epoch);
|
||||
true
|
||||
}
|
||||
|
||||
fn restart(&mut self) {
|
||||
self.objects.retain(|_, s| s.arrived && s.held_for_base);
|
||||
for s in self.objects.values_mut() {
|
||||
(s.envelope, s.held_for_base) = (None, true);
|
||||
s.applied.clear();
|
||||
s.refused.clear();
|
||||
s.held.clear();
|
||||
}
|
||||
self.base_delivered = false;
|
||||
}
|
||||
|
||||
/// The stage the sink asked for and the stage the serving party said it would
|
||||
/// deliver. They are stated only beside a delivered stage.
|
||||
pub fn request(&mut self, requested: Option<u8>, committed: Option<u8>) {
|
||||
(self.requested, self.committed) = (requested, committed);
|
||||
}
|
||||
|
||||
/// A layer envelope that arrived, with the evidence of its delivery receipt, before
|
||||
/// the sink offers it. Arriving again keeps the stronger evidence (staged-delivery §4.3).
|
||||
pub fn arrived(&mut self, epoch: u64, envelope: &[u8], evidence: ReceiptEvidence) -> Result<(), ObserveError> {
|
||||
let w = Wai::from_bytes(envelope).map_err(|_| ObserveError::NotALayer)?;
|
||||
let r = w.manifest.refines.as_ref().ok_or(ObserveError::NotALayer)?;
|
||||
let r = Refines::from_value(r, RefForm::ContentHash).map_err(|_| ObserveError::NotALayer)?;
|
||||
if r.base != hex(&self.base) {
|
||||
return Err(ObserveError::OtherBase);
|
||||
}
|
||||
if !self.at(epoch) {
|
||||
return Err(ObserveError::StaleEpoch);
|
||||
}
|
||||
let hash = crate::jwp_hash::content_hash(envelope);
|
||||
let seen = self.objects.entry(hash).or_insert_with(|| Seen {
|
||||
layer: r.layer,
|
||||
span: r.span.map(|s| (s.from, s.count)),
|
||||
bytes: 0,
|
||||
arrived: false,
|
||||
evidence,
|
||||
envelope: None,
|
||||
held_for_base: false,
|
||||
applied: BTreeSet::new(),
|
||||
refused: BTreeMap::new(),
|
||||
held: BTreeSet::new(),
|
||||
});
|
||||
// The envelope's own `refines`, which its content hash authenticates, says what
|
||||
// it is, whatever an earlier receipt-only sighting supposed (`dropped`).
|
||||
seen.layer = r.layer;
|
||||
seen.span = r.span.map(|s| (s.from, s.count));
|
||||
seen.arrived = true;
|
||||
seen.bytes = envelope.len() as u64;
|
||||
if rank(evidence) > rank(seen.evidence) {
|
||||
seen.evidence = evidence;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A layer object that never arrived, known by its object receipt: its content hash,
|
||||
/// its layer, the units it covers (`None` for every unit) and what was verified of
|
||||
/// the receipt — by inclusion, or nothing (jwp-receipts §4.4). An object that arrived
|
||||
/// is not made dropped by this.
|
||||
pub fn dropped(&mut self, epoch: u64, content_hash: [u8; 32], layer: u8, span: Option<(u32, u32)>, evidence: ReceiptEvidence) {
|
||||
if !self.at(epoch) {
|
||||
return;
|
||||
}
|
||||
let evidence = if evidence == ReceiptEvidence::DeliveredVerified { ReceiptEvidence::InclusionVerified } else { evidence };
|
||||
self.objects.entry(content_hash).or_insert_with(|| Seen {
|
||||
layer,
|
||||
span,
|
||||
bytes: 0,
|
||||
arrived: false,
|
||||
evidence,
|
||||
envelope: None,
|
||||
held_for_base: false,
|
||||
applied: BTreeSet::new(),
|
||||
refused: BTreeMap::new(),
|
||||
held: BTreeSet::new(),
|
||||
});
|
||||
}
|
||||
|
||||
/// The reports the sink gave — on an offer, a release or an expiry — with the sink's
|
||||
/// epoch for the base after it gave them. A report about an object this observer has
|
||||
/// not seen arrive is not this set's, and is ignored.
|
||||
pub fn reports(&mut self, epoch: u64, reports: &[OfferReport]) {
|
||||
if !self.at(epoch) {
|
||||
return;
|
||||
}
|
||||
for r in reports {
|
||||
if let Some(s) = self.objects.get_mut(&r.content_hash).filter(|s| s.arrived) {
|
||||
s.report(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The claim's body as of now, over `chain`, the base's chain at the sink, whose epoch
|
||||
/// for the base is `epoch`. A claim never spans two epochs: if the sink has forgotten
|
||||
/// the base since the last event, the claim is made over an observation started over.
|
||||
pub fn claim(&self, chain: &StageChain, epoch: u64) -> StageClaim {
|
||||
if self.epoch.is_some_and(|e| epoch > e) {
|
||||
let mut fresh = self.clone();
|
||||
fresh.restart();
|
||||
fresh.epoch = Some(epoch);
|
||||
return fresh.claim(chain, epoch);
|
||||
}
|
||||
let n = chain.n_units();
|
||||
let mut objects: Vec<StageObject> = self
|
||||
.objects
|
||||
.iter()
|
||||
.map(|(hash, s)| {
|
||||
let (from, count) = s.span.unwrap_or((0, n));
|
||||
let (status, reason) = s.status(from, count);
|
||||
StageObject {
|
||||
layer: u64::from(s.layer),
|
||||
object: *hash,
|
||||
units: (u64::from(from), u64::from(count)),
|
||||
bytes: s.bytes,
|
||||
status,
|
||||
evidence: evidence_of(s.evidence),
|
||||
reason,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
objects.sort_by_key(|o| (o.layer, o.units.0, o.object));
|
||||
let mut units_by_stage = vec![0u64; 1 + chain.units().iter().map(|u| usize::from(u.stage)).max().unwrap_or(0)];
|
||||
for u in chain.units() {
|
||||
units_by_stage[usize::from(u.stage)] += 1;
|
||||
}
|
||||
let lowest = units_by_stage.iter().position(|c| *c != 0).unwrap_or(0) as u64;
|
||||
// A unit presented below the stage its layers would have reached on time: one a
|
||||
// layer missed the deadline of (§8).
|
||||
let missed: BTreeSet<u32> = self
|
||||
.objects
|
||||
.values()
|
||||
.flat_map(|s| s.refused.iter().filter(|(_, r)| **r == StageRefusal::DeadlineMissed).map(|(u, _)| *u))
|
||||
.collect();
|
||||
let tiers: Vec<Option<DeterminismTier>> = (0..n).map(|u| chain.tier(u)).collect();
|
||||
let tier = if tiers.iter().all(|t| *t == Some(DeterminismTier::DecodeEquivalence)) {
|
||||
"decode-equivalence"
|
||||
} else if tiers.iter().all(|t| t.is_some()) {
|
||||
"entropy-consistency"
|
||||
} else {
|
||||
"none"
|
||||
};
|
||||
let mut c = StageClaim {
|
||||
vocabulary: STAGE_VOCABULARY.into(),
|
||||
op_requested: None,
|
||||
op_committed: None,
|
||||
op_delivered: None,
|
||||
presented: stage_point(lowest),
|
||||
units_by_stage,
|
||||
tier: tier.into(),
|
||||
stage_hash: chain.clip_sha256(),
|
||||
bytes_envelopes: self.base_bytes + objects.iter().map(|o| o.bytes).sum::<u64>(),
|
||||
objects,
|
||||
conflicts: chain.units().iter().filter(|u| u.frozen).count() as u64,
|
||||
deadline_misses: missed.len() as u64,
|
||||
};
|
||||
if self.base_delivered {
|
||||
c.op_delivered = Some(stage_point(c.delivered_stage()));
|
||||
c.op_requested = self.requested.map(|l| stage_point(u64::from(l)));
|
||||
c.op_committed = self.committed.map(|l| stage_point(u64::from(l)));
|
||||
}
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
/// Evidence ordered by strength (staged-delivery §4.3).
|
||||
fn rank(e: ReceiptEvidence) -> u8 {
|
||||
match e {
|
||||
ReceiptEvidence::DeliveredVerified => 2,
|
||||
ReceiptEvidence::InclusionVerified => 1,
|
||||
ReceiptEvidence::None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn hex(h: &[u8; 32]) -> String {
|
||||
h.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
469
wai/wai-rs/src/staged/claim_tests.rs
Normal file
469
wai/wai-rs/src/staged/claim_tests.rs
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
//! Tests of `staged::claim`: a sink's `stage` claim states what the sink did.
|
||||
|
||||
use super::claim::{ObserveError, StageObserver};
|
||||
use super::schedule::UnitClock;
|
||||
use super::wir1::Wir1Cost;
|
||||
use super::{tier, BaseInfo, ChainConfig, HoldBound, Live, OfferReport, ReceiptEvidence, Samples, StageSink, UnitBuf};
|
||||
use crate::claim_receipt::{stage_point, verify_claim_json, ClaimBody, ClaimReceipt, StageClaim, StageEvidence, StageStatus, SubjectType};
|
||||
use crate::container::{RefForm, Refines, Wai};
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn chash(b: &[u8]) -> [u8; 32] {
|
||||
crate::jwp_hash::content_hash(b)
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
fn corpus() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../staged-conformance")
|
||||
}
|
||||
|
||||
fn seal(base: [u8; 32], c: StageClaim) -> Result<ClaimReceipt, crate::claim_receipt::Rejection> {
|
||||
let key = ed25519_dalek::SigningKey::from_bytes(&[0x44; 32]);
|
||||
ClaimReceipt::seal(&key, "did:example:sink", SubjectType::Object, base, 0, "2026-10-06T00:00:00Z", ClaimBody::Stage(c), None)
|
||||
}
|
||||
|
||||
/// A case's base, its units read from its stage-0 buffer.
|
||||
fn units_from_stage0(base: &Value, stage0: &[u8]) -> Vec<UnitBuf> {
|
||||
let mut at = 0usize;
|
||||
base["units"]
|
||||
.as_array()
|
||||
.expect("units")
|
||||
.iter()
|
||||
.map(|u| {
|
||||
let s: Vec<u32> = u["shape"].as_array().expect("shape").iter().map(|x| x.as_u64().expect("dim") as u32).collect();
|
||||
let n = s[0] as usize * s[1] as usize * s[2] as usize;
|
||||
let samples = if u["elem"] == "u8" {
|
||||
let v = stage0[at..at + n].to_vec();
|
||||
at += n;
|
||||
Samples::U8(v)
|
||||
} else {
|
||||
let v = stage0[at..at + 2 * n].chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect();
|
||||
at += 2 * n;
|
||||
Samples::I16(v)
|
||||
};
|
||||
UnitBuf { shape: (s[0], s[1], s[2]), samples }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn cfg_from(v: &Value) -> ChainConfig {
|
||||
let u = |x: &Value| x.as_u64().expect("an integer");
|
||||
ChainConfig {
|
||||
bound: HoldBound { max_envelopes: u(&v["bound"]["max_envelopes"]) as usize, max_age_ms: u(&v["bound"]["max_age_ms"]) },
|
||||
cost_limit: (!v["cost_limit"].is_null()).then(|| Wir1Cost {
|
||||
output_bytes: u(&v["cost_limit"]["output_bytes"]),
|
||||
symbols: u(&v["cost_limit"]["symbols"]),
|
||||
working_bytes: u(&v["cost_limit"]["working_bytes"]),
|
||||
}),
|
||||
budget: u(&v["budget"]),
|
||||
record_budget: u(&v["record_budget"]),
|
||||
live: (!v["live"].is_null()).then(|| Live {
|
||||
clock: UnitClock {
|
||||
t0_ms: u(&v["live"]["t0_ms"]),
|
||||
unit_ms: u(&v["live"]["unit_ms"]),
|
||||
target_latency_ms: u(&v["live"]["target_latency_ms"]),
|
||||
},
|
||||
lead_ms: v["live"]["lead_ms"].as_array().expect("lead_ms").iter().map(u).collect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The base a layer envelope refines, if it reads as one.
|
||||
fn refines_base(env: &[u8]) -> Option<String> {
|
||||
let w = Wai::from_bytes(env).ok()?;
|
||||
Refines::from_value(w.manifest.refines.as_ref()?, RefForm::ContentHash).ok().map(|r| r.base)
|
||||
}
|
||||
|
||||
/// An event, with the sink's epoch for every base of the case when it happened.
|
||||
enum Event {
|
||||
Arrived(Vec<u8>, ReceiptEvidence, Vec<u64>),
|
||||
Reports(Vec<OfferReport>, Vec<u64>),
|
||||
}
|
||||
|
||||
/// staged-delivery §10.2 over the whole corpus: every case of `staged-conformance/` is
|
||||
/// replayed through a `StageSink`, and every base it holds at the end through a
|
||||
/// `StageObserver` fed the same arrivals and reports, each with the sink's epoch for the
|
||||
/// base when it happened (`StageSink::base_epoch`), so the observer starts over by itself
|
||||
/// when the sink forgot the base (§4.4, §14). Each claim must seal (§2.4.10's rules),
|
||||
/// verify on its wire form, and state for every object exactly what the sink did with it
|
||||
/// in the base's last epoch: `applied` to every unit it covers, `partial` to some, any
|
||||
/// other status to none; `dropped` only for an object that never arrived.
|
||||
#[test]
|
||||
fn every_corpus_case_states_what_the_sink_did() {
|
||||
let mut cases: Vec<PathBuf> =
|
||||
std::fs::read_dir(corpus().join("cases")).expect("the corpus's cases").flatten().map(|e| e.path()).collect();
|
||||
cases.sort();
|
||||
let (mut claims, mut problems) = (0usize, Vec::new());
|
||||
for dir in &cases {
|
||||
let case: Value = serde_json::from_slice(&std::fs::read(dir.join("case.json")).expect("case.json")).expect("JSON");
|
||||
let name = case["name"].as_str().expect("name").to_owned();
|
||||
let mut bases = Vec::new();
|
||||
for b in case["bases"].as_array().expect("bases") {
|
||||
let env = std::fs::read(dir.join(b["file"].as_str().expect("file"))).expect("base");
|
||||
let stage0 = std::fs::read(dir.join(b["stage0"].as_str().expect("stage0"))).expect("stage0");
|
||||
let info = BaseInfo {
|
||||
content_hash: chash(&env),
|
||||
capability: b["decoded_via"].as_str().expect("decoded_via").into(),
|
||||
via_fallback: b["via_fallback"] == true,
|
||||
tier: tier::from_token(b["tier"].as_str().expect("tier")),
|
||||
attested: b["attested"] == true,
|
||||
first_unit: b["first_unit"].as_u64().expect("first_unit"),
|
||||
};
|
||||
let units = units_from_stage0(b, &stage0);
|
||||
bases.push((info, units, env.len() as u64));
|
||||
}
|
||||
let files: BTreeMap<String, Vec<u8>> = case["files"]
|
||||
.as_array()
|
||||
.expect("files")
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let n = f["file"].as_str().expect("file").to_owned();
|
||||
let b = std::fs::read(dir.join(&n)).unwrap_or_default();
|
||||
(n, b)
|
||||
})
|
||||
.collect();
|
||||
let mut sink = StageSink::new(cfg_from(&case["config"]));
|
||||
// Per base: whether it is attested now, and whether it was when its first layer
|
||||
// was offered (only then is a delivered stage stated).
|
||||
let mut attested_now = vec![false; bases.len()];
|
||||
let mut attested_at_first: Vec<Option<bool>> = vec![None; bases.len()];
|
||||
let epochs = |sink: &StageSink| bases.iter().map(|(info, _, _)| sink.base_epoch(&info.content_hash)).collect::<Vec<u64>>();
|
||||
let mut seen_epoch = vec![0u64; bases.len()];
|
||||
let mut events = Vec::new();
|
||||
// What the sink applied each object to, by the epoch of its base when it did.
|
||||
let mut truth: BTreeMap<([u8; 32], u64), BTreeSet<u32>> = BTreeMap::new();
|
||||
let mut arrived: BTreeSet<[u8; 32]> = BTreeSet::new();
|
||||
for step in case["steps"].as_array().expect("steps") {
|
||||
let reports = match step["op"].as_str().expect("op") {
|
||||
"offer" => {
|
||||
let env = &files[step["file"].as_str().expect("file")];
|
||||
let ev = ReceiptEvidence::from_token(step["evidence"].as_str().expect("evidence")).expect("a token");
|
||||
if let Some(b) = refines_base(env) {
|
||||
for (i, (info, _, _)) in bases.iter().enumerate() {
|
||||
if hex(&info.content_hash) == b && attested_at_first[i].is_none() {
|
||||
attested_at_first[i] = Some(attested_now[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
events.push(Event::Arrived(env.clone(), ev, epochs(&sink)));
|
||||
arrived.insert(chash(env));
|
||||
sink.offer(env, step["at_ms"].as_u64().expect("at_ms"), ev).unwrap_or_default()
|
||||
}
|
||||
"base" => {
|
||||
let i = step["index"].as_u64().expect("index") as usize;
|
||||
let (info, units, _) = bases[i].clone();
|
||||
attested_now[i] |= info.attested;
|
||||
sink.add_base(info, units)
|
||||
}
|
||||
"expire" => sink.expire(step["at_ms"].as_u64().expect("at_ms")),
|
||||
"remove" => {
|
||||
let i = step["index"].as_u64().expect("index") as usize;
|
||||
sink.remove_base(&bases[i].0.content_hash).unwrap_or_default()
|
||||
}
|
||||
"attest" => {
|
||||
let i = step["index"].as_u64().expect("index") as usize;
|
||||
attested_now[i] = true;
|
||||
sink.mark_attested(&bases[i].0.content_hash);
|
||||
vec![]
|
||||
}
|
||||
op => panic!("{name}: op {op}"),
|
||||
};
|
||||
let now = epochs(&sink);
|
||||
for r in &reports {
|
||||
let of = files.values().find(|f| chash(f) == r.content_hash).and_then(|f| refines_base(f));
|
||||
let i = of.and_then(|b| bases.iter().position(|(info, _, _)| hex(&info.content_hash) == b));
|
||||
let epoch = i.map_or(0, |i| now[i]);
|
||||
truth.entry((r.content_hash, epoch)).or_default().extend(r.applied.iter().copied());
|
||||
}
|
||||
for (i, e) in now.iter().enumerate() {
|
||||
if *e != seen_epoch[i] {
|
||||
// Forgotten: decoded again it is a base never decoded and never attested.
|
||||
attested_now[i] = false;
|
||||
attested_at_first[i] = None;
|
||||
seen_epoch[i] = *e;
|
||||
}
|
||||
}
|
||||
events.push(Event::Reports(reports, now));
|
||||
}
|
||||
for (i, (info, _, base_bytes)) in bases.iter().enumerate() {
|
||||
let Some(chain) = sink.chain(&info.content_hash) else { continue };
|
||||
let delivered_first = attested_at_first[i].unwrap_or(attested_now[i]);
|
||||
let epoch = sink.base_epoch(&info.content_hash);
|
||||
let mut obs = StageObserver::new(info.content_hash, *base_bytes, delivered_first);
|
||||
for e in &events {
|
||||
match e {
|
||||
Event::Arrived(env, ev, at) => {
|
||||
let _ = obs.arrived(at[i], env, *ev);
|
||||
}
|
||||
Event::Reports(r, at) => obs.reports(at[i], r),
|
||||
}
|
||||
}
|
||||
if epoch > 0 {
|
||||
// The base's receipt in its last epoch is the caller's to state.
|
||||
obs.set_base_delivered(delivered_first);
|
||||
}
|
||||
let claim = obs.claim(chain, epoch);
|
||||
claims += 1;
|
||||
match seal(info.content_hash, claim.clone()) {
|
||||
Ok(r) => {
|
||||
if let Err(e) = verify_claim_json(&r.to_json()) {
|
||||
problems.push(format!("{name} base {i}: the sealed claim does not verify: {e}"));
|
||||
}
|
||||
}
|
||||
Err(e) => problems.push(format!(
|
||||
"{name} base {i}: seal refused: {e}\n units_by_stage {:?}, op_delivered {:?}, objects {:?}",
|
||||
claim.units_by_stage,
|
||||
claim.op_delivered,
|
||||
claim.objects.iter().map(|o| (o.layer, o.units, o.bytes, o.status, o.evidence, o.reason.clone())).collect::<Vec<_>>()
|
||||
)),
|
||||
}
|
||||
for o in &claim.objects {
|
||||
let covered: BTreeSet<u32> = (o.units.0 as u32..(o.units.0 + o.units.1) as u32).collect();
|
||||
let applied: BTreeSet<u32> =
|
||||
truth.get(&(o.object, epoch)).map_or(BTreeSet::new(), |a| a.intersection(&covered).copied().collect());
|
||||
let ok = match o.status {
|
||||
StageStatus::Applied => applied == covered,
|
||||
StageStatus::Partial => !applied.is_empty() && applied != covered,
|
||||
StageStatus::Late | StageStatus::Refused | StageStatus::Dropped => applied.is_empty(),
|
||||
};
|
||||
if !ok {
|
||||
problems.push(format!(
|
||||
"{name} base {i}: object {}.. layer {} units {:?} stated {:?} ({:?}); the sink applied it to {:?}",
|
||||
&hex(&o.object)[..8],
|
||||
o.layer,
|
||||
o.units,
|
||||
o.status,
|
||||
o.reason,
|
||||
applied
|
||||
));
|
||||
}
|
||||
if o.status == StageStatus::Dropped && arrived.contains(&o.object) {
|
||||
problems.push(format!("{name} base {i}: object {}.. stated dropped, and it arrived", &hex(&o.object)[..8]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(claims >= cases.len(), "{claims} claims over {} cases", cases.len());
|
||||
assert!(problems.is_empty(), "{} problem(s):\n{}", problems.len(), problems.join("\n"));
|
||||
}
|
||||
|
||||
/// A base from a corpus case, its units read from its stage-0 buffer.
|
||||
fn corpus_base(case: &str, file: &str, attested: bool) -> (BaseInfo, Vec<UnitBuf>, Vec<u8>) {
|
||||
let dir = corpus().join("cases").join(case);
|
||||
let c: Value = serde_json::from_slice(&std::fs::read(dir.join("case.json")).expect("case.json")).expect("JSON");
|
||||
let b = c["bases"].as_array().expect("bases").iter().find(|b| b["file"] == file).expect("the base").clone();
|
||||
let env = std::fs::read(dir.join(file)).expect("base");
|
||||
let stage0 = std::fs::read(dir.join(b["stage0"].as_str().expect("stage0"))).expect("stage0");
|
||||
let info = BaseInfo {
|
||||
content_hash: chash(&env),
|
||||
capability: b["decoded_via"].as_str().expect("decoded_via").into(),
|
||||
via_fallback: false,
|
||||
tier: tier::from_token(b["tier"].as_str().expect("tier")),
|
||||
attested,
|
||||
first_unit: 0,
|
||||
};
|
||||
let units = units_from_stage0(&b, &stage0);
|
||||
(info, units, env)
|
||||
}
|
||||
|
||||
fn layer_file(case: &str, file: &str) -> Vec<u8> {
|
||||
std::fs::read(corpus().join("cases").join(case).join(file)).expect("layer")
|
||||
}
|
||||
|
||||
fn cfg() -> ChainConfig {
|
||||
ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, budget: 1 << 30, record_budget: 1 << 20, live: None }
|
||||
}
|
||||
|
||||
/// An envelope that arrives states its own layer and span, which its content hash
|
||||
/// authenticates, over whatever a receipt seen before it supposed: a layer over one
|
||||
/// frame, first known by its receipt with every unit guessed, is listed over that frame.
|
||||
#[test]
|
||||
fn an_arrival_states_its_own_layer_and_span() {
|
||||
let (info, units, env) = corpus_base("attested_after_decode", "base_1.wai", true);
|
||||
let l1_f1 = layer_file("attested_after_decode", "L1_f1.wai"); // refines.span: frame 1 only
|
||||
let mut sink = StageSink::new(cfg());
|
||||
sink.add_base(info.clone(), units);
|
||||
let mut obs = StageObserver::new(info.content_hash, env.len() as u64, true);
|
||||
obs.dropped(sink.base_epoch(&info.content_hash), chash(&l1_f1), 1, None, ReceiptEvidence::InclusionVerified);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l1_f1, ReceiptEvidence::DeliveredVerified).expect("a layer of this base");
|
||||
let r = sink.offer(&l1_f1, 1, ReceiptEvidence::DeliveredVerified).expect("a WAI1");
|
||||
assert_eq!(r[0].applied, vec![1]);
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &r);
|
||||
let claim = obs.claim(sink.chain(&info.content_hash).expect("the chain"), sink.base_epoch(&info.content_hash));
|
||||
let o = &claim.objects[0];
|
||||
assert_eq!((o.units, o.status, o.evidence), ((1, 1), StageStatus::Applied, StageEvidence::DeliveredVerified));
|
||||
assert_eq!(claim.op_delivered, Some(stage_point(0)), "one frame of two delivered at layer 1 is not stage 1");
|
||||
seal(info.content_hash, claim).expect("it seals");
|
||||
}
|
||||
|
||||
/// Over a base whose receipt was not verified first, a layer applies whatever was
|
||||
/// verified of its receipt, and the sink's claim, which states no delivered stage, seals
|
||||
/// with the evidence it had.
|
||||
#[test]
|
||||
fn an_unattested_set_applied_on_inclusion_evidence_seals() {
|
||||
let (info, units, env) = corpus_base("inclusion_only_evidence", "base.wai", false);
|
||||
let l1 = layer_file("inclusion_only_evidence", "L1.wai");
|
||||
let mut sink = StageSink::new(cfg());
|
||||
sink.add_base(info.clone(), units);
|
||||
let mut obs = StageObserver::new(info.content_hash, env.len() as u64, false);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l1, ReceiptEvidence::InclusionVerified).expect("a layer of this base");
|
||||
let r = sink.offer(&l1, 1, ReceiptEvidence::InclusionVerified).expect("a WAI1");
|
||||
assert_eq!((r[0].envelope, r[0].applied.clone()), (None, vec![0, 1]));
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &r);
|
||||
let claim = obs.claim(sink.chain(&info.content_hash).expect("the chain"), sink.base_epoch(&info.content_hash));
|
||||
assert_eq!((claim.objects[0].status, claim.objects[0].evidence), (StageStatus::Applied, StageEvidence::InclusionVerified));
|
||||
assert_eq!(claim.op_delivered, None);
|
||||
seal(info.content_hash, claim).expect("it seals");
|
||||
assert_eq!(obs.arrived(0, b"not an envelope", ReceiptEvidence::None), Err(ObserveError::NotALayer));
|
||||
}
|
||||
|
||||
/// A layer refused, then offered again and applied, is stated applied: a later report
|
||||
/// that refuses nothing clears the earlier refusal.
|
||||
#[test]
|
||||
fn a_layer_refused_then_applied_is_applied() {
|
||||
let (info, units, env) = corpus_base("inclusion_only_evidence", "base.wai", true);
|
||||
let l1 = layer_file("inclusion_only_evidence", "L1.wai");
|
||||
let mut sink = StageSink::new(cfg());
|
||||
sink.add_base(info.clone(), units);
|
||||
let mut obs = StageObserver::new(info.content_hash, env.len() as u64, true);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l1, ReceiptEvidence::InclusionVerified).expect("a layer");
|
||||
let first = sink.offer(&l1, 1, ReceiptEvidence::InclusionVerified).expect("a WAI1");
|
||||
assert_eq!(first[0].envelope, Some(super::StageRefusal::RefinementUnattested));
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &first);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l1, ReceiptEvidence::DeliveredVerified).expect("a layer");
|
||||
let second = sink.offer(&l1, 2, ReceiptEvidence::DeliveredVerified).expect("a WAI1");
|
||||
assert_eq!((second[0].envelope, second[0].applied.clone()), (None, vec![0, 1]));
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &second);
|
||||
let claim = obs.claim(sink.chain(&info.content_hash).expect("the chain"), sink.base_epoch(&info.content_hash));
|
||||
let o = &claim.objects[0];
|
||||
assert_eq!((o.status, o.reason.clone(), o.evidence), (StageStatus::Applied, None, StageEvidence::DeliveredVerified));
|
||||
seal(info.content_hash, claim).expect("it seals");
|
||||
}
|
||||
|
||||
/// A layer refused for its receipt, then offered again with a receipt verified over its
|
||||
/// bytes and held for units below its stage, is stated as the sink last decided: held
|
||||
/// (`layer-gap`), not refused for the receipt it no longer lacks.
|
||||
#[test]
|
||||
fn a_layer_refused_then_held_states_the_hold() {
|
||||
let (info, units, env) = corpus_base("out_of_order", "base.wai", true);
|
||||
let l2 = layer_file("out_of_order", "L2.wai");
|
||||
let mut sink = StageSink::new(cfg());
|
||||
sink.add_base(info.clone(), units);
|
||||
let mut obs = StageObserver::new(info.content_hash, env.len() as u64, true);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l2, ReceiptEvidence::InclusionVerified).expect("a layer");
|
||||
let first = sink.offer(&l2, 1, ReceiptEvidence::InclusionVerified).expect("a WAI1");
|
||||
assert_eq!(first[0].envelope, Some(super::StageRefusal::RefinementUnattested));
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &first);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l2, ReceiptEvidence::DeliveredVerified).expect("a layer");
|
||||
let second = sink.offer(&l2, 2, ReceiptEvidence::DeliveredVerified).expect("a WAI1");
|
||||
assert_eq!(second[0].envelope, None);
|
||||
assert!(!second[0].held.is_empty(), "held for units below its stage");
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &second);
|
||||
let claim = obs.claim(sink.chain(&info.content_hash).expect("the chain"), sink.base_epoch(&info.content_hash));
|
||||
let o = &claim.objects[0];
|
||||
assert_eq!((o.status, o.reason.as_deref()), (StageStatus::Refused, Some("layer-gap")));
|
||||
seal(info.content_hash, claim).expect("it seals");
|
||||
}
|
||||
|
||||
/// A layer applied, then offered again with weaker evidence and refused for it, is
|
||||
/// still stated applied: what the sink applied stays applied.
|
||||
#[test]
|
||||
fn a_layer_applied_then_refused_on_a_later_offer_stays_applied() {
|
||||
let (info, units, env) = corpus_base("inclusion_only_evidence", "base.wai", true);
|
||||
let l1 = layer_file("inclusion_only_evidence", "L1.wai");
|
||||
let mut sink = StageSink::new(cfg());
|
||||
sink.add_base(info.clone(), units);
|
||||
let mut obs = StageObserver::new(info.content_hash, env.len() as u64, true);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l1, ReceiptEvidence::DeliveredVerified).expect("a layer");
|
||||
let first = sink.offer(&l1, 1, ReceiptEvidence::DeliveredVerified).expect("a WAI1");
|
||||
assert_eq!(first[0].applied, vec![0, 1]);
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &first);
|
||||
obs.arrived(sink.base_epoch(&info.content_hash), &l1, ReceiptEvidence::InclusionVerified).expect("a layer");
|
||||
let again = sink.offer(&l1, 2, ReceiptEvidence::InclusionVerified).expect("a WAI1");
|
||||
obs.reports(sink.base_epoch(&info.content_hash), &again);
|
||||
let claim = obs.claim(sink.chain(&info.content_hash).expect("the chain"), sink.base_epoch(&info.content_hash));
|
||||
let o = &claim.objects[0];
|
||||
assert_eq!((o.status, o.reason.as_deref(), o.evidence), (StageStatus::Applied, None, StageEvidence::DeliveredVerified), "the later offer: {:?}", again);
|
||||
seal(info.content_hash, claim).expect("it seals");
|
||||
}
|
||||
|
||||
fn budget_cfg() -> ChainConfig {
|
||||
ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, budget: 1 << 30, record_budget: 112, live: None }
|
||||
}
|
||||
|
||||
/// A layer held for its base across the sink's forgetting that base survives in the
|
||||
/// observation, and is stated applied when the base is decoded again and the sink
|
||||
/// releases it. The observer reads only the sink's epochs; nobody polls `remembers`.
|
||||
#[test]
|
||||
fn a_layer_held_across_a_forget_is_stated_applied() {
|
||||
let (b0, u0, e0) = corpus_base("record_budget", "base_0.wai", false);
|
||||
let (b1, u1, _) = corpus_base("record_budget", "base_1.wai", false);
|
||||
let (l1a0, l1b0, l1a1) =
|
||||
(layer_file("record_budget", "L1a_0.wai"), layer_file("record_budget", "L1b_0.wai"), layer_file("record_budget", "L1a_1.wai"));
|
||||
let mut sink = StageSink::new(budget_cfg());
|
||||
let mut obs = StageObserver::new(b0.content_hash, e0.len() as u64, false);
|
||||
let h = b0.content_hash;
|
||||
let r = sink.add_base(b0.clone(), u0.clone());
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
obs.arrived(sink.base_epoch(&h), &l1a0, ReceiptEvidence::None).expect("a layer");
|
||||
let r = sink.offer(&l1a0, 3, ReceiptEvidence::None).expect("a WAI1");
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
let r = sink.remove_base(&h).unwrap_or_default();
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
obs.arrived(sink.base_epoch(&h), &l1b0, ReceiptEvidence::None).expect("a layer");
|
||||
let r = sink.offer(&l1b0, 5, ReceiptEvidence::None).expect("a WAI1");
|
||||
assert!(r[0].held_for_base, "held for a base the sink does not hold");
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
sink.add_base(b1.clone(), u1);
|
||||
sink.offer(&l1a1, 7, ReceiptEvidence::None).expect("a WAI1");
|
||||
sink.remove_base(&b1.content_hash);
|
||||
assert_eq!(sink.base_epoch(&h), 1, "the record budget forgot base 0");
|
||||
let r = sink.add_base(b0, u0);
|
||||
assert_eq!(r.iter().find(|x| x.content_hash == chash(&l1b0)).map(|x| x.applied.clone()), Some(vec![0, 1]));
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
let claim = obs.claim(sink.chain(&h).expect("the chain"), sink.base_epoch(&h));
|
||||
let objects: Vec<_> = claim.objects.iter().map(|o| (o.object, o.status)).collect();
|
||||
assert_eq!(objects, vec![(chash(&l1b0), StageStatus::Applied)], "the forgotten epoch's layer is gone, the held one applied");
|
||||
assert_eq!(claim.units_by_stage, vec![0, 2]);
|
||||
seal(h, claim).expect("it seals");
|
||||
}
|
||||
|
||||
/// A claim never spans two epochs. A base attested and refined, then forgotten and
|
||||
/// decoded again with no receipt and nothing applied: the observer, which last heard of
|
||||
/// the base in its first epoch, makes the claim over the second, so it states no delivered
|
||||
/// stage and no layer, over units at stage 0.
|
||||
#[test]
|
||||
fn a_claim_never_spans_two_epochs() {
|
||||
let (mut b0, u0, e0) = corpus_base("record_budget", "base_0.wai", true);
|
||||
let (b1, u1, _) = corpus_base("record_budget", "base_1.wai", false);
|
||||
let (l1a0, l1a1) = (layer_file("record_budget", "L1a_0.wai"), layer_file("record_budget", "L1a_1.wai"));
|
||||
let mut sink = StageSink::new(budget_cfg());
|
||||
let h = b0.content_hash;
|
||||
let mut obs = StageObserver::new(h, e0.len() as u64, true);
|
||||
obs.request(Some(1), Some(1));
|
||||
let r = sink.add_base(b0.clone(), u0.clone());
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
obs.arrived(sink.base_epoch(&h), &l1a0, ReceiptEvidence::DeliveredVerified).expect("a layer");
|
||||
let r = sink.offer(&l1a0, 3, ReceiptEvidence::DeliveredVerified).expect("a WAI1");
|
||||
assert_eq!(r[0].applied, vec![0, 1]);
|
||||
obs.reports(sink.base_epoch(&h), &r);
|
||||
sink.remove_base(&h);
|
||||
sink.add_base(b1.clone(), u1);
|
||||
sink.offer(&l1a1, 7, ReceiptEvidence::None).expect("a WAI1");
|
||||
sink.remove_base(&b1.content_hash);
|
||||
b0.attested = false;
|
||||
sink.add_base(b0, u0);
|
||||
assert!(sink.remembers(&h), "remembered again: polling it would show nothing");
|
||||
let chain = sink.chain(&h).expect("the chain");
|
||||
assert!(chain.units().iter().all(|u| u.stage == 0));
|
||||
let claim = obs.claim(chain, sink.base_epoch(&h));
|
||||
assert_eq!((claim.op_delivered.clone(), claim.op_committed.clone()), (None, None));
|
||||
assert!(claim.objects.is_empty());
|
||||
assert_eq!(claim.units_by_stage, vec![2]);
|
||||
seal(h, claim).expect("it seals");
|
||||
}
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
//! ([`OfferReport`]). The residual form's payload is [`wir1`]; its predictors are
|
||||
//! [`predict`]; tiers compose by [`tier`]; the live schedule is [`schedule`].
|
||||
|
||||
// staged-delivery §10.2: a `stage` claim from what a sink observed.
|
||||
#[cfg(feature = "receipt_claims")]
|
||||
pub mod claim;
|
||||
pub mod predict;
|
||||
pub mod schedule;
|
||||
pub mod tier;
|
||||
|
|
@ -17,6 +20,8 @@ pub mod wir1_arith;
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(all(test, feature = "receipt_claims"))]
|
||||
mod claim_tests;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
|
@ -475,6 +480,18 @@ impl ReceiptEvidence {
|
|||
}
|
||||
}
|
||||
|
||||
/// What a JWP verifier established about a layer's object (jwp-receipts §3, §4.4) is the
|
||||
/// evidence §10 reads: accepted over its envelope bytes, or verified by inclusion alone.
|
||||
#[cfg(feature = "receipt_claims")]
|
||||
impl From<crate::jwp_delivery::Evidence> for ReceiptEvidence {
|
||||
fn from(e: crate::jwp_delivery::Evidence) -> Self {
|
||||
match e {
|
||||
crate::jwp_delivery::Evidence::DeliveredVerified => Self::DeliveredVerified,
|
||||
crate::jwp_delivery::Evidence::InclusionVerified => Self::InclusionVerified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A unit's samples, in the element type of its kind.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Samples {
|
||||
|
|
@ -1460,6 +1477,8 @@ pub struct StageSink {
|
|||
tick: u64,
|
||||
/// Whether every chain records its applications ([`Self::set_trace`]).
|
||||
trace: bool,
|
||||
/// Per base, how many times the sink has forgotten it ([`Self::base_epoch`]).
|
||||
epochs: BTreeMap<[u8; 32], u64>,
|
||||
}
|
||||
|
||||
/// What a sink keeps of one base it does not hold.
|
||||
|
|
@ -1481,7 +1500,7 @@ impl StageSink {
|
|||
/// A sink holding no base, with the bound, cost limit, budget and schedule every
|
||||
/// chain it creates uses.
|
||||
pub fn new(cfg: ChainConfig) -> Self {
|
||||
StageSink { cfg, chains: BTreeMap::new(), pending: vec![], kept: BTreeMap::new(), kept_order: BTreeMap::new(), kept_bytes: 0, tick: 0, trace: false }
|
||||
StageSink { cfg, chains: BTreeMap::new(), pending: vec![], kept: BTreeMap::new(), kept_order: BTreeMap::new(), kept_bytes: 0, tick: 0, trace: false, epochs: BTreeMap::new() }
|
||||
}
|
||||
|
||||
/// The chain of the base whose content hash is `base`.
|
||||
|
|
@ -1591,6 +1610,8 @@ impl StageSink {
|
|||
/// bases written longest ago until what is kept is within the record budget (§4.4).
|
||||
fn put_kept(&mut self, base: [u8; 32], mut k: Kept) {
|
||||
if k.records.is_empty() && !k.attested {
|
||||
// Nothing to keep: the sink no longer remembers the base.
|
||||
self.forgot(base);
|
||||
return;
|
||||
}
|
||||
self.tick += 1;
|
||||
|
|
@ -1602,6 +1623,7 @@ impl StageSink {
|
|||
let Some((_, oldest)) = self.kept_order.pop_first() else { break };
|
||||
let gone = self.kept.remove(&oldest).expect("kept");
|
||||
self.kept_bytes -= gone.counted_bytes();
|
||||
self.forgot(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1617,11 +1639,37 @@ impl StageSink {
|
|||
Some(reports)
|
||||
}
|
||||
|
||||
/// Whether the sink remembers `base`: it holds its chain, or keeps its records or its
|
||||
/// attestation (§4.4). A base the sink does not remember is, if it decodes it again,
|
||||
/// one it never decoded and never attested (§14); its epoch ([`Self::base_epoch`])
|
||||
/// says how many times that has happened.
|
||||
pub fn remembers(&self, base: &[u8; 32]) -> bool {
|
||||
self.chains.contains_key(base) || self.kept.contains_key(base)
|
||||
}
|
||||
|
||||
/// Discard what the sink keeps of a base it does not hold (§14): its record and its
|
||||
/// attestation. Decoded again, the base starts with no history and unattested, as
|
||||
/// one never decoded. Whether there was anything to discard.
|
||||
pub fn forget_base(&mut self, base: &[u8; 32]) -> bool {
|
||||
self.take_kept(base).is_some()
|
||||
let had = self.take_kept(base).is_some();
|
||||
if had {
|
||||
self.forgot(*base);
|
||||
}
|
||||
had
|
||||
}
|
||||
|
||||
/// How many times the sink has forgotten `base` (§4.4, §14): each time it stops
|
||||
/// remembering it — its records and attestation discarded by
|
||||
/// [`Self::forget_base`], evicted under the record budget, or its chain dropped with
|
||||
/// nothing to keep — the epoch increments. Decoded again, a base in a later epoch is
|
||||
/// one the sink never decoded and never attested, so a `stage` claim's observation of
|
||||
/// it starts over ([`claim::StageObserver`] records the epoch of every event).
|
||||
pub fn base_epoch(&self, base: &[u8; 32]) -> u64 {
|
||||
self.epochs.get(base).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn forgot(&mut self, base: [u8; 32]) {
|
||||
*self.epochs.entry(base).or_default() += 1;
|
||||
}
|
||||
|
||||
/// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A
|
||||
|
|
|
|||
|
|
@ -725,6 +725,120 @@ fn inclusion_evidence_never_applies() {
|
|||
assert_eq!(c.offer(&l1, 0, ReceiptEvidence::DeliveredVerified).unwrap()[0].applied, vec![0, 1, 2, 3]);
|
||||
}
|
||||
|
||||
/// A layer's evidence is what a JWP verifier establishes for it (jwp-receipts §3,
|
||||
/// §4.4): the base and the layer each on its own track under one key, their chains kept
|
||||
/// per (key, track); the layer's object receipt checked without its bytes is
|
||||
/// inclusion-verified and is refused, and checked over its bytes it applies.
|
||||
#[cfg(feature = "receipt_claims")]
|
||||
#[test]
|
||||
fn jwp_evidence_decides_layer_attestation() {
|
||||
use crate::jwp_delivery::{GroupBuilder, ReceiptChains};
|
||||
let (base_env, info, units) = base4();
|
||||
let b = info.content_hash;
|
||||
let below: Vec<Vec<u8>> = (0..4).map(frame).collect();
|
||||
let l1 = layer(&b, one(&b), 1, None, &below, 50);
|
||||
let key = ed25519_dalek::SigningKey::from_bytes(&[0x11; 32]);
|
||||
let seal = |track: u64, env: &[u8]| {
|
||||
let mut g = GroupBuilder::default();
|
||||
g.add(h(env), 0, "origin.example", None);
|
||||
let (g, o) = g.finish(&key, "did:example:publisher", track, 0, "2026-10-06T00:00:00Z", "2026-10-06T00:00:01Z", None, false).unwrap();
|
||||
(g, o.into_iter().next().unwrap())
|
||||
};
|
||||
let ((gb, ob), (gl, ol)) = (seal(7, &base_env), seal(8, &l1));
|
||||
let mut chains = ReceiptChains::new();
|
||||
let base_evidence = ReceiptEvidence::from(chains.verify_object(&gb, &ob, Some(&base_env)).unwrap());
|
||||
assert_eq!(base_evidence, ReceiptEvidence::DeliveredVerified);
|
||||
let mut c = StageChain::new(BaseInfo { attested: true, ..info }, units, cfg());
|
||||
let dropped = ReceiptEvidence::from(chains.verify_object(&gl, &ol, None).unwrap());
|
||||
assert_eq!(dropped, ReceiptEvidence::InclusionVerified);
|
||||
assert_eq!(c.offer(&l1, 0, dropped).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested));
|
||||
let delivered = ReceiptEvidence::from(chains.verify_object(&gl, &ol, Some(&l1)).unwrap());
|
||||
assert_eq!(c.offer(&l1, 0, delivered).unwrap()[0].applied, vec![0, 1, 2, 3]);
|
||||
assert_eq!(chains.chains(), 2, "one chain per track");
|
||||
}
|
||||
|
||||
/// staged-delivery §10.2: a sink states, from what it observed, a `stage` claim that a
|
||||
/// verifier accepts. Four per-frame first layers applied, delivered over their bytes;
|
||||
/// the second layer dropped at a relay, known by its receipt verified by inclusion: the
|
||||
/// stage delivered is 1, the commitment of 2 was not met, and the claim seals.
|
||||
#[cfg(feature = "receipt_claims")]
|
||||
#[test]
|
||||
fn a_sink_states_its_stage_claim() {
|
||||
use crate::claim_receipt::{ClaimBody, ClaimReceipt, StageEvidence, StageStatus, SubjectType};
|
||||
use super::claim::StageObserver;
|
||||
let (base_env, info, units) = base4();
|
||||
let b = info.content_hash;
|
||||
let mut c = StageChain::new(BaseInfo { attested: true, ..info.clone() }, units.clone(), cfg());
|
||||
let mut obs = StageObserver::new(b, base_env.len() as u64, true);
|
||||
obs.request(Some(2), Some(2));
|
||||
let mut hs = Vec::new();
|
||||
for f in 0..4u32 {
|
||||
let l = layer(&b, one(&b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f);
|
||||
obs.arrived(0, &l, ReceiptEvidence::DeliveredVerified).unwrap();
|
||||
obs.reports(0, &c.offer(&l, 0, ReceiptEvidence::DeliveredVerified).unwrap());
|
||||
hs.push(h(&l));
|
||||
}
|
||||
let frames: Vec<Vec<u8>> = (0..4).map(|f| unit_frame(&c, f)).collect();
|
||||
let l2 = layer(&b, runs(&hs), 2, None, &frames, 90);
|
||||
obs.dropped(0, h(&l2), 2, None, ReceiptEvidence::InclusionVerified);
|
||||
let claim = obs.claim(&c, 0);
|
||||
assert_eq!(claim.units_by_stage, vec![0, 4]);
|
||||
assert_eq!(claim.op_delivered, Some(crate::claim_receipt::stage_point(1)));
|
||||
assert_eq!(claim.commitment_met(), Some(false));
|
||||
let l2_seen = claim.objects.last().unwrap();
|
||||
assert_eq!((l2_seen.layer, l2_seen.status, l2_seen.evidence, l2_seen.bytes), (2, StageStatus::Dropped, StageEvidence::InclusionVerified, 0));
|
||||
assert_eq!(claim.stage_hash, c.clip_sha256());
|
||||
let sink = ed25519_dalek::SigningKey::from_bytes(&[0x44; 32]);
|
||||
let seal = |body| ClaimReceipt::seal(&sink, "did:example:sink", SubjectType::Object, b, 0, "2026-10-06T00:00:00Z", ClaimBody::Stage(body), None);
|
||||
let sealed = seal(claim).expect("a sink's claim passes §2.4.10's rules");
|
||||
assert_eq!(crate::claim_receipt::verify_claim_json(&sealed.to_json()).map(|_| ()), Ok(()));
|
||||
|
||||
// Live: frame 2's first layer arrives inside its lead and is late; the second layer
|
||||
// applies to frames 0, 1 and 3 and is held for frame 2 until the bound.
|
||||
let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 1000 }, lead_ms: vec![10, 10] }), ..cfg() };
|
||||
let mut c = StageChain::new(BaseInfo { attested: true, ..info.clone() }, units.clone(), live);
|
||||
let mut obs = StageObserver::new(b, base_env.len() as u64, true);
|
||||
let mut hs = Vec::new();
|
||||
for f in 0..4u32 {
|
||||
let l = layer(&b, one(&b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f);
|
||||
obs.arrived(0, &l, ReceiptEvidence::DeliveredVerified).unwrap();
|
||||
obs.reports(0, &c.offer(&l, if f == 2 { 1195 } else { 0 }, ReceiptEvidence::DeliveredVerified).unwrap());
|
||||
hs.push(h(&l));
|
||||
}
|
||||
let frames: Vec<Vec<u8>> = (0..4).map(|f| unit_frame(&c, f)).collect();
|
||||
let l2 = layer(&b, runs(&hs), 2, None, &frames, 90);
|
||||
obs.arrived(0, &l2, ReceiptEvidence::DeliveredVerified).unwrap();
|
||||
obs.reports(0, &c.offer(&l2, 0, ReceiptEvidence::DeliveredVerified).unwrap());
|
||||
obs.reports(0, &c.expire(2000));
|
||||
let claim = obs.claim(&c, 0);
|
||||
let status: Vec<(u64, StageStatus, Option<&str>)> = claim.objects.iter().map(|o| (o.layer, o.status, o.reason.as_deref())).collect();
|
||||
assert_eq!(status, vec![
|
||||
(1, StageStatus::Applied, None),
|
||||
(1, StageStatus::Applied, None),
|
||||
(1, StageStatus::Late, None),
|
||||
(1, StageStatus::Applied, None),
|
||||
(2, StageStatus::Partial, Some("layer-gap")),
|
||||
]);
|
||||
assert_eq!((claim.units_by_stage.clone(), claim.deadline_misses), (vec![1, 0, 3], 1));
|
||||
assert_eq!(claim.presented, crate::claim_receipt::stage_point(0));
|
||||
assert_eq!(claim.op_delivered, Some(crate::claim_receipt::stage_point(2)), "every layer arrived delivered, one too late");
|
||||
seal(claim).expect("the live claim seals");
|
||||
|
||||
// Over a base whose receipt was not verified first, nothing is stated as delivered,
|
||||
// nor a request or a commitment beside it.
|
||||
let mut c = StageChain::new(info, units, cfg());
|
||||
let mut obs = StageObserver::new(b, base_env.len() as u64, false);
|
||||
obs.request(Some(1), Some(1));
|
||||
let l1 = layer(&b, one(&b), 1, None, &(0..4).map(frame).collect::<Vec<_>>(), 50);
|
||||
obs.arrived(0, &l1, ReceiptEvidence::None).unwrap();
|
||||
obs.reports(0, &c.offer(&l1, 0, ReceiptEvidence::None).unwrap());
|
||||
let claim = obs.claim(&c, 0);
|
||||
assert_eq!((claim.op_requested.clone(), claim.op_committed.clone(), claim.op_delivered.clone()), (None, None, None));
|
||||
assert_eq!(claim.objects[0].evidence, StageEvidence::None);
|
||||
seal(claim).expect("an unattested set's claim seals");
|
||||
assert_eq!(obs.arrived(0, &envelope(CAP_VIDEO_INT_REFINE, None, vec![]), ReceiptEvidence::None), Err(super::claim::ObserveError::NotALayer));
|
||||
}
|
||||
|
||||
/// Test 11: the tier table, and a stage hash only at decode-equivalence.
|
||||
#[test]
|
||||
fn compose_table() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue