receipt-ledger: bill what the work spends — bytes out, a package's work per run and its storage by byte-time, every hop's energy, the output its hop delivered #556

Merged
dcharlot merged 1 commit from wai/ledger-physics into main 2026-10-31 12:26:27 -04:00
20 changed files with 3441 additions and 274 deletions

View file

@ -6,6 +6,102 @@ wire format may change and such changes are recorded here.
## Unreleased
### The receipt ledger bills what the work spends: bytes out, a package's work per run and its storage, every hop's energy, the output its hop delivered
- **A job's output is the object its hop delivered** ([extensions/receipt-ledger.md](extensions/receipt-ledger.md)
§5.1): an output's object is its `codestream`, named by its content hash, and where the job's route record states
what its served attempt delivered (route-record version 2, `outputs`), each output receipt a job source gives is of
one of those objects, once, or the source is refused **`output_not_delivered`**, at `outputs` and its index. A line
bills a job's subpixels from its output receipts, so a receipt of an object the hop did not deliver billed subpixels
nobody delivered for that job, and one given twice billed them twice. The route record binds the hop's receipt to
its outputs (#552); this binds each output receipt to the same outputs. It holds under tariff versions 3 and 4; under
version 4 a record of version 1, which states no outputs, holds its outputs to none, and under version 3 its outputs
are read as before.
- **Routing figures are never charges** (§5.1): a route record's `cost`, `energy`, `expected_cost`,
`expected_ttfb_ms`, and its price book's `price` and `failures`, are the router's figures for choosing an endpoint;
no line reads them, and a job line bills its billed attempt alone, at the tariff's price the buyer signed. Two
controls hold it: a failed hop counted as a job, and a job line at the router's list price, neither recomputes.
- **The room-session receipt, version 2** ([extensions/live-metering.md](extensions/live-metering.md) §4.1, §4.4,
§4.6): a sixteenth unit, `bytes_out`, the payload bytes the hops sent the session's sessions, summed from their
ledgers' `bytes_out` (§3.4), and a fourteenth tariff price, per 10^9 of those bytes. What delivering a session costs
scales with the bytes carried as well as with the time it ran. A grant's consumption entry states its `bytes_out`
(206 bytes), and its funds are ⌈(1000 × *N* + `bytes_out` × the price) / 10^15⌉ minor units, the time and the bytes
rounded up once together. The ledgers count exactly the sessions under a grant or none, so the grants' bytes out sum
to the receipt's; a hop link's bytes, which no ledger holds, are carriage between relays and are not counted. A
receipt of version 1 reads, verifies and prices exactly as before; a version-1 reader cannot read version 2.
- **Tariff version 4** (receipt-ledger §4.7; `WLT1`, `WLI1` with version byte 4) prices three more things by what
spending on them scales with. A **room's bytes out**: a fourteenth room price, a room line of unit `bytes_out` from a
receipt of version 2, and a receipt that states a tariff states all fourteen prices (one of version 1 states no
price of bytes, read as 0, so its funds conflict with a tariff that prices bytes). A **package's work, per run**:
packaging is deterministic, so a package made again (a customer asking for the work done and measured again) gives
the same receipt from a run that did the work again, and every version before billed a package once per receipt and
refused it. In version 4 a package has a job's shape: the run that made it, a routed job of `package.build` whose
output is the package (its receipt's text the object its hop delivered, verified against its indexes), is the work,
billed once per run at the tariff's job prices per job and per **work unit**, a new job unit, the package receipt's
`work_units` (packaging §9.1), the work its figure is expressed against; the package receipt is the product, cited
by each run and billed once by none; a run replayed under a new route record is refused (`record_rebilled`,
`record_duplicate`). The run's energy is its hop's figure, and the receipt's own figure, the same work's, is not
stated again (scope `packaging` is empty in version 4). A **package's storage by byte-time**: the tariff's one package
price is per 10^9 byte-days; a `package` source is the product held and bills its storage alone: one the previous
invoice lists is held through the invoice's whole period, written by its start, and is billed ⌊Σ `stored_bytes` ×
(`end` − `start`) / 86 400 000 000⌋ byte-days and its receipt listed again; the invoice that first lists a package
bills it no storage, since no record shows when in that period it was written. The period is read from the chain's
own invoices, which the seller signs and the buyer holds. The invoice states a sixth unmetered count, so a version-3
reader cannot read it.
- **Every hop's energy** (§6.6): under version 4 an invoice states the energy of every attempt of its jobs, the ones
not billed (a failed hop, a hop that served past the deadline) in a scope of their own, `unbilled_hops`, beside the
billed hops' and never summed with them; such a hop's figure that no row can state is `job_figure_invalid`. A failed
hop spent energy and delivered nothing; before, no row stated it, so the jobs' energy was less than their hops
spent. No line reads the scope: the buyer is still not billed for a failed hop.
- **Jobs routed under the route record's version 2 in the ledger corpus**: an encode, a failover whose failed hop was
metered, a decode, a hop past the deadline, three hops that failed and a failover whose failed hop's figure states no
class, routed by the route corpus's router under its price book of version 2, per operation with measured failures,
each served hop stating its outputs; and two runs of `package.build` making one package, under a price book of
version 2 of their own, and a replay of the first. `routes/vectors.json` is unchanged (`route_record::corpus` adds
the helpers that route them for the ledger).
- **The corpora.** `live-conformance/metering/vectors.json` adds five vectors (a receipt of version 2 with its bytes
priced; bytes out restated, bytes left out of the consumption, bytes priced with no tariff, a receipt of version 3)
and keeps every other byte for byte; `live_metering_verify.py` reads version 2 and requires its new mutated copies to
fail. `live-conformance/ledger/vectors.json` adds twenty vectors (version-2-routed jobs under version 3 and
their two controls; version 4's chain of two invoices and its controls); `receipt_ledger_verify.py` reads version 4,
holds outputs to their content hashes with `claim-conformance/_blake3.py`, requires the new mutated copies to fail,
and drops seven more of its checks (`output-delivered`, `unbilled-hops`, `storage-held`, `package-again`,
`run-work`, `bytes-out`, `conflict-bytes`). The ledger service (`wai/wai-ledger`) reads a source by its tariff's
version and, under version 4, bills each package the last invoice holds again for its storage, and a package made
again by a new run.
- **Breaking:** `api:rust:wai::live_metering::SessionReceipt::version`: a receipt states its version, 1 or 2. A struct
literal adds `version: live_metering::RECORD_VERSION`.
- **Breaking:** `api:rust:wai::live_metering::Units::bytes_out`: version 2's bytes out, 0 in version 1.
- **Breaking:** `api:rust:wai::live_metering::Tariff::bytes`: version 2's price per 10^9 bytes out, 0 in version 1.
- **Breaking:** `api:rust:wai::live_metering::Consumption::version`: an entry states its receipt's version.
- **Breaking:** `api:rust:wai::live_metering::Consumption::bytes_out`: version 2's bytes out of a grant.
- **Breaking:** `api:rust:wai::live_metering::Consumption::encode`: returns a `Vec<u8>`, 198 bytes in version 1 and
206 in version 2.
- **Breaking:** `api:rust:wai::live_metering::Computed::version`: the version of the receipt it was computed for.
- **Breaking:** `api:rust:wai::live_metering::SessionPlan::version`: the version of the receipt a plan aggregates into.
- **Breaking:** `api:rust:wai::receipt_ledger::ROOM_PRICES`: 14, a tariff's room prices with version 4's price of bytes
out; `ROOM_PRICES_1` is the 13 of versions 1 to 3.
- **Breaking:** `api:rust:wai::receipt_ledger::Tariff::room`: fourteen prices, the fourteenth written in version 4 and
read as 0 in versions 1 to 3.
- **Breaking:** `api:rust:wai::receipt_ledger::Invoice::unmetered`: six counts, scope 6 (`unbilled_hops`) last; an
invoice writes and reads its version's.
- **Breaking:** `api:rust:wai::receipt_ledger::Computed::unmetered`: the same six.
- **Breaking:** `api:rust:wai::receipt_ledger::JobFacts::packages`: under version 4, a job's output packages, each its
receipt hash and work units.
- **Breaking:** `api:rust:wai::receipt_ledger::JOB_UNITS`: three names, `work_units` the third (version 4).
- **Breaking:** `api:rust:wai::receipt_ledger::Tariff::package`: in version 4 only the third, per 10^9 byte-days, is
written and read; the first two are 0.
- **Breaking:** `api:rust:wai::receipt_ledger::PackageFacts::held_us`: under version 4, the period a package is held
through, `None` when first billed.
- **Breaking:** `api:rust:wai::receipt_ledger::JobFacts::unbilled`: under version 4, the figures of a job's attempts
that are not billed.
- **Breaking:** `api:rust:wai::receipt_ledger::Reason::OutputNotDelivered`: a job's output receipt of an object its
served hop did not deliver, or given twice (§5.1); a match over the reasons adds it.
- **Breaking:** `api:rust:wai::receipt_ledger::Reason::ALL`: the codes of §10, `output_not_delivered` among them.
- **Breaking:** `record:corpus:wai/live-conformance/ledger/vectors.json`: the control `invoice_version` gives an
invoice of version 5, since version 4 is now defined, and is refused `unknown_version` as before; every other vector
keeps its outcome, and the new vectors are added.
### The receipt ledger prices media jobs per operation, and bills the work done
- **Tariff version 3** ([extensions/receipt-ledger.md](extensions/receipt-ledger.md) §4.6): each job price

View file

@ -1073,7 +1073,8 @@ object or per viewer, so its size is bounded by what the room published, whateve
The room authority, or a key its origin's key log lists with `live/meter`, signs the room-session
receipt, `WMR1`, naming the records, the takes and the composition receipts it aggregates and
stating publisher-seconds, subscriber-seconds by layer class, composition-seconds, events sent
and delivered and recorded-seconds, every hop's joules with its class and a total per class,
and delivered and recorded-seconds (and, in a receipt of version 2, the bytes its hops sent the
session's sessions, priced per byte), every hop's joules with its class and a total per class,
never one across classes, and each grant's consumption against its joule and funds ceilings,
a hop's joules attributed to grants by the bytes each moved, an accounting convention and not a
saving any grant made; a verifier recomputes every number from the records. The reference relay
@ -1527,7 +1528,8 @@ And for **invoicing from receipts**, invoice lines a customer can recompute from
`wai.delivery.ledger`: a seller and a buyer both sign a tariff, `WLT1`, one currency and one price
for every unit the records state (a room session's publisher-, subscriber-, composition- and
recorded-seconds and its events; the acknowledged bytes of each object class, carried over MOQT or
HTTP; a package's packages, entries and stored bytes; an ad decision's impressions, presented under
HTTP; a package's packages, entries and stored bytes, or, in a tariff of version 4, its work units
and its stored bytes by the days they are held, beside a room's bytes out; an ad decision's impressions, presented under
the players' claims or billable on beacons, or presented in a stream another party stitched), and
the scope of rooms, delivering parties, packagers, and ad deciders and stitchers it bills. Each source is verified by the extension that defines it, a room-session
receipt or a settlement statement that states a tariff must state this one, and the seller signs an

View file

@ -12,8 +12,9 @@
> the **room-session receipt**, `WMR1`, which names the per-hop records, the takes and the
> composition receipts it aggregates, and states the session's units (publisher-seconds,
> subscriber-seconds by layer class, composition-seconds, events sent and delivered,
> recorded-seconds), the joules of every hop with its class, a total per class and never
> one across classes, and each grant's consumption against its joule and funds ceilings.
> recorded-seconds, and in version 2 the bytes the hops sent), the joules of every hop with
> its class, a total per class and never one across classes, and each grant's consumption
> against its joule and funds ceilings.
> Every number in the receipt is recomputed by its verifier from the records it names.
> Clients add quality-of-experience observations, CMCD version 2 Event-mode reports carried
> on the CMCD event timeline, of which only an aggregate over enough sessions enters the
@ -319,7 +320,7 @@ When the session is over, the room authority, or a metering key (§4.3), signs t
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WMR1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `version` | 1 | 1; 2 for a receipt that states the bytes its hops sent and prices them (§4.4, §4.6) |
| `room` | 32 | the room id |
| `signer` | 32 | the key that signs the receipt |
| `start`, `end` | 8 each | u64: the room session, [`start`, `end`) |
@ -327,14 +328,19 @@ When the session is over, the room authority, or a metering key (§4.3), signs t
| `takes` | 1 + 32 × *k* | a u8 count, then the digest of each take manifest it aggregates ([capture-binding](capture-binding.md) §7.1), strictly ascending |
| `compose` | 4 + 32 | u32: the composition receipts it aggregates ([live-compose](live-compose.md) §8); then their **list digest**, SHA-256 over their digests in ascending order, concatenated (32 zero bytes when none) |
| `qoe` | 4 + 32 | u32: the session logs of QoE observations it aggregates (§6); then the SHA-256 of the logs document (32 zero bytes when none) |
| `units` | 120 | fifteen u64: `publisher_us`, `subscriber_us` for layer classes 0 to 8, `sessions`, `composition_us`, `events_sent`, `events_delivered`, `recorded_us` (§4.4) |
| `units` | 120, in version 2 128 | fifteen u64: `publisher_us`, `subscriber_us` for layer classes 0 to 8, `sessions`, `composition_us`, `events_sent`, `events_delivered`, `recorded_us` (§4.4); in version 2 a sixteenth, `bytes_out` |
| `unmetered` | 1 | u8: the hop entries whose figure is the unmetered marker |
| `totals` | 1 + 13 × *m* | a u8 count *m*, then for each acquisition class among the hop entries: `tag` (u8), `figures` (u32, the hop entries of that class) and `joules` (u64, their sum), in strictly ascending order of `tag` (§4.5) |
| `tariff` | 3 + 52 | `currency` (3; zero for no tariff), then thirteen u32 prices in millionths of a minor unit: per second of `publisher_us`, of `subscriber_us` for each layer class 0 to 8, of `composition_us` and of `recorded_us`, and per event sent (§4.6) |
| `tariff` | 3 + 52, in version 2 3 + 56 | `currency` (3; zero for no tariff), then thirteen u32 prices in millionths of a minor unit: per second of `publisher_us`, of `subscriber_us` for each layer class 0 to 8, of `composition_us` and of `recorded_us`, and per event sent (§4.6); in version 2 a fourteenth, per 10^9 bytes of `bytes_out` |
| `ceilings` | 4 + 32 + 4 + 33 × *p* | u32: the grants of the session (§4.6); the **consumption root** (32); then a u32 count *p* and each grant past a ceiling: `grant` (32) and `flags` (u8: bit 0 past its joule ceiling, bit 1 past its funds ceiling), in strictly ascending order of `grant` |
| `qoe_cell` | 49 | the QoE aggregate (§4.7): `suppressed` (u8), `started`, `start_failed`, `exit_before_start` (u32 each), `stalls`, `stall_ms`, `engaged_ms` (u64 each), `vst_count` (u32), `vst_ms` (u64) |
| `sig` | 64 | §4.2 |
Version 2 is version 1's fields in the same order, with `units` and `tariff` each one figure
longer, the figure last. A reader of version 1 cannot read a receipt of version 2, and a
receipt of version 1 reads, verifies and prices exactly as before: version 2 is a version of
its own, as a per-hop record's version 2 is (§3.1).
### 4.2 Digest, signature and rules
The **digest** of a receipt is SHA-256 over its bytes before `sig`; its **signature** is
@ -351,8 +357,8 @@ other way to state. It refuses as `session_invalid` a receipt that breaks any of
2 or 3, and its class reads and meets §3.6's `energy_invalid` rules; each total's `tag` is
2 to 5 and its `figures` above 0;
- `compose`'s digest is zero exactly when its count is, and so is `qoe`'s;
- with no tariff (`currency` zero) every price is 0; otherwise `currency` is three capitals
`A`–`Z`;
- with no tariff (`currency` zero) every price is 0, the price of bytes among them; otherwise
`currency` is three capitals `A`–`Z`;
- each past grant's `flags` is 1, 2 or 3;
- `suppressed` is 0 or 1, and with 1 every other field of the cell is 0.
@ -408,6 +414,15 @@ Every unit is an integer of room-clock microseconds or a count, over the room se
+ *o*, `end` + *o*), each chunk's capture range on the room clock, within the session. A
take with no held commitment records nothing. What a device captured and did not commit
while capturing is not counted.
- **`bytes_out`**, in a receipt of version 2: the sum over the hop records of their ledger
entries' `bytes_out` (§3.4), the payload bytes the hops sent the room's sessions. What
delivering a session costs scales with the bytes carried to it as well as with the time it
ran: a viewer of a high layer and one of a low layer subscribe for the same seconds and are
sent different bytes. The ledgers count exactly the sessions that ran under a grant or
under none, so the sum of the grants' `bytes_out` (§4.6) is the receipt's. A hop link's
bytes are in the relays' track entries and in no ledger (§3.10): carriage between the
relays of a room is how its operator built the room, not a session's delivery, and is not
counted here.
The units do not sum the DVR figures of version 2 records (§3.9) in this revision.
@ -458,11 +473,16 @@ and every grant that was in effect for a present principal at some instant of th
its span is largely what running the hop costs, not a cost per byte, so a grant that moves
fewer bytes is attributed a smaller share of the same figure and does not lower the hop's
energy by that share, and nothing here states what the network between hops spent;
- `bytes_out`, in a receipt of version 2: the sum of its ledger entries' `bytes_out` over
the hop records;
- **funds**, where the tariff's `currency` is the grant's: ⌈*N* / 10^12⌉ minor units of it,
with *N* = `publisher_us` × *p*<sub>pub</sub> + Σ<sub>*c*</sub> `subscriber_us`[*c*] ×
*p*<sub>sub,*c*</sub> + `composition_us` × *p*<sub>comp</sub> + `recorded_us` ×
*p*<sub>rec</sub> + `events_sent` × *p*<sub>ev</sub> × 10^6, at most 2^64 − 2; 2^64 − 1
states that no funds were computed (no tariff, or another currency).
states that no funds were computed (no tariff, or another currency). In a receipt of
version 2 they are ⌈(1000 × *N* + `bytes_out` × *p*<sub>bytes</sub>) / 10^15⌉ minor units,
*N* in 10^-12 of a minor unit and *p*<sub>bytes</sub> per 10^9 bytes, so the time and the
bytes are rounded up once, together; with a price of bytes of 0 they are version 1's.
Its verdicts, against its ceilings, each the first row of the table below that applies,
taken in the order none, past, unmetered, partial, within for the joule ceiling, and none,
@ -486,7 +506,8 @@ A grant's **consumption entry** is 198 bytes: `grant` (32), `joules_ceiling` (8)
`funds_ceiling` (8), `currency` (3), `publisher_us` (8), `subscriber_us` (9 × 8),
`composition_us` (8), `recorded_us` (8), `events_sent` (8), the attributed joules of tags 2,
3, 4 and 5 (4 × 8), `unmetered` (u8: 1 when its share is unmetered at some hop), `funds` (8),
and the joule and funds verdicts (u8 each). Its leaf is SHA-256 over the 25 bytes
and the joule and funds verdicts (u8 each). In a receipt of version 2 it is 206 bytes: its
`bytes_out` (8) follows `events_sent`. Its leaf is SHA-256 over the 25 bytes
`wai:metering-consumption` (ASCII) and 0x01, followed by the entry; the **consumption root**
is the root of the grants' leaves in order, and the receipt's `ceilings` states their number,
the root and every grant with a verdict of 2. Every principal can be shown its own entry with
@ -802,7 +823,8 @@ the reference relay signed in its test, `relay-record.wmh` with its ledger
receipt and what it names, with the outcome §5.4 gives them:
- **Accepted**: a lesson room with a relay, a compositor and two recorded takes, its receipt
whole; mixed acquisition classes (a relay's counter reading, a compositor's model, an ingest
whole; the same session in a receipt of version 2, its bytes out summed from the ledgers,
by grant too, and priced in each grant's funds; mixed acquisition classes (a relay's counter reading, a compositor's model, an ingest
gateway's estimate) with one total per class; a viewer's grant past its joule ceiling and
another past its funds ceiling, named in the receipt; a large audience, ten thousand
subscriber sessions whose relay record is the size of the lesson's, with a QoE cell of more
@ -813,7 +835,10 @@ receipt and what it names, with the outcome §5.4 gives them:
codes a receipt, a record, a take, a composition receipt and a room raise from the
extensions this one reads; among them version 2 records whose DVR figures exceed their
entries' (`record_invalid`) or whose length is past B₂(*M*) (`record_too_large`), and a
record of version 3 (`unknown_version`).
record of version 3 (`unknown_version`); and receipts of version 2 stating a byte more than
the ledgers (`unit_mismatch`), whose grants' entries leave their bytes out
(`ceiling_mismatch`), or pricing bytes with no tariff (`session_invalid`), and a receipt of
version 3 (`unknown_version`).
A conforming verifier gives every vector the outcome, the reason, the place and the facts
the corpus states. `wai_live_metering_vectors verify` and the test `tests/live_metering.rs`

View file

@ -26,7 +26,14 @@
> receipt of its output, in the units those receipts state. A tariff of version 3 prices a
> job by its capability **and its operation** (§4.6), since an encode and a decode spend
> different energy, and bills a job's **work** once, its route record and its hops' receipts,
> its output's receipt being the evidence a re-run may cite again (§6.9). No new transport, no payment
> its output's receipt being the evidence a re-run may cite again (§6.9). A tariff of version 4
> also bills what the rest of the work spends (§4.7): a room's bytes out, which a room-session
> receipt of version 2 states (live-metering §4.4), at a price per 10^9 bytes; a package's work
> once per run that made it, by the work units it states, its receipt the product each run
> cites; a package's storage by byte-days for each period it is held; and its invoices state
> every hop's energy, the hops not billed in a scope of their own (§6.6). Whatever the version,
> a job's output receipt is of an object its served hop states it delivered, where its route
> record states it (§5.1), and a route record's routing figures are never charges. No new transport, no payment
> and no tax: an invoice is the recomputable figures a payment rests on. The reference implementation is the
> `receipt_ledger` module of `wai-rs` (feature `receipt_ledger`) and the service
> `wai/wai-ledger`; the corpus is [`live-conformance/ledger/`](../live-conformance/ledger/),
@ -87,8 +94,9 @@ a buyer's rooms, deliveries, packages and ads together. Three rules keep every f
consistent with what the records say:
1. **A record that states a tariff states this one.** A room-session receipt of a source either
states no tariff (currency zero) or the tariff's currency and its thirteen room prices, in
live-metering §4.1's order; a settlement statement either states no tariff or the tariff's
states no tariff (currency zero) or the tariff's currency and its room prices, in
live-metering §4.1's order: the thirteen, and the price of bytes out, which a receipt of
version 1 and a tariff of versions 1 to 3 do not state and read as 0; a settlement statement either states no tariff or the tariff's
currency and its four delivery prices, in settlement §7.1's order. A record under another
tariff is refused `tariff_conflict` (§7.2 step 5): the funds its grants were held to, or the
amount its party stated, were computed at other prices, and an invoice at these prices would
@ -98,9 +106,10 @@ consistent with what the records say:
statement's line. A statement's own amount rounds once per payer; the invoice rounds once per
party and class, so the two can differ by less than one millionth of a minor unit per payer
line, and the delivery report (§8.2) states both.
3. **Units are never converted.** A room is billed in its own units, a delivery in bytes, a
package in packages, entries and stored bytes, an ad in impressions, a job in jobs or in
the subpixels its output's receipt states (§4.5). No line turns one unit
3. **Units are never converted.** A room is billed in its own units (in version 4, its bytes
out too), a delivery in bytes, a package in packages, entries and stored bytes (in version
4, in byte-days held), an ad in impressions, a job in jobs or in the subpixels its output's
receipt states (§4.5), and in version 4 in the work units of the packages it made. No line turns one unit
into another, and no line is priced from joules: the energy of an invoice is stated beside
its lines (§6.6), per class, and is not an amount.
@ -115,16 +124,16 @@ records recomputes all three.
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WLT1` (ASCII) |
| `version` | 1 | 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and bills their work (§4.6, §6.9) |
| `version` | 1 | 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and bills their work (§4.6, §6.9); 4 for one that also prices bytes out, a package's work and its storage by byte-time (§4.7) |
| `seller` | 32 | the seller's key, which signs the tariff and every invoice under it |
| `buyer` | 32 | the buyer's key, which accepts the tariff by signing it |
| `start`, `end` | 8 each | u64: the period the tariff is good for |
| `currency` | 3 | three capitals `A`–`Z` (ISO 4217) |
| `room` | 52 | thirteen u32: millionths of a minor unit per second of `publisher_us`, of `subscriber_us` for each layer class 0 to 8, of `composition_us` and of `recorded_us`, and per event sent (live-metering §4.1's order) |
| `room` | 52, in version 4 56 | thirteen u32: millionths of a minor unit per second of `publisher_us`, of `subscriber_us` for each layer class 0 to 8, of `composition_us` and of `recorded_us`, and per event sent (live-metering §4.1's order); in version 4 a fourteenth, per 10^9 bytes of `bytes_out` (§4.7) |
| `delivery` | 32 | four u64: millionths of a minor unit per 10^9 acknowledged bytes of each object class, in class order (settlement §7.1) |
| `package` | 24 | three u64: millionths of a minor unit per package receipt, per entry, and per 10^9 stored bytes |
| `package` | 24, in version 4 8 | three u64: millionths of a minor unit per package receipt, per entry, and per 10^9 stored bytes; in version 4 one, per 10^9 byte-days held (§4.7) |
| `ads` | 16 | two u64: millionths of a minor unit per presented impression, and per billable impression |
| `job` | 2 + Σ (18 + *c*), in version 3 2 + Σ (19 + *c* + *o*) | versions 2 and 3, absent in version 1: a u16 count *j*, then each job price (§4.5, §4.6): `cap_len` (u8), `capability` (`cap_len` bytes), in version 3 `op_len` (u8) and `operation` (`op_len` bytes), then `unit` (u8), `divisor` (u64) and `price` (u64) |
| `job` | 2 + Σ (18 + *c*), in versions 3 and 4 2 + Σ (19 + *c* + *o*) | versions 2 to 4, absent in version 1: a u16 count *j*, then each job price (§4.5, §4.6): `cap_len` (u8), `capability` (`cap_len` bytes), in versions 3 and 4 `op_len` (u8) and `operation` (`op_len` bytes), then `unit` (u8), `divisor` (u64) and `price` (u64) |
| `scope` | 2 + 33 × *n* | a u16 count *n*, then each entry: `kind` (u8) and `id` (32), in strictly ascending order of (`kind`, `id`) |
| `seller_sig` | 64 | §4.2 |
| `buyer_sig` | 64 | §4.2 |
@ -143,17 +152,18 @@ tariff the buyer did not sign bills nobody.
A reader reads exactly the bytes it is given as one tariff, in the order of §4.1, and refuses at
the first of these that holds: fewer than 4 bytes, `truncated`; a magic other than `WLT1`,
`malformed`; no fifth byte, `truncated`; a version other than 1, 2 and 3, `unknown_version`; the
`malformed`; no fifth byte, `truncated`; a version other than 1 to 4, `unknown_version`; the
bytes end before a field the layout requires, `truncated`; bytes after `buyer_sig`, `malformed`.
It then refuses as `tariff_invalid` a tariff that breaks any of these:
- `start` is below `end`; `currency` is three capitals `A`–`Z`;
- `seller` and `buyer` are not zero, and not the same key;
- `scope` has at least one entry, in strictly ascending order of (`kind`, `id`), each `kind` 1 to
4 (1 to 5 in versions 2 and 3) and each `id` not zero;
4 (1 to 5 in versions 2 to 4) and each `id` not zero;
- in version 2, `job` has at most 255 prices, each as §4.5 requires, in strictly ascending
order of (`capability`'s bytes, `unit`); in version 3, at most 255, each as §4.6 requires,
in strictly ascending order of (`capability`'s bytes, `operation`'s bytes, `unit`).
order of (`capability`'s bytes, `unit`); in versions 3 and 4, at most 255, each as §4.6
requires, in strictly ascending order of (`capability`'s bytes, `operation`'s bytes, `unit`);
each `unit` 1 or 2, and in version 4 1 to 3.
Then the seller's signature, then the buyer's: `bad_key`, `bad_signature`.
@ -167,7 +177,7 @@ A scope entry names whose records the tariff bills:
| 2 | a party's key | a settlement statement's `party` is the `id` |
| 3 | a packager's key | a package receipt's `signer_pubkey` is the `id` |
| 4 | an ad decider's or a stitcher's key | every receipt of an ads source has `signer_pubkey` the `id` (live-ads §3.4); a stitched source's records are accepted only when their `signer` is such an `id`: the verifier's **signers** (stitched-presentation §7.1) are the scope's entries of kind 4 |
| 5 | a router's key (versions 2 and 3) | a job source's route record has `signer` the `id` (route-record §5.1) |
| 5 | a router's key (versions 2 to 4) | a job source's route record has `signer` the `id` (route-record §5.1) |
The scope is what the buyer agreed to pay for: a record signed by a party the tariff does not name
is refused `outside_scope` (§7.2 step 5), whoever submits it.
@ -184,6 +194,7 @@ The units are the records' own figures, never converted:
|---|---|---|
| 1 | `jobs` | 1 |
| 2 | `subpixels` | the sum of its output receipts' `work_pixel_ops` (integer-payloads §15.1): the subpixels its output decodes to |
| 3 | `work_units` (version 4) | the sum of its output packages' `work_units` (packaging §9.1, §4.7) |
A job the tariff has no price for, by its serving capability, is billed nothing, and a buyer
pays for a capability only at a price it signed. A package job stays a package source (§5.1),
@ -208,6 +219,53 @@ a job price that names an operation, and the buyer of a version-2 tariff signed
rules. Version 3 also bills a job's work rather than its output's identity (§5.2, §5.3, §6.9).
A tariff of version 2, its invoices and its sources read, verify and bill exactly as before.
### 4.7 Bytes, work and storage (version 4)
A tariff of version 4 prices jobs as version 3 does, and prices three more things by what
spending on them scales with:
1. **A room's bytes out.** Its fourteenth room price is per 10^9 bytes of a room-session
receipt's `bytes_out`, the payload bytes the hops sent the session's sessions, summed from
their ledgers (live-metering §4.4, a receipt of version 2). A room's time units price how
long its sessions ran; what delivering them costs scales with the bytes carried too, and a
viewer of a high layer and one of a low layer subscribe for the same seconds and are sent
different bytes. The receipt states the price in its own tariff and counts it in each
grant's funds (live-metering §4.6), so a receipt that states a tariff states this one's
fourteen prices (§3). A receipt of version 1 states no bytes, and bills none.
2. **A package's work, per run.** Packaging is deterministic: the same source packed again under
the same policy gives the same package, under the same receipt, from a run that did the work
again with new energy (a customer asks for the work to be done and measured again). So a
package is billed as a job is (§6.9): the **run** that made it is the work, and its receipt is
the product, which each run that made it cites. Billing once per receipt refused a package
made again, though the work was done again. A run is a job of the catalogue's operation
`package.build`, routed and receipted as any job is (route-record §6): a `job` source whose
output is the package (§5.1), billed at the tariff's job prices for its serving capability
and `package.build`, per job and per **work unit**: the package receipt's `work_units`, the
work its figure is expressed against (packaging §9.1, jwp-receipts), one per entry, each a
base or a refinement the packer coded and wrote, so a price per unit bills what the run
spent in proportion to its size. The chain bills each run's route record and hop receipts
once, and a run replayed under a new route record is refused (`record_rebilled`,
`record_duplicate`), as any job's; the package receipt (kind 5) is not billed once, and is
cited by each run's line. The run's energy is its hop's figure (§6.6), and the receipt's own
figure, the same work's, is not stated again. A `package` source is the product held: it
bills its storage alone (below), never its work, which only a run states.
3. **A package's storage, by byte-time.** Its one package price is per 10^9 **byte-days**:
what keeping bytes costs scales with how many are kept and for how long, which a price per
stored byte once per receipt ignores. A package whose receipt the previous invoice of the
chain lists is **held** through the invoice's whole period: it was written by the end of
that invoice's period, at most this one's `start`. The invoice bills it ⌊Σ `stored_bytes` ×
(`end` − `start`) / 86 400 000 000⌋ byte-days over its packager's held packages, the
period in microseconds and rounded down once per line, and lists its receipt again. The
invoice that first lists a package bills no storage for it: no record shows when within that
period it was written, and a buyer pays only for time a record shows the bytes were kept. The period is read from the chain itself, its invoices'
`start` and `end`, which the seller signs and the buyer holds (§6.9), and the bytes from
the package receipt.
A version-3 reader cannot read a tariff of version 4 (its room prices are one longer and its
package prices two shorter, and a job price may be of unit 3) or an invoice of version 4 (§6.1:
it states a sixth unmetered count), so version 4 is a version of its own. A tariff of versions
1 to 3, its invoices and its sources read, verify and bill exactly as before.
## 5. Sources
### 5.1 Kinds and their documents
@ -222,7 +280,7 @@ extension's verifier is given. Bytes are lowercase hexadecimal strings.
| `package` | `receipt` (the text of `package.receipt.json`), `indexes` (the index files in the receipt's component order) | packaging §9, a receipt of version 2 against its indexes |
| `ads` | `descriptor`, `grants`, `key_log`, `chains`, `policy`, `pt0_ms`, `receipts` (form-2 receipts in their JSON carriage), `pods`, `claims`, `creatives`, `issuers` | live-ads §9, with ad-presentation §9 under a policy that requires claims |
| `stitched` | `records` (pod-input records), `claims` (stitched claims), `manifests`, `responses` (VAST and VMAP documents), `receipts` (form-1 receipts in their JSON carriage), `creatives`, `issuers` | stitched-presentation §7, its signers the tariff's (§4.4) |
| `job` (versions 2 and 3) | `request`, `pricebook`, `policy`, `record`, `receipts`, `keys` (a route record's verification document, route-record §7.1, its `receipts` and `keys` not `null`) and `outputs`, a list of objects of exactly `receipt` (the text of a `wai.video.int_motion` decode receipt's JSON form, integer-payloads §15.2) and `codestream` (the `WIV1` payload it describes) | route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its `codestream` (§15.1 there), or `output_invalid` at `outputs` and its index |
| `job` (versions 2 to 4) | `request`, `pricebook`, `policy`, `record`, `receipts`, `keys` (a route record's verification document, route-record §7.1, its `receipts` and `keys` not `null`) and `outputs`, a list of objects of exactly `receipt` (the text of a `wai.video.int_motion` decode receipt's JSON form, integer-payloads §15.2) and `codestream` (the `WIV1` payload it describes: the object the hop delivered, named by its content hash), or, in version 4, of exactly `package` (the text of a package receipt of version 2, `package.receipt.json`: the object the hop delivered) and `indexes` (its index files in its component order) | route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its `codestream` (§15.1 there), each package verified against its indexes by packaging §9, or `output_invalid` at `outputs` and its index; then each output held to what its hop delivered, or `output_not_delivered` there |
A source is accepted when its extension's verifier accepts it; for `ads`, when the outcome is
**accept** (live-ads §9.4), a **gap** being `source_gap`. A `stitched` source is always
@ -241,6 +299,33 @@ attempt's OER/2 receipt: the unmetered marker
half to the even one, with the receipt's `energy.class`. A billed hop's receipt whose figure
has no class, or rounds below 1 µJ or past what a u64 holds, cannot be stated in a row
(§6.6): the source is refused `job_figure_invalid`, at `receipts` and the receipt's index.
Under a tariff of version 4 every attempt's figure is stated (§6.6), so the same holds of each
attempt that is not billed, a failed hop and one that served past the deadline.
**A job's output is the object its hop delivered.** An output's **object** is its
`codestream`, or a package's receipt text, named by its content hash (route-record §2,
jwp-receipts §2.1). A route record
of version 2 states the objects its served attempt delivered, by content hash, and holds that
hop's receipt to them (route-record §5.2, §6). Each output a job source gives is then one of
those objects, and no two of its outputs are one object: otherwise the source is refused
`output_not_delivered`, at `outputs` and the output's index. The reason is the subpixels a
line bills (§6.3): they are read from the output receipts, and an output receipt of an object
the hop did not deliver bills subpixels nobody delivered for this job, as a receipt given
twice bills them twice. So the chain closes: the hop's receipt names its outputs, the record
names the hop's receipt and the outputs, and each output receipt is of one of them. Under a
tariff of version 4, a record that states no outputs (a record of version 1, or one no attempt
served) holds every output to none, and a job it routed is billed by the job alone; under a
tariff of version 3, a record of version 1's outputs are read as before.
**Routing figures are never charges.** A route record's candidates' `cost`, `energy`,
`expected_cost` and `expected_ttfb_ms`, and its price book's `price` and `failures`, are the
router's figures for choosing an endpoint (route-record §4.2, §4.4): what a hop costs the
router, and what the failures it measured cost it, per successful job. No line reads them. A
job line bills its billed attempt alone, at its list price, the tariff's job price for its
serving capability and operation, which the buyer signed: a failed attempt, what a provider
billed for it and the expected cost per successful job never reach a line, however the router
ranked, and a line that counts a failed hop as a job, or prices one at the router's figures,
does not recompute.
### 5.2 What a source contributes
@ -256,7 +341,7 @@ the others are the ledger's own:
| 2 | a per-hop record, `WMH1` | its digest (live-metering §3.5) | a `room` source: each record its receipt names; a `delivery` source: each record its statement names in `hops` |
| 3 | a settlement statement, `WSS1` | its digest (settlement §7.4) | a `delivery` source: its statement |
| 4 | an acknowledgement or declaration, `WDA1` | its digest (settlement §3.5) | a `delivery` source: each record its statement names in `records` |
| 5 | a package receipt | its `receipt_hash` (packaging §9) | a `package` source |
| 5 | a package receipt | its `receipt_hash` (packaging §9) | a `package` source; a `job` source (version 4): each output package |
| 6 | a `wai.video.ssai` receipt of the break form | its identity (live-ads §8.4) | an `ads` source: each receipt |
| 7 | a presentation claim, `WPC1` | its digest (ad-presentation §5.3) | an `ads` source: each claim that reads |
| 8 | a pod-input record, `WSI1` | its digest (stitched-presentation §4.4) | a `stitched` source: each record it accepts |
@ -265,9 +350,9 @@ the others are the ledger's own:
| 11 | an invoice, `WLI1` | its digest (§6.7, as [revenue-share](revenue-share.md) §7.4 cites it) | no source: cited by revenue-share §7.4 |
| 12 | a cue event | its event digest ([opportunity-report](opportunity-report.md) §7.3, live-events §3.2) | no source: cited by opportunity-report §7.3 |
| 13 | a room policy | its digest ([opportunity-report](opportunity-report.md) §7.3, live-ads §3.2) | no source: cited by opportunity-report §7.3 |
| 14 | an OER/2 receipt | the 32 bytes its `receipt_id` names (oer-receipts §5) | a `job` source (versions 2 and 3): the receipt of each attempt of its record |
| 15 | a `wai.video.int_motion` decode receipt | its receipt hash (integer-payloads §15.1) | a `job` source (versions 2 and 3): each output receipt |
| 16 | a route record | its digest (route-record §5.3) | a `job` source (versions 2 and 3): its record |
| 14 | an OER/2 receipt | the 32 bytes its `receipt_id` names (oer-receipts §5) | a `job` source (versions 2 to 4): the receipt of each attempt of its record |
| 15 | a `wai.video.int_motion` decode receipt | its receipt hash (integer-payloads §15.1) | a `job` source (versions 2 to 4): each output receipt |
| 16 | a route record | its digest (route-record §5.3) | a `job` source (versions 2 to 4): its record |
Kinds 17 to 127 are unassigned. Kinds **128 to 255 are reserved for private and provisional
use**: an implementation's own records, such as a hosted service's provisional records carried
@ -282,10 +367,11 @@ receipts, a `stitched` source's its accepted records, and a `job` source's its r
contributed once. An OER/2 receipt a `job` source contributes that an earlier `job` source of the
invoice also contributes is refused `record_duplicate` too: one hop serves one job, so no hop is
billed for two. Under a tariff of version 2, so is an output receipt: one output is one job's.
Under a tariff of version 3 it is not: an output receipt is the evidence of what a job delivered,
not the work, and the same request run again on a deterministic codec delivers the same output
under the same receipt from a new hop that did the work again (§6.9). Two lines of one invoice
that cite one output receipt therefore cite different hop receipts, whichever lines they are.
Under a tariff of version 3 or 4 it is not: an output receipt is the evidence of what a job
delivered, not the work, and the same request run again on a deterministic codec delivers the
same object, of the same content hash, under the same receipt from a new hop that did the work
again (§6.9). Two lines of one invoice that cite one output receipt therefore cite different hop
receipts, whichever lines they are.
### 5.3 Placement in time
@ -296,14 +382,15 @@ A source is **in** a period when:
- `delivery`: the statement's period lies within it;
- `ads`: the `t` of the break each receipt names, placed by the room's `t0`, is within it;
- `package` and `stitched`: always: a package receipt and a pod-input record state no time, and
the chain (§6.9) bills each once;
the chain (§6.9) bills each once; under a tariff of version 4, a package's work once and its
storage for the periods it is held through (§4.7);
- `job`: its route record's `accepted` is within it.
Two room receipts of one room whose sessions overlap, two statements of one party whose periods
overlap, two receipts of the invoice's ads sources naming one break and one session, and, under
a tariff of version 2, two route records of one router naming one `job`, bill one thing twice:
the later is refused `record_overlap`. Under a tariff of version 3, two route records of one job
are two runs of it, each billed from its own hop, and no hop twice (§5.2): a job's id names what
the later is refused `record_overlap`. Under a tariff of version 3 or 4, two route records of one
job are two runs of it, each billed from its own hop, and no hop twice (§5.2): a job's id names what
was asked, not the work, and a request run again is worked again.
## 6. The invoice
@ -313,7 +400,7 @@ was asked, not the work, and a request run again is worked again.
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WLI1` (ASCII) |
| `version` | 1 | its tariff's version, 1, 2 or 3 |
| `version` | 1 | its tariff's version, 1 to 4 |
| `seller`, `buyer` | 32 each | the tariff's |
| `tariff` | 32 | the tariff's digest |
| `prev` | 32 | the digest of the invoice before it in its chain (§6.9); zero for the first |
@ -322,7 +409,7 @@ was asked, not the work, and a request run again is worked again.
| `records` | 4 + 33 × *r* | a u32 count, then each record (§5.2): `kind` (u8) and `id` (32), in strictly ascending order of (`kind`, `id`) |
| `lines` | 2 + 94 × *l* | a u16 count, then each line (§6.3), in strictly ascending order of (`source_kind`, `source`, `unit`) |
| `energy` | 1 + 18 × *e* | a u8 count, then each row (§6.6): `scope` (u8), `tag` (u8), `figures` (u32), `joules` (u64, µJ), `max_ppm` (u32), in strictly ascending order of (`scope`, `tag`) |
| `unmetered` | 16 or 20 | four u32: the unmetered figures of scopes 1 to 4 (§6.6); in versions 2 and 3, five, scopes 1 to 5 |
| `unmetered` | 16, 20 or 24 | four u32: the unmetered figures of scopes 1 to 4 (§6.6); in versions 2 and 3, five, scopes 1 to 5; in version 4, six, scopes 1 to 6 |
| `amount` | 8 | u64: the sum of the lines' amounts |
| `sig` | 64 | §6.7 |
@ -337,7 +424,7 @@ A line is 94 bytes:
| field | size | meaning |
|---|---|---|
| `source_kind` | 1 | 1 room, 2 delivery, 3 package, 4 ads, 5 job (versions 2 and 3) |
| `source_kind` | 1 | 1 room, 2 delivery, 3 package, 4 ads, 5 job (versions 2 to 4) |
| `source` | 32 | the room id, the delivering party, the packager, the ad decider or the router |
| `unit` | 1 | the unit (below) |
| `quantity` | 8 | u64: the quantity of the unit the records state |
@ -356,16 +443,19 @@ each:
| 1 | 11 | `composition_us` | their `composition_us` | `room` 10 | 10^6 |
| 1 | 12 | `recorded_us` | their `recorded_us` | `room` 11 | 10^6 |
| 1 | 13 | `events_sent` | their `events_sent` | `room` 12 | 1 |
| 1 | 14 | `bytes_out` (version 4) | their `bytes_out`, of receipts of version 2 (§4.7) | `room` 13 | 10^9 |
| 2 | 1–4 | `live`, `fetch`, `fill`, `ancillary` | the bytes of the statement's delivery lines of that class that are not free (settlement §7.2) | `delivery` `unit` − 1 | 10^9 |
| 3 | 1 | `packages` | the number of package receipts | `package` 0 | 1 |
| 3 | 2 | `entries` | their `total_entries` | `package` 1 | 1 |
| 3 | 3 | `stored_bytes` | their `stored_bytes` | `package` 2 | 10^9 |
| 3 | 3 | `byte_days` (version 4, its only package unit) | ⌊Σ `stored_bytes` × (`end` − `start`) / 86 400 000 000⌋ over the package receipts the previous invoice lists, which are held (§4.7) | `package` 0 of version 4 | 10^9 |
| 4 | 1 | `presented_impressions` | under a policy that requires claims, the impressions that count (ad-presentation §9.2); of a stitched source, the presentations of its accepted records' ads (stitched-presentation §7.4), one per session | `ads` 0 | 1 |
| 4 | 2 | `billable_impressions` | under a policy that does not, the billable impressions (quartile at least 1) | `ads` 1 | 1 |
| 5 | *k* | the *k*-th job price's unit | over the router's job sources with a billed hop whose serving capability is that price's `capability` (and, in version 3, whose operation is its `operation`): one per job (`jobs`), or each job's output receipts' `work_pixel_ops` (`subpixels`) | the *k*-th job price's `price` | its `divisor` |
| 5 | *k* | the *k*-th job price's unit | over the router's job sources with a billed hop whose serving capability is that price's `capability` (and, in versions 3 and 4, whose operation is its `operation`): one per job (`jobs`), each job's output receipts' `work_pixel_ops` (`subpixels`), or, in version 4, its output packages' `work_units` (`work_units`) | the *k*-th job price's `price` | its `divisor` |
There is one line for each source and unit whose quantity is above 0, a price of 0 included, and
none for a quantity of 0. A delivery line's bytes are those its party's downstream parties signed
In version 4 a package source bills only its `byte_days`, and its work is its run's job line.
There is one line for each source and unit whose
quantity is above 0, a price of 0 included, and none for a quantity of 0. A delivery line's bytes are those its party's downstream parties signed
for (settlement §3.7): a declared byte and a free retrieval are never billed, and the delivery
report states both (§8.2). An ad line counts impressions under one rule or the other, never both:
a policy that requires claims bills only what the players' own claims show presented, and one that
@ -385,20 +475,23 @@ Each line cites the records it came from:
- a delivery line: the statements of its party with a delivery line of the class that is not free,
and each acknowledgement (mode 1) they name with an entry of the class that is not free (a
retrieval class with a zero `grant` is free);
- a package line: the package receipts of its packager whose figure of the unit is above 0;
- a package line: the package receipts of its packager whose figure of the unit is above 0; in
version 4, the held receipts its `byte_days` count;
- an ad line: the receipts of its decider with at least one impression counted under the unit, and,
for `presented_impressions`, each counted impression's own claim; of a stitched source, its
stitcher's accepted records, and each presented own claim of their ads;
- a job line: for each job it counts, its route record and its billed hop's OER/2 receipt, and,
for `subpixels`, each of its output receipts whose `work_pixel_ops` is above 0. In version 3,
an output receipt that two jobs of the line delivered is cited once and its `work_pixel_ops`
counted for each (§6.9).
for `subpixels`, each of its output receipts whose `work_pixel_ops` is above 0. In versions 3
and 4, an output receipt that two jobs of the line delivered is cited once and its
`work_pixel_ops` counted for each (§6.9); in version 4, for `work_units`, each of its output
packages whose `work_units` is above 0, a package that two runs of the line made cited once
and counted for each.
`cited` is their number, and `cites` is SHA-256 over the 17 bytes `wai:ledger-cites` (ASCII) and
0x01, followed by each cited record's `kind` and identity, in ascending order of (`kind`,
identity). Every cited record is in `records`. A buyer recomputes a line from the records it
cites alone, and shows one line to an auditor with its records and nothing else. One line is
the exception: a version-3 job line two of whose jobs delivered one output receipt names each job
the exception: a job line of version 3 or 4 two of whose jobs delivered one output receipt names each job
and that output once, and how many of its jobs delivered the output is stated by their sources,
each of which pairs its job with its outputs (§5.1).
@ -410,9 +503,17 @@ Each record that states an energy figure states it once, in one **scope**:
|---|---|
| 1 `room_hops` | the per-hop records named by the room receipts (each restated by its receipt, live-metering §4.5) |
| 2 `delivery_hops` | the per-hop records named by the statements and by no room receipt of the invoice |
| 3 `packaging` | the package receipts' `joules_micro` and class |
| 3 `packaging` | the package receipts' `joules_micro` and class; in version 4 none: a package's figure is its run's, which the run's hop states (§4.7) |
| 4 `ad_decisions` | the ad receipts' `joules_micro` and class; a pod-input record states no figure |
| 5 `jobs` (versions 2 and 3) | each job source's billed hop's figure (§5.1): the hop that served the job, in the class its OER/2 receipt states; a job with no billed hop states none |
| 5 `jobs` (versions 2 to 4) | each job source's billed hop's figure (§5.1): the hop that served the job, in the class its OER/2 receipt states; a job with no billed hop states none |
| 6 `unbilled_hops` (version 4) | each job source's attempts that are not billed (route-record §5.2): a failed hop and one that served past the deadline, each its OER/2 receipt's figure (§5.1) |
A failed hop spent energy and delivered nothing, and a hop that served late spent it and
delivered too late: neither is billed, and before version 4 neither appeared in any row, so an
invoice's energy for its jobs was less than their hops spent. Version 4 states them in a scope
of their own, beside the billed hops' and never summed with them: the jobs' energy is scope 5
and scope 6 together, per class, and what the buyer was billed for is scope 5 alone. No line
reads scope 6.
For each scope, `unmetered` counts its figures that are the unmetered marker, which are in no row
and **MUST NOT** be presented as zero joules. For each scope and each tag among its metered
@ -443,11 +544,11 @@ invoice with a row of tag 0. It refuses as `private_kind` an invoice with a reco
- `start` is below `end`; `currency` is three capitals `A`–`Z`;
- each list is in the strictly ascending order §6.1 gives it; each record's `kind` is 1 to 9 (in
versions 2 and 3, also 14 to 16: kinds 10 to 13 are no ledger source's, §5.2);
- each line's `source_kind` is 1 to 4 (1 to 5 in versions 2 and 3), its `unit` a unit of its kind
(§6.3; for kind 5, at least 1), its `quantity` and `cited` above 0;
- each row's `scope` is 1 to 4 (1 to 5 in versions 2 and 3), its `tag` 2 to 5 and its `figures`
above 0.
versions 2 to 4, also 14 to 16: kinds 10 to 13 are no ledger source's, §5.2);
- each line's `source_kind` is 1 to 4 (1 to 5 in versions 2 to 4), its `unit` a unit of its kind
in its version (§6.3; for kind 5, at least 1), its `quantity` and `cited` above 0;
- each row's `scope` is 1 to 4 (1 to 5 in versions 2 and 3, 1 to 6 in version 4), its `tag` 2 to 5
and its `figures` above 0.
Then the signature (§6.7): `bad_key`, `bad_signature`.
@ -455,14 +556,16 @@ Then the signature (§6.7): `bad_key`, `bad_signature`.
The invoices of one tariff form a **chain**: each names in `prev` the invoice before it, the first
names zero, and their periods do not overlap. A ledger keeps one chain per tariff. No record is in
the `records` of two invoices of a chain, but, under a tariff of version 3, an output receipt
(kind 15): a record billed once is billed never again, whatever period a later invoice states, and
the `records` of two invoices of a chain, but, under a tariff of version 3 or 4, an output receipt
(kind 15), and under one of version 4 a package receipt (kind 5), which each run that made it
cites and each invoice whose period it is held through lists again to bill its storage (§4.7): a record billed once is billed never again, whatever period a later invoice states, and
a package receipt, which states no time, is billed in the invoice that first holds it.
What a chain bills once for a job is its **work**, and its output's receipt is the **evidence**.
A run of a job is a hop that did it: new energy, and a new OER/2 receipt and route record, new
on every run. An output receipt is not the work: the codecs are deterministic, so the same request
run again delivers the same output under the same receipt. Under a tariff of version 3, therefore:
run again delivers the same objects, of the same content hashes, under the same receipt. Under a
tariff of version 3 or 4, therefore:
- the records a chain bills once for a job are its route record and its hops' OER/2 receipts,
the billed hop's among them;
@ -476,8 +579,10 @@ run again delivers the same output under the same receipt. Under a tariff of ver
Under a tariff of version 2, an output receipt is billed once as well, and a re-run of a job
whose output an invoice of the chain holds is refused, as that tariff's buyer signed it. A
package keeps its rule in every version: it is billed as a delivered product, once per package
receipt, which is a rule of its own and not a job's.
package keeps its rule in versions 1 to 3: it is billed as a delivered product, once per package
receipt, which is a rule of its own and not a job's. In version 4 it has a job's shape: the run
that made it is the work, billed once per run, and its receipt the product, cited by each run;
its storage is billed for each period it is held (§4.7).
## 7. Verification
@ -511,8 +616,8 @@ It runs these steps and stops at the first refusal, which it reports with the re
6. **Across sources**, at `source` and the index of the later: a head contributed twice
(`record_duplicate`); a receipt, statement or ad receipt overlapping an earlier one, or, in
version 2, a route record of the router and job an earlier one names (§5.3,
`record_overlap`); and a record that is in the previous invoice's `records`, in version 3 an
output receipt excepted (§6.9, `record_rebilled`).
`record_overlap`); and a record that is in the previous invoice's `records`, in versions 3 and
4 an output receipt excepted, and in version 4 a package receipt (§6.9, `record_rebilled`).
7. **The figures**, at `invoice`, recomputed from the sources as §6.2 to §6.6 compute them and
compared in this order:
1. a line whose source and unit no source gives, or that states a larger quantity than the
@ -529,9 +634,10 @@ A figure that cannot be held in its field is a difference.
- **refuse**: the reason and where.
- **accept**: the invoice's digest; the tariff's digest; the currency and the period; the number of
records; each line with its source kind, source, unit name (and, for a job line, the job
price's `capability`, and in version 3 its `operation`), quantity, price, amount and its cited
records; each energy row by scope and class label with its figures, joules and `max_ppm`; each
scope's unmetered figures and coverage (scopes 1 to 4, and in versions 2 and 3 scope 5); the
price's `capability`, and in versions 3 and 4 its `operation`), quantity, price, amount and its
cited records; each energy row by scope and class label with its figures, joules and `max_ppm`;
each scope's unmetered figures and coverage (scopes 1 to 4, in versions 2 and 3 scope 5, and in
version 4 scopes 5 and 6); the
amount; and the reports of §8.
## 8. Reports
@ -625,6 +731,7 @@ The codes this extension adds, each with where it is raised:
| `private_kind` | §5.2, §6.8, §7.2 step 2 |
| `output_invalid` | §5.1, §7.2 step 5 |
| `job_figure_invalid` | §5.1, §7.2 step 5 |
| `output_not_delivered` | §5.1, §7.2 step 5 |
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
tariff or an invoice that does not read, and `malformed` for a source that does not read (§5.1); `bad_key` and `bad_signature` for their signatures;
@ -645,7 +752,9 @@ from the corpora of the extensions it reads (`live-conformance/metering/`,
delivery statements of two parties signed for it and, for each media job, a sink's
`wai.video.int_motion` decode receipt of its output over a payload of
`wai-rs/tests/vectors/int_video/`, and, for version 3, the route records of a decode, a re-run, a
replayed hop receipt and a claimed one, routed and sealed under the route corpus's own seeds. The sources are
replayed hop receipt and a claimed one, routed and sealed under the route corpus's own seeds, as
are jobs routed under the route record's version 2, its price book of version 2 with its
per-operation entries and measured failures, the served hop stating its outputs. The sources are
held once, by name, and each vector is a verification document (§7.1) naming its sources, with the
outcome §7.3 gives it:
@ -664,7 +773,15 @@ outcome §7.3 gives it:
each at its own price; the next invoice of that chain, billing the first job run again, its
route record and hop receipt new and its output receipt one the first invoice holds; the job
and its re-run in one invoice, their shared output receipt cited once and its subpixels billed
for each; and a decode under a tariff that prices only the encode, which bills nothing.
for each; and a decode under a tariff that prices only the encode, which bills nothing; jobs
routed under the route record's version 2 billed under version 3 (an encode, a failover billed
at the endpoint that served, a decode, a hop past the deadline and three that failed); and,
under a tariff of version 4, a lesson room's receipt of version 2 billed by its bytes out
beside its time, a package listed and the run of `package.build` that made it, billed per run
and per work unit, and those jobs with every hop's energy, the failed and late hops' in their
own scope, one of them unmetered; and the next invoice of that chain, billing the package it
holds for the byte-days of its own period and the same package made again by a new run, its
work billed again and its receipt cited again, beside a second package listed.
- **Refused**: each reason of §10 at least once, each vector a control of one defect; a source of
each kind outside the scope, a room receipt and a statement under other prices, and a source of
each timed kind outside the period; a refusal of each source kind by its own extension; and, for
@ -675,7 +792,17 @@ outcome §7.3 gives it:
another text's kind; and, for version 3, job prices out of order by operation, a version-2
invoice under a version-3 tariff, a hop receipt the previous invoice billed brought again
under a new route record, and one hop receipt claimed by two jobs billed on two lines; and,
under a tariff of version 2, the same re-run, refused because its output receipt was billed.
under a tariff of version 2, the same re-run, refused because its output receipt was billed;
and, for a job routed under the route record's version 2, an output receipt of a clip its hop
did not deliver and one output given twice (`output_not_delivered`); and, for version 4, a
price of bytes other than the receipt's and a receipt of version 1 under a price of bytes
(`tariff_conflict`), bytes out above the receipt's, storage billed for a package first listed,
a held package's storage over the tariff's period rather than the invoice's, a package's run
replayed under a new route record in the next invoice (`record_rebilled`) and in the same one
(`record_duplicate`), a failed hop billed as a job and a job line at the router's list price, the unbilled
hops' energy left out or added to the billed hops' row, an unbilled hop whose figure states no
class (`job_figure_invalid`), an output of a route record of version 1, and an invoice of
version 3 under a tariff of version 4.
`wai_receipt_ledger_vectors verify` and the test `tests/receipt_ledger.rs` check that the file
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that every reason of §10
@ -686,7 +813,8 @@ it shares no code with `wai-rs`, verifies signatures with WAI's strict rule writ
implementation (`live_metering_verify.py`, `settlement_verify.py`, `live_ads_verify.py` with
`ad_presentation_verify.py`, `stitched_presentation_verify.py`, `route_record_verify.py`, and
`pack-conformance/pack_verify.py`) and each output receipt by `_int_motion_receipt.py`, written
from integer-payloads §15, recomputes every line,
from integer-payloads §15, computes each output's content hash with BLAKE3 written from its
specification (`claim-conformance/_blake3.py`), recomputes every line,
citation, row and report itself, gives every vector the same outcome, and requires every mutated
copy of the corpus to fail.
@ -710,6 +838,17 @@ copy of the corpus to fail.
ran, as a party and a downstream key can bill bytes (settlement §13). Whether a request is
worked again or answered from outputs still kept is the router's decision, not the ledger's;
the ledger bills what was worked. A tariff of version 2 keeps billing an output once.
- **Bytes, work and storage.** Under a tariff of version 4 a room's bytes out rest on its hops'
ledgers, which each hop signs by their root (live-metering §3.4), as its time rests on their
records; a package's work rests on the run that made it, its route record and its hop's
receipt, new on every run, and on its receipt's work units, which its indexes recompute; and
its storage on the chain: a package is billed a period's storage only when the invoice before lists it,
so it was written by the start of the period billed, and a buyer holding the chain sees each
period billed once. Whether the seller kept the bytes through the period is the seller's
statement, as a delivery is the acknowledging parties' (settlement §13).
- **Every hop's energy.** Under a tariff of version 4 an invoice states the energy every hop of
its jobs spent, the hops the buyer was not billed for in a scope of their own, so a seller
whose router fails often shows it in the energy it states, and is not paid for it.
- **What the figures rest on.** An invoice is as true as its records: a delivery line rests on
acknowledgements, so a party and a downstream key that collude can bill bytes that never moved,
as settlement §13 says; a room line rests on per-hop records and the room's events; an ad line

View file

@ -54,7 +54,23 @@ The accepted vectors:
- `one_output_under_two_hops` (version 3): `job-0001` and its re-run in one invoice: both billed,
the output receipt they share cited once and its subpixels billed for each hop;
- `an_operation_the_tariff_does_not_price` (version 3): a tariff that prices `int_motion`'s encode
and not its decode: the decode bills nothing, and its hop's energy is stated.
and not its decode: the decode bills nothing, and its hop's energy is stated;
- `version_2_routes_under_version_3` (version 3, §5.1): jobs routed under the route record's
version 2, its price book pricing each operation apart with measured failures and each served
hop stating its outputs: an encode, a failover whose failed hop was metered, billed at the
endpoint that served, a decode, a hop past the deadline and three that failed, which bill
nothing, and a failover whose failed hop's figure states no class, which version 3 does not
read; each output receipt is of the clip its hop states it delivered;
- `version_4_by_the_byte_and_the_work` (version 4, §4.7): the lesson room's receipt of version 2
([`metering/`](../metering/)'s `bytes_out_priced`) billed by its bytes out beside its time, a
package listed, no storage yet, and the run of `package.build` that made it (`package_run`)
billed per run and per work unit of the package, its receipt cited, and those jobs at their own
prices, with every hop's energy: the billed hops' in `jobs`, the failed and late hops' in
`unbilled_hops`, one of them unmetered;
- `version_4_storage_held_through_a_period` (version 4): the next invoice of that chain bills the
package the first holds for the byte-days of its own period, and the same package made again by
a new run (`package_run_again`), its work billed again and its receipt cited again, beside a
second package listed.
Every other vector is refused, a control of one defect: each reason of §10, the reused codes, and a
refusal of each source kind by its own extension.
@ -63,7 +79,7 @@ refusal of each source kind by its own extension.
seeds (the seller, the buyers, the two delivering parties, their viewers and objects) and from
sources taken unchanged from the corpora of the extensions the ledger reads: rooms and their
receipts from [`metering/`](../metering/) (`lesson`, `mixed_classes`, `receipt_bad_signature`,
`relay_recorded`), the forwarding viewer's statement and the room its hop records name from
`relay_recorded`, `bytes_out_priced`), the forwarding viewer's statement and the room its hop records name from
[`settlement/`](../settlement/), ad rooms from [`ads/`](../ads/) (`live_break`, `gap`) and
[`presentation/`](../presentation/) (`video_rule`, `ceiling_exceeded_by_presented`), streams
stitched by others from [`stitched/`](../stitched/) (`hls_stitched`, `form1_prefetch`,
@ -76,7 +92,9 @@ manifests, responses and receipts they name, by their bytes), media jobs from
`wai-rs/tests/vectors/int_video/int_video_bitstream.wiv1`; the version-3 jobs (`job_decode`,
`job_rerun`, `job_rerun_replays_its_hop`, `job_claims_another_hop`) routed here by that corpus's
router under its price book and policy, their hop receipts sealed under its hop key, the replayed
and the claimed one being `served_first_try`'s own; and the
and the claimed one being `served_first_try`'s own; the jobs of version 2 (`job_v2_*`) routed
here by that router under the route corpus's price book of version 2, and the runs of
`package.build` (`package_run*`) under a price book of version 2 of its own; and the
packages `video-base-01` and `video-staged-01` of
[`pack-conformance/packs/`](../../pack-conformance/packs/). When one of those corpora is
regenerated, so is this one. Nothing here was fetched from outside the repository.

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,9 @@ live_compose_verify.py (live-compose §8.3). Everything else is here, from the t
- the per-hop session record (§3): read in version 1 or 2 by its own layout, its class, its
rules (version 2's DVR figures held to its entries), the size bound of its version, its
ledger and the ledger's root;
- the room-session receipt (§4): read, its rules, who may sign it;
- the room-session receipt (§4): read in version 1 or 2 by its own layout (version 2 states the
bytes the hops sent the session's sessions, and prices them per 10^9 bytes), its rules, who
may sign it;
- the room's replay (§5.3): who publishes, the grants in effect, the media tracks
published;
- the units (§4.4), the joules per hop and per class (§4.5), each grant's consumption and
@ -53,7 +55,7 @@ Refuse = LEV.Refuse
HOP_MAGIC = b"WMH1"
HOP_VERSIONS = (1, 2)
SESSION_MAGIC = b"WMR1"
SESSION_VERSIONS = (1,)
SESSION_VERSIONS = (1, 2)
DVR_FIELDS = ("dvr_objects_out", "dvr_bytes_out", "dvr_objects_stored", "dvr_bytes_stored", "dvr_byte_s", "dvr_dropped")
HOP_DOMAIN = b"wai:metering-hop\x01"
SESSION_DOMAIN = b"wai:metering-session\x01"
@ -310,9 +312,9 @@ def ledger_root(entries):
def read_receipt(b, room):
"""§4.2: the receipt, `classes_summed` for a total of tag 0, its rules, its room, its signature."""
read_head(b, SESSION_MAGIC, SESSION_VERSIONS)
version = read_head(b, SESSION_MAGIC, SESSION_VERSIONS)
r = R(b, 5)
x = {"room": r.take(32), "signer": r.take(32), "start": r.u64(), "end": r.u64()}
x = {"version": version, "room": r.take(32), "signer": r.take(32), "start": r.u64(), "end": r.u64()}
x["hops"], classes_ok = [], True
for _ in range(r.u8()):
h = {"record": r.take(32), "hop": r.take(32), "role": r.u8(), "joules": r.u64()}
@ -322,11 +324,12 @@ def read_receipt(b, room):
x["takes"] = [r.take(32) for _ in range(r.u8())]
x["compose_count"], x["compose_digest"] = r.u32(), r.take(32)
x["qoe_count"], x["qoe_digest"] = r.u32(), r.take(32)
x["units"] = units_of([r.u64() for _ in range(15)])
# §4.1: version 2 states a sixteenth unit, the bytes out (§4.4), and a fourteenth price.
x["units"] = units_of([r.u64() for _ in range(16 if version == 2 else 15)])
x["unmetered"] = r.u8()
x["totals"] = [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]
x["currency"] = r.take(3)
x["prices"] = [r.u32() for _ in range(13)]
x["prices"] = [r.u32() for _ in range(14 if version == 2 else 13)]
x["grants"], x["root"] = r.u32(), r.take(32)
x["past"] = [(r.take(32), r.u8()) for _ in range(r.u32())]
cell = [r.u8()] + [r.u32() for _ in range(3)] + [r.u64() for _ in range(3)] + [r.u32(), r.u64()]
@ -355,8 +358,11 @@ def read_receipt(b, room):
def units_of(v):
return {"publisher_us": v[0], "subscriber_us": v[1:10], "sessions": v[10], "composition_us": v[11],
"events_sent": v[12], "events_delivered": v[13], "recorded_us": v[14]}
u = {"publisher_us": v[0], "subscriber_us": v[1:10], "sessions": v[10], "composition_us": v[11],
"events_sent": v[12], "events_delivered": v[13], "recorded_us": v[14]}
if len(v) == 16:
u["bytes_out"] = v[15]
return u
def cell_dict(c):
@ -595,7 +601,7 @@ def u64s(*v):
return b"".join(struct.pack("<Q", x) for x in v)
def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, currency):
def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, currency, version=1):
"""§5.1 steps 4 to 7 and the computations of §4: what the session gives, or Stop."""
room = ctx.id
published = set()
@ -673,13 +679,15 @@ def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, cu
# §4.4 the units and §4.6 the consumption.
units = {"publisher_us": 0, "subscriber_us": [0] * CLASSES, "sessions": 0, "composition_us": 0, "events_sent": 0,
"events_delivered": 0, "recorded_us": 0}
if version == 2:
units["bytes_out"] = 0 # §4.4: the ledgers' bytes out, version 2
per = {}
def grant(g):
if g not in per:
jc, fc, cur = ctx.ceilings(g) or (NO_CEILING, NO_CEILING, b"\x00\x00\x00")
per[g] = {"grant": g, "jc": jc, "fc": fc, "currency": cur, "pub": 0, "sub": [0] * CLASSES, "comp": 0, "rec": 0,
"ev": 0, "attr": [0, 0, 0, 0], "unmetered": False, "metered": False}
"ev": 0, "out": 0, "attr": [0, 0, 0, 0], "unmetered": False, "metered": False}
return per[g]
for st, a, z in ctx.over(s, e):
for p in publishers(st):
@ -698,6 +706,9 @@ def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, cu
g = grant(l["grant"])
for c in range(CLASSES):
g["sub"][c] += l["sub"][c]
if version == 2:
units["bytes_out"] += l["out"]
g["out"] += l["out"]
b = l["in"] + l["out"]
if b == 0:
continue
@ -747,7 +758,10 @@ def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, cu
if priced:
n = c["pub"] * prices[0] + sum(c["sub"][k] * prices[1 + k] for k in range(CLASSES)) + c["comp"] * prices[10] \
+ c["rec"] * prices[11] + c["ev"] * prices[12] * 10 ** 6
funds = min(-(-n // 10 ** 12), NO_FUNDS - 1)
if version == 2: # §4.6: N in 10^-12 of a minor unit, bytes at a price per 10^9 bytes
funds = min(-(-(n * 1000 + c["out"] * prices[13]) // 10 ** 15), NO_FUNDS - 1)
else:
funds = min(-(-n // 10 ** 12), NO_FUNDS - 1)
if c["fc"] == NO_CEILING:
fv = 0
elif not priced:
@ -756,9 +770,10 @@ def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, cu
fv = 2
else:
fv = 1
raw = (g + u64s(c["jc"], c["fc"]) + c["currency"] + u64s(c["pub"], *c["sub"], c["comp"], c["rec"], c["ev"], *c["attr"])
+ bytes([1 if c["unmetered"] else 0]) + u64s(funds) + bytes([jv, fv]))
assert len(raw) == 198
out = u64s(c["out"]) if version == 2 else b""
raw = (g + u64s(c["jc"], c["fc"]) + c["currency"] + u64s(c["pub"], *c["sub"], c["comp"], c["rec"], c["ev"]) + out
+ u64s(*c["attr"]) + bytes([1 if c["unmetered"] else 0]) + u64s(funds) + bytes([jv, fv]))
assert len(raw) == (206 if version == 2 else 198)
c.update(funds=funds, jv=jv, fv=fv, leaf=sha(CONSUMPTION_DOMAIN, raw))
consumption.append(c)
return {"records": records, "m": m, "units": units, "totals": totals, "takes": take_digests, "compose_digests": digests,
@ -819,15 +834,15 @@ def verify_vector(v, doc, cache):
s, e = rc["start"], rc["end"]
try:
if len(comps) != rc["compose_count"] or list_digest([given_digest(b) for b in comps]) != rc["compose_digest"]:
evaluate(ctx, s, e, hops, named_takes, [], descriptors, None, rc["prices"], rc["currency"])
evaluate(ctx, s, e, hops, named_takes, [], descriptors, None, rc["prices"], rc["currency"], rc["version"])
raise Stop("record_missing", ["compose"])
named_logs = None
if rc["qoe_count"] != 0:
if logs is None or sha(logs) != rc["qoe_digest"]:
evaluate(ctx, s, e, hops, named_takes, comps, descriptors, None, rc["prices"], rc["currency"])
evaluate(ctx, s, e, hops, named_takes, comps, descriptors, None, rc["prices"], rc["currency"], rc["version"])
raise Stop("record_missing", ["qoe"])
named_logs = logs
comp = evaluate(ctx, s, e, hops, named_takes, comps, descriptors, named_logs, rc["prices"], rc["currency"])
comp = evaluate(ctx, s, e, hops, named_takes, comps, descriptors, named_logs, rc["prices"], rc["currency"], rc["version"])
if len(comp["metrics"]) != rc["qoe_count"]:
raise Stop("unit_mismatch", ["qoe"])
if comp["units"] != rc["units"]:
@ -867,6 +882,7 @@ def verify_vector(v, doc, cache):
"events_sent": c["ev"], "attributed": {LABELS[t]: c["attr"][t - 2] for t in (2, 3, 4, 5)},
"unmetered": c["unmetered"], "funds": None if c["funds"] == NO_FUNDS else c["funds"],
"joules_verdict": JOULES_VERDICT[c["jv"]], "funds_verdict": FUNDS_VERDICT[c["fv"]], "leaf": c["leaf"].hex(),
**({"bytes_out": c["out"]} if rc["version"] == 2 else {}),
} for c in comp["consumption"]],
"consumption_root": root.hex(),
"past": [{"grant": g.hex(), "flags": f} for g, f in past],
@ -962,7 +978,15 @@ def mutations(doc):
h.update(record=flip(h["record"], n - 64 - 48 + k))
lesson_units = ["lesson", "signed_by_authority"]
bp = "bytes_out_priced"
priced = next(k for k, c in enumerate(doc["vectors"][idx[bp]]["consumption"]) if c.get("bytes_out", 0) > 0 and c["funds"] is not None)
return [
mutate("bytes_out_priced: bytes out stated one less", [bp], lambda d: vec(d, bp)["units"].update(bytes_out=vec(d, bp)["units"]["bytes_out"] - 1)),
mutate("bytes_out_priced: bytes out left out of the units", [bp], lambda d: vec(d, bp)["units"].pop("bytes_out")),
mutate("bytes_out_priced: a grant's bytes out stated one more", [bp], lambda d: cons(d, bp, priced).update(bytes_out=cons(d, bp, priced)["bytes_out"] + 1)),
mutate("bytes_out_priced: a grant's funds without its bytes", [bp], lambda d: cons(d, bp, priced).update(funds=cons(d, bp, priced)["funds"] - 1)),
mutate("lesson: bytes out stated by a receipt of version 1", ["lesson"], lambda d: vec(d, "lesson")["units"].update(bytes_out=0)),
mutate("receipt_unknown_version: stated malformed", ["receipt_unknown_version"], lambda d: vec(d, "receipt_unknown_version").update(reason="malformed")),
mutate("lesson: a bit of the receipt's signature", ["lesson"], lambda d: vec(d, "lesson").update(receipt=flip(vec(d, "lesson")["receipt"], len(vec(d, "lesson")["receipt"]) // 2 - 1))),
mutate("lesson: publisher-seconds stated one more", ["lesson"], lambda d: vec(d, "lesson")["units"].update(publisher_us=vec(d, "lesson")["units"]["publisher_us"] + 1)),
mutate("lesson: recorded-seconds stated one less", ["lesson"], lambda d: vec(d, "lesson")["units"].update(recorded_us=vec(d, "lesson")["units"]["recorded_us"] - 1)),

View file

@ -1,7 +1,8 @@
# live-conformance/metering — live metering
The corpus of [`extensions/live-metering.md`](../../extensions/live-metering.md) §10: rooms,
and vectors, each a room-session receipt (`WMR1`) with what it names (the per-hop session
and vectors, each a room-session receipt (`WMR1`, of version 1, or of version 2 stating and
pricing its hops' bytes out) with what it names (the per-hop session
records, `WMH1`, of version 1 or of version 2 with a hop's DVR figures, each with its ledger; the composition descriptors and receipts, `WCD1` and
`WCR1`; the take manifests, `WTM1`; and the logs document of QoE observations), and the
outcome a verifier gives it.
@ -21,10 +22,11 @@ vector states (§5.4):
session (`media_tracks`, the *M* of §3.7); each hop record's `digest`, `size`, size
`bound` (B(*M*) for version 1, B₂(*M*) for version 2) and `version`, and for a version 2
record its six DVR figures by their §3.9 names (`dvr_objects_out`, `dvr_bytes_out`,
`dvr_objects_stored`, `dvr_bytes_stored`, `dvr_byte_s`, `dvr_dropped`); the `units`; the `unmetered` hop entries; the `totals`, one per class; the
`dvr_objects_stored`, `dvr_bytes_stored`, `dvr_byte_s`, `dvr_dropped`); the `units` (with
`bytes_out` for a receipt of version 2); the `unmetered` hop entries; the `totals`, one per class; the
`coverage`, `whole` or `partial`; each grant's `consumption` entry (its ceilings, its units,
its attributed joules by class, whether its share is `unmetered`, its `funds`, its two
verdicts and its `leaf`); the `consumption_root`; the grants `past` a ceiling with their
verdicts and its `leaf`, and for a receipt of version 2 its `bytes_out`); the `consumption_root`; the grants `past` a ceiling with their
flags; and the `qoe_cell`.
A verifier that reaches another outcome, reason, place or fact does not conform.
@ -88,9 +90,16 @@ The vectors:
than the lesson relay's under a bound 48 bytes higher, stating the frames it stored, the
byte-seconds it held them, one dropped for not verifying against its receipt, and the ten
it filled a late viewer with; every other record of the corpus is version 1);
`bytes_out_priced` (the lesson's receipt of version 2: the bytes its hops sent the session's
sessions, summed from their ledgers, by grant too, and priced per 10^9 bytes in its tariff
and in each grant's funds; every other receipt of the corpus is version 1);
- refused, one defect each: the receipt (`receipt_bad_signature`, `receipt_truncated`,
`receipt_wrong_room`, `session_invalid`, `signer_not_authorised`, `signer_revoked`,
`total_of_no_class`); the hop records (`record_missing`, `record_too_large`,
`total_of_no_class`, and for version 2 `bytes_out_mismatch`, a byte more than the ledgers
(`unit_mismatch`), `bytes_out_left_out_of_the_consumption`, the grants' entries without
their bytes (`ceiling_mismatch`), `bytes_priced_with_no_tariff` (`session_invalid`), and
`receipt_unknown_version`, the lesson's receipt with its fifth byte 3 and signed again
(`unknown_version`)); the hop records (`record_missing`, `record_too_large`,
`record_invalid`, `energy_invalid`, `record_wrong_room`, `record_bad_signature`,
`entry_restated_wrong`, `hop_not_authorised`, `hop_class_not_authorised`, `hop_revoked`,
`span_outside_session`, `track_unknown`, `ledger_invalid`, `ledger_ceiling_mismatch`,

View file

@ -2712,6 +2712,531 @@
"vst_ms": 0
}
},
{
"name": "bytes_out_priced",
"about": "the lesson's receipt of version 2: the bytes its hops sent the session's sessions, summed from their ledgers' bytes out, by grant too, and priced per 10^9 bytes in its tariff and in each grant's funds",
"room": "lesson",
"receipt": "574d5231022c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa8be2039bc5d06e7cf9d0188c3f22c659f1687b08b69b97f50b134552c46da783000000000000000000366e0100000000028070e1923387154875aacbeb894c20dc919c95dd4128758b5d47df86652c4a21a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10280de8002000000000400003a9800000000016e360001c3193ab4736af61acb0b4177aa6ec8fa8ee0924b3f727e9f21eb20b9dc72ef2d0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b720100c31801000000000400004e2000000000016e3600010265b94ec2ec2932104a7583c08050f61547332fe434464d598cad6464025b3040b598d58ceff895b484641dfdea9bf329f40b464895eb31b2271a83af17cce7fb0a000000d2e2268edd1bfc8a0dbebf1f2287b93e200bc8affd02c3e92ac872b3200731a103000000d290530d77935fe173d0d5dcbb2588170f730b423f93543b4f3ceaee06d4e78e2055cc0400000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000070000000000000040548900000000002c00000000000000f000000000000000002d31010000000054366100000000000001040200000080a1990300000000455552da0a00006d050000da0a0000b41500008d2000000000000000000000000000000000000016010000b41500006d0500006400000000943577080000006cc71f1f881b4962c2ff15d8a70c76f6539fa47221af8686664bcf683598018f0000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000694e666812a794f689b2f33591a4b1172f73786ab626a446e9445c10c0a5a00c1c7b78aa7a33ddde167b170dd8c3346a442b6979011879f70e9c6ff58a3d8003",
"hops": [
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b7201000000000000000000366e010000000000c31801000000000400004e2000000000016e360001110001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939023a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f011700000000000000e0040700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0242000000000000004024140000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b6030000000000007094000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366502d80e000000000000c05102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db013a0200000000000040ca22000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db023a0200000000000040ca2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c2c18a1d4c5f9a784336c16e2755ec99f12ba6990cacb81cb23f94aa048d6034013a02000000000000485904000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000011600000000000000781e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000002cd000000000000003cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000001720000000000000098850000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000002580000000000000020670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000f00000000000000008000000a6c37ecf5177e5e8d350fcc7c97ae32f9046bf15337e07d97453cd8dc79f4aa36d8d6332a0f2a20836f88a057863237aabd71abb0b5f41b8fed92fbbf04d851f567827e521b3ea570cdb126bc2c44e7940eb33690b8244e00abee459e82a8302",
"ledger": "0000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff00000002000000000000000000000018600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018566202000000000c2a9846621d91f9309170b29b95e158e278002ce4cc03a2d097917836a391c0ffffffffffffffffffffffffffffffff000000010000000000000000000000d0870c0000000000a087630100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006dde44606fd8377d9fa0809b2b36b708de3e6119d8c1c3d1f0d675917c28c6198096980000000000ffffffffffffffff000000010000000000000000000000709d0b0000000000607a60010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000e004070000000000c8431200000000000000000000000000801b6801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf4a5892c8a1bef2afba730a4ec868d0830f9355d10f540b757205945c588cb5ffffffffffffffffffffffffffffffff000000010000002c720200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d1ee853e5c092553edc99a0cc2468a7307afd83533a3a34f401b0695da343314ffffffffffffffffffffffffffffffff0000000100000020e53600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d436e9616073a533831cfbec646020b5d4dcc9d1df4b792ccaff6e7e23ee686affffffffffffffffffffffffffffffff0000000100000014b400000000000084e022000000000000000000000000000000000000000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e433cf0201e9b443d808e4406925b6a4a90e9963c6235c742a0ecec87a59e478ffffffffffffffff6400000000000000455552010000000000000000000000d0870c0000000000c0f35e010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c1020000000000000000801b68010000000080de8002000000000400003a9800000000016e360001050001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f021700000000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b60300000000000070940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000b2083f6a8d3e1c901b93bfb87f5ffe83344214430e91d08f4e422c5ac7f0c0869f25a4e826f34f770f8f63bcea36ec697bbc398f81c06aa8ad11bb1f7a33323de1911cf31bd046195483ef8a8b22a5778adb66cfde02d9e8f8e46360a5577b06",
"ledger": "b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000c843120000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
],
"descriptors": [
"57434431012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaad6b5a6849f61a8bbd9d4db526a9fc1c73ef949590cdcd9b0b65ce6375262d9bd0000000000000000a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10005d00201101010010108000100086665617475726564056f72646572000000000000000000000280bb0000046761696e0370616e0c0c13afc5df1dffed9f80c566f13be392cc90ad1ceba5db69473a7993f8a7ff2e64cb5d094b2fdf651392e4a2d4f317cc6a436ab9b0cd5afe626493549677937002"
],
"compositions": [
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f000127a00000000008a826f7851f7a89f5ecf581141292c7f69e9d004ed08b01d25dd3c66e30f43f602540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f08000000000000000000000000000000c264cd78485650e03db23e16774c0566ac52d21be1b9edacaad39fa34dc3100c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593908000000000000000000000000000000e43f0568538eef406fb5ed7901b131c0b789d9c0fdb04d61d590af9bec169a1dd2ed35929ea1d408e2da06ee1c840bacab38929016f1a7ba820a80472485c9c6159b98672266a380458e260007c2baec97749e4941eb6ac370d1660fb0e5f900",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04054890000000000ee27588426463e6a765b4b8b5791670a7ad61be12ef1995102f9220928d6038d02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f090000000000000000000000000000005e05448cb224cb6aa2726e4c0fe448b127e032169bd2ffea55b4693d53ef487f01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593909000000000000000000000000000000f993d88a6b1a587e0980017103aa4fcdbdf1d7b1c948f95a8bfa36a26493bb1e89b7c717020a8a296d3688d7988d8f2e70e54228d60c58514564a12937b2bb9421199fdee27ccf49acb52a32fa0f698b3eb5b28c35c47a797ae5e7d531f7db0a",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f08096980000000000d8bb5ef530c1fe8d1a0478d5ae231aac802d9f99dff906a66aa111420948048a02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0a000000000000000000000000000000539e2bb145135d7de5b00ebf1b1178f83ef60d1b5ceedabf98bbb424a1d0a96301e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390a00000000000000000000000000000031a3cdcd31f70593af11aaf7bc4d22cc1ec5c18ee0d918fb2edd5a9c98b5a4758e74da34c2fb00e235278a3010c44dac99131fa49acc910e04f3929e7a6014833b553fad2cd31d127ad862b76999324556e76388192020c8022db9db6cafb803",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0d8a70000000000c65182e1c2868d42d9c1a244545f3c3416b14e481ade5c42b1979419f360b3e502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0b000000000000000000000000000000491fe4bc5c4bafa0066ec9c2baf4b4287fcd09a778e36d5996a97ab827b7eaae01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390b000000000000000000000000000000414a7eb6f2353358646d5b861979c533b344cc01fb9dafdb9d6518c4ad7e3f6f01a03c915891dac04736b7858fdf34c4137e10f593ab5cb3a64900914b360f6b242ee3ead8c5499f325411942dbe827bd51176f2b049c4453a654c49dc701b09",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0001bb70000000000381618c2dfecc2dea3881cb1d4ed69e43dec01f07ebda92e08cd08ff935ed44502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0c0000000000000000000000000000001915289342627f7f5a8bf4ccaf04c0418735ae65e41ca8692ebd3fca27ea764b01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390c000000000000000000000000000000194d05dbb7218051137b7537fcb320653e7f67071a19ae363969aee47cf08bede3b8b1012f1fc03a132f8793e0eae4389c23da524a7f6482a1d5cdf26ff04249cb81d4f19d8b164ec8eb381c66ba2704c57c7bb6f2389114f14bbfd632199302",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0405dc600000000007629d9fb8d6ac9e846d7ebdbe0a3a64b29ad51f7e4db40be5f96e447abcf6f0202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0d000000000000000000000000000000f98b3489983bd0476673dc05db4f09c092411512ed2da6578e9c254eee0d72f501e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390d000000000000000000000000000000bc87dab2315f8ea4f21a4b5adcb838b8d89f6d67f93bbdf5bf175069ad81fa5d123e5e3721d1b781cfcc4650a2d571df1142f2160b377be26b692636e885cd037553b9041ac8f13b1f2064568957993ff69556b8673f0ad6e6caafd8b4887c07",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0809fd50000000000d32fbadccdb9742ffff99344fe07069e2e89b1d7f2d99859ad48336fadacd7c202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0e00000000000000000000000000000053c09229456148744a940ba828e839eca0cf46d7c15e2c3533b4c65dcb0b4ff701e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390e000000000000000000000000000000b5702d26fbc4708196f576c1d46b7fc92471154fe81689707069bcb43173ecedc36859b37258c17a4294d56d36befaa9fbd975ce62e5deb542372aef4c9240b43ba584fc98e7c5a5066e1ba4060e96ca4e50daa116366c3aca9dfccef285f405",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0e1e40000000000fe93ba9f3d79ff2d3aeb01726ac4728adf60676494fbd0e596f5972be5f4639102540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0f000000000000000000000000000000341b9ec871ad41102a671b153ccd014f32cfe167bf5cca3989470a574196d0a401e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390f0000000000000000000000000000001eafd27ec73cf8676b02ebb4fa82a5bc67b2298a442322797db8cd996f8866d7bfdca1dadbfc723c27a3b6963a74c78570a41403a9cb454643631157e8bddbda8ccd54062aeec216b94406595aeb140b59afd5e697733f45980831357c0bc804",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f00024f4000000000092a195ef7c5e97512b5eed1dd778db0bd1f1c75cf8cf25b61ec800296e459e6302540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f10000000000000000000000000000000c693ed9bd4639c29ceb962b071e0531512929a139ebed7c5e12989d28b316ddc01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593910000000000000000000000000000000c5043e90fe94a06a8b47242bb084c7dc5f8e46aebbe9b2c5a8db30d44d1a8b55ab981922c453fc2c78b58f7ec56293bb26d02c335079fdd1e246e33607ef473f513bd939cfe20da8b94af0963156c7218be49ffd1efbc9c14429fddf91bf5006",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04066030100000000d00e25bb474153c30c4bf88f0fb890a0dd10342388c0be87aa93399d54bbaf5502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f110000000000000000000000000000005f50d92c07b2f57953a701633e9d59575664bfe9b13644d5db11c5d04629b08c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593911000000000000000000000000000000339aa736ed16c43427079481a1b58bb9d370f104bbcba7a9d915ba1528b1a75da2dabf39b4d5d2518a8bef4340b8c5e7b32250ac222cc3e8648a42730b81eeac78da15f7fa7f42f7bb131311eb22aa7f89ada8940b2dd41c18c113a694a50808"
],
"takes": [
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaae57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f559425465004500032d6893f980a7dad483af70b4e116a10b7caa4e2052593699b2c4d5251f98cf34d11f86821c2cd542f6bbf5152a07ad54b271f65b25309018a0bf0e356fbe70268d3166f7ac16582f4cde87f1ce46d109e4bd0562c3d436da0f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba1307020a0000000000000000000000807f612a01000000c0c1702a01000000300000000000000037090d56c333ad6c4392ffbb8f80b7854c7ae3d562e182d6b76685f99db2ec1f0100000000000000c0c1702a010000000004802a0100000055000000000000003b9b3da8c3121692dbe9b75ff7b37a77773cf7d71d8b7c885253d5b3855c53b902000000000000000004802a0100000040468f2a010000003a0000000000000097f5ee9829746c16c29946a44489846e4a385eb6d62804ba22a18a99a92642ab030000000000000040468f2a0100000080889e2a010000005f000000000000004bb0df0483b6370199dc9196f695ec65b029d8ffb09dfd7ec24bf5d5c59a72f3040000000000000080889e2a01000000c0caad2a010000004400000000000000733b22d5d80b9a80443b60a14334000a8ab43f139364fa9cd3a547e8a7c523640500000000000000c0caad2a01000000000dbd2a0100000069000000000000009395a39fcaaab116f14a5e4f1e51abe694d712e0f912827045471c6283730a480600000000000000000dbd2a01000000404fcc2a010000004e000000000000002b869dfefc2fe0873e06d23c09f1629470a788c6b8f2aa8e6aafb1298cd73fe60700000000000000404fcc2a010000008091db2a0100000033000000000000009f10cb7a3feccf63f5c0c398375eaf998628d314dc8159d15845fe5a0f8a006b08000000000000008091db2a01000000c0d3ea2a0100000058000000000000008b03281ec96d96d0e76e48ef184b7a60d4856eddf77629af961fe54746404ef60900000000000000c0d3ea2a010000000016fa2a010000003d000000000000001fb42eb87b4ea4dc72d02c9e6ea82aab91be4bbef122892b8921e2f552a462678883ea090e9f374f91bacaac0864af7b6dafa57c4f8a3ac0040ec7fc669f1f2506000000d7b83f99f213619227fbccadf7069c3f6cffca8e86f2aba6ffb8d15212698fa51be10b08c6e09a1b9314f45c65bf3c9b6bd507cfa5df3f013d50657245ee48c5602f2236c9c17b8a1cca6bae144f8f162627d27095a7256e1c5b8bc199c84fb3d535c9375be68b107355dcfeaab2a78192ba09570642f09de57b7bab1438eadc703cab1c2158dee2277fea9a9b909df1f7926a3a56996b7db1bfbaaba40ffa2970412cc3dc8306ba8f88774f1a622d1bf21b9ef51fb85b990d279f66fed977340159425465004500032d6893f980a7dad483af70b4e116a10b7caa4e205259369900000000000000000000000000000000000000000000000000000000000000001f250bafd7daed705bd36f8138b7419700ffe4c20991a2a46993dfd25b592a3643977ddbb511d2b51471498d51135c1a724a069a507032f2a3ff93aaa9787f0d",
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa7d3b3098d82fddc8585faa3e84b8f2c13cb7fc723a01bfb8bbacfba4ed388eee2b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c6b7091c801fbc133089d9612f76d94401ffd0199edd399f9db0beaf3e8a611d45859f57a4fec068477f3a45544119f5202eaf32ee3796401d6319ec7b1bf692810f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba130702080000000000000000000000405489000000000010679c000000000030000000000000008fae533ea900dd435a27c7ac6bbe43a78ca53f4ac8bf916accd92a8d374cba94010000000000000010679c0000000000e079af000000000055000000000000006cb63b81a4b11a270dfd91c37eaaed324b1b55999789174218e9990aa186455f0200000000000000e079af0000000000b08cc200000000003a00000000000000e515ec9e690d2bf9be7ce9bd8547b84406445c82682346f3cf84ccb305cf4ae50300000000000000b08cc20000000000809fd500000000005f000000000000008ab19bbbd6173bd1339419c327fadbf9337f7d77aff9e7082ee68da5132756c80400000000000000809fd5000000000050b2e80000000000440000000000000017fca124fd9c32581a3a6f032bc985481cef9852f5f07ba3120fd851511d20ba050000000000000050b2e8000000000020c5fb00000000006900000000000000be2b55d6011931c758ef4700f002f24a947e8e95428049653bc0fc44055b5563060000000000000020c5fb0000000000f0d70e01000000004e0000000000000083da93c67f107a2933b1befbdaaf558a44b2adf41cd3e085f1d48767e8dc3fde0700000000000000f0d70e0100000000c0ea2101000000003300000000000000989f78372ee677d01a1a45c0f4e33989e1e30935ef1f70e0426de38e2cb5ae8a5b8ad24298b11d4afe4acabf254c55d99e1badc4d20a3f293fef3c8e9870e6eb060000000571c24d887ed7ee13662e1b6b5916b05446674f04a48bb9657a88aa791aa4f78c7c1730862c2541afc6faf68f8f1a4b34c99f6c9ea331b8be3a60e97734ac814bcc3352c9690a6b19b9aaa4301286ce746571ec9a1dfd777ce3cdb1ab47ca1ea6e6c44a5d482193b91fac6a4b86aae708a8fe967079b4dc31dbe6e7da840cab0b041b0865ddff65ae4f592bac31b3e3627cb44ed3bfd4678f33dfcdd03c9e684fa04199a174e37753164a4ac950986de9a4abcc3c2cdce827249a9f063289b8012b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c600000000000000000000000000000000000000000000000000000000000000008fdd3b426a73d6a2e5ba04781aafc491c5950f29be46baca5bc57f7e54ba95b22dee29ffae444fb5eeee0e941d74e4e71f8dffbe4f45979a1e8c49aebc023c0c"
],
"logs": "[[{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":0,\"ts_ms\":1792108800500,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":1,\"ts_ms\":1792108801100,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":2,\"ts_ms\":1792108822500,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":0,\"ts_ms\":1792108800510,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":1,\"ts_ms\":1792108801147,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":2,\"ts_ms\":1792108805510,\"event\":\"stall-start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":3,\"ts_ms\":1792108805910,\"event\":\"stall-end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":4,\"ts_ms\":1792108822510,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":0,\"ts_ms\":1792108800520,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":1,\"ts_ms\":1792108801194,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":2,\"ts_ms\":1792108822520,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}]]",
"outcome": "accept",
"digest": "e256da6efef6efbefcd49c8af9d166f06fdef3cfafc8e189314ec5ff46648d94",
"media_tracks": 7,
"records": [
{
"digest": "8070e1923387154875aacbeb894c20dc919c95dd4128758b5d47df86652c4a21",
"size": 744,
"bound": 2172,
"version": 1
},
{
"digest": "c3193ab4736af61acb0b4177aa6ec8fa8ee0924b3f727e9f21eb20b9dc72ef2d",
"size": 1824,
"bound": 2172,
"version": 1
}
],
"units": {
"publisher_us": 80500000,
"subscriber_us": [
69400000,
23600000,
23600000,
0,
0,
0,
0,
0,
39999000
],
"sessions": 7,
"composition_us": 9000000,
"events_sent": 44,
"events_delivered": 240,
"recorded_us": 20000000,
"bytes_out": 6370900
},
"unmetered": 0,
"totals": [
{
"class": "OnChipCounter",
"figures": 2,
"joules": 60400000
}
],
"coverage": "whole",
"consumption": [
{
"grant": "0000000000000000000000000000000000000000000000000000000000000000",
"joules_ceiling": null,
"funds_ceiling": null,
"currency": null,
"publisher_us": 0,
"subscriber_us": [
0,
0,
0,
0,
0,
0,
0,
0,
39999000
],
"composition_us": 0,
"recorded_us": 0,
"events_sent": 0,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 44485,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "none",
"funds_verdict": "none",
"leaf": "091f689c679b284f349733d4b6cb73e18fa709e914855b5ec6e607e3aa81ae4d",
"bytes_out": 24600
},
{
"grant": "0c2a9846621d91f9309170b29b95e158e278002ce4cc03a2d097917836a391c0",
"joules_ceiling": null,
"funds_ceiling": null,
"currency": null,
"publisher_us": 0,
"subscriber_us": [
23300000,
0,
0,
0,
0,
0,
0,
0,
0
],
"composition_us": 0,
"recorded_us": 0,
"events_sent": 1,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 1485034,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "none",
"funds_verdict": "none",
"leaf": "9b7cf64b028f04a02dd78dfc0c45a7e47a5af63092d1e686ca56e2aa70c22415",
"bytes_out": 821200
},
{
"grant": "6dde44606fd8377d9fa0809b2b36b708de3e6119d8c1c3d1f0d675917c28c619",
"joules_ceiling": 10000000,
"funds_ceiling": null,
"currency": null,
"publisher_us": 0,
"subscriber_us": [
23100000,
0,
0,
0,
0,
0,
0,
0,
0
],
"composition_us": 0,
"recorded_us": 0,
"events_sent": 1,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 1376532,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "within",
"funds_verdict": "none",
"leaf": "18f6d4d40aea22895681d27fe8064979de00795b923c749b609e00a97c6b71ab",
"bytes_out": 761200
},
{
"grant": "b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0",
"joules_ceiling": null,
"funds_ceiling": null,
"currency": null,
"publisher_us": 23500000,
"subscriber_us": [
0,
23600000,
0,
0,
0,
0,
0,
0,
0
],
"composition_us": 9000000,
"recorded_us": 0,
"events_sent": 0,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 44996471,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "none",
"funds_verdict": "none",
"leaf": "5079123b7744cb7c4c5e651c9d309ee38855ad2965a0933073f0ebb117af82fd",
"bytes_out": 1657000
},
{
"grant": "cf4a5892c8a1bef2afba730a4ec868d0830f9355d10f540b757205945c588cb5",
"joules_ceiling": null,
"funds_ceiling": null,
"currency": null,
"publisher_us": 19000000,
"subscriber_us": [
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"composition_us": 0,
"recorded_us": 10000000,
"events_sent": 6,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 289881,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "none",
"funds_verdict": "none",
"leaf": "5b54b79f411c0bda28b120f058a6d480217f13bd55a5a0c4c407d04e7c4e2a6e",
"bytes_out": 0
},
{
"grant": "d1ee853e5c092553edc99a0cc2468a7307afd83533a3a34f401b0695da343314",
"joules_ceiling": null,
"funds_ceiling": null,
"currency": null,
"publisher_us": 19000000,
"subscriber_us": [
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"composition_us": 0,
"recorded_us": 10000000,
"events_sent": 6,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 6505797,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "none",
"funds_verdict": "none",
"leaf": "c77d7eb1315aa96a2ea74383e521bfb876c05243ffc4b130e50dd4d280f6bb13",
"bytes_out": 0
},
{
"grant": "d436e9616073a533831cfbec646020b5d4dcc9d1df4b792ccaff6e7e23ee686a",
"joules_ceiling": null,
"funds_ceiling": null,
"currency": null,
"publisher_us": 19000000,
"subscriber_us": [
0,
0,
23600000,
0,
0,
0,
0,
0,
0
],
"composition_us": 0,
"recorded_us": 0,
"events_sent": 6,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 4216760,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": null,
"joules_verdict": "none",
"funds_verdict": "none",
"leaf": "96d0e5507800b5b5f43b4082cde00dd2185be51d0f34847829b8b7a379b31eed",
"bytes_out": 2285700
},
{
"grant": "e433cf0201e9b443d808e4406925b6a4a90e9963c6235c742a0ecec87a59e478",
"joules_ceiling": null,
"funds_ceiling": 100,
"currency": "EUR",
"publisher_us": 0,
"subscriber_us": [
23000000,
0,
0,
0,
0,
0,
0,
0,
0
],
"composition_us": 0,
"recorded_us": 0,
"events_sent": 0,
"attributed": {
"ModelBased": 0,
"Estimator": 0,
"OnChipCounter": 1485034,
"CalibratedInstrument": 0
},
"unmetered": false,
"funds": 2,
"joules_verdict": "none",
"funds_verdict": "within",
"leaf": "3c1f4c40d28861b136f967d011a7f1ce3748f4823bace690a9c2db12eb2e74ce",
"bytes_out": 821200
}
],
"consumption_root": "6cc71f1f881b4962c2ff15d8a70c76f6539fa47221af8686664bcf683598018f",
"past": [],
"qoe_cell": {
"suppressed": true,
"started": 0,
"start_failed": 0,
"exit_before_start": 0,
"stalls": 0,
"stall_ms": 0,
"engaged_ms": 0,
"vst_count": 0,
"vst_ms": 0
}
},
{
"name": "bytes_out_mismatch",
"about": "a receipt of version 2 stating one byte out more than its hops' ledgers",
"room": "lesson",
"receipt": "574d5231022c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa8be2039bc5d06e7cf9d0188c3f22c659f1687b08b69b97f50b134552c46da783000000000000000000366e0100000000028070e1923387154875aacbeb894c20dc919c95dd4128758b5d47df86652c4a21a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10280de8002000000000400003a9800000000016e360001c3193ab4736af61acb0b4177aa6ec8fa8ee0924b3f727e9f21eb20b9dc72ef2d0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b720100c31801000000000400004e2000000000016e3600010265b94ec2ec2932104a7583c08050f61547332fe434464d598cad6464025b3040b598d58ceff895b484641dfdea9bf329f40b464895eb31b2271a83af17cce7fb0a000000d2e2268edd1bfc8a0dbebf1f2287b93e200bc8affd02c3e92ac872b3200731a103000000d290530d77935fe173d0d5dcbb2588170f730b423f93543b4f3ceaee06d4e78e2055cc0400000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000070000000000000040548900000000002c00000000000000f000000000000000002d31010000000055366100000000000001040200000080a1990300000000455552da0a00006d050000da0a0000b41500008d2000000000000000000000000000000000000016010000b41500006d0500006400000000943577080000006cc71f1f881b4962c2ff15d8a70c76f6539fa47221af8686664bcf683598018f000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022aecc25a4d055345167ae6f9d39a4030cfb4a5c59129f40272ba994699201f1497efee74b554b0b91ea5c4fad4a72bd0fc70878f03152bfc51d3a581f92ae0f",
"hops": [
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b7201000000000000000000366e010000000000c31801000000000400004e2000000000016e360001110001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939023a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f011700000000000000e0040700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0242000000000000004024140000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b6030000000000007094000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366502d80e000000000000c05102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db013a0200000000000040ca22000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db023a0200000000000040ca2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c2c18a1d4c5f9a784336c16e2755ec99f12ba6990cacb81cb23f94aa048d6034013a02000000000000485904000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000011600000000000000781e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000002cd000000000000003cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000001720000000000000098850000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000002580000000000000020670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000f00000000000000008000000a6c37ecf5177e5e8d350fcc7c97ae32f9046bf15337e07d97453cd8dc79f4aa36d8d6332a0f2a20836f88a057863237aabd71abb0b5f41b8fed92fbbf04d851f567827e521b3ea570cdb126bc2c44e7940eb33690b8244e00abee459e82a8302",
"ledger": "0000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff00000002000000000000000000000018600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018566202000000000c2a9846621d91f9309170b29b95e158e278002ce4cc03a2d097917836a391c0ffffffffffffffffffffffffffffffff000000010000000000000000000000d0870c0000000000a087630100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006dde44606fd8377d9fa0809b2b36b708de3e6119d8c1c3d1f0d675917c28c6198096980000000000ffffffffffffffff000000010000000000000000000000709d0b0000000000607a60010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000e004070000000000c8431200000000000000000000000000801b6801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf4a5892c8a1bef2afba730a4ec868d0830f9355d10f540b757205945c588cb5ffffffffffffffffffffffffffffffff000000010000002c720200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d1ee853e5c092553edc99a0cc2468a7307afd83533a3a34f401b0695da343314ffffffffffffffffffffffffffffffff0000000100000020e53600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d436e9616073a533831cfbec646020b5d4dcc9d1df4b792ccaff6e7e23ee686affffffffffffffffffffffffffffffff0000000100000014b400000000000084e022000000000000000000000000000000000000000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e433cf0201e9b443d808e4406925b6a4a90e9963c6235c742a0ecec87a59e478ffffffffffffffff6400000000000000455552010000000000000000000000d0870c0000000000c0f35e010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c1020000000000000000801b68010000000080de8002000000000400003a9800000000016e360001050001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f021700000000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b60300000000000070940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000b2083f6a8d3e1c901b93bfb87f5ffe83344214430e91d08f4e422c5ac7f0c0869f25a4e826f34f770f8f63bcea36ec697bbc398f81c06aa8ad11bb1f7a33323de1911cf31bd046195483ef8a8b22a5778adb66cfde02d9e8f8e46360a5577b06",
"ledger": "b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000c843120000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
],
"descriptors": [
"57434431012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaad6b5a6849f61a8bbd9d4db526a9fc1c73ef949590cdcd9b0b65ce6375262d9bd0000000000000000a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10005d00201101010010108000100086665617475726564056f72646572000000000000000000000280bb0000046761696e0370616e0c0c13afc5df1dffed9f80c566f13be392cc90ad1ceba5db69473a7993f8a7ff2e64cb5d094b2fdf651392e4a2d4f317cc6a436ab9b0cd5afe626493549677937002"
],
"compositions": [
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f000127a00000000008a826f7851f7a89f5ecf581141292c7f69e9d004ed08b01d25dd3c66e30f43f602540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f08000000000000000000000000000000c264cd78485650e03db23e16774c0566ac52d21be1b9edacaad39fa34dc3100c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593908000000000000000000000000000000e43f0568538eef406fb5ed7901b131c0b789d9c0fdb04d61d590af9bec169a1dd2ed35929ea1d408e2da06ee1c840bacab38929016f1a7ba820a80472485c9c6159b98672266a380458e260007c2baec97749e4941eb6ac370d1660fb0e5f900",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04054890000000000ee27588426463e6a765b4b8b5791670a7ad61be12ef1995102f9220928d6038d02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f090000000000000000000000000000005e05448cb224cb6aa2726e4c0fe448b127e032169bd2ffea55b4693d53ef487f01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593909000000000000000000000000000000f993d88a6b1a587e0980017103aa4fcdbdf1d7b1c948f95a8bfa36a26493bb1e89b7c717020a8a296d3688d7988d8f2e70e54228d60c58514564a12937b2bb9421199fdee27ccf49acb52a32fa0f698b3eb5b28c35c47a797ae5e7d531f7db0a",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f08096980000000000d8bb5ef530c1fe8d1a0478d5ae231aac802d9f99dff906a66aa111420948048a02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0a000000000000000000000000000000539e2bb145135d7de5b00ebf1b1178f83ef60d1b5ceedabf98bbb424a1d0a96301e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390a00000000000000000000000000000031a3cdcd31f70593af11aaf7bc4d22cc1ec5c18ee0d918fb2edd5a9c98b5a4758e74da34c2fb00e235278a3010c44dac99131fa49acc910e04f3929e7a6014833b553fad2cd31d127ad862b76999324556e76388192020c8022db9db6cafb803",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0d8a70000000000c65182e1c2868d42d9c1a244545f3c3416b14e481ade5c42b1979419f360b3e502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0b000000000000000000000000000000491fe4bc5c4bafa0066ec9c2baf4b4287fcd09a778e36d5996a97ab827b7eaae01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390b000000000000000000000000000000414a7eb6f2353358646d5b861979c533b344cc01fb9dafdb9d6518c4ad7e3f6f01a03c915891dac04736b7858fdf34c4137e10f593ab5cb3a64900914b360f6b242ee3ead8c5499f325411942dbe827bd51176f2b049c4453a654c49dc701b09",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0001bb70000000000381618c2dfecc2dea3881cb1d4ed69e43dec01f07ebda92e08cd08ff935ed44502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0c0000000000000000000000000000001915289342627f7f5a8bf4ccaf04c0418735ae65e41ca8692ebd3fca27ea764b01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390c000000000000000000000000000000194d05dbb7218051137b7537fcb320653e7f67071a19ae363969aee47cf08bede3b8b1012f1fc03a132f8793e0eae4389c23da524a7f6482a1d5cdf26ff04249cb81d4f19d8b164ec8eb381c66ba2704c57c7bb6f2389114f14bbfd632199302",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0405dc600000000007629d9fb8d6ac9e846d7ebdbe0a3a64b29ad51f7e4db40be5f96e447abcf6f0202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0d000000000000000000000000000000f98b3489983bd0476673dc05db4f09c092411512ed2da6578e9c254eee0d72f501e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390d000000000000000000000000000000bc87dab2315f8ea4f21a4b5adcb838b8d89f6d67f93bbdf5bf175069ad81fa5d123e5e3721d1b781cfcc4650a2d571df1142f2160b377be26b692636e885cd037553b9041ac8f13b1f2064568957993ff69556b8673f0ad6e6caafd8b4887c07",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0809fd50000000000d32fbadccdb9742ffff99344fe07069e2e89b1d7f2d99859ad48336fadacd7c202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0e00000000000000000000000000000053c09229456148744a940ba828e839eca0cf46d7c15e2c3533b4c65dcb0b4ff701e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390e000000000000000000000000000000b5702d26fbc4708196f576c1d46b7fc92471154fe81689707069bcb43173ecedc36859b37258c17a4294d56d36befaa9fbd975ce62e5deb542372aef4c9240b43ba584fc98e7c5a5066e1ba4060e96ca4e50daa116366c3aca9dfccef285f405",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0e1e40000000000fe93ba9f3d79ff2d3aeb01726ac4728adf60676494fbd0e596f5972be5f4639102540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0f000000000000000000000000000000341b9ec871ad41102a671b153ccd014f32cfe167bf5cca3989470a574196d0a401e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390f0000000000000000000000000000001eafd27ec73cf8676b02ebb4fa82a5bc67b2298a442322797db8cd996f8866d7bfdca1dadbfc723c27a3b6963a74c78570a41403a9cb454643631157e8bddbda8ccd54062aeec216b94406595aeb140b59afd5e697733f45980831357c0bc804",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f00024f4000000000092a195ef7c5e97512b5eed1dd778db0bd1f1c75cf8cf25b61ec800296e459e6302540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f10000000000000000000000000000000c693ed9bd4639c29ceb962b071e0531512929a139ebed7c5e12989d28b316ddc01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593910000000000000000000000000000000c5043e90fe94a06a8b47242bb084c7dc5f8e46aebbe9b2c5a8db30d44d1a8b55ab981922c453fc2c78b58f7ec56293bb26d02c335079fdd1e246e33607ef473f513bd939cfe20da8b94af0963156c7218be49ffd1efbc9c14429fddf91bf5006",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04066030100000000d00e25bb474153c30c4bf88f0fb890a0dd10342388c0be87aa93399d54bbaf5502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f110000000000000000000000000000005f50d92c07b2f57953a701633e9d59575664bfe9b13644d5db11c5d04629b08c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593911000000000000000000000000000000339aa736ed16c43427079481a1b58bb9d370f104bbcba7a9d915ba1528b1a75da2dabf39b4d5d2518a8bef4340b8c5e7b32250ac222cc3e8648a42730b81eeac78da15f7fa7f42f7bb131311eb22aa7f89ada8940b2dd41c18c113a694a50808"
],
"takes": [
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaae57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f559425465004500032d6893f980a7dad483af70b4e116a10b7caa4e2052593699b2c4d5251f98cf34d11f86821c2cd542f6bbf5152a07ad54b271f65b25309018a0bf0e356fbe70268d3166f7ac16582f4cde87f1ce46d109e4bd0562c3d436da0f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba1307020a0000000000000000000000807f612a01000000c0c1702a01000000300000000000000037090d56c333ad6c4392ffbb8f80b7854c7ae3d562e182d6b76685f99db2ec1f0100000000000000c0c1702a010000000004802a0100000055000000000000003b9b3da8c3121692dbe9b75ff7b37a77773cf7d71d8b7c885253d5b3855c53b902000000000000000004802a0100000040468f2a010000003a0000000000000097f5ee9829746c16c29946a44489846e4a385eb6d62804ba22a18a99a92642ab030000000000000040468f2a0100000080889e2a010000005f000000000000004bb0df0483b6370199dc9196f695ec65b029d8ffb09dfd7ec24bf5d5c59a72f3040000000000000080889e2a01000000c0caad2a010000004400000000000000733b22d5d80b9a80443b60a14334000a8ab43f139364fa9cd3a547e8a7c523640500000000000000c0caad2a01000000000dbd2a0100000069000000000000009395a39fcaaab116f14a5e4f1e51abe694d712e0f912827045471c6283730a480600000000000000000dbd2a01000000404fcc2a010000004e000000000000002b869dfefc2fe0873e06d23c09f1629470a788c6b8f2aa8e6aafb1298cd73fe60700000000000000404fcc2a010000008091db2a0100000033000000000000009f10cb7a3feccf63f5c0c398375eaf998628d314dc8159d15845fe5a0f8a006b08000000000000008091db2a01000000c0d3ea2a0100000058000000000000008b03281ec96d96d0e76e48ef184b7a60d4856eddf77629af961fe54746404ef60900000000000000c0d3ea2a010000000016fa2a010000003d000000000000001fb42eb87b4ea4dc72d02c9e6ea82aab91be4bbef122892b8921e2f552a462678883ea090e9f374f91bacaac0864af7b6dafa57c4f8a3ac0040ec7fc669f1f2506000000d7b83f99f213619227fbccadf7069c3f6cffca8e86f2aba6ffb8d15212698fa51be10b08c6e09a1b9314f45c65bf3c9b6bd507cfa5df3f013d50657245ee48c5602f2236c9c17b8a1cca6bae144f8f162627d27095a7256e1c5b8bc199c84fb3d535c9375be68b107355dcfeaab2a78192ba09570642f09de57b7bab1438eadc703cab1c2158dee2277fea9a9b909df1f7926a3a56996b7db1bfbaaba40ffa2970412cc3dc8306ba8f88774f1a622d1bf21b9ef51fb85b990d279f66fed977340159425465004500032d6893f980a7dad483af70b4e116a10b7caa4e205259369900000000000000000000000000000000000000000000000000000000000000001f250bafd7daed705bd36f8138b7419700ffe4c20991a2a46993dfd25b592a3643977ddbb511d2b51471498d51135c1a724a069a507032f2a3ff93aaa9787f0d",
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa7d3b3098d82fddc8585faa3e84b8f2c13cb7fc723a01bfb8bbacfba4ed388eee2b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c6b7091c801fbc133089d9612f76d94401ffd0199edd399f9db0beaf3e8a611d45859f57a4fec068477f3a45544119f5202eaf32ee3796401d6319ec7b1bf692810f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba130702080000000000000000000000405489000000000010679c000000000030000000000000008fae533ea900dd435a27c7ac6bbe43a78ca53f4ac8bf916accd92a8d374cba94010000000000000010679c0000000000e079af000000000055000000000000006cb63b81a4b11a270dfd91c37eaaed324b1b55999789174218e9990aa186455f0200000000000000e079af0000000000b08cc200000000003a00000000000000e515ec9e690d2bf9be7ce9bd8547b84406445c82682346f3cf84ccb305cf4ae50300000000000000b08cc20000000000809fd500000000005f000000000000008ab19bbbd6173bd1339419c327fadbf9337f7d77aff9e7082ee68da5132756c80400000000000000809fd5000000000050b2e80000000000440000000000000017fca124fd9c32581a3a6f032bc985481cef9852f5f07ba3120fd851511d20ba050000000000000050b2e8000000000020c5fb00000000006900000000000000be2b55d6011931c758ef4700f002f24a947e8e95428049653bc0fc44055b5563060000000000000020c5fb0000000000f0d70e01000000004e0000000000000083da93c67f107a2933b1befbdaaf558a44b2adf41cd3e085f1d48767e8dc3fde0700000000000000f0d70e0100000000c0ea2101000000003300000000000000989f78372ee677d01a1a45c0f4e33989e1e30935ef1f70e0426de38e2cb5ae8a5b8ad24298b11d4afe4acabf254c55d99e1badc4d20a3f293fef3c8e9870e6eb060000000571c24d887ed7ee13662e1b6b5916b05446674f04a48bb9657a88aa791aa4f78c7c1730862c2541afc6faf68f8f1a4b34c99f6c9ea331b8be3a60e97734ac814bcc3352c9690a6b19b9aaa4301286ce746571ec9a1dfd777ce3cdb1ab47ca1ea6e6c44a5d482193b91fac6a4b86aae708a8fe967079b4dc31dbe6e7da840cab0b041b0865ddff65ae4f592bac31b3e3627cb44ed3bfd4678f33dfcdd03c9e684fa04199a174e37753164a4ac950986de9a4abcc3c2cdce827249a9f063289b8012b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c600000000000000000000000000000000000000000000000000000000000000008fdd3b426a73d6a2e5ba04781aafc491c5950f29be46baca5bc57f7e54ba95b22dee29ffae444fb5eeee0e941d74e4e71f8dffbe4f45979a1e8c49aebc023c0c"
],
"logs": "[[{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":0,\"ts_ms\":1792108800500,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":1,\"ts_ms\":1792108801100,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":2,\"ts_ms\":1792108822500,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":0,\"ts_ms\":1792108800510,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":1,\"ts_ms\":1792108801147,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":2,\"ts_ms\":1792108805510,\"event\":\"stall-start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":3,\"ts_ms\":1792108805910,\"event\":\"stall-end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":4,\"ts_ms\":1792108822510,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":0,\"ts_ms\":1792108800520,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":1,\"ts_ms\":1792108801194,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":2,\"ts_ms\":1792108822520,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}]]",
"outcome": "refuse",
"reason": "unit_mismatch",
"at": [
"units"
]
},
{
"name": "bytes_out_left_out_of_the_consumption",
"about": "a receipt of version 2 whose grants' consumption entries leave out their bytes out and their price",
"room": "lesson",
"receipt": "574d5231022c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa8be2039bc5d06e7cf9d0188c3f22c659f1687b08b69b97f50b134552c46da783000000000000000000366e0100000000028070e1923387154875aacbeb894c20dc919c95dd4128758b5d47df86652c4a21a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10280de8002000000000400003a9800000000016e360001c3193ab4736af61acb0b4177aa6ec8fa8ee0924b3f727e9f21eb20b9dc72ef2d0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b720100c31801000000000400004e2000000000016e3600010265b94ec2ec2932104a7583c08050f61547332fe434464d598cad6464025b3040b598d58ceff895b484641dfdea9bf329f40b464895eb31b2271a83af17cce7fb0a000000d2e2268edd1bfc8a0dbebf1f2287b93e200bc8affd02c3e92ac872b3200731a103000000d290530d77935fe173d0d5dcbb2588170f730b423f93543b4f3ceaee06d4e78e2055cc0400000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000070000000000000040548900000000002c00000000000000f000000000000000002d31010000000054366100000000000001040200000080a1990300000000455552da0a00006d050000da0a0000b41500008d2000000000000000000000000000000000000016010000b41500006d0500006400000000943577080000000d3a5365cdd4a6acda275ad18cbec48f1fae9e6f3af5513449d166bffeee68950000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000717d7cc7ddf1ab1c2925b425aa85e608e3284e86607ba1b208f135217743bcb387050558fb7375933168b25cc4bdec2f9ffde43edf36639d6fa758ceea474705",
"hops": [
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b7201000000000000000000366e010000000000c31801000000000400004e2000000000016e360001110001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939023a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f011700000000000000e0040700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0242000000000000004024140000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b6030000000000007094000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366502d80e000000000000c05102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db013a0200000000000040ca22000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db023a0200000000000040ca2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c2c18a1d4c5f9a784336c16e2755ec99f12ba6990cacb81cb23f94aa048d6034013a02000000000000485904000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000011600000000000000781e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000002cd000000000000003cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000001720000000000000098850000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000002580000000000000020670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000f00000000000000008000000a6c37ecf5177e5e8d350fcc7c97ae32f9046bf15337e07d97453cd8dc79f4aa36d8d6332a0f2a20836f88a057863237aabd71abb0b5f41b8fed92fbbf04d851f567827e521b3ea570cdb126bc2c44e7940eb33690b8244e00abee459e82a8302",
"ledger": "0000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff00000002000000000000000000000018600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018566202000000000c2a9846621d91f9309170b29b95e158e278002ce4cc03a2d097917836a391c0ffffffffffffffffffffffffffffffff000000010000000000000000000000d0870c0000000000a087630100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006dde44606fd8377d9fa0809b2b36b708de3e6119d8c1c3d1f0d675917c28c6198096980000000000ffffffffffffffff000000010000000000000000000000709d0b0000000000607a60010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000e004070000000000c8431200000000000000000000000000801b6801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf4a5892c8a1bef2afba730a4ec868d0830f9355d10f540b757205945c588cb5ffffffffffffffffffffffffffffffff000000010000002c720200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d1ee853e5c092553edc99a0cc2468a7307afd83533a3a34f401b0695da343314ffffffffffffffffffffffffffffffff0000000100000020e53600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d436e9616073a533831cfbec646020b5d4dcc9d1df4b792ccaff6e7e23ee686affffffffffffffffffffffffffffffff0000000100000014b400000000000084e022000000000000000000000000000000000000000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e433cf0201e9b443d808e4406925b6a4a90e9963c6235c742a0ecec87a59e478ffffffffffffffff6400000000000000455552010000000000000000000000d0870c0000000000c0f35e010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c1020000000000000000801b68010000000080de8002000000000400003a9800000000016e360001050001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f021700000000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b60300000000000070940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000b2083f6a8d3e1c901b93bfb87f5ffe83344214430e91d08f4e422c5ac7f0c0869f25a4e826f34f770f8f63bcea36ec697bbc398f81c06aa8ad11bb1f7a33323de1911cf31bd046195483ef8a8b22a5778adb66cfde02d9e8f8e46360a5577b06",
"ledger": "b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000c843120000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
],
"descriptors": [
"57434431012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaad6b5a6849f61a8bbd9d4db526a9fc1c73ef949590cdcd9b0b65ce6375262d9bd0000000000000000a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10005d00201101010010108000100086665617475726564056f72646572000000000000000000000280bb0000046761696e0370616e0c0c13afc5df1dffed9f80c566f13be392cc90ad1ceba5db69473a7993f8a7ff2e64cb5d094b2fdf651392e4a2d4f317cc6a436ab9b0cd5afe626493549677937002"
],
"compositions": [
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f000127a00000000008a826f7851f7a89f5ecf581141292c7f69e9d004ed08b01d25dd3c66e30f43f602540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f08000000000000000000000000000000c264cd78485650e03db23e16774c0566ac52d21be1b9edacaad39fa34dc3100c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593908000000000000000000000000000000e43f0568538eef406fb5ed7901b131c0b789d9c0fdb04d61d590af9bec169a1dd2ed35929ea1d408e2da06ee1c840bacab38929016f1a7ba820a80472485c9c6159b98672266a380458e260007c2baec97749e4941eb6ac370d1660fb0e5f900",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04054890000000000ee27588426463e6a765b4b8b5791670a7ad61be12ef1995102f9220928d6038d02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f090000000000000000000000000000005e05448cb224cb6aa2726e4c0fe448b127e032169bd2ffea55b4693d53ef487f01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593909000000000000000000000000000000f993d88a6b1a587e0980017103aa4fcdbdf1d7b1c948f95a8bfa36a26493bb1e89b7c717020a8a296d3688d7988d8f2e70e54228d60c58514564a12937b2bb9421199fdee27ccf49acb52a32fa0f698b3eb5b28c35c47a797ae5e7d531f7db0a",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f08096980000000000d8bb5ef530c1fe8d1a0478d5ae231aac802d9f99dff906a66aa111420948048a02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0a000000000000000000000000000000539e2bb145135d7de5b00ebf1b1178f83ef60d1b5ceedabf98bbb424a1d0a96301e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390a00000000000000000000000000000031a3cdcd31f70593af11aaf7bc4d22cc1ec5c18ee0d918fb2edd5a9c98b5a4758e74da34c2fb00e235278a3010c44dac99131fa49acc910e04f3929e7a6014833b553fad2cd31d127ad862b76999324556e76388192020c8022db9db6cafb803",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0d8a70000000000c65182e1c2868d42d9c1a244545f3c3416b14e481ade5c42b1979419f360b3e502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0b000000000000000000000000000000491fe4bc5c4bafa0066ec9c2baf4b4287fcd09a778e36d5996a97ab827b7eaae01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390b000000000000000000000000000000414a7eb6f2353358646d5b861979c533b344cc01fb9dafdb9d6518c4ad7e3f6f01a03c915891dac04736b7858fdf34c4137e10f593ab5cb3a64900914b360f6b242ee3ead8c5499f325411942dbe827bd51176f2b049c4453a654c49dc701b09",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0001bb70000000000381618c2dfecc2dea3881cb1d4ed69e43dec01f07ebda92e08cd08ff935ed44502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0c0000000000000000000000000000001915289342627f7f5a8bf4ccaf04c0418735ae65e41ca8692ebd3fca27ea764b01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390c000000000000000000000000000000194d05dbb7218051137b7537fcb320653e7f67071a19ae363969aee47cf08bede3b8b1012f1fc03a132f8793e0eae4389c23da524a7f6482a1d5cdf26ff04249cb81d4f19d8b164ec8eb381c66ba2704c57c7bb6f2389114f14bbfd632199302",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0405dc600000000007629d9fb8d6ac9e846d7ebdbe0a3a64b29ad51f7e4db40be5f96e447abcf6f0202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0d000000000000000000000000000000f98b3489983bd0476673dc05db4f09c092411512ed2da6578e9c254eee0d72f501e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390d000000000000000000000000000000bc87dab2315f8ea4f21a4b5adcb838b8d89f6d67f93bbdf5bf175069ad81fa5d123e5e3721d1b781cfcc4650a2d571df1142f2160b377be26b692636e885cd037553b9041ac8f13b1f2064568957993ff69556b8673f0ad6e6caafd8b4887c07",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0809fd50000000000d32fbadccdb9742ffff99344fe07069e2e89b1d7f2d99859ad48336fadacd7c202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0e00000000000000000000000000000053c09229456148744a940ba828e839eca0cf46d7c15e2c3533b4c65dcb0b4ff701e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390e000000000000000000000000000000b5702d26fbc4708196f576c1d46b7fc92471154fe81689707069bcb43173ecedc36859b37258c17a4294d56d36befaa9fbd975ce62e5deb542372aef4c9240b43ba584fc98e7c5a5066e1ba4060e96ca4e50daa116366c3aca9dfccef285f405",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0e1e40000000000fe93ba9f3d79ff2d3aeb01726ac4728adf60676494fbd0e596f5972be5f4639102540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0f000000000000000000000000000000341b9ec871ad41102a671b153ccd014f32cfe167bf5cca3989470a574196d0a401e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390f0000000000000000000000000000001eafd27ec73cf8676b02ebb4fa82a5bc67b2298a442322797db8cd996f8866d7bfdca1dadbfc723c27a3b6963a74c78570a41403a9cb454643631157e8bddbda8ccd54062aeec216b94406595aeb140b59afd5e697733f45980831357c0bc804",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f00024f4000000000092a195ef7c5e97512b5eed1dd778db0bd1f1c75cf8cf25b61ec800296e459e6302540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f10000000000000000000000000000000c693ed9bd4639c29ceb962b071e0531512929a139ebed7c5e12989d28b316ddc01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593910000000000000000000000000000000c5043e90fe94a06a8b47242bb084c7dc5f8e46aebbe9b2c5a8db30d44d1a8b55ab981922c453fc2c78b58f7ec56293bb26d02c335079fdd1e246e33607ef473f513bd939cfe20da8b94af0963156c7218be49ffd1efbc9c14429fddf91bf5006",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04066030100000000d00e25bb474153c30c4bf88f0fb890a0dd10342388c0be87aa93399d54bbaf5502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f110000000000000000000000000000005f50d92c07b2f57953a701633e9d59575664bfe9b13644d5db11c5d04629b08c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593911000000000000000000000000000000339aa736ed16c43427079481a1b58bb9d370f104bbcba7a9d915ba1528b1a75da2dabf39b4d5d2518a8bef4340b8c5e7b32250ac222cc3e8648a42730b81eeac78da15f7fa7f42f7bb131311eb22aa7f89ada8940b2dd41c18c113a694a50808"
],
"takes": [
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaae57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f559425465004500032d6893f980a7dad483af70b4e116a10b7caa4e2052593699b2c4d5251f98cf34d11f86821c2cd542f6bbf5152a07ad54b271f65b25309018a0bf0e356fbe70268d3166f7ac16582f4cde87f1ce46d109e4bd0562c3d436da0f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba1307020a0000000000000000000000807f612a01000000c0c1702a01000000300000000000000037090d56c333ad6c4392ffbb8f80b7854c7ae3d562e182d6b76685f99db2ec1f0100000000000000c0c1702a010000000004802a0100000055000000000000003b9b3da8c3121692dbe9b75ff7b37a77773cf7d71d8b7c885253d5b3855c53b902000000000000000004802a0100000040468f2a010000003a0000000000000097f5ee9829746c16c29946a44489846e4a385eb6d62804ba22a18a99a92642ab030000000000000040468f2a0100000080889e2a010000005f000000000000004bb0df0483b6370199dc9196f695ec65b029d8ffb09dfd7ec24bf5d5c59a72f3040000000000000080889e2a01000000c0caad2a010000004400000000000000733b22d5d80b9a80443b60a14334000a8ab43f139364fa9cd3a547e8a7c523640500000000000000c0caad2a01000000000dbd2a0100000069000000000000009395a39fcaaab116f14a5e4f1e51abe694d712e0f912827045471c6283730a480600000000000000000dbd2a01000000404fcc2a010000004e000000000000002b869dfefc2fe0873e06d23c09f1629470a788c6b8f2aa8e6aafb1298cd73fe60700000000000000404fcc2a010000008091db2a0100000033000000000000009f10cb7a3feccf63f5c0c398375eaf998628d314dc8159d15845fe5a0f8a006b08000000000000008091db2a01000000c0d3ea2a0100000058000000000000008b03281ec96d96d0e76e48ef184b7a60d4856eddf77629af961fe54746404ef60900000000000000c0d3ea2a010000000016fa2a010000003d000000000000001fb42eb87b4ea4dc72d02c9e6ea82aab91be4bbef122892b8921e2f552a462678883ea090e9f374f91bacaac0864af7b6dafa57c4f8a3ac0040ec7fc669f1f2506000000d7b83f99f213619227fbccadf7069c3f6cffca8e86f2aba6ffb8d15212698fa51be10b08c6e09a1b9314f45c65bf3c9b6bd507cfa5df3f013d50657245ee48c5602f2236c9c17b8a1cca6bae144f8f162627d27095a7256e1c5b8bc199c84fb3d535c9375be68b107355dcfeaab2a78192ba09570642f09de57b7bab1438eadc703cab1c2158dee2277fea9a9b909df1f7926a3a56996b7db1bfbaaba40ffa2970412cc3dc8306ba8f88774f1a622d1bf21b9ef51fb85b990d279f66fed977340159425465004500032d6893f980a7dad483af70b4e116a10b7caa4e205259369900000000000000000000000000000000000000000000000000000000000000001f250bafd7daed705bd36f8138b7419700ffe4c20991a2a46993dfd25b592a3643977ddbb511d2b51471498d51135c1a724a069a507032f2a3ff93aaa9787f0d",
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa7d3b3098d82fddc8585faa3e84b8f2c13cb7fc723a01bfb8bbacfba4ed388eee2b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c6b7091c801fbc133089d9612f76d94401ffd0199edd399f9db0beaf3e8a611d45859f57a4fec068477f3a45544119f5202eaf32ee3796401d6319ec7b1bf692810f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba130702080000000000000000000000405489000000000010679c000000000030000000000000008fae533ea900dd435a27c7ac6bbe43a78ca53f4ac8bf916accd92a8d374cba94010000000000000010679c0000000000e079af000000000055000000000000006cb63b81a4b11a270dfd91c37eaaed324b1b55999789174218e9990aa186455f0200000000000000e079af0000000000b08cc200000000003a00000000000000e515ec9e690d2bf9be7ce9bd8547b84406445c82682346f3cf84ccb305cf4ae50300000000000000b08cc20000000000809fd500000000005f000000000000008ab19bbbd6173bd1339419c327fadbf9337f7d77aff9e7082ee68da5132756c80400000000000000809fd5000000000050b2e80000000000440000000000000017fca124fd9c32581a3a6f032bc985481cef9852f5f07ba3120fd851511d20ba050000000000000050b2e8000000000020c5fb00000000006900000000000000be2b55d6011931c758ef4700f002f24a947e8e95428049653bc0fc44055b5563060000000000000020c5fb0000000000f0d70e01000000004e0000000000000083da93c67f107a2933b1befbdaaf558a44b2adf41cd3e085f1d48767e8dc3fde0700000000000000f0d70e0100000000c0ea2101000000003300000000000000989f78372ee677d01a1a45c0f4e33989e1e30935ef1f70e0426de38e2cb5ae8a5b8ad24298b11d4afe4acabf254c55d99e1badc4d20a3f293fef3c8e9870e6eb060000000571c24d887ed7ee13662e1b6b5916b05446674f04a48bb9657a88aa791aa4f78c7c1730862c2541afc6faf68f8f1a4b34c99f6c9ea331b8be3a60e97734ac814bcc3352c9690a6b19b9aaa4301286ce746571ec9a1dfd777ce3cdb1ab47ca1ea6e6c44a5d482193b91fac6a4b86aae708a8fe967079b4dc31dbe6e7da840cab0b041b0865ddff65ae4f592bac31b3e3627cb44ed3bfd4678f33dfcdd03c9e684fa04199a174e37753164a4ac950986de9a4abcc3c2cdce827249a9f063289b8012b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c600000000000000000000000000000000000000000000000000000000000000008fdd3b426a73d6a2e5ba04781aafc491c5950f29be46baca5bc57f7e54ba95b22dee29ffae444fb5eeee0e941d74e4e71f8dffbe4f45979a1e8c49aebc023c0c"
],
"logs": "[[{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":0,\"ts_ms\":1792108800500,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":1,\"ts_ms\":1792108801100,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":2,\"ts_ms\":1792108822500,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":0,\"ts_ms\":1792108800510,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":1,\"ts_ms\":1792108801147,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":2,\"ts_ms\":1792108805510,\"event\":\"stall-start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":3,\"ts_ms\":1792108805910,\"event\":\"stall-end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":4,\"ts_ms\":1792108822510,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":0,\"ts_ms\":1792108800520,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":1,\"ts_ms\":1792108801194,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":2,\"ts_ms\":1792108822520,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}]]",
"outcome": "refuse",
"reason": "ceiling_mismatch",
"at": [
"ceilings"
]
},
{
"name": "bytes_priced_with_no_tariff",
"about": "a receipt of version 2 with no tariff that prices bytes out",
"room": "lesson",
"receipt": "574d5231022c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa8be2039bc5d06e7cf9d0188c3f22c659f1687b08b69b97f50b134552c46da783000000000000000000366e0100000000028070e1923387154875aacbeb894c20dc919c95dd4128758b5d47df86652c4a21a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10280de8002000000000400003a9800000000016e360001c3193ab4736af61acb0b4177aa6ec8fa8ee0924b3f727e9f21eb20b9dc72ef2d0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b720100c31801000000000400004e2000000000016e3600010265b94ec2ec2932104a7583c08050f61547332fe434464d598cad6464025b3040b598d58ceff895b484641dfdea9bf329f40b464895eb31b2271a83af17cce7fb0a000000d2e2268edd1bfc8a0dbebf1f2287b93e200bc8affd02c3e92ac872b3200731a103000000d290530d77935fe173d0d5dcbb2588170f730b423f93543b4f3ceaee06d4e78e2055cc0400000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000070000000000000040548900000000002c00000000000000f000000000000000002d31010000000054366100000000000001040200000080a199030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000800000024414336db9dfed2a78649f5bd05555889f8f2d50a92d22e84b831fa1fd59f2c0000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b0500cf7cc23f94451cba243242772595a107430f96727a4516d0e908944eeb38efdcf2b69dc3b2b457456dd01f60dbbcb5fa2e9987dd801f47094d70e542408",
"hops": [
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b7201000000000000000000366e010000000000c31801000000000400004e2000000000016e360001110001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939023a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f011700000000000000e0040700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0242000000000000004024140000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b6030000000000007094000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366502d80e000000000000c05102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db013a0200000000000040ca22000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db023a0200000000000040ca2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c2c18a1d4c5f9a784336c16e2755ec99f12ba6990cacb81cb23f94aa048d6034013a02000000000000485904000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000011600000000000000781e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000002cd000000000000003cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000001720000000000000098850000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000002580000000000000020670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000f00000000000000008000000a6c37ecf5177e5e8d350fcc7c97ae32f9046bf15337e07d97453cd8dc79f4aa36d8d6332a0f2a20836f88a057863237aabd71abb0b5f41b8fed92fbbf04d851f567827e521b3ea570cdb126bc2c44e7940eb33690b8244e00abee459e82a8302",
"ledger": "0000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff00000002000000000000000000000018600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018566202000000000c2a9846621d91f9309170b29b95e158e278002ce4cc03a2d097917836a391c0ffffffffffffffffffffffffffffffff000000010000000000000000000000d0870c0000000000a087630100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006dde44606fd8377d9fa0809b2b36b708de3e6119d8c1c3d1f0d675917c28c6198096980000000000ffffffffffffffff000000010000000000000000000000709d0b0000000000607a60010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000e004070000000000c8431200000000000000000000000000801b6801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf4a5892c8a1bef2afba730a4ec868d0830f9355d10f540b757205945c588cb5ffffffffffffffffffffffffffffffff000000010000002c720200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d1ee853e5c092553edc99a0cc2468a7307afd83533a3a34f401b0695da343314ffffffffffffffffffffffffffffffff0000000100000020e53600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d436e9616073a533831cfbec646020b5d4dcc9d1df4b792ccaff6e7e23ee686affffffffffffffffffffffffffffffff0000000100000014b400000000000084e022000000000000000000000000000000000000000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e433cf0201e9b443d808e4406925b6a4a90e9963c6235c742a0ecec87a59e478ffffffffffffffff6400000000000000455552010000000000000000000000d0870c0000000000c0f35e010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c1020000000000000000801b68010000000080de8002000000000400003a9800000000016e360001050001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f021700000000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b60300000000000070940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000b2083f6a8d3e1c901b93bfb87f5ffe83344214430e91d08f4e422c5ac7f0c0869f25a4e826f34f770f8f63bcea36ec697bbc398f81c06aa8ad11bb1f7a33323de1911cf31bd046195483ef8a8b22a5778adb66cfde02d9e8f8e46360a5577b06",
"ledger": "b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000c843120000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
],
"descriptors": [
"57434431012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaad6b5a6849f61a8bbd9d4db526a9fc1c73ef949590cdcd9b0b65ce6375262d9bd0000000000000000a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10005d00201101010010108000100086665617475726564056f72646572000000000000000000000280bb0000046761696e0370616e0c0c13afc5df1dffed9f80c566f13be392cc90ad1ceba5db69473a7993f8a7ff2e64cb5d094b2fdf651392e4a2d4f317cc6a436ab9b0cd5afe626493549677937002"
],
"compositions": [
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f000127a00000000008a826f7851f7a89f5ecf581141292c7f69e9d004ed08b01d25dd3c66e30f43f602540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f08000000000000000000000000000000c264cd78485650e03db23e16774c0566ac52d21be1b9edacaad39fa34dc3100c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593908000000000000000000000000000000e43f0568538eef406fb5ed7901b131c0b789d9c0fdb04d61d590af9bec169a1dd2ed35929ea1d408e2da06ee1c840bacab38929016f1a7ba820a80472485c9c6159b98672266a380458e260007c2baec97749e4941eb6ac370d1660fb0e5f900",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04054890000000000ee27588426463e6a765b4b8b5791670a7ad61be12ef1995102f9220928d6038d02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f090000000000000000000000000000005e05448cb224cb6aa2726e4c0fe448b127e032169bd2ffea55b4693d53ef487f01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593909000000000000000000000000000000f993d88a6b1a587e0980017103aa4fcdbdf1d7b1c948f95a8bfa36a26493bb1e89b7c717020a8a296d3688d7988d8f2e70e54228d60c58514564a12937b2bb9421199fdee27ccf49acb52a32fa0f698b3eb5b28c35c47a797ae5e7d531f7db0a",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f08096980000000000d8bb5ef530c1fe8d1a0478d5ae231aac802d9f99dff906a66aa111420948048a02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0a000000000000000000000000000000539e2bb145135d7de5b00ebf1b1178f83ef60d1b5ceedabf98bbb424a1d0a96301e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390a00000000000000000000000000000031a3cdcd31f70593af11aaf7bc4d22cc1ec5c18ee0d918fb2edd5a9c98b5a4758e74da34c2fb00e235278a3010c44dac99131fa49acc910e04f3929e7a6014833b553fad2cd31d127ad862b76999324556e76388192020c8022db9db6cafb803",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0d8a70000000000c65182e1c2868d42d9c1a244545f3c3416b14e481ade5c42b1979419f360b3e502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0b000000000000000000000000000000491fe4bc5c4bafa0066ec9c2baf4b4287fcd09a778e36d5996a97ab827b7eaae01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390b000000000000000000000000000000414a7eb6f2353358646d5b861979c533b344cc01fb9dafdb9d6518c4ad7e3f6f01a03c915891dac04736b7858fdf34c4137e10f593ab5cb3a64900914b360f6b242ee3ead8c5499f325411942dbe827bd51176f2b049c4453a654c49dc701b09",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0001bb70000000000381618c2dfecc2dea3881cb1d4ed69e43dec01f07ebda92e08cd08ff935ed44502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0c0000000000000000000000000000001915289342627f7f5a8bf4ccaf04c0418735ae65e41ca8692ebd3fca27ea764b01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390c000000000000000000000000000000194d05dbb7218051137b7537fcb320653e7f67071a19ae363969aee47cf08bede3b8b1012f1fc03a132f8793e0eae4389c23da524a7f6482a1d5cdf26ff04249cb81d4f19d8b164ec8eb381c66ba2704c57c7bb6f2389114f14bbfd632199302",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0405dc600000000007629d9fb8d6ac9e846d7ebdbe0a3a64b29ad51f7e4db40be5f96e447abcf6f0202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0d000000000000000000000000000000f98b3489983bd0476673dc05db4f09c092411512ed2da6578e9c254eee0d72f501e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390d000000000000000000000000000000bc87dab2315f8ea4f21a4b5adcb838b8d89f6d67f93bbdf5bf175069ad81fa5d123e5e3721d1b781cfcc4650a2d571df1142f2160b377be26b692636e885cd037553b9041ac8f13b1f2064568957993ff69556b8673f0ad6e6caafd8b4887c07",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0809fd50000000000d32fbadccdb9742ffff99344fe07069e2e89b1d7f2d99859ad48336fadacd7c202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0e00000000000000000000000000000053c09229456148744a940ba828e839eca0cf46d7c15e2c3533b4c65dcb0b4ff701e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390e000000000000000000000000000000b5702d26fbc4708196f576c1d46b7fc92471154fe81689707069bcb43173ecedc36859b37258c17a4294d56d36befaa9fbd975ce62e5deb542372aef4c9240b43ba584fc98e7c5a5066e1ba4060e96ca4e50daa116366c3aca9dfccef285f405",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0e1e40000000000fe93ba9f3d79ff2d3aeb01726ac4728adf60676494fbd0e596f5972be5f4639102540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0f000000000000000000000000000000341b9ec871ad41102a671b153ccd014f32cfe167bf5cca3989470a574196d0a401e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390f0000000000000000000000000000001eafd27ec73cf8676b02ebb4fa82a5bc67b2298a442322797db8cd996f8866d7bfdca1dadbfc723c27a3b6963a74c78570a41403a9cb454643631157e8bddbda8ccd54062aeec216b94406595aeb140b59afd5e697733f45980831357c0bc804",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f00024f4000000000092a195ef7c5e97512b5eed1dd778db0bd1f1c75cf8cf25b61ec800296e459e6302540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f10000000000000000000000000000000c693ed9bd4639c29ceb962b071e0531512929a139ebed7c5e12989d28b316ddc01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593910000000000000000000000000000000c5043e90fe94a06a8b47242bb084c7dc5f8e46aebbe9b2c5a8db30d44d1a8b55ab981922c453fc2c78b58f7ec56293bb26d02c335079fdd1e246e33607ef473f513bd939cfe20da8b94af0963156c7218be49ffd1efbc9c14429fddf91bf5006",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04066030100000000d00e25bb474153c30c4bf88f0fb890a0dd10342388c0be87aa93399d54bbaf5502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f110000000000000000000000000000005f50d92c07b2f57953a701633e9d59575664bfe9b13644d5db11c5d04629b08c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593911000000000000000000000000000000339aa736ed16c43427079481a1b58bb9d370f104bbcba7a9d915ba1528b1a75da2dabf39b4d5d2518a8bef4340b8c5e7b32250ac222cc3e8648a42730b81eeac78da15f7fa7f42f7bb131311eb22aa7f89ada8940b2dd41c18c113a694a50808"
],
"takes": [
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaae57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f559425465004500032d6893f980a7dad483af70b4e116a10b7caa4e2052593699b2c4d5251f98cf34d11f86821c2cd542f6bbf5152a07ad54b271f65b25309018a0bf0e356fbe70268d3166f7ac16582f4cde87f1ce46d109e4bd0562c3d436da0f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba1307020a0000000000000000000000807f612a01000000c0c1702a01000000300000000000000037090d56c333ad6c4392ffbb8f80b7854c7ae3d562e182d6b76685f99db2ec1f0100000000000000c0c1702a010000000004802a0100000055000000000000003b9b3da8c3121692dbe9b75ff7b37a77773cf7d71d8b7c885253d5b3855c53b902000000000000000004802a0100000040468f2a010000003a0000000000000097f5ee9829746c16c29946a44489846e4a385eb6d62804ba22a18a99a92642ab030000000000000040468f2a0100000080889e2a010000005f000000000000004bb0df0483b6370199dc9196f695ec65b029d8ffb09dfd7ec24bf5d5c59a72f3040000000000000080889e2a01000000c0caad2a010000004400000000000000733b22d5d80b9a80443b60a14334000a8ab43f139364fa9cd3a547e8a7c523640500000000000000c0caad2a01000000000dbd2a0100000069000000000000009395a39fcaaab116f14a5e4f1e51abe694d712e0f912827045471c6283730a480600000000000000000dbd2a01000000404fcc2a010000004e000000000000002b869dfefc2fe0873e06d23c09f1629470a788c6b8f2aa8e6aafb1298cd73fe60700000000000000404fcc2a010000008091db2a0100000033000000000000009f10cb7a3feccf63f5c0c398375eaf998628d314dc8159d15845fe5a0f8a006b08000000000000008091db2a01000000c0d3ea2a0100000058000000000000008b03281ec96d96d0e76e48ef184b7a60d4856eddf77629af961fe54746404ef60900000000000000c0d3ea2a010000000016fa2a010000003d000000000000001fb42eb87b4ea4dc72d02c9e6ea82aab91be4bbef122892b8921e2f552a462678883ea090e9f374f91bacaac0864af7b6dafa57c4f8a3ac0040ec7fc669f1f2506000000d7b83f99f213619227fbccadf7069c3f6cffca8e86f2aba6ffb8d15212698fa51be10b08c6e09a1b9314f45c65bf3c9b6bd507cfa5df3f013d50657245ee48c5602f2236c9c17b8a1cca6bae144f8f162627d27095a7256e1c5b8bc199c84fb3d535c9375be68b107355dcfeaab2a78192ba09570642f09de57b7bab1438eadc703cab1c2158dee2277fea9a9b909df1f7926a3a56996b7db1bfbaaba40ffa2970412cc3dc8306ba8f88774f1a622d1bf21b9ef51fb85b990d279f66fed977340159425465004500032d6893f980a7dad483af70b4e116a10b7caa4e205259369900000000000000000000000000000000000000000000000000000000000000001f250bafd7daed705bd36f8138b7419700ffe4c20991a2a46993dfd25b592a3643977ddbb511d2b51471498d51135c1a724a069a507032f2a3ff93aaa9787f0d",
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa7d3b3098d82fddc8585faa3e84b8f2c13cb7fc723a01bfb8bbacfba4ed388eee2b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c6b7091c801fbc133089d9612f76d94401ffd0199edd399f9db0beaf3e8a611d45859f57a4fec068477f3a45544119f5202eaf32ee3796401d6319ec7b1bf692810f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba130702080000000000000000000000405489000000000010679c000000000030000000000000008fae533ea900dd435a27c7ac6bbe43a78ca53f4ac8bf916accd92a8d374cba94010000000000000010679c0000000000e079af000000000055000000000000006cb63b81a4b11a270dfd91c37eaaed324b1b55999789174218e9990aa186455f0200000000000000e079af0000000000b08cc200000000003a00000000000000e515ec9e690d2bf9be7ce9bd8547b84406445c82682346f3cf84ccb305cf4ae50300000000000000b08cc20000000000809fd500000000005f000000000000008ab19bbbd6173bd1339419c327fadbf9337f7d77aff9e7082ee68da5132756c80400000000000000809fd5000000000050b2e80000000000440000000000000017fca124fd9c32581a3a6f032bc985481cef9852f5f07ba3120fd851511d20ba050000000000000050b2e8000000000020c5fb00000000006900000000000000be2b55d6011931c758ef4700f002f24a947e8e95428049653bc0fc44055b5563060000000000000020c5fb0000000000f0d70e01000000004e0000000000000083da93c67f107a2933b1befbdaaf558a44b2adf41cd3e085f1d48767e8dc3fde0700000000000000f0d70e0100000000c0ea2101000000003300000000000000989f78372ee677d01a1a45c0f4e33989e1e30935ef1f70e0426de38e2cb5ae8a5b8ad24298b11d4afe4acabf254c55d99e1badc4d20a3f293fef3c8e9870e6eb060000000571c24d887ed7ee13662e1b6b5916b05446674f04a48bb9657a88aa791aa4f78c7c1730862c2541afc6faf68f8f1a4b34c99f6c9ea331b8be3a60e97734ac814bcc3352c9690a6b19b9aaa4301286ce746571ec9a1dfd777ce3cdb1ab47ca1ea6e6c44a5d482193b91fac6a4b86aae708a8fe967079b4dc31dbe6e7da840cab0b041b0865ddff65ae4f592bac31b3e3627cb44ed3bfd4678f33dfcdd03c9e684fa04199a174e37753164a4ac950986de9a4abcc3c2cdce827249a9f063289b8012b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c600000000000000000000000000000000000000000000000000000000000000008fdd3b426a73d6a2e5ba04781aafc491c5950f29be46baca5bc57f7e54ba95b22dee29ffae444fb5eeee0e941d74e4e71f8dffbe4f45979a1e8c49aebc023c0c"
],
"logs": "[[{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":0,\"ts_ms\":1792108800500,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":1,\"ts_ms\":1792108801100,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":2,\"ts_ms\":1792108822500,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":0,\"ts_ms\":1792108800510,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":1,\"ts_ms\":1792108801147,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":2,\"ts_ms\":1792108805510,\"event\":\"stall-start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":3,\"ts_ms\":1792108805910,\"event\":\"stall-end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":4,\"ts_ms\":1792108822510,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":0,\"ts_ms\":1792108800520,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":1,\"ts_ms\":1792108801194,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":2,\"ts_ms\":1792108822520,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}]]",
"outcome": "refuse",
"reason": "session_invalid",
"at": [
"receipt"
]
},
{
"name": "receipt_unknown_version",
"about": "a receipt of version 3: the lesson's with its fifth byte 3, signed again",
"room": "lesson",
"receipt": "574d5231032c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa8be2039bc5d06e7cf9d0188c3f22c659f1687b08b69b97f50b134552c46da783000000000000000000366e0100000000028070e1923387154875aacbeb894c20dc919c95dd4128758b5d47df86652c4a21a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10280de8002000000000400003a9800000000016e360001c3193ab4736af61acb0b4177aa6ec8fa8ee0924b3f727e9f21eb20b9dc72ef2d0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b720100c31801000000000400004e2000000000016e3600010265b94ec2ec2932104a7583c08050f61547332fe434464d598cad6464025b3040b598d58ceff895b484641dfdea9bf329f40b464895eb31b2271a83af17cce7fb0a000000d2e2268edd1bfc8a0dbebf1f2287b93e200bc8affd02c3e92ac872b3200731a103000000d290530d77935fe173d0d5dcbb2588170f730b423f93543b4f3ceaee06d4e78e2055cc0400000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000070000000000000040548900000000002c00000000000000f000000000000000002d3101000000000001040200000080a1990300000000455552da0a00006d050000da0a0000b41500008d2000000000000000000000000000000000000016010000b41500006d05000064000000080000000d3a5365cdd4a6acda275ad18cbec48f1fae9e6f3af5513449d166bffeee68950000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d91f7a28e842c4c6fd6df0f08bf5162be2cc80280aeaa9417fb671a36164352ccee121738ae1c115ebcc712dd5235451bcd57410226ee54b986ff33bcf2b3608",
"hops": [
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa0a29cf514deebfd5ca029b1d658b198bade764af98e09848c6fd53b129678b7201000000000000000000366e010000000000c31801000000000400004e2000000000016e360001110001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939023a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d02d80e00000000000000470900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f011700000000000000e0040700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0242000000000000004024140000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b6030000000000007094000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366502d80e000000000000c05102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db013a0200000000000040ca22000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019d7a22d6b2f60ddeaf9f0b43a147004709d988a73527b65d08e0ea6372f910db023a0200000000000040ca2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c2c18a1d4c5f9a784336c16e2755ec99f12ba6990cacb81cb23f94aa048d6034013a02000000000000485904000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000011600000000000000781e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000002cd000000000000003cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000001720000000000000098850000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000002580000000000000020670000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000c0f5220400000000801b680100000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000001856620200000000f00000000000000008000000a6c37ecf5177e5e8d350fcc7c97ae32f9046bf15337e07d97453cd8dc79f4aa36d8d6332a0f2a20836f88a057863237aabd71abb0b5f41b8fed92fbbf04d851f567827e521b3ea570cdb126bc2c44e7940eb33690b8244e00abee459e82a8302",
"ledger": "0000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffff00000002000000000000000000000018600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018566202000000000c2a9846621d91f9309170b29b95e158e278002ce4cc03a2d097917836a391c0ffffffffffffffffffffffffffffffff000000010000000000000000000000d0870c0000000000a087630100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006dde44606fd8377d9fa0809b2b36b708de3e6119d8c1c3d1f0d675917c28c6198096980000000000ffffffffffffffff000000010000000000000000000000709d0b0000000000607a60010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000e004070000000000c8431200000000000000000000000000801b6801000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf4a5892c8a1bef2afba730a4ec868d0830f9355d10f540b757205945c588cb5ffffffffffffffffffffffffffffffff000000010000002c720200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d1ee853e5c092553edc99a0cc2468a7307afd83533a3a34f401b0695da343314ffffffffffffffffffffffffffffffff0000000100000020e53600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d436e9616073a533831cfbec646020b5d4dcc9d1df4b792ccaff6e7e23ee686affffffffffffffffffffffffffffffff0000000100000014b400000000000084e022000000000000000000000000000000000000000000801b680100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e433cf0201e9b443d808e4406925b6a4a90e9963c6235c742a0ecec87a59e478ffffffffffffffff6400000000000000455552010000000000000000000000d0870c0000000000c0f35e010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"record": "574d4831012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c1020000000000000000801b68010000000080de8002000000000400003a9800000000016e360001050001025980e26a212cf4c098b0f8a1a36a15333c6975497e60c073b9cd8242943b4c01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f5939013a02000000000000d80b0d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313ea2ca301a5e17a28cb2d4b11014eaed6c09c8bb27b777993c556d712aa84d01b603000000000000c0510200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f021700000000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000195ab21fa90d692e9fe90bade8cd132b0d17d75f25efa1fab73239d188617366501b60300000000000070940000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000b2083f6a8d3e1c901b93bfb87f5ffe83344214430e91d08f4e422c5ac7f0c0869f25a4e826f34f770f8f63bcea36ec697bbc398f81c06aa8ad11bb1f7a33323de1911cf31bd046195483ef8a8b22a5778adb66cfde02d9e8f8e46360a5577b06",
"ledger": "b9091845dcf9a7fc8e2a12e1e1b2f986888b33fb22f1b42b384cdf47ecfc33a0ffffffffffffffffffffffffffffffff00000001000000c843120000000000e004070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
],
"descriptors": [
"57434431012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaad6b5a6849f61a8bbd9d4db526a9fc1c73ef949590cdcd9b0b65ce6375262d9bd0000000000000000a693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c10005d00201101010010108000100086665617475726564056f72646572000000000000000000000280bb0000046761696e0370616e0c0c13afc5df1dffed9f80c566f13be392cc90ad1ceba5db69473a7993f8a7ff2e64cb5d094b2fdf651392e4a2d4f317cc6a436ab9b0cd5afe626493549677937002"
],
"compositions": [
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f000127a00000000008a826f7851f7a89f5ecf581141292c7f69e9d004ed08b01d25dd3c66e30f43f602540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f08000000000000000000000000000000c264cd78485650e03db23e16774c0566ac52d21be1b9edacaad39fa34dc3100c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593908000000000000000000000000000000e43f0568538eef406fb5ed7901b131c0b789d9c0fdb04d61d590af9bec169a1dd2ed35929ea1d408e2da06ee1c840bacab38929016f1a7ba820a80472485c9c6159b98672266a380458e260007c2baec97749e4941eb6ac370d1660fb0e5f900",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04054890000000000ee27588426463e6a765b4b8b5791670a7ad61be12ef1995102f9220928d6038d02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f090000000000000000000000000000005e05448cb224cb6aa2726e4c0fe448b127e032169bd2ffea55b4693d53ef487f01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593909000000000000000000000000000000f993d88a6b1a587e0980017103aa4fcdbdf1d7b1c948f95a8bfa36a26493bb1e89b7c717020a8a296d3688d7988d8f2e70e54228d60c58514564a12937b2bb9421199fdee27ccf49acb52a32fa0f698b3eb5b28c35c47a797ae5e7d531f7db0a",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f08096980000000000d8bb5ef530c1fe8d1a0478d5ae231aac802d9f99dff906a66aa111420948048a02540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0a000000000000000000000000000000539e2bb145135d7de5b00ebf1b1178f83ef60d1b5ceedabf98bbb424a1d0a96301e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390a00000000000000000000000000000031a3cdcd31f70593af11aaf7bc4d22cc1ec5c18ee0d918fb2edd5a9c98b5a4758e74da34c2fb00e235278a3010c44dac99131fa49acc910e04f3929e7a6014833b553fad2cd31d127ad862b76999324556e76388192020c8022db9db6cafb803",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0d8a70000000000c65182e1c2868d42d9c1a244545f3c3416b14e481ade5c42b1979419f360b3e502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0b000000000000000000000000000000491fe4bc5c4bafa0066ec9c2baf4b4287fcd09a778e36d5996a97ab827b7eaae01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390b000000000000000000000000000000414a7eb6f2353358646d5b861979c533b344cc01fb9dafdb9d6518c4ad7e3f6f01a03c915891dac04736b7858fdf34c4137e10f593ab5cb3a64900914b360f6b242ee3ead8c5499f325411942dbe827bd51176f2b049c4453a654c49dc701b09",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0001bb70000000000381618c2dfecc2dea3881cb1d4ed69e43dec01f07ebda92e08cd08ff935ed44502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0c0000000000000000000000000000001915289342627f7f5a8bf4ccaf04c0418735ae65e41ca8692ebd3fca27ea764b01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390c000000000000000000000000000000194d05dbb7218051137b7537fcb320653e7f67071a19ae363969aee47cf08bede3b8b1012f1fc03a132f8793e0eae4389c23da524a7f6482a1d5cdf26ff04249cb81d4f19d8b164ec8eb381c66ba2704c57c7bb6f2389114f14bbfd632199302",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0405dc600000000007629d9fb8d6ac9e846d7ebdbe0a3a64b29ad51f7e4db40be5f96e447abcf6f0202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0d000000000000000000000000000000f98b3489983bd0476673dc05db4f09c092411512ed2da6578e9c254eee0d72f501e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390d000000000000000000000000000000bc87dab2315f8ea4f21a4b5adcb838b8d89f6d67f93bbdf5bf175069ad81fa5d123e5e3721d1b781cfcc4650a2d571df1142f2160b377be26b692636e885cd037553b9041ac8f13b1f2064568957993ff69556b8673f0ad6e6caafd8b4887c07",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0809fd50000000000d32fbadccdb9742ffff99344fe07069e2e89b1d7f2d99859ad48336fadacd7c202540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0e00000000000000000000000000000053c09229456148744a940ba828e839eca0cf46d7c15e2c3533b4c65dcb0b4ff701e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390e000000000000000000000000000000b5702d26fbc4708196f576c1d46b7fc92471154fe81689707069bcb43173ecedc36859b37258c17a4294d56d36befaa9fbd975ce62e5deb542372aef4c9240b43ba584fc98e7c5a5066e1ba4060e96ca4e50daa116366c3aca9dfccef285f405",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f0c0e1e40000000000fe93ba9f3d79ff2d3aeb01726ac4728adf60676494fbd0e596f5972be5f4639102540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f0f000000000000000000000000000000341b9ec871ad41102a671b153ccd014f32cfe167bf5cca3989470a574196d0a401e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f59390f0000000000000000000000000000001eafd27ec73cf8676b02ebb4fa82a5bc67b2298a442322797db8cd996f8866d7bfdca1dadbfc723c27a3b6963a74c78570a41403a9cb454643631157e8bddbda8ccd54062aeec216b94406595aeb140b59afd5e697733f45980831357c0bc804",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f00024f4000000000092a195ef7c5e97512b5eed1dd778db0bd1f1c75cf8cf25b61ec800296e459e6302540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f10000000000000000000000000000000c693ed9bd4639c29ceb962b071e0531512929a139ebed7c5e12989d28b316ddc01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593910000000000000000000000000000000c5043e90fe94a06a8b47242bb084c7dc5f8e46aebbe9b2c5a8db30d44d1a8b55ab981922c453fc2c78b58f7ec56293bb26d02c335079fdd1e246e33607ef473f513bd939cfe20da8b94af0963156c7218be49ffd1efbc9c14429fddf91bf5006",
"57435231012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaaa693d88d083f28b47f2645ed8b2cff1c4c564f4dbef3f273acf037d6375060c185eb41cedc9e3c326c0cc6087de900fb1cd1f72636e2f81db1070f1675c148f04066030100000000d00e25bb474153c30c4bf88f0fb890a0dd10342388c0be87aa93399d54bbaf5502540adcb6a36dc21f91576d7d44c1a247caa4f09cd659376e20820bd77215fc1f110000000000000000000000000000005f50d92c07b2f57953a701633e9d59575664bfe9b13644d5db11c5d04629b08c01e57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f5159614c2c42bc1b1258aee7faf878dcc0ae9505962d2efde8ce5b156a44f593911000000000000000000000000000000339aa736ed16c43427079481a1b58bb9d370f104bbcba7a9d915ba1528b1a75da2dabf39b4d5d2518a8bef4340b8c5e7b32250ac222cc3e8648a42730b81eeac78da15f7fa7f42f7bb131311eb22aa7f89ada8940b2dd41c18c113a694a50808"
],
"takes": [
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaae57d3fbe7257b2118009296bf654b41c7823e730f86568799b01b442d7a300f559425465004500032d6893f980a7dad483af70b4e116a10b7caa4e2052593699b2c4d5251f98cf34d11f86821c2cd542f6bbf5152a07ad54b271f65b25309018a0bf0e356fbe70268d3166f7ac16582f4cde87f1ce46d109e4bd0562c3d436da0f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba1307020a0000000000000000000000807f612a01000000c0c1702a01000000300000000000000037090d56c333ad6c4392ffbb8f80b7854c7ae3d562e182d6b76685f99db2ec1f0100000000000000c0c1702a010000000004802a0100000055000000000000003b9b3da8c3121692dbe9b75ff7b37a77773cf7d71d8b7c885253d5b3855c53b902000000000000000004802a0100000040468f2a010000003a0000000000000097f5ee9829746c16c29946a44489846e4a385eb6d62804ba22a18a99a92642ab030000000000000040468f2a0100000080889e2a010000005f000000000000004bb0df0483b6370199dc9196f695ec65b029d8ffb09dfd7ec24bf5d5c59a72f3040000000000000080889e2a01000000c0caad2a010000004400000000000000733b22d5d80b9a80443b60a14334000a8ab43f139364fa9cd3a547e8a7c523640500000000000000c0caad2a01000000000dbd2a0100000069000000000000009395a39fcaaab116f14a5e4f1e51abe694d712e0f912827045471c6283730a480600000000000000000dbd2a01000000404fcc2a010000004e000000000000002b869dfefc2fe0873e06d23c09f1629470a788c6b8f2aa8e6aafb1298cd73fe60700000000000000404fcc2a010000008091db2a0100000033000000000000009f10cb7a3feccf63f5c0c398375eaf998628d314dc8159d15845fe5a0f8a006b08000000000000008091db2a01000000c0d3ea2a0100000058000000000000008b03281ec96d96d0e76e48ef184b7a60d4856eddf77629af961fe54746404ef60900000000000000c0d3ea2a010000000016fa2a010000003d000000000000001fb42eb87b4ea4dc72d02c9e6ea82aab91be4bbef122892b8921e2f552a462678883ea090e9f374f91bacaac0864af7b6dafa57c4f8a3ac0040ec7fc669f1f2506000000d7b83f99f213619227fbccadf7069c3f6cffca8e86f2aba6ffb8d15212698fa51be10b08c6e09a1b9314f45c65bf3c9b6bd507cfa5df3f013d50657245ee48c5602f2236c9c17b8a1cca6bae144f8f162627d27095a7256e1c5b8bc199c84fb3d535c9375be68b107355dcfeaab2a78192ba09570642f09de57b7bab1438eadc703cab1c2158dee2277fea9a9b909df1f7926a3a56996b7db1bfbaaba40ffa2970412cc3dc8306ba8f88774f1a622d1bf21b9ef51fb85b990d279f66fed977340159425465004500032d6893f980a7dad483af70b4e116a10b7caa4e205259369900000000000000000000000000000000000000000000000000000000000000001f250bafd7daed705bd36f8138b7419700ffe4c20991a2a46993dfd25b592a3643977ddbb511d2b51471498d51135c1a724a069a507032f2a3ff93aaa9787f0d",
"57544d31012c3f0f62be557631417a492ef4abb5499bf32c6c980dbc9c0539769ecc2bcaaa7d3b3098d82fddc8585faa3e84b8f2c13cb7fc723a01bfb8bbacfba4ed388eee2b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c6b7091c801fbc133089d9612f76d94401ffd0199edd399f9db0beaf3e8a611d45859f57a4fec068477f3a45544119f5202eaf32ee3796401d6319ec7b1bf692810f222415114ff3e773663df797c88d6e11b094c4ffc6694581d843dbfbe8ba130702080000000000000000000000405489000000000010679c000000000030000000000000008fae533ea900dd435a27c7ac6bbe43a78ca53f4ac8bf916accd92a8d374cba94010000000000000010679c0000000000e079af000000000055000000000000006cb63b81a4b11a270dfd91c37eaaed324b1b55999789174218e9990aa186455f0200000000000000e079af0000000000b08cc200000000003a00000000000000e515ec9e690d2bf9be7ce9bd8547b84406445c82682346f3cf84ccb305cf4ae50300000000000000b08cc20000000000809fd500000000005f000000000000008ab19bbbd6173bd1339419c327fadbf9337f7d77aff9e7082ee68da5132756c80400000000000000809fd5000000000050b2e80000000000440000000000000017fca124fd9c32581a3a6f032bc985481cef9852f5f07ba3120fd851511d20ba050000000000000050b2e8000000000020c5fb00000000006900000000000000be2b55d6011931c758ef4700f002f24a947e8e95428049653bc0fc44055b5563060000000000000020c5fb0000000000f0d70e01000000004e0000000000000083da93c67f107a2933b1befbdaaf558a44b2adf41cd3e085f1d48767e8dc3fde0700000000000000f0d70e0100000000c0ea2101000000003300000000000000989f78372ee677d01a1a45c0f4e33989e1e30935ef1f70e0426de38e2cb5ae8a5b8ad24298b11d4afe4acabf254c55d99e1badc4d20a3f293fef3c8e9870e6eb060000000571c24d887ed7ee13662e1b6b5916b05446674f04a48bb9657a88aa791aa4f78c7c1730862c2541afc6faf68f8f1a4b34c99f6c9ea331b8be3a60e97734ac814bcc3352c9690a6b19b9aaa4301286ce746571ec9a1dfd777ce3cdb1ab47ca1ea6e6c44a5d482193b91fac6a4b86aae708a8fe967079b4dc31dbe6e7da840cab0b041b0865ddff65ae4f592bac31b3e3627cb44ed3bfd4678f33dfcdd03c9e684fa04199a174e37753164a4ac950986de9a4abcc3c2cdce827249a9f063289b8012b956ba992dbd2d36d81fc4a4a771b7048b732a2e36ceea2f57448216ef566c600000000000000000000000000000000000000000000000000000000000000008fdd3b426a73d6a2e5ba04781aafc491c5950f29be46baca5bc57f7e54ba95b22dee29ffae444fb5eeee0e941d74e4e71f8dffbe4f45979a1e8c49aebc023c0c"
],
"logs": "[[{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":0,\"ts_ms\":1792108800500,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":1,\"ts_ms\":1792108801100,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"aecd92c612a3b9704723441772d4704e\",\"sn\":2,\"ts_ms\":1792108822500,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":0,\"ts_ms\":1792108800510,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":1,\"ts_ms\":1792108801147,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":2,\"ts_ms\":1792108805510,\"event\":\"stall-start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":3,\"ts_ms\":1792108805910,\"event\":\"stall-end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"b5b91401f0762725dedbee0f0e447680\",\"sn\":4,\"ts_ms\":1792108822510,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}],[{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":0,\"ts_ms\":1792108800520,\"event\":\"start\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":1,\"ts_ms\":1792108801194,\"event\":\"present\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":{\"unit\":0,\"layers\":1,\"first\":true,\"late_ms\":0,\"decode_us\":900},\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null},{\"v\":1,\"sid\":\"59b8375b353f73cd469c9ecaf790f7bb\",\"sn\":2,\"ts_ms\":1792108822520,\"event\":\"end\",\"cid\":null,\"st\":\"ll\",\"sf\":\"o\",\"object\":null,\"present\":null,\"switch\":null,\"layer_change\":null,\"capable\":null,\"cohort\":null,\"metrics\":null,\"claim\":null}]]",
"outcome": "refuse",
"reason": "unknown_version",
"at": [
"receipt"
]
},
{
"name": "receipt_bad_signature",
"about": "the receipt's signature with one bit flipped",

View file

@ -15,9 +15,15 @@ it is; and, under a tariff of version 2 or 3, a media job by route_record_verify
_int_motion_receipt.py (integer-payloads §15). Everything else is here:
- the tariff (§4): reading, rules, both signatures, its scope, its job prices per capability
(version 2) or per capability and operation (version 3);
(version 2) or per capability and operation (versions 3 and 4), and in version 4 its price of
a room's bytes out, of the work units of a package a run made, and of a package's storage by
byte-days (§4.7);
- the invoice (§6): reading, rules, signature, chain, and what a chain bills once (§6.9): in
version 3 a job's work, its route record and hop receipts, and never its output's receipt;
versions 3 and 4 a job's work, its route record and hop receipts, and never its output's
receipt; in version 4 a package's work once per run that made it, its receipt cited by each,
and its storage for each period it is held;
- a job's outputs held to the objects its served hop states it delivered (§5.1), by their
content hashes, computed with ../claim-conformance/_blake3.py;
- what each source contributes (§5.2), its placement in time (§5.3), and the checks across
sources (§7.2 step 6);
- every line, citation, energy row and amount recomputed (§6.2 to §6.6), compared in §7.2 step
@ -60,6 +66,7 @@ import stitched_presentation_verify as STV # noqa: E402
import route_record_verify as RRV # noqa: E402 (it puts ../oer-conformance on the path)
import oer2_verify as OER # noqa: E402
import _int_motion_receipt as IMR # noqa: E402
from _blake3 import blake3 # noqa: E402
from decimal import Decimal, ROUND_HALF_EVEN, localcontext, MAX_PREC, MAX_EMAX, MIN_EMIN # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "receipt-ledger.md")
@ -77,9 +84,13 @@ OER_RECEIPT, VIDEO_RECEIPT, ROUTE_RECORD = 14, 15, 16 # §5.2, version
PRIVATE = 128 # §5.2: 128 to 255, private and provisional use
KIND_NAMES = {ROOM: "room", DELIVERY: "delivery", PACKAGE: "package", ADS: "ads", JOB: "job"}
STITCHED = "stitched" # §5.1: a source kind billed as ads
SCOPES = {1: "room_hops", 2: "delivery_hops", 3: "packaging", 4: "ad_decisions", 5: "jobs"} # §6.6
JOB_UNITS = ["jobs", "subpixels"] # §4.5
JOB_VERSIONS = (2, 3) # §4.5, §4.6: the versions that price jobs
SCOPES = {1: "room_hops", 2: "delivery_hops", 3: "packaging", 4: "ad_decisions", 5: "jobs", 6: "unbilled_hops"} # §6.6
CONTENT_DOMAIN = b"moq-jwp:object\x01" # route-record §2, jwp-receipts §2.1
DAY_US = 86400 * 10 ** 6 # §4.7: a byte-day's day
JOB_UNITS = ["jobs", "subpixels", "work_units"] # §4.5; the third in version 4 (§4.7)
JOB_VERSIONS = (2, 3, 4) # §4.5, §4.6, §4.7: the versions that price jobs
WORK_VERSIONS = (3, 4) # §4.6, §6.9: priced per operation, billed by the work
V4 = 4 # §4.7: bytes out, work units, storage by byte-days
CAPABILITY = re.compile(r"\Awai\.[a-z0-9_.]{0,124}\Z")
OPERATION = re.compile(r"\A[a-z][a-z0-9]*\.[a-z][a-z0-9]*\Z") # §4.6, route-record §2
LABELS = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
@ -92,6 +103,18 @@ UNITS = { # §6.3: name,
PACKAGE: [("packages", 1), ("entries", 1), ("stored_bytes", 10 ** 9)],
ADS: [("presented_impressions", 1), ("billable_impressions", 1)],
}
UNITS_4 = dict(UNITS) # §4.7, §6.3: version 4's
UNITS_4[ROOM] = UNITS[ROOM] + [("bytes_out", 10 ** 9)]
UNITS_4[PACKAGE] = [None, None, ("byte_days", 10 ** 9)] # a package source bills its storage alone
def units(version):
return UNITS_4 if version == V4 else UNITS
def scopes(version):
"""§6.1: the scopes an invoice of `version` states."""
return 6 if version == V4 else 5 if version in JOB_VERSIONS else 4
SKIP = set()
@ -134,7 +157,7 @@ def head(b, magic):
return "malformed"
if len(b) < 5:
return "truncated"
if b[4] not in (1, 2, 3):
if b[4] not in (1, 2, 3, 4):
return "unknown_version"
return None
@ -160,16 +183,18 @@ def read_tariff(b):
r = R(b, 5)
try:
t = {"seller": r.take(32), "buyer": r.take(32), "start": r.u(8), "end": r.u(8), "currency": r.take(3)}
t["room"] = [r.u(4) for _ in range(13)]
# §4.1: version 4 states a fourteenth room price, of bytes out; versions 1 to 3 read it as 0.
t["room"] = [r.u(4) for _ in range(14 if b[4] == V4 else 13)] + ([] if b[4] == V4 else [0])
t["delivery"] = [r.u(8) for _ in range(4)]
t["package"] = [r.u(8) for _ in range(3)]
# §4.1: version 4 states one package price, per 10^9 byte-days held; the others read as 0.
t["package"] = [0, 0, r.u(8)] if b[4] == V4 else [r.u(8) for _ in range(3)]
t["ads"] = [r.u(8) for _ in range(2)]
t["version"] = b[4]
t["job"] = [] # (capability, operation, unit, divisor, price)
if t["version"] in JOB_VERSIONS: # §4.5, §4.6
for _ in range(r.u(2)):
cap = r.take(r.u(1))
op = r.take(r.u(1)) if t["version"] == 3 else b""
op = r.take(r.u(1)) if t["version"] in WORK_VERSIONS else b""
t["job"].append((cap, op, r.u(1), r.u(8), r.u(8)))
t["scope"] = [(r.u(1), r.take(32)) for _ in range(r.u(2))]
body_end = r.p
@ -183,8 +208,9 @@ def read_tariff(b):
cap, op = j[0].decode("utf-8"), j[1].decode("utf-8")
except UnicodeDecodeError:
return False
named = OPERATION.match(op) is not None and len(j[1]) <= 128 if t["version"] == 3 else op == ""
return CAPABILITY.match(cap) is not None and named and 1 <= j[2] <= len(JOB_UNITS) and j[3] >= 1
named = OPERATION.match(op) is not None and len(j[1]) <= 128 if t["version"] in WORK_VERSIONS else op == ""
units_priced = 3 if t["version"] == V4 else 2
return CAPABILITY.match(cap) is not None and named and 1 <= j[2] <= units_priced and j[3] >= 1
top = 5 if t["version"] in JOB_VERSIONS else 4
rules = (t["start"] < t["end"] and capitals(t["currency"]) and t["seller"] != ZERO and t["buyer"] != ZERO
and t["seller"] != t["buyer"] and t["scope"] and ascending(t["scope"])
@ -210,7 +236,7 @@ def price(t, kind, unit):
def divisor(t, kind, unit):
return t["job"][unit - 1][3] if kind == JOB else UNITS[kind][unit - 1][1]
return t["job"][unit - 1][3] if kind == JOB else units(t["version"])[kind][unit - 1][1]
# ---------------------------------------------------------------- §6 the invoice
@ -231,7 +257,8 @@ def read_invoice(b, at):
i["energy"] = [(r.u(1), r.u(1), r.u(4), r.u(8), r.u(4)) for _ in range(r.u(1))]
i["version"] = b[4]
v2 = i["version"] in JOB_VERSIONS
i["unmetered"] = [r.u(4) for _ in range(5 if v2 else 4)] + ([] if v2 else [0])
n = scopes(i["version"])
i["unmetered"] = [r.u(4) for _ in range(n)] + [0] * (6 - n)
i["amount"] = r.u(8)
sig = r.take(64)
except ValueError:
@ -246,12 +273,13 @@ def read_invoice(b, at):
def unit_ok(x):
if x["kind"] == JOB:
return v2 and x["unit"] >= 1
return x["kind"] in UNITS and 1 <= x["unit"] <= len(UNITS[x["kind"]])
u = units(i["version"])
return x["kind"] in u and 1 <= x["unit"] <= len(u[x["kind"]]) and u[x["kind"]][x["unit"] - 1] is not None
rules = (i["start"] < i["end"] and capitals(i["currency"]) and ascending(i["records"])
and all(1 <= k <= 9 or (v2 and OER_RECEIPT <= k <= ROUTE_RECORD) for k, _ in i["records"]) and ascending(keys)
and all(unit_ok(x) and x["quantity"] > 0 and x["cited"] > 0 for x in i["lines"])
and ascending([(e[0], e[1]) for e in i["energy"]])
and all(1 <= e[0] <= (5 if v2 else 4) and 2 <= e[1] <= 5 and e[2] > 0 for e in i["energy"]))
and all(1 <= e[0] <= scopes(i["version"]) and 2 <= e[1] <= 5 and e[2] > 0 for e in i["energy"]))
if not rules and "invoice-rules" not in SKIP:
raise Refuse("invoice_invalid", at)
bad = verify_sig(i["seller"], INVOICE_DOMAIN, b[:-64], sig)
@ -283,9 +311,13 @@ def room_source(s):
d = LRV.read_descriptor(bytes.fromhex(s["room"]["descriptor"]))
rc = LMV.read_receipt(bytes.fromhex(s["receipt"]), d["room"])
u = rc["units"]
units = [u["publisher_us"]] + list(u["subscriber_us"]) + [u["composition_us"], u["recorded_us"], u["events_sent"]]
# A receipt of version 2 states its bytes out and their price (live-metering §4.4, §4.6);
# version 1 states neither, read as 0.
figures = [u["publisher_us"]] + list(u["subscriber_us"]) + [u["composition_us"], u["recorded_us"], u["events_sent"],
u.get("bytes_out", 0)]
prices = list(rc["prices"]) + [0] * (14 - len(rc["prices"]))
return {"kind": ROOM, "digest": rc["digest"], "room": d["room"], "t0": d["t0"], "start": rc["start"], "end": rc["end"],
"units": units, "currency": rc["currency"], "prices": rc["prices"],
"units": figures, "currency": rc["currency"], "prices": prices,
"hops": [(h["record"], figure(h["joules"], h["class"])) for h in rc["hops"]]}
@ -336,7 +368,8 @@ def package_source(s):
tag = TAGS[j["energy_provenance"]]
cls = (tag, j["energy_uncertainty"]["relative_ppm"] if tag in (4, 5) else 0)
return {"kind": PACKAGE, "digest": bytes.fromhex(j["receipt_hash"]), "signer": bytes.fromhex(j["signer_pubkey"]),
"entries": j["total_entries"], "stored": j["stored_bytes"], "figure": figure(j["joules_micro"], cls)}
"entries": j["total_entries"], "stored": j["stored_bytes"],
"figure": figure(j["joules_micro"], cls), "held": None}
def ads_source(s):
@ -445,7 +478,12 @@ def job_figure(r):
return (tag, j, c["relative_ppm"] if tag in (4, 5) else 0)
def job_source(s):
def content_hash(b):
"""route-record §2: an object's content hash (jwp-receipts §2.1)."""
return blake3(CONTENT_DOMAIN + b)
def job_source(s, version):
if set(s) != {"kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"} \
or s["receipts"] is None or s["keys"] is None or not isinstance(s["outputs"], list):
raise Refuse("malformed", [])
@ -465,21 +503,62 @@ def job_source(s):
raise Refuse("job_figure_invalid", ["receipts", j])
billed = (cap.encode("utf-8"), oer_identity(a["receipt"]))
break
outputs = []
unbilled = [] # §6.6, version 4: every hop's figure
if version == V4 and "unbilled-hops" not in SKIP:
for a in rec["attempts"]:
if a["billed"] is True:
continue
j = next(i for i, t in enumerate(texts) if isinstance(t, dict) and t.get("receipt_id") == a["receipt"])
f = job_figure(texts[j])
if f is None:
raise Refuse("job_figure_invalid", ["receipts", j])
unbilled.append(f)
# §5.1: the objects the served hop states it delivered, when its record states them (version
# 2); under version 4 a record that states none holds every output to none.
if rec["version"] == 2:
delivered = {bytes.fromhex(h) for a in rec["attempts"] if a["outcome"] == "served" for h in a["outputs"]}
elif version == V4:
delivered = set()
else:
delivered = None
if "output-delivered" in SKIP:
delivered = None
seen = set()
outputs, packages = [], []
hexs = re.compile(r"(?:[0-9a-f]{2})*")
for i, x in enumerate(s["outputs"]):
if not isinstance(x, dict) or set(x) != {"receipt", "codestream"} or not isinstance(x["receipt"], str) \
or not isinstance(x["codestream"], str) or not re.fullmatch(r"(?:[0-9a-f]{2})*", x["codestream"]):
raise Refuse("malformed", [])
try:
r = IMR.read(x["receipt"])
except IMR.Bad:
raise Refuse("output_invalid", ["outputs", i])
if "output" not in SKIP and IMR.rederived(r, bytes.fromhex(x["codestream"])) is not None:
raise Refuse("output_invalid", ["outputs", i])
outputs.append((IMR.receipt_hash(r), r["work"]))
if version == V4 and isinstance(x, dict) and set(x) == {"package", "indexes"}:
# §4.7: a package a run of `package.build` made, its object its receipt's text.
if not isinstance(x["package"], str) or not isinstance(x["indexes"], list) \
or not all(isinstance(h, str) and hexs.fullmatch(h) for h in x["indexes"]):
raise Refuse("malformed", [])
try:
pr = PV.read_receipt(x["package"].encode("utf-8"))
PV.verify_receipt_v2(pr, [bytes.fromhex(h) for h in x["indexes"]])
except PV.Refused:
raise Refuse("output_invalid", ["outputs", i])
obj = x["package"].encode("utf-8")
packages.append((bytes.fromhex(pr["json"]["receipt_hash"]), pr["json"]["work_units"] if "run-work" not in SKIP else 0))
else:
if not isinstance(x, dict) or set(x) != {"receipt", "codestream"} or not isinstance(x["receipt"], str) \
or not isinstance(x["codestream"], str) or not hexs.fullmatch(x["codestream"]):
raise Refuse("malformed", [])
try:
r = IMR.read(x["receipt"])
except IMR.Bad:
raise Refuse("output_invalid", ["outputs", i])
if "output" not in SKIP and IMR.rederived(r, bytes.fromhex(x["codestream"])) is not None:
raise Refuse("output_invalid", ["outputs", i])
obj = bytes.fromhex(x["codestream"])
outputs.append((IMR.receipt_hash(r), r["work"]))
if delivered is not None:
h = content_hash(obj)
if h not in delivered or h in seen:
raise Refuse("output_not_delivered", ["outputs", i])
seen.add(h)
return {"kind": JOB, "digest": bytes.fromhex(RRV.digest(rec)), "router": bytes.fromhex(rec["signer"]), "job": rec["job"],
"operation": rec["operation"].encode("utf-8"), "accepted": rec["accepted"], "hops": [oer_identity(a["receipt"]) for a in rec["attempts"]], "billed": billed,
"figure": fig, "outputs": outputs}
"figure": fig, "outputs": outputs, "packages": packages, "unbilled": unbilled}
_SOURCES = {}
@ -488,7 +567,8 @@ _SOURCES = {}
def verify_source(s, signers, version=1):
"""§5.1: the source verified by its own extension, a stitched source with `signers`, a job
source under a tariff of version 2 or 3, or Refuse with that extension's place."""
key = json.dumps([s, sorted(k.hex() for k in signers), version in JOB_VERSIONS, sorted(SKIP & {"job-figure", "output"})], sort_keys=True)
key = json.dumps([s, sorted(k.hex() for k in signers), version if isinstance(s, dict) and s.get("kind") == "job" else version in JOB_VERSIONS,
sorted(SKIP & {"job-figure", "output", "output-delivered", "unbilled-hops", "run-work"})], sort_keys=True)
if key not in _SOURCES:
try:
_SOURCES[key] = read_source(s, signers, version)
@ -506,7 +586,7 @@ def read_source(s, signers, version):
if s.get("kind") == "stitched":
return stitched_source(s, signers)
if s.get("kind") == "job" and version in JOB_VERSIONS:
return job_source(s)
return job_source(s, version)
readers = {"room": room_source, "delivery": delivery_source, "package": package_source, "ads": ads_source}
if s.get("kind") not in readers:
raise Refuse("malformed", [])
@ -541,6 +621,7 @@ def contributed(f):
out |= {(STITCHED_CLAIM, c) for c in f["claims"]}
elif f["kind"] == JOB:
out |= {(OER_RECEIPT, h) for h in f["hops"]} | {(VIDEO_RECEIPT, h) for h, _ in f["outputs"]}
out |= {(PACK, h) for h, _ in f["packages"]}
return out
@ -555,7 +636,9 @@ def check_source(t, start, end, f):
if f["kind"] == ROOM:
if not in_scope(t, 1, f["room"]):
return "outside_scope"
if f["currency"] != bytes(3) and (f["currency"] != t["currency"] or f["prices"] != t["room"]) and "conflict" not in SKIP:
# §3: the thirteen prices, and the price of bytes out (§4.7), 0 where either states none.
prices = f["prices"] if "conflict-bytes" not in SKIP else f["prices"][:13] + t["room"][13:]
if f["currency"] != bytes(3) and (f["currency"] != t["currency"] or prices != t["room"]) and "conflict" not in SKIP:
return "tariff_conflict"
if not within(f["t0"] + f["start"], f["t0"] + f["end"]):
return "outside_period"
@ -607,10 +690,23 @@ def bills_again(f, g, version):
def billed_once(version, kind):
"""§6.9: whether a chain bills a record of `kind` once. In version 3 a job's output receipt is
the evidence of what it delivered, which a re-run delivers again; its work, the route record and
the hop receipts, is billed once."""
return not (kind == VIDEO_RECEIPT and (version == 3 or "output-once" in SKIP))
"""§6.9: whether a chain bills a record of `kind` once. In versions 3 and 4 a job's output
receipt is the evidence of what it delivered, which a re-run delivers again; its work, the route
record and the hop receipts, is billed once. In version 4 a package receipt is listed again by
each invoice that bills its storage (§4.7)."""
if kind == PACK and version == V4 and "package-again" not in SKIP:
return False
return not (kind == VIDEO_RECEIPT and (version in WORK_VERSIONS or "output-once" in SKIP))
def hold(t, facts, previous, start, end):
"""§4.7: under version 4, a package the previous invoice lists is held through [start, end)."""
if t["version"] != V4:
return
held = {i for k, i in (previous["records"] if previous else []) if k == PACK}
for k, f in enumerate(facts):
if f["kind"] == PACKAGE: # a copy: a source's facts are shared by every vector that gives it
facts[k] = dict(f, held=end - start if f["digest"] in held and "storage-held" not in SKIP else None)
def check_across(facts, previous, version=1):
@ -660,7 +756,7 @@ def compute(t, facts):
e[1] |= set(cites)
for f in facts:
if f["kind"] == ROOM:
for unit in range(1, 14):
for unit in range(1, 15 if t["version"] == V4 and "bytes-out" not in SKIP else 14):
add(ROOM, f["room"], unit, f["units"][unit - 1], [(SESSION, f["digest"])])
elif f["kind"] == DELIVERY:
st = f["statement"]
@ -672,6 +768,9 @@ def compute(t, facts):
cites += [(ACK, a["digest"]) for a in f["acks"]
if a["mode"] == 1 and any(e["class"] == c and not free(c, e["grant"]) for e in a["entries"])]
add(DELIVERY, st["party"], c, n, cites)
elif f["kind"] == PACKAGE and t["version"] == V4: # §4.7: the product held, its storage alone
if f["held"] is not None:
add(PACKAGE, f["signer"], 3, f["stored"] * f["held"], [(PACK, f["digest"])]) # byte-microseconds
elif f["kind"] == PACKAGE:
cite = [(PACK, f["digest"])]
add(PACKAGE, f["signer"], 1, 1, cite)
@ -686,14 +785,17 @@ def compute(t, facts):
continue
cap, hop = f["billed"]
for k, (pcap, pop, unit, _, _) in enumerate(t["job"]):
if pcap != cap or (t["version"] == 3 and pop != f["operation"] and "operation" not in SKIP):
if pcap != cap or (t["version"] in WORK_VERSIONS and pop != f["operation"] and "operation" not in SKIP):
continue
cites = [(ROUTE_RECORD, f["digest"]), (OER_RECEIPT, hop)]
if unit == 1:
n = 1
else:
elif unit == 2:
cites += [(VIDEO_RECEIPT, h) for h, w in f["outputs"] if w > 0]
n = sum(w for _, w in f["outputs"])
else: # §4.7: the work units of its packages
cites += [(PACK, h) for h, w in f["packages"] if w > 0]
n = sum(w for _, w in f["packages"])
add(JOB, f["router"], k + 1, n, cites)
else:
unit = 1 if f["claims_required"] else 2
@ -704,6 +806,10 @@ def compute(t, facts):
lines, total = [], 0
for (kind, source, unit) in sorted(q):
n, cited = q[(kind, source, unit)]
if t["version"] == V4 and kind == PACKAGE and unit == 3:
n //= DAY_US # §4.7: byte-days, rounded down once per line
if n == 0:
continue
p = price(t, kind, unit)
a = n * p // divisor(t, kind, unit)
if n > U64 or a > U64:
@ -725,12 +831,14 @@ def compute(t, facts):
seen.add(d)
figs.append((2, fig))
for f in facts:
if f["kind"] == PACKAGE:
if f["kind"] == PACKAGE and t["version"] != V4: # §6.6: in version 4, its run's hop states it
figs.append((3, f["figure"]))
elif f["kind"] == ADS:
figs += [(4, r["figure"]) for r in f["receipts"]]
elif f["kind"] == JOB and f["figure"] is not None:
figs.append((5, f["figure"]))
elif f["kind"] == JOB:
if f["figure"] is not None:
figs.append((5, f["figure"]))
figs += [(6, x) for x in f["unbilled"]]
rows, unmetered, sums = rows_of(figs)
if rows is None or total > U64:
return None
@ -738,7 +846,7 @@ def compute(t, facts):
def rows_of(figs):
unmetered = [0, 0, 0, 0, 0]
unmetered = [0, 0, 0, 0, 0, 0]
acc = {}
for s, (tag, joules, ppm) in figs:
if tag is None:
@ -925,6 +1033,7 @@ def verify(doc):
across = check_across(facts, prev, t["version"]) # step 6
if across:
raise Refuse(across[1], ["source", across[0]])
hold(t, facts, prev, inv["start"], inv["end"])
c = compute(t, facts) # step 7
if c is None:
raise Refuse("invoice_mismatch", ["invoice"])
@ -939,19 +1048,19 @@ def verify(doc):
def line_json(x, t):
"""§7.3: a line, a job line naming its job price's unit and capability."""
out = {"source_kind": KIND_NAMES[x["kind"]], "source": x["source"].hex(),
"unit": JOB_UNITS[t["job"][x["unit"] - 1][2] - 1] if x["kind"] == JOB else UNITS[x["kind"]][x["unit"] - 1][0],
"unit": JOB_UNITS[t["job"][x["unit"] - 1][2] - 1] if x["kind"] == JOB else units(t["version"])[x["kind"]][x["unit"] - 1][0],
"quantity": x["quantity"], "price": x["price"], "amount": x["amount"],
"cites": [[k, i.hex()] for k, i in x["cited_records"]]}
if x["kind"] == JOB:
out["capability"] = t["job"][x["unit"] - 1][0].decode("utf-8")
if t["version"] == 3:
if t["version"] in WORK_VERSIONS:
out["operation"] = t["job"][x["unit"] - 1][1].decode("utf-8")
return out
def accepted(inv, facts, c, t):
unmetered, coverage = {}, {}
for s in range(1, 6 if inv["version"] in JOB_VERSIONS else 5):
for s in range(1, scopes(inv["version"]) + 1):
n = inv["unmetered"][s - 1]
unmetered[SCOPES[s]] = n
coverage[SCOPES[s]] = "partial" if n > 0 else "whole" if any(r[0] == s for r in inv["energy"]) else "none"
@ -1070,6 +1179,35 @@ def mutations(doc):
m = copy.deepcopy(doc)
m["vectors"][k]["sources"] = ["job_rerun_replays_its_hop"]
out.append(("a_rerun_billed: its re-run replaced by one replaying the billed hop", m))
if "version_4_by_the_byte_and_the_work" in names: # version 4, §4.7, §6.6
k = names["version_4_by_the_byte_and_the_work"]
lines = doc["vectors"][k]["outcome"]["lines"]
bo = next(i for i, x in enumerate(lines) if x["unit"] == "bytes_out")
m = copy.deepcopy(doc)
m["vectors"][k]["outcome"]["lines"][bo]["quantity"] -= 1
out.append(("version_4_by_the_byte_and_the_work: its bytes out stated one less", m))
m = copy.deepcopy(doc)
m["vectors"][k]["outcome"]["energy"] = [e for e in doc["vectors"][k]["outcome"]["energy"] if e["scope"] != "unbilled_hops"]
out.append(("version_4_by_the_byte_and_the_work: the unbilled hops' rows dropped", m))
m = copy.deepcopy(doc)
m["vectors"][k]["outcome"]["coverage"]["unbilled_hops"] = "whole"
out.append(("version_4_by_the_byte_and_the_work: the unbilled hops' coverage stated whole", m))
wu = next(i for i, x in enumerate(lines) if x["unit"] == "work_units")
m = copy.deepcopy(doc)
m["vectors"][k]["outcome"]["lines"][wu]["unit"] = "entries"
out.append(("version_4_by_the_byte_and_the_work: its work units named entries", m))
k = names["version_4_storage_held_through_a_period"]
lines = doc["vectors"][k]["outcome"]["lines"]
bd = next(i for i, x in enumerate(lines) if x["unit"] == "byte_days")
m = copy.deepcopy(doc)
m["vectors"][k]["outcome"]["lines"][bd]["quantity"] += 1
out.append(("version_4_storage_held_through_a_period: a byte-day more", m))
m = copy.deepcopy(doc)
m["vectors"][k]["previous"] = None
out.append(("version_4_storage_held_through_a_period: the invoice before it not given", m))
m = copy.deepcopy(doc)
m["sources"]["job_v2_encode"]["outputs"] = doc["sources"]["job_v2_another_output"]["outputs"]
out.append(("job_v2_encode: its output replaced by another clip's receipt", m))
for k, v in enumerate(doc["vectors"]):
if v["outcome"]["outcome"] == "refuse":
m = copy.deepcopy(doc)
@ -1081,7 +1219,8 @@ def mutations(doc):
CHECKS = ["signatures", "buyer-signature", "tariff-rules", "invoice-rules", "row-of-no-class", "binding", "outside-tariff",
"previous-given", "previous-overlap", "scope", "conflict", "period", "duplicate", "overlap", "rebilled", "quantity",
"summed", "citations", "mismatch", "job-figure", "output", "job-duplicate", "private-kind", "operation", "output-once"]
"summed", "citations", "mismatch", "job-figure", "output", "job-duplicate", "private-kind", "operation", "output-once",
"output-delivered", "unbilled-hops", "storage-held", "package-again", "run-work", "bytes-out", "conflict-bytes"]
def main(corpus_dir):

View file

@ -66,6 +66,15 @@ impl Chain {
fn billed(&self, h: &Held) -> bool {
h.facts.heads().iter().all(|r| self.billed.contains(r))
}
/// Whether the next invoice bills `h`: a source the chain has not billed, or, under a tariff
/// of version 4, a package the last invoice holds, whose storage each period bills (§4.7).
fn bills(&self, h: &Held) -> bool {
let held = || {
matches!(h.facts, Facts::Package(_)) && self.tariff.version == rl::VERSION_4 && self.invoices.last().is_some_and(|(i, _, _)| h.facts.heads().iter().all(|r| i.records.contains(r)))
};
!self.billed(h) || held()
}
}
pub struct Ledger {
@ -100,7 +109,7 @@ impl Ledger {
for (_, p) in numbered(&d.join("sources"), "json")? {
let text = std::fs::read_to_string(&p).map_err(|e| format!("{}: {e}", p.display()))?;
let doc: Value = serde_json::from_str(&text).map_err(|e| format!("{}: {e}", p.display()))?;
let facts = rl::verify_source(&doc, &rl::signers(&chain.tariff)).map_err(|(r, w)| format!("{}: refused {r} at {w}", p.display()))?;
let facts = rl::verify_source_of(&doc, &rl::signers(&chain.tariff), chain.tariff.version).map_err(|(r, w)| format!("{}: refused {r} at {w}", p.display()))?;
chain.sources.push(Held { doc, facts });
}
let mut prev = [0u8; 32];
@ -148,7 +157,7 @@ impl Ledger {
/// and checked against everything held under the tariff (step 6), then kept.
pub fn add_source(&mut self, tariff: &[u8; 32], doc: Value) -> Result<Vec<(u8, [u8; 32])>, Error> {
let c = self.chain(tariff)?;
let facts = rl::verify_source(&doc, &rl::signers(&c.tariff)).map_err(|(r, at)| {
let facts = rl::verify_source_of(&doc, &rl::signers(&c.tariff), c.tariff.version).map_err(|(r, at)| {
let mut a = vec![json!("source")];
a.extend(at.as_array().cloned().unwrap_or_default());
Error::Refused((r, Value::Array(a)))
@ -157,9 +166,12 @@ impl Ledger {
let mut all: Vec<Facts> = c.sources.iter().map(|h| h.facts.clone()).collect();
all.push(facts.clone());
rl::check_across(c.tariff.version, &all, None).map_err(|(_, r)| refuse(r.code(), json!(["source"])))?;
// A record the chain billed is billed never again; under a tariff of version 3, a job's
// output receipt is cited again by a re-run, whose hop and route record are new (§6.9).
if facts.records().iter().any(|r| rl::billed_once(c.tariff.version, r.0) && c.billed.contains(r)) {
// A record the chain billed is billed never again; under a tariff of version 3 or 4, a
// job's output receipt is cited again by a re-run, whose hop and route record are new
// (§6.9). A package source is brought once: the ledger holds it, and under version 4 bills
// its storage each period from what it holds (§4.7).
let package_again = matches!(facts, Facts::Package(_)) && facts.heads().iter().all(|h| c.billed.contains(h));
if package_again || facts.records().iter().any(|r| rl::billed_once(c.tariff.version, r.0) && c.billed.contains(r)) {
return Err(refuse(Reason::RecordRebilled.code(), json!(["source"])));
}
let n = c.sources.len() as u64;
@ -176,7 +188,7 @@ impl Ledger {
pub fn invoice(&mut self, tariff: &[u8; 32], start: u64, end: u64) -> Result<Issued, Error> {
let c = self.chain(tariff)?;
let t = &c.tariff;
let selected: Vec<Value> = c.sources.iter().filter(|h| !c.billed(h) && rl::check_source(t, start, end, &h.facts).is_ok()).map(|h| h.doc.clone()).collect();
let selected: Vec<Value> = c.sources.iter().filter(|h| c.bills(h) && rl::check_source(t, start, end, &h.facts).is_ok()).map(|h| h.doc.clone()).collect();
let prev = c.invoices.last().map(|(i, _, _)| i);
let inv = rl::make_invoice(&self.key, t, prev, start, end, &selected)?;
let bytes = inv.to_bytes();

View file

@ -1,7 +1,8 @@
//! The service's ledger against the committed corpus (`live-conformance/ledger/`): given the
//! corpus's tariff and sources in the corpus's order, it issues the corpus's invoices byte for
//! byte, with the outcomes the corpus states; it recovers its chain from its directory after a
//! restart; it bills each source once, and under a tariff of version 3 a job's work once; and
//! restart; it bills each source once, under a tariff of version 3 a job's work once, and under
//! one of version 4 a package's work once per run and its storage each period it holds it; and
//! one process holds its directory.
use std::path::PathBuf;
@ -134,6 +135,33 @@ fn a_rerun_is_billed_and_a_replayed_hop_is_not() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Under a tariff of version 4 the ledger bills a package's work once per run and its storage for
/// each period it holds it (receipt-ledger §4.7): it issues the corpus's chain of version 4 byte
/// for byte, the package the first invoice listed held into the next and billed its byte-days
/// there beside the run that made it again; it refuses that package brought again as a source,
/// and the replay of its first run.
#[test]
fn a_package_is_billed_its_work_once_and_its_storage_each_period() {
let doc = corpus();
let (a, b) = (vector(&doc, "version_4_by_the_byte_and_the_work"), vector(&doc, "version_4_storage_held_through_a_period"));
let dir = scratch("storage");
let t = tariff_of(a);
let mut l = Ledger::open(&dir, seller()).unwrap();
l.add_tariff(&unhex(a["tariff"].as_str().unwrap()).unwrap()).unwrap();
add_sources(&mut l, &doc, a);
let (start, end) = (a["outcome"]["start"].as_u64().unwrap(), a["outcome"]["end"].as_u64().unwrap());
let i = l.invoice(&t, start, end).unwrap();
assert_eq!(Value::String(crate::ledger::hex(&i.invoice)), a["invoice"], "the corpus's first invoice of version 4, byte for byte");
assert_eq!(reason(l.add_source(&t, doc["sources"]["package_base"].clone()).unwrap_err()), "record_duplicate", "a package the ledger holds already");
assert_eq!(reason(l.add_source(&t, doc["sources"]["package_run_replayed"].clone()).unwrap_err()), "record_duplicate", "the first run's hop receipt, held already");
l.add_source(&t, doc["sources"]["package_staged"].clone()).unwrap();
l.add_source(&t, doc["sources"]["package_run_again"].clone()).unwrap();
let j = l.invoice(&t, end, b["outcome"]["end"].as_u64().unwrap()).unwrap();
assert_eq!(Value::String(crate::ledger::hex(&j.invoice)), b["invoice"], "the held package's storage, billed in the next invoice");
assert_eq!(j.outcome, b["outcome"]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn one_process_holds_the_directory_and_tariffs_are_its_sellers() {
let doc = corpus();

View file

@ -940,6 +940,7 @@ mod tests {
fn session_receipt(totals: Vec<crate::live_metering::Total>, unmetered: u8) -> crate::live_metering::SessionReceipt {
use crate::live_metering::*;
SessionReceipt {
version: RECORD_VERSION,
room: [7; 32],
signer: [8; 32],
start: 0,

View file

@ -14,7 +14,9 @@
//!
//! - §3: the per-hop record in versions 1 and 2, its track entries, its ledger, version 2's
//! DVR figures and the size bound of each version;
//! - §4: the receipt, its units, its totals, its ceilings and its QoE cell;
//! - §4: the receipt in versions 1 and 2, its units, its totals, its ceilings and its QoE
//! cell; version 2 adds the bytes the hops sent the session's sessions, priced per 10^9
//! bytes;
//! - §5: verification, over the room ([`crate::live_room`]), the takes
//! ([`crate::capture_take`]) and the composition receipts ([`crate::live_compose`]);
//! - §6: the QoE observations' CMCD event-timeline record and `application/cmcd` batch.
@ -55,6 +57,9 @@ pub const SESSION_MAGIC: [u8; 4] = *b"WMR1";
pub const RECORD_VERSION: u8 = 1;
/// The version of a per-hop record with DVR figures (§3.1, §3.9).
pub const HOP_VERSION_DVR: u8 = 2;
/// The version of a receipt that states the bytes its hops sent and prices them (§4.1,
/// §4.4, §4.6).
pub const SESSION_VERSION_BYTES: u8 = 2;
/// The domain a per-hop record's signature is made over (§3.5).
pub const HOP_DOMAIN: &[u8] = b"wai:metering-hop\x01";
/// The domain a receipt's signature is made over (§4.2).
@ -79,8 +84,12 @@ pub const QOE_MIN_SESSIONS: usize = 50;
pub const ENTRY_LEN: usize = 90;
/// A ledger entry's length (§3.4).
pub const LEDGER_ENTRY_LEN: usize = 143;
/// A consumption entry's length (§4.6).
/// A consumption entry's length (§4.6), in a receipt of version 1.
pub const CONSUMPTION_LEN: usize = 198;
/// A consumption entry's length in a receipt of version 2, which adds its `bytes_out` (§4.6).
pub const CONSUMPTION_LEN_2: usize = 206;
/// The divisor of the price of bytes out (§4.6): per 10^9 bytes.
pub const BYTES_DIVISOR: u128 = 1_000_000_000;
/// The longest calibration reference a record carries (§3.6).
pub const MAX_CALIBRATION_REF: usize = 255;
/// The fixed fields of a version 1 record (§3.7), and its longest class evidence.
@ -813,7 +822,7 @@ pub struct Total {
}
/// A tariff (§4.6): prices in millionths of a minor unit, per second of each time unit
/// and per event sent. A zero `currency` is no tariff.
/// and per event sent, and in version 2 per 10^9 bytes out. A zero `currency` is no tariff.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Tariff {
pub currency: [u8; 3],
@ -822,6 +831,20 @@ pub struct Tariff {
pub composition: u32,
pub recorded: u32,
pub event: u32,
/// Version 2: per 10^9 bytes the hops sent the session's sessions (§4.6); 0 in version 1,
/// which states no such price.
pub bytes: u32,
}
impl Tariff {
/// Its prices in §4.1's order: thirteen, and in version 2 the price of bytes out.
pub fn prices(&self, version: u8) -> Vec<u32> {
let mut p: Vec<u32> = std::iter::once(self.publisher).chain(self.subscriber).chain([self.composition, self.recorded, self.event]).collect();
if version == SESSION_VERSION_BYTES {
p.push(self.bytes);
}
p
}
}
/// The session's units (§4.4).
@ -834,20 +857,27 @@ pub struct Units {
pub events_sent: u64,
pub events_delivered: u64,
pub recorded_us: u64,
/// Version 2: the payload bytes the hops sent the session's sessions, their ledger
/// entries' `bytes_out` (§4.4); 0 in version 1, which states none.
pub bytes_out: u64,
}
impl Units {
fn encode(&self, o: &mut Vec<u8>) {
fn encode(&self, o: &mut Vec<u8>, version: u8) {
o.extend_from_slice(&self.publisher_us.to_le_bytes());
put_u64s(o, &self.subscriber_us);
put_u64s(o, &[self.sessions, self.composition_us, self.events_sent, self.events_delivered, self.recorded_us]);
if version == SESSION_VERSION_BYTES {
o.extend_from_slice(&self.bytes_out.to_le_bytes());
}
}
fn read(r: &mut Rd) -> Result<Units, Reason> {
fn read(r: &mut Rd, version: u8) -> Result<Units, Reason> {
let publisher_us = r.u64()?;
let subscriber_us = r.u64s()?;
let [sessions, composition_us, events_sent, events_delivered, recorded_us] = r.u64s()?;
Ok(Units { publisher_us, subscriber_us, sessions, composition_us, events_sent, events_delivered, recorded_us })
let bytes_out = if version == SESSION_VERSION_BYTES { r.u64()? } else { 0 };
Ok(Units { publisher_us, subscriber_us, sessions, composition_us, events_sent, events_delivered, recorded_us, bytes_out })
}
}
@ -897,6 +927,8 @@ impl QoeCell {
/// A room-session receipt (§4.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionReceipt {
/// 1; 2 for a receipt that states the bytes its hops sent and prices them (§4.1).
pub version: u8,
pub room: [u8; 32],
pub signer: [u8; 32],
pub start: u64,
@ -924,7 +956,7 @@ impl SessionReceipt {
pub fn body(&self) -> Vec<u8> {
let mut o = Vec::new();
o.extend_from_slice(&SESSION_MAGIC);
o.push(RECORD_VERSION);
o.push(self.version);
o.extend_from_slice(&self.room);
o.extend_from_slice(&self.signer);
o.extend_from_slice(&self.start.to_le_bytes());
@ -945,7 +977,7 @@ impl SessionReceipt {
o.extend_from_slice(&self.compose_digest);
o.extend_from_slice(&self.qoe_count.to_le_bytes());
o.extend_from_slice(&self.qoe_digest);
self.units.encode(&mut o);
self.units.encode(&mut o, self.version);
o.push(self.unmetered);
o.push(u8::try_from(self.totals.len()).expect("at most 255 totals"));
for t in &self.totals {
@ -954,8 +986,7 @@ impl SessionReceipt {
o.extend_from_slice(&t.joules.to_le_bytes());
}
o.extend_from_slice(&self.tariff.currency);
let t = &self.tariff;
for p in [t.publisher].iter().chain(&t.subscriber).chain(&[t.composition, t.recorded, t.event]) {
for p in self.tariff.prices(self.version) {
o.extend_from_slice(&p.to_le_bytes());
}
o.extend_from_slice(&self.grants.to_le_bytes());
@ -989,7 +1020,7 @@ impl SessionReceipt {
/// Read a receipt (§4.2) of the room `room_id`: the record, `classes_summed` for a
/// total of tag 0, its rules, its room, its signature.
pub fn parse(b: &[u8], room_id: &[u8; 32]) -> Result<SessionReceipt, Reason> {
read_head(b, &SESSION_MAGIC, &[RECORD_VERSION])?;
let version = read_head(b, &SESSION_MAGIC, &[RECORD_VERSION, SESSION_VERSION_BYTES])?;
let mut r = Rd { b, p: 5 };
let (room, signer, start, end) = (r.arr()?, r.arr()?, r.u64()?, r.u64()?);
let n = r.u8()? as usize;
@ -1005,7 +1036,7 @@ impl SessionReceipt {
let takes = (0..k).map(|_| r.arr()).collect::<Result<Vec<[u8; 32]>, _>>()?;
let (compose_count, compose_digest) = (r.u32()?, r.arr()?);
let (qoe_count, qoe_digest) = (r.u32()?, r.arr()?);
let units = Units::read(&mut r)?;
let units = Units::read(&mut r, version)?;
let unmetered = r.u8()?;
let m = r.u8()? as usize;
let mut totals = Vec::with_capacity(m);
@ -1014,6 +1045,7 @@ impl SessionReceipt {
}
let currency = r.arr()?;
let prices: [u32; 13] = r.u32s()?;
let bytes = if version == SESSION_VERSION_BYTES { r.u32()? } else { 0 };
let tariff = Tariff {
currency,
publisher: prices[0],
@ -1021,6 +1053,7 @@ impl SessionReceipt {
composition: prices[10],
recorded: prices[11],
event: prices[12],
bytes,
};
let (grants, consumption_root) = (r.u32()?, r.arr()?);
let p = r.u32()? as usize;
@ -1036,7 +1069,7 @@ impl SessionReceipt {
if totals.iter().any(|t| t.tag == 0) {
return Err(Reason::ClassesSummed);
}
let tariff_ok = if currency == [0; 3] { prices.iter().all(|&p| p == 0) } else { currency.iter().all(u8::is_ascii_uppercase) };
let tariff_ok = if currency == [0; 3] { prices.iter().all(|&p| p == 0) && bytes == 0 } else { currency.iter().all(u8::is_ascii_uppercase) };
let rules = start < end
&& hops.windows(2).all(|w| w[0].record < w[1].record)
&& hops.iter().all(|h| (1..=3).contains(&h.role))
@ -1058,6 +1091,7 @@ impl SessionReceipt {
}
verify_sig(&signer, SESSION_DOMAIN, &b[..b.len() - 64], &sig)?;
Ok(SessionReceipt {
version,
room,
signer,
start,
@ -1084,6 +1118,8 @@ impl SessionReceipt {
/// A grant's consumption entry (§4.6).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Consumption {
/// Its receipt's version: an entry of version 2 states `bytes_out`.
pub version: u8,
pub grant: [u8; 32],
pub joules_ceiling: u64,
pub funds_ceiling: u64,
@ -1093,6 +1129,8 @@ pub struct Consumption {
pub composition_us: u64,
pub recorded_us: u64,
pub events_sent: u64,
/// Version 2: the bytes its ledger entries state the hops sent its sessions (§4.6).
pub bytes_out: u64,
/// The attributed joules of tags 2, 3, 4 and 5.
pub attributed: [u64; 4],
/// Its share is unmetered at some hop.
@ -1104,8 +1142,9 @@ pub struct Consumption {
}
impl Consumption {
pub fn encode(&self) -> [u8; CONSUMPTION_LEN] {
let mut o = Vec::with_capacity(CONSUMPTION_LEN);
/// Its bytes: [`CONSUMPTION_LEN`] of them in version 1, [`CONSUMPTION_LEN_2`] in version 2.
pub fn encode(&self) -> Vec<u8> {
let mut o = Vec::with_capacity(CONSUMPTION_LEN_2);
o.extend_from_slice(&self.grant);
o.extend_from_slice(&self.joules_ceiling.to_le_bytes());
o.extend_from_slice(&self.funds_ceiling.to_le_bytes());
@ -1113,12 +1152,15 @@ impl Consumption {
o.extend_from_slice(&self.publisher_us.to_le_bytes());
put_u64s(&mut o, &self.subscriber_us);
put_u64s(&mut o, &[self.composition_us, self.recorded_us, self.events_sent]);
if self.version == SESSION_VERSION_BYTES {
o.extend_from_slice(&self.bytes_out.to_le_bytes());
}
put_u64s(&mut o, &self.attributed);
o.push(self.unmetered as u8);
o.extend_from_slice(&self.funds.to_le_bytes());
o.push(self.joules_verdict);
o.push(self.funds_verdict);
o.try_into().expect("198 bytes")
o
}
/// The entry's leaf (§4.6).
@ -1280,6 +1322,8 @@ pub struct SessionInputs<'a> {
/// What a session gives, computed from its records (§4.4–§4.7).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Computed {
/// The version of the receipt it was computed for: version 2 counts bytes out.
pub version: u8,
pub hops: Vec<HopFacts>,
pub media_tracks: u64,
pub units: Units,
@ -1365,7 +1409,7 @@ fn lookup<'a>(given: &'a [Vec<u8>], digest: &[u8; 32]) -> Option<&'a [u8]> {
/// §5.1 steps 4 to 7 and the computations of §4: every refusal those steps make, and what
/// the session gives.
fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) -> Result<Computed, (Place, Reason)> {
fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff, version: u8) -> Result<Computed, (Place, Reason)> {
let room_id = ctx.vr.id;
// The media tracks the room published in the session (§5.3).
let published: BTreeSet<[u8; 32]> = ctx.over(start, end).iter().flat_map(|(st, _, _)| ctx.published(st)).collect();
@ -1446,6 +1490,7 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
let entry = |g: [u8; 32]| -> Consumption {
let (jc, fc, cur) = ctx.ceilings(&g).unwrap_or((NO_CEILING, NO_CEILING, [0; 3]));
Consumption {
version,
grant: g,
joules_ceiling: jc,
funds_ceiling: fc,
@ -1455,6 +1500,7 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
composition_us: 0,
recorded_us: 0,
events_sent: 0,
bytes_out: 0,
attributed: [0; 4],
unmetered: false,
funds: NO_FUNDS,
@ -1462,6 +1508,8 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
funds_verdict: 0,
}
};
// Version 2 counts the bytes the hops sent the session's sessions, by grant (§4.4).
let bytes = version == SESSION_VERSION_BYTES;
for (st, a, b) in ctx.over(start, end) {
for p in st.publishers() {
units.publisher_us += b - a;
@ -1487,6 +1535,10 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
for k in 0..CLASSES {
c.subscriber_us[k] += e.subscriber_us[k];
}
if bytes {
units.bytes_out += e.bytes_out;
c.bytes_out += e.bytes_out;
}
if e.moved() == 0 {
continue;
}
@ -1568,6 +1620,7 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
};
}
Ok(Computed {
version,
hops,
media_tracks: m,
units,
@ -1635,7 +1688,9 @@ fn committed_capture(m: &take::Manifest, f: &take::FoundTake, start: u64, end: u
.sum()
}
/// §4.6: a grant's funds, in minor units.
/// §4.6: a grant's funds, in minor units. *N* is in 10^-12 of a minor unit; in version 2 the
/// bytes out add ⌈(1000 × *N* + `bytes_out` × the price of bytes) / 10^15⌉, the price being
/// per 10^9 bytes, so one rounding up covers both.
fn funds_of(c: &Consumption, t: &Tariff) -> u64 {
let mut n: u128 = c.publisher_us as u128 * t.publisher as u128;
for k in 0..CLASSES {
@ -1643,7 +1698,11 @@ fn funds_of(c: &Consumption, t: &Tariff) -> u64 {
}
n += c.composition_us as u128 * t.composition as u128 + c.recorded_us as u128 * t.recorded as u128;
n += c.events_sent as u128 * t.event as u128 * 1_000_000;
let f = n.div_ceil(1_000_000_000_000);
let f = if c.version == SESSION_VERSION_BYTES {
(n * 1_000 + c.bytes_out as u128 * t.bytes as u128).div_ceil(1_000 * 1_000_000_000_000)
} else {
n.div_ceil(1_000_000_000_000)
};
u64::try_from(f).map_or(NO_FUNDS - 1, |f| f.min(NO_FUNDS - 1))
}
@ -1732,7 +1791,7 @@ pub fn verify_session(i: &SessionInputs) -> SessionOutcome {
if digests.len() as u64 != r.compose_count as u64 || list_digest(&digests) != r.compose_digest {
// The hop entries and the takes come first (steps 4 and 5).
let named = Named { hops, takes, compositions: vec![], descriptors: i.descriptors, logs: None };
if let Err((at, reason)) = evaluate(&ctx, r.start, r.end, &named, &r.tariff) {
if let Err((at, reason)) = evaluate(&ctx, r.start, r.end, &named, &r.tariff, r.version) {
return refuse(at, reason);
}
return refuse(Place::Compose(None), Reason::RecordMissing);
@ -1744,7 +1803,7 @@ pub fn verify_session(i: &SessionInputs) -> SessionOutcome {
Some(b) => Some(b),
None => {
let named = Named { hops, takes, compositions, descriptors: i.descriptors, logs: None };
if let Err((at, reason)) = evaluate(&ctx, r.start, r.end, &named, &r.tariff) {
if let Err((at, reason)) = evaluate(&ctx, r.start, r.end, &named, &r.tariff, r.version) {
return refuse(at, reason);
}
return refuse(Place::Qoe, Reason::RecordMissing);
@ -1752,7 +1811,7 @@ pub fn verify_session(i: &SessionInputs) -> SessionOutcome {
}
};
let named = Named { hops, takes, compositions, descriptors: i.descriptors, logs };
let c = match evaluate(&ctx, r.start, r.end, &named, &r.tariff) {
let c = match evaluate(&ctx, r.start, r.end, &named, &r.tariff, r.version) {
Ok(c) => c,
Err((at, reason)) => return refuse(at, reason),
};
@ -1797,6 +1856,8 @@ pub struct SessionPlan<'a> {
pub takes: &'a [Vec<u8>],
pub logs: Option<&'a [u8]>,
pub tariff: Tariff,
/// The receipt's version: 2 states and prices the bytes out (§4.1).
pub version: u8,
}
/// Aggregate a session into an unsigned receipt with `signer`, refusing what §5 refuses.
@ -1815,8 +1876,9 @@ pub fn aggregate(p: &SessionPlan, signer: [u8; 32]) -> Result<(SessionReceipt, C
descriptors: p.descriptors,
logs: p.logs,
};
let c = evaluate(&ctx, p.start, p.end, &named, &p.tariff)?;
let c = evaluate(&ctx, p.start, p.end, &named, &p.tariff, p.version)?;
let r = SessionReceipt {
version: p.version,
room: ctx.vr.id,
signer,
start: p.start,
@ -2058,6 +2120,7 @@ mod tests {
#[test]
fn classes_summed_is_told_from_a_wrong_total() {
let mut c = Computed {
version: RECORD_VERSION,
hops: vec![],
media_tracks: 0,
units: Units::default(),
@ -2072,6 +2135,7 @@ mod tests {
consumption: vec![],
};
let r = |totals: Vec<Total>| SessionReceipt {
version: RECORD_VERSION,
room: [0; 32],
signer: [0; 32],
start: 0,
@ -2105,6 +2169,7 @@ mod tests {
#[test]
fn funds_round_up() {
let mut c = Consumption {
version: RECORD_VERSION,
grant: [1; 32],
joules_ceiling: NO_CEILING,
funds_ceiling: 10,
@ -2114,6 +2179,7 @@ mod tests {
composition_us: 0,
recorded_us: 0,
events_sent: 0,
bytes_out: 0,
attributed: [0; 4],
unmetered: false,
funds: NO_FUNDS,
@ -2129,6 +2195,24 @@ mod tests {
c.events_sent = u64::MAX;
let t = Tariff { currency: *b"EUR", event: u32::MAX, ..Tariff::default() };
assert_eq!(funds_of(&c, &t), NO_FUNDS - 1, "funds past 64 bits saturate below the marker");
// Version 2: the bytes out at their price per 10^9 bytes, rounded up once with the rest.
c.version = SESSION_VERSION_BYTES;
c.events_sent = 0;
c.publisher_us = 0;
c.bytes_out = 1_500_000_000;
let t = Tariff { currency: *b"EUR", bytes: 2_000_000, ..Tariff::default() };
assert_eq!(funds_of(&c, &t), 3, "1.5 GB at two minor units per GB");
c.bytes_out = 1;
assert_eq!(funds_of(&c, &t), 1, "one byte still costs one minor unit, rounded up");
c.publisher_us = 500_000;
c.bytes_out = 500_000_000;
let t = Tariff { currency: *b"EUR", publisher: 1_000_000, bytes: 1_000_000, ..Tariff::default() };
assert_eq!(funds_of(&c, &t), 1, "half a minor unit of time and half of bytes round up once, to one");
c.version = RECORD_VERSION;
assert_eq!(funds_of(&c, &t), 1, "version 1 prices no bytes");
assert_eq!(c.encode().len(), CONSUMPTION_LEN);
c.version = SESSION_VERSION_BYTES;
assert_eq!(c.encode().len(), CONSUMPTION_LEN_2);
}
fn obs(v: serde_json::Value) -> Observation {

View file

@ -960,7 +960,7 @@ fn logs(n: usize) -> Vec<u8> {
// ---- the receipts ----
fn tariff() -> Tariff {
Tariff { currency: *b"EUR", publisher: 2_778, subscriber: [1_389, 2_778, 5_556, 8_333, 0, 0, 0, 0, 278], composition: 5_556, recorded: 1_389, event: 100 }
Tariff { currency: *b"EUR", publisher: 2_778, subscriber: [1_389, 2_778, 5_556, 8_333, 0, 0, 0, 0, 278], composition: 5_556, recorded: 1_389, event: 100, bytes: 0 }
}
/// Everything a vector gives a verifier but the room.
@ -995,11 +995,35 @@ fn receipt(room: &WrittenRoom, g: &Given, span: (u64, u64), signer: u8) -> Sessi
takes: &g.takes,
logs: g.logs.as_deref(),
tariff: tariff(),
version: RECORD_VERSION,
};
let (r, _) = aggregate(&plan, pk(signer)).unwrap_or_else(|e| panic!("the corpus's session aggregates: {e:?}"));
r.sign(&key(signer))
}
/// Version 2's tariff: the lesson's prices, and 2,000 cents per 10^9 bytes out (§4.6).
fn tariff_bytes() -> Tariff {
Tariff { bytes: 2_000_000_000, ..tariff() }
}
/// A receipt of version 2 over `g` under `t`, unsigned: the bytes its hops sent its sessions
/// stated and priced (§4.4, §4.6).
fn receipt_2(room: &WrittenRoom, g: &Given, span: (u64, u64), t: Tariff) -> SessionReceipt {
let plan = SessionPlan {
room: room.input(),
start: span.0,
end: span.1,
hops: &g.hops,
descriptors: &g.descriptors,
compositions: &g.compositions,
takes: &g.takes,
logs: g.logs.as_deref(),
tariff: t,
version: SESSION_VERSION_BYTES,
};
aggregate(&plan, pk(METER)).unwrap_or_else(|e| panic!("the corpus's session aggregates in version 2: {e:?}")).0
}
// ---- the vectors ----
/// What a vector is written to reach.
@ -1088,6 +1112,39 @@ fn vectors(rooms: &[(&'static str, WrittenRoom)]) -> Vec<Vector> {
dvr_given.clone(),
Intent::Accept,
));
// Version 2: the bytes the hops sent the session's sessions, priced per 10^9 bytes.
let bytes_r = receipt_2(l, &given, span, tariff_bytes()).sign(&key(METER));
v.push(vector(
"bytes_out_priced",
"the lesson's receipt of version 2: the bytes its hops sent the session's sessions, summed from their ledgers' bytes out, by grant too, and priced per 10^9 bytes in its tariff and in each grant's funds",
"lesson",
&bytes_r,
given.clone(),
Intent::Accept,
));
let mut more = bytes_r.clone();
more.units.bytes_out += 1;
v.push(raw("bytes_out_mismatch", "a receipt of version 2 stating one byte out more than its hops' ledgers", "lesson", resign(more, METER), given.clone(), Intent::Refuse("unit_mismatch")));
let unpriced = {
// The consumption of version 2 computed without the bytes: the root of version 1's
// entries, stated in a receipt of version 2.
let v1 = receipt_2(l, &given, span, tariff_bytes());
let mut plan_1 = v1.clone();
plan_1.consumption_root = receipt(l, &given, span, METER).consumption_root;
plan_1
};
v.push(raw("bytes_out_left_out_of_the_consumption", "a receipt of version 2 whose grants' consumption entries leave out their bytes out and their price", "lesson", resign(unpriced, METER), given.clone(), Intent::Refuse("ceiling_mismatch")));
let free_bytes = SessionReceipt { tariff: Tariff { bytes: 1, ..Tariff::default() }, ..receipt_2(l, &given, span, Tariff::default()) };
v.push(raw("bytes_priced_with_no_tariff", "a receipt of version 2 with no tariff that prices bytes out", "lesson", resign(free_bytes, METER), given.clone(), Intent::Refuse("session_invalid")));
{
// The lesson's receipt with its fifth byte 3, signed again: only its version is wrong.
let mut b = base.to_bytes();
b[4] = 3;
let n = b.len() - 64;
let sig = sign(&key(METER), SESSION_DOMAIN, &b[..n]);
b[n..].copy_from_slice(&sig);
v.push(raw("receipt_unknown_version", "a receipt of version 3: the lesson's with its fifth byte 3, signed again", "lesson", b, given.clone(), Intent::Refuse("unknown_version")));
}
// ---- refused: the receipt ----
let mut flipped = base.to_bytes();
@ -1275,6 +1332,7 @@ fn vectors(rooms: &[(&'static str, WrittenRoom)]) -> Vec<Vector> {
takes: &dvr_given.takes,
logs: dvr_given.logs.as_deref(),
tariff: tariff(),
version: RECORD_VERSION,
};
let m = aggregate(&plan, pk(METER)).expect("the DVR relay's session aggregates").1.media_tracks;
let big = dvr_with(&|r| {
@ -1433,12 +1491,17 @@ fn funds_verdict(v: u8) -> &'static str {
["none", "within", "past", "no_tariff"][v as usize]
}
fn units_json(u: &Units) -> Value {
json!({
/// The units (§4.4): a receipt of version 2 states its bytes out too.
fn units_json(u: &Units, version: u8) -> Value {
let mut v = json!({
"publisher_us": u.publisher_us, "subscriber_us": u.subscriber_us, "sessions": u.sessions,
"composition_us": u.composition_us, "events_sent": u.events_sent, "events_delivered": u.events_delivered,
"recorded_us": u.recorded_us,
})
});
if version == SESSION_VERSION_BYTES {
v["bytes_out"] = u.bytes_out.into();
}
v
}
fn cell_json(c: &QoeCell) -> Value {
@ -1451,7 +1514,7 @@ fn cell_json(c: &QoeCell) -> Value {
fn consumption_json(c: &Consumption) -> Value {
let attributed: Map<String, Value> = (2..=5u8).map(|t| (tag_label(t).to_string(), Value::from(c.attributed[(t - 2) as usize]))).collect();
json!({
let mut v = json!({
"grant": hex(&c.grant), "joules_ceiling": ceiling_json(c.joules_ceiling), "funds_ceiling": ceiling_json(c.funds_ceiling),
"currency": if c.currency == [0; 3] { Value::Null } else { String::from_utf8_lossy(&c.currency).into() },
"publisher_us": c.publisher_us, "subscriber_us": c.subscriber_us, "composition_us": c.composition_us,
@ -1459,7 +1522,11 @@ fn consumption_json(c: &Consumption) -> Value {
"funds": if c.funds == NO_FUNDS { Value::Null } else { c.funds.into() },
"joules_verdict": joules_verdict(c.joules_verdict), "funds_verdict": funds_verdict(c.funds_verdict),
"leaf": hex(&c.leaf()),
})
});
if c.version == SESSION_VERSION_BYTES {
v["bytes_out"] = c.bytes_out.into();
}
v
}
/// The names of version 2's DVR figures, in §3.9's order.
@ -1494,7 +1561,7 @@ fn outcome_json(o: &SessionOutcome) -> Map<String, Value> {
m.insert("digest".into(), hex(digest).into());
m.insert("media_tracks".into(), c.media_tracks.into());
m.insert("records".into(), c.hops.iter().map(record_json).collect());
m.insert("units".into(), units_json(&c.units));
m.insert("units".into(), units_json(&c.units, c.version));
m.insert("unmetered".into(), c.unmetered.into());
m.insert("totals".into(), c.totals.iter().map(|(t, (f, j))| json!({"class": tag_label(*t), "figures": f, "joules": *j as u64})).collect());
m.insert("coverage".into(), if c.whole() { "whole" } else { "partial" }.into());

View file

@ -16,11 +16,14 @@
//! What this module implements, by section of the extension:
//!
//! - §4: the tariff ([`Tariff`]), its two signatures and its scope, and its job prices: per
//! serving capability in version 2, per serving capability and operation in version 3;
//! serving capability in version 2, per serving capability and operation in versions 3 and
//! 4; version 4 prices a room's bytes out, the work units of a package each run that made it
//! states, and a package's storage by byte-time (§4.7);
//! - §5: the sources ([`verify_source`]) and what each contributes ([`Facts`]);
//! - §6: the invoice ([`Invoice`]), its lines, citations, energy and chain ([`compute`]), and
//! what a chain bills once ([`billed_once`]): in version 3, a job's work, never its output's
//! receipt;
//! what a chain bills once ([`billed_once`]): in versions 3 and 4, a job's work, never its
//! output's receipt; in version 4, a package's work once per run and its storage for each
//! period it is held, and every hop's energy, the unbilled ones in a scope of their own;
//! - §7: verification ([`verify`]); §8: the reports.
//!
//! The corpus `live-conformance/ledger/` is written by [`corpus::generate`] and checked by
@ -57,6 +60,9 @@ pub const VERSION_2: u8 = 2;
/// The version of a tariff that prices jobs per operation and bills a job's work, and of its
/// invoices (§4.6, §6.9).
pub const VERSION_3: u8 = 3;
/// The version of a tariff that prices delivery by the byte, a package's work and its storage
/// by byte-time, and of its invoices, which state every hop's energy (§4.7, §6.6).
pub const VERSION_4: u8 = 4;
pub const TARIFF_DOMAIN: &[u8] = b"wai:ledger-tariff\x01";
pub const ACCEPT_DOMAIN: &[u8] = b"wai:ledger-accept\x01";
pub const INVOICE_DOMAIN: &[u8] = b"wai:ledger-invoice\x01";
@ -65,8 +71,13 @@ pub const CITES_DOMAIN: &[u8] = b"wai:ledger-cites\x01";
pub const LINE_LEN: usize = 94;
/// An energy row's length (§6.1).
pub const ROW_LEN: usize = 18;
/// The room prices of a tariff, as live-metering §4.1 orders them.
pub const ROOM_PRICES: usize = 13;
/// The room prices of a tariff, as live-metering §4.1 orders them in a receipt of version 2:
/// thirteen, and in version 4 a fourteenth, per 10^9 bytes out.
pub const ROOM_PRICES: usize = 14;
/// The room prices of a tariff of versions 1 to 3, which price no bytes out.
pub const ROOM_PRICES_1: usize = 13;
/// A day, in microseconds: the time of version 4's storage price (§4.7).
pub const DAY_US: u128 = 86_400_000_000;
/// The kinds of a source and of a line (§6.3).
pub mod source_kind {
@ -116,21 +127,42 @@ pub mod scope {
pub const AD_DECISIONS: u8 = 4;
/// Version 2 (§6.6).
pub const JOBS: u8 = 5;
/// Version 4 (§6.6): the hops of a job that are not billed, a failed one and one that
/// served past the deadline.
pub const UNBILLED_HOPS: u8 = 6;
pub fn name(s: u8) -> &'static str {
["", "room_hops", "delivery_hops", "packaging", "ad_decisions", "jobs"].get(s as usize).copied().unwrap_or("")
["", "room_hops", "delivery_hops", "packaging", "ad_decisions", "jobs", "unbilled_hops"].get(s as usize).copied().unwrap_or("")
}
/// The scopes an invoice of `version` states (§6.1): 4, 5 in versions 2 and 3, 6 in 4.
pub fn count(version: u8) -> u8 {
match version {
super::VERSION_2 | super::VERSION_3 => JOBS,
super::VERSION_4 => UNBILLED_HOPS,
_ => AD_DECISIONS,
}
}
}
/// The unit names of each source kind, in unit order (§6.3).
/// The unit names of each source kind, in unit order (§6.3), in a tariff of versions 1 to 3.
pub fn unit_name(kind: u8, unit: u8) -> Option<&'static str> {
const ROOM: [&str; 13] = [
unit_name_in(VERSION_3, kind, unit)
}
/// The unit names of each source kind in an invoice of `version` (§6.3): version 4 adds a
/// room's `bytes_out`, and bills a package source by its `byte_days` alone (§4.7), unit 3.
pub fn unit_name_in(version: u8, kind: u8, unit: u8) -> Option<&'static str> {
const ROOM: [&str; 14] = [
"publisher_us", "subscriber_us_0", "subscriber_us_1", "subscriber_us_2", "subscriber_us_3", "subscriber_us_4", "subscriber_us_5", "subscriber_us_6", "subscriber_us_7",
"subscriber_us_8", "composition_us", "recorded_us", "events_sent",
"subscriber_us_8", "composition_us", "recorded_us", "events_sent", "bytes_out",
];
let v4 = version == VERSION_4;
let names: &[&str] = match kind {
source_kind::ROOM => &ROOM,
source_kind::ROOM if v4 => &ROOM,
source_kind::ROOM => &ROOM[..ROOM_PRICES_1],
source_kind::DELIVERY => &["live", "fetch", "fill", "ancillary"],
source_kind::PACKAGE if v4 => return (unit == 3).then_some("byte_days"),
source_kind::PACKAGE => &["packages", "entries", "stored_bytes"],
source_kind::ADS => &["presented_impressions", "billable_impressions"],
_ => &[],
@ -142,6 +174,7 @@ pub fn unit_name(kind: u8, unit: u8) -> Option<&'static str> {
pub fn divisor(kind: u8, unit: u8) -> u128 {
match (kind, unit) {
(source_kind::ROOM, 13) => 1,
(source_kind::ROOM, 14) => 1_000_000_000,
(source_kind::ROOM, _) => 1_000_000,
(source_kind::DELIVERY, _) => 1_000_000_000,
(source_kind::PACKAGE, 3) => 1_000_000_000,
@ -174,11 +207,12 @@ pub enum Reason {
PrivateKind,
OutputInvalid,
JobFigureInvalid,
OutputNotDelivered,
}
impl Reason {
/// §10's order.
pub const ALL: [Reason; 18] = [
pub const ALL: [Reason; 19] = [
Reason::TariffInvalid,
Reason::InvoiceInvalid,
Reason::TariffMismatch,
@ -197,6 +231,7 @@ impl Reason {
Reason::PrivateKind,
Reason::OutputInvalid,
Reason::JobFigureInvalid,
Reason::OutputNotDelivered,
];
/// The codes of other extensions this one raises itself, besides those of its sources.
@ -222,6 +257,7 @@ impl Reason {
Reason::PrivateKind => "private_kind",
Reason::OutputInvalid => "output_invalid",
Reason::JobFigureInvalid => "job_figure_invalid",
Reason::OutputNotDelivered => "output_not_delivered",
}
}
}
@ -316,7 +352,7 @@ impl<'a> Rd<'a> {
}
}
/// The first steps of reading (§4.3, §6.8): the magic and the version, 1, 2 or 3.
/// The first steps of reading (§4.3, §6.8): the magic and the version, 1 to 4.
fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<u8, &'static str> {
if b.len() < 4 {
return Err(TRUNCATED);
@ -326,14 +362,25 @@ fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<u8, &'static str> {
}
match b.get(4) {
None => Err(TRUNCATED),
Some(&v) if v == VERSION || v == VERSION_2 || v == VERSION_3 => Ok(v),
Some(&v) if (VERSION..=VERSION_4).contains(&v) => Ok(v),
Some(_) => Err(UNKNOWN_VERSION),
}
}
/// Whether a tariff or an invoice of `version` prices jobs (§4.5, §4.6): versions 2 and 3.
/// Whether a tariff or an invoice of `version` prices jobs (§4.5, §4.6): versions 2 to 4.
pub fn prices_jobs(version: u8) -> bool {
version == VERSION_2 || version == VERSION_3
(VERSION_2..=VERSION_4).contains(&version)
}
/// Whether a tariff of `version` prices a job by its operation and bills its work (§4.6, §6.9):
/// versions 3 and 4.
pub fn bills_work(version: u8) -> bool {
version == VERSION_3 || version == VERSION_4
}
/// The room prices a tariff of `version` states (§4.1): fourteen in version 4, else thirteen.
pub fn room_prices(version: u8) -> usize {
if version == VERSION_4 { ROOM_PRICES } else { ROOM_PRICES_1 }
}
/// A job price (§4.5, §4.6): a capability, in version 3 an operation, a unit (1 `jobs`, 2
@ -349,8 +396,14 @@ pub struct JobPrice {
pub price: u64,
}
/// The job units, in unit order (§4.5).
pub const JOB_UNITS: [&str; 2] = ["jobs", "subpixels"];
/// The job units, in unit order (§4.5): `work_units`, the work its output packages state, in
/// version 4 only (§4.7).
pub const JOB_UNITS: [&str; 3] = ["jobs", "subpixels", "work_units"];
/// The job units a tariff of `version` prices (§4.5, §4.7): two, and three in version 4.
pub fn job_units(version: u8) -> u8 {
if version == VERSION_4 { 3 } else { 2 }
}
fn job_capability(c: &str) -> bool {
(1..=128).contains(&c.len()) && c.starts_with("wai.") && c.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'.')
@ -375,18 +428,21 @@ fn ascending<T: PartialOrd>(v: &[T]) -> bool {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tariff {
/// 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and
/// bills their work (§4.6, §6.9).
/// bills their work (§4.6, §6.9); 4 for one that also prices bytes out, a package's work and
/// its storage by byte-time (§4.7).
pub version: u8,
pub seller: [u8; 32],
pub buyer: [u8; 32],
pub start: u64,
pub end: u64,
pub currency: [u8; 3],
/// Live-metering §4.1's thirteen prices, in its order.
/// Live-metering §4.1's thirteen prices, in its order, and in version 4 a fourteenth, per
/// 10^9 bytes out; versions 1 to 3 state no fourteenth, read as 0.
pub room: [u32; ROOM_PRICES],
/// Settlement §7.1's four prices, in class order.
pub delivery: [u64; 4],
/// Per package receipt, per entry, per 10^9 stored bytes.
/// Per package receipt, per entry, per 10^9 stored bytes; in version 4 only the third, per
/// 10^9 byte-days held, the first two neither written nor read, 0 (§4.7).
pub package: [u64; 3],
/// Per presented impression, per billable impression.
pub ads: [u64; 2],
@ -410,10 +466,11 @@ impl Tariff {
o.extend_from_slice(&self.start.to_le_bytes());
o.extend_from_slice(&self.end.to_le_bytes());
o.extend_from_slice(&self.currency);
for p in self.room {
for p in &self.room[..room_prices(self.version)] {
o.extend_from_slice(&p.to_le_bytes());
}
for p in self.delivery.iter().chain(&self.package).chain(&self.ads) {
let package = if self.version == VERSION_4 { &self.package[2..] } else { &self.package[..] };
for p in self.delivery.iter().chain(package).chain(&self.ads) {
o.extend_from_slice(&p.to_le_bytes());
}
if prices_jobs(self.version) {
@ -421,7 +478,7 @@ impl Tariff {
for j in &self.job {
o.push(u8::try_from(j.capability.len()).expect("a capability of at most 255 bytes"));
o.extend_from_slice(j.capability.as_bytes());
if self.version == VERSION_3 {
if bills_work(self.version) {
o.push(u8::try_from(j.operation.len()).expect("an operation of at most 255 bytes"));
o.extend_from_slice(j.operation.as_bytes());
}
@ -469,7 +526,7 @@ impl Tariff {
let (start, end) = (r.u64()?, r.u64()?);
let currency = r.arr()?;
let mut room = [0u32; ROOM_PRICES];
for p in &mut room {
for p in &mut room[..room_prices(version)] {
*p = r.u32()?;
}
let mut delivery = [0u64; 4];
@ -477,7 +534,7 @@ impl Tariff {
*p = r.u64()?;
}
let mut package = [0u64; 3];
for p in &mut package {
for p in &mut package[if version == VERSION_4 { 2 } else { 0 }..] {
*p = r.u64()?;
}
let ads = [r.u64()?, r.u64()?];
@ -490,7 +547,7 @@ impl Tariff {
for _ in 0..n {
let len = r.u8()? as usize;
let capability = text(r.take(len)?);
let operation = if version == VERSION_3 {
let operation = if bills_work(version) {
let len = r.u8()? as usize;
text(r.take(len)?)
} else {
@ -522,8 +579,8 @@ impl Tariff {
&& t.job.len() <= 255
&& t.job.windows(2).all(|w| (w[0].capability.as_bytes(), w[0].operation.as_bytes(), w[0].unit) < (w[1].capability.as_bytes(), w[1].operation.as_bytes(), w[1].unit))
&& t.job.iter().all(|j| {
let operation = if t.version == VERSION_3 { job_operation(&j.operation) } else { j.operation.is_empty() };
job_capability(&j.capability) && operation && (1..=JOB_UNITS.len() as u8).contains(&j.unit) && j.divisor >= 1
let operation = if bills_work(t.version) { job_operation(&j.operation) } else { j.operation.is_empty() };
job_capability(&j.capability) && operation && (1..=job_units(t.version)).contains(&j.unit) && j.divisor >= 1
});
if !rules {
return Err((Reason::TariffInvalid.code(), at()));
@ -593,7 +650,7 @@ pub struct Row {
/// An invoice (§6.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Invoice {
/// Its tariff's version, 1, 2 or 3.
/// Its tariff's version, 1 to 4.
pub version: u8,
pub seller: [u8; 32],
pub buyer: [u8; 32],
@ -605,8 +662,9 @@ pub struct Invoice {
pub records: Vec<(u8, [u8; 32])>,
pub lines: Vec<Line>,
pub energy: Vec<Row>,
/// Scopes 1 to 4, and in versions 2 and 3 scope 5; a version-1 invoice states no fifth.
pub unmetered: [u32; 5],
/// Scopes 1 to 4, in versions 2 and 3 scope 5, and in version 4 scope 6 (§6.1); an invoice
/// states only its version's.
pub unmetered: [u32; 6],
pub amount: u64,
pub sig: [u8; 64],
}
@ -647,8 +705,7 @@ impl Invoice {
o.extend_from_slice(&e.joules.to_le_bytes());
o.extend_from_slice(&e.max_ppm.to_le_bytes());
}
let scopes = if prices_jobs(self.version) { 5 } else { 4 };
for u in &self.unmetered[..scopes] {
for u in &self.unmetered[..scope::count(self.version) as usize] {
o.extend_from_slice(&u.to_le_bytes());
}
o.extend_from_slice(&self.amount.to_le_bytes());
@ -704,7 +761,10 @@ impl Invoice {
for _ in 0..n {
energy.push(Row { scope: r.u8()?, tag: r.u8()?, figures: r.u32()?, joules: r.u64()?, max_ppm: r.u32()? });
}
let unmetered = [r.u32()?, r.u32()?, r.u32()?, r.u32()?, if v2 { r.u32()? } else { 0 }];
let mut unmetered = [0u32; 6];
for u in &mut unmetered[..scope::count(version) as usize] {
*u = r.u32()?;
}
let amount = r.u64()?;
let sig = r.arr()?;
if !r.done() {
@ -725,11 +785,11 @@ impl Invoice {
&& i.records.iter().all(|(k, _)| (1..=9).contains(k) || (v2 && (record_kind::OER_RECEIPT..=record_kind::ROUTE_RECORD).contains(k)))
&& ascending(&keys)
&& i.lines.iter().all(|l| {
let unit_ok = if l.source_kind == source_kind::JOB { v2 && l.unit >= 1 } else { unit_name(l.source_kind, l.unit).is_some() };
let unit_ok = if l.source_kind == source_kind::JOB { v2 && l.unit >= 1 } else { unit_name_in(version, l.source_kind, l.unit).is_some() };
unit_ok && l.quantity > 0 && l.cited > 0
})
&& ascending(&rows)
&& i.energy.iter().all(|e| (1..=if v2 { 5 } else { 4 }).contains(&e.scope) && (2..=5).contains(&e.tag) && e.figures > 0);
&& i.energy.iter().all(|e| (1..=scope::count(version)).contains(&e.scope) && (2..=5).contains(&e.tag) && e.figures > 0);
if !rules {
return Err(Reason::InvoiceInvalid.code());
}
@ -801,7 +861,9 @@ impl RoomFacts {
2..=10 => u.subscriber_us[unit as usize - 2],
11 => u.composition_us,
12 => u.recorded_us,
_ => u.events_sent,
13 => u.events_sent,
// A receipt of version 2's bytes out (live-metering §4.4); version 1 states none.
_ => u.bytes_out,
}
}
}
@ -825,6 +887,11 @@ pub struct PackageFacts {
pub total_entries: u64,
pub stored_bytes: u64,
pub figure: Figure,
/// Under a tariff of version 4, the microseconds of the invoice's period the package was
/// held through: its receipt is in the previous invoice's records, so it was written by the
/// end of that invoice's period, at most this one's start (§4.7). `None` for a package the
/// invoice bills for the first time, and under versions 1 to 3.
pub held_us: Option<u64>,
}
/// One impression of an ad receipt, as the report states it (§8.1).
@ -905,6 +972,13 @@ pub struct JobFacts {
pub figure: Option<Figure>,
/// Each output receipt: its receipt hash and its `work_pixel_ops`.
pub outputs: Vec<([u8; 32], u64)>,
/// Under a tariff of version 4, each output package (§4.7): its receipt hash and the work
/// units it states.
pub packages: Vec<([u8; 32], u64)>,
/// Under a tariff of version 4, the figure of each attempt that is not billed, a failed hop
/// or one that served past the deadline, in attempt order (§6.6); empty under versions 2
/// and 3, which state no such figure.
pub unbilled: Vec<Figure>,
}
/// What a verified source gives (§5.2).
@ -946,6 +1020,7 @@ impl Facts {
Facts::Job(j) => {
s.extend(j.hops.iter().map(|h| (record_kind::OER_RECEIPT, *h)));
s.extend(j.outputs.iter().map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
s.extend(j.packages.iter().map(|(h, _)| (record_kind::PACKAGE, *h)));
}
}
s
@ -1142,6 +1217,7 @@ fn package_source(o: &Map<String, Value>) -> Result<Facts, Refusal> {
total_entries: r.total_entries,
stored_bytes: r.stored_bytes,
figure: Figure { joules: r.joules_micro, class: r.energy_provenance.clone() },
held_us: None,
}))
}
@ -1339,7 +1415,7 @@ fn job_figure(r: &Value) -> Option<Figure> {
Some(Figure { joules, class: Some(class) })
}
fn job_source(o: &Map<String, Value>) -> Result<Facts, Refusal> {
fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
use crate::route_record as rr;
if !has_exactly(o, &["kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"]) {
return Err(bad_source());
@ -1356,25 +1432,73 @@ fn job_source(o: &Map<String, Value>) -> Result<Facts, Refusal> {
let texts: Vec<Value> = doc["receipts"].as_array().expect("a list").iter().map(|t| crate::oer::load(t.as_str().expect("a text").as_bytes()).expect("read")).collect();
let attempts = record["attempts"].as_array().expect("a list");
let hops: Vec<[u8; 32]> = attempts.iter().map(|a| oer_identity(a["receipt"].as_str().expect("an id")).expect("an id")).collect();
let receipt_of = |a: &Value| texts.iter().position(|r| r.get("receipt_id") == Some(&a["receipt"])).expect("a checked receipt");
let (mut billed, mut figure) = (None, None);
if let Some(a) = attempts.iter().find(|a| a["billed"] == true) {
let endpoint = a["endpoint"].as_str().expect("a token");
let cap = record["candidates"].as_array().expect("a list").iter().find(|c| c["endpoint"] == endpoint).and_then(|c| c["capability"].as_str()).expect("a survivor");
let id = a["receipt"].as_str().expect("an id");
let j = texts.iter().position(|r| r.get("receipt_id").and_then(Value::as_str) == Some(id)).expect("a checked receipt");
let j = receipt_of(a);
figure = Some(job_figure(&texts[j]).ok_or((Reason::JobFigureInvalid.code(), json!(["receipts", j])))?);
billed = Some((cap.to_string(), oer_identity(id).expect("an id")));
}
let mut outs = Vec::with_capacity(outputs.len());
// Version 4 states every hop's energy: each attempt that is not billed, in a scope of its
// own (§6.6), so its figure must be one a row can state, as the billed hop's must.
let mut unbilled = Vec::new();
if version == VERSION_4 {
for a in attempts.iter().filter(|a| a["billed"] != true) {
let j = receipt_of(a);
unbilled.push(job_figure(&texts[j]).ok_or((Reason::JobFigureInvalid.code(), json!(["receipts", j])))?);
}
}
// What the hop that served states it delivered (route-record §5.2, version 2), by content
// hash; a record of version 1 states nothing, and under a tariff of version 4 holds its
// outputs to nothing (§5.1). Under version 3 a record of version 1's outputs are read as
// before.
let delivered: Option<Vec<[u8; 32]>> = if record["version"] == 2 {
Some(
attempts
.iter()
.filter(|a| a["outcome"] == "served")
.flat_map(|a| a["outputs"].as_array().expect("a list").iter().map(|h| unhex(h.as_str().expect("a hash")).expect("hex").try_into().expect("32 bytes")))
.collect(),
)
} else if version == VERSION_4 {
Some(Vec::new())
} else {
None
};
let mut seen: BTreeSet<[u8; 32]> = BTreeSet::new();
let (mut outs, mut packages) = (Vec::with_capacity(outputs.len()), Vec::new());
for (i, x) in outputs.iter().enumerate() {
let bad = || (Reason::OutputInvalid.code(), json!(["outputs", i]));
let x = x.as_object().filter(|x| has_exactly(x, &["receipt", "codestream"])).ok_or_else(bad_source)?;
let (Some(text), Some(code)) = (x["receipt"].as_str(), bytes_of(&x["codestream"])) else { return Err(bad_source()) };
let r = crate::video_receipt::VideoReceipt::from_json(text).map_err(|_| bad())?;
if !r.verify_reconstruction_wiv1(&code) {
return Err(bad());
let x = x.as_object().ok_or_else(bad_source)?;
// Each output's object: an int_motion codestream, or in version 4 a package's receipt,
// the package a run of `package.build` made (§4.7), verified against its indexes.
let object = if version == VERSION_4 && has_exactly(x, &["package", "indexes"]) {
let (Some(text), Some(indexes)) = (x["package"].as_str(), hexes(&x["indexes"])) else { return Err(bad_source()) };
let r = PackageReceiptV2::from_json(text).map_err(|_| bad())?;
let ix: Vec<&[u8]> = indexes.iter().map(Vec::as_slice).collect();
r.verify_detailed(&ix).map_err(|_| bad())?;
packages.push((r.receipt_hash(), r.work_units));
text.as_bytes().to_vec()
} else {
let x = Some(x).filter(|x| has_exactly(x, &["receipt", "codestream"])).ok_or_else(bad_source)?;
let (Some(text), Some(code)) = (x["receipt"].as_str(), bytes_of(&x["codestream"])) else { return Err(bad_source()) };
let r = crate::video_receipt::VideoReceipt::from_json(text).map_err(|_| bad())?;
if !r.verify_reconstruction_wiv1(&code) {
return Err(bad());
}
outs.push((r.receipt_hash(), r.work_pixel_ops));
code
};
// The output is its object: one the served hop delivered, once.
if let Some(d) = &delivered {
let h = rr::content_hash(&object);
if !d.contains(&h) || !seen.insert(h) {
return Err((Reason::OutputNotDelivered.code(), json!(["outputs", i])));
}
}
outs.push((r.receipt_hash(), r.work_pixel_ops));
}
Ok(Facts::Job(JobFacts {
record: rr::digest(&record),
@ -1386,17 +1510,20 @@ fn job_source(o: &Map<String, Value>) -> Result<Facts, Refusal> {
billed,
figure,
outputs: outs,
packages,
unbilled,
}))
}
/// §5.1: verify one source by its extension, a stitched source with `signers` (§4.4); a job
/// source is read here whatever the tariff, and only a tariff of version 2 or 3 bills it. A
/// refusal's place is the extension's own, relative to the source.
/// source is read here as a tariff of version 3 reads it, and only a tariff of version 2 to 4
/// bills it. A refusal's place is the extension's own, relative to the source.
pub fn verify_source(v: &Value, signers: &[[u8; 32]]) -> Result<Facts, Refusal> {
verify_source_of(v, signers, VERSION_3)
}
fn verify_source_of(v: &Value, signers: &[[u8; 32]], version: u8) -> Result<Facts, Refusal> {
/// §5.1 under a tariff of `version`: a job source is read by that version's rules (§5.1, §6.6).
pub fn verify_source_of(v: &Value, signers: &[[u8; 32]], version: u8) -> Result<Facts, Refusal> {
let o = v.as_object().ok_or_else(bad_source)?;
match o.get("kind").and_then(Value::as_str) {
Some("room") => room_source(o),
@ -1404,7 +1531,7 @@ fn verify_source_of(v: &Value, signers: &[[u8; 32]], version: u8) -> Result<Fact
Some("package") => package_source(o),
Some("ads") => ads_source(o),
Some("stitched") => stitched_source(o, signers),
Some("job") if prices_jobs(version) => job_source(o),
Some("job") if prices_jobs(version) => job_source(o, version),
_ => Err(bad_source()),
}
}
@ -1421,12 +1548,11 @@ pub fn check_source(t: &Tariff, start: u64, end: u64, f: &Facts) -> Result<(), R
if !t.in_scope(source_kind::ROOM, &r.room) {
return Err(Reason::OutsideScope);
}
// A receipt of version 1 states no price of bytes, nor a tariff of versions 1 to 3:
// each reads as 0, so a receipt's funds computed at another price of bytes conflict.
let rt = &r.tariff;
if rt.currency != [0; 3] {
let prices: Vec<u32> = std::iter::once(rt.publisher).chain(rt.subscriber).chain([rt.composition, rt.recorded, rt.event]).collect();
if rt.currency != t.currency || prices != t.room {
return Err(Reason::TariffConflict);
}
if rt.currency != [0; 3] && (rt.currency != t.currency || rt.prices(metering::SESSION_VERSION_BYTES) != t.room) {
return Err(Reason::TariffConflict);
}
if !within(r.t0 as u128 + r.start as u128, r.t0 as u128 + r.end as u128) {
return Err(Reason::OutsidePeriod);
@ -1495,11 +1621,27 @@ fn overlap(version: u8, f: &Facts, g: &Facts) -> bool {
}
/// Whether a chain under a tariff of `version` bills a record of `kind` once (§6.9): every
/// record but, in version 3, a job's output receipt. A job's work, its route record and its
/// hops' OER/2 receipts, is new on every run and billed once; its output receipt is the
/// evidence of what it delivered, which a deterministic re-run delivers again.
/// record but, in versions 3 and 4, a job's output receipt, and in version 4 a package receipt.
/// A job's work, its route record and its hops' OER/2 receipts, is new on every run and billed
/// once; its output receipt is the evidence of what it delivered, which a deterministic re-run
/// delivers again. In version 4 a package is such an output: the run that made it is billed
/// once per run, its receipt cited by each, and its storage billed by every invoice whose
/// period it is held through (§4.7).
pub fn billed_once(version: u8, kind: u8) -> bool {
!(version == VERSION_3 && kind == record_kind::VIDEO_RECEIPT)
!((bills_work(version) && kind == record_kind::VIDEO_RECEIPT) || (version == VERSION_4 && kind == record_kind::PACKAGE))
}
/// Under a tariff of version 4, mark each package the previous invoice holds as held through
/// the period [`start`, `end`) (§4.7); under versions 1 to 3, nothing.
pub fn hold(t: &Tariff, facts: &mut [Facts], previous: Option<&Invoice>, start: u64, end: u64) {
if t.version != VERSION_4 {
return;
}
for f in facts {
if let Facts::Package(p) = f {
p.held_us = previous.filter(|i| i.records.contains(&(record_kind::PACKAGE, p.receipt_hash))).map(|_| end - start);
}
}
}
/// §7.2 step 6 under a tariff of `version`: the index of the source refused and the reason, or
@ -1543,7 +1685,7 @@ pub struct Computed {
/// Each line, and the records it cites.
pub lines: Vec<(Line, Cited)>,
pub energy: Vec<Row>,
pub unmetered: [u32; 5],
pub unmetered: [u32; 6],
pub amount: u64,
/// Per scope and tag, the sum of its figures, for the summed-classes check.
pub sums: BTreeMap<(u8, u8), u128>,
@ -1557,8 +1699,10 @@ fn is_free(class: u8, grant: &[u8; 32]) -> bool {
settlement::class::is_retrieval(class) && *grant == [0; 32]
}
/// The figures of §6.2 to §6.6 over verified sources.
/// The figures of §6.2 to §6.6 over verified sources, a package's held through the period
/// marked by [`hold`].
pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
let v4 = t.version == VERSION_4;
let mut records: BTreeSet<(u8, [u8; 32])> = BTreeSet::new();
for f in facts {
records.extend(f.records());
@ -1576,7 +1720,7 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
for f in facts {
match f {
Facts::Room(r) => {
for unit in 1..=13u8 {
for unit in 1..=room_prices(t.version) as u8 {
add(source_kind::ROOM, r.room, unit, r.unit(unit) as u128, &[(record_kind::SESSION, r.digest)]);
}
}
@ -1596,6 +1740,14 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
add(source_kind::DELIVERY, s.party, c, bytes, &cites);
}
}
// A package source is the product held: its storage for each period it is held
// through, in byte-microseconds here, byte-days on its line; its work is the run's
// that made it, a job's (§4.7).
Facts::Package(p) if v4 => {
if let Some(us) = p.held_us {
add(source_kind::PACKAGE, p.signer, 3, p.stored_bytes as u128 * us as u128, &[(record_kind::PACKAGE, p.receipt_hash)]);
}
}
Facts::Package(p) => {
let cite = [(record_kind::PACKAGE, p.receipt_hash)];
add(source_kind::PACKAGE, p.signer, 1, 1, &cite);
@ -1624,15 +1776,21 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
// cited once and counted for each (§6.5).
Facts::Job(j) => {
let Some((cap, hop)) = &j.billed else { continue };
let priced = |p: &JobPrice| p.capability == *cap && (t.version != VERSION_3 || p.operation == j.operation);
let priced = |p: &JobPrice| p.capability == *cap && (!bills_work(t.version) || p.operation == j.operation);
for (k, p) in t.job.iter().enumerate().filter(|(_, p)| priced(p)) {
let unit = u8::try_from(k + 1).expect("at most 255 job prices");
let mut cites = vec![(record_kind::ROUTE_RECORD, j.record), (record_kind::OER_RECEIPT, *hop)];
let n = if p.unit == 1 {
1
} else {
cites.extend(j.outputs.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
j.outputs.iter().map(|(_, w)| *w as u128).sum()
let n = match p.unit {
1 => 1,
2 => {
cites.extend(j.outputs.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
j.outputs.iter().map(|(_, w)| *w as u128).sum()
}
// Version 4: the work units its output packages state (§4.7).
_ => {
cites.extend(j.packages.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::PACKAGE, *h)));
j.packages.iter().map(|(_, w)| *w as u128).sum()
}
};
add(source_kind::JOB, j.router, unit, n, &cites);
}
@ -1642,6 +1800,11 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
let mut lines = Vec::with_capacity(q.len());
let mut amount: u128 = 0;
for ((kind, source, unit), (n, cited)) in q {
// Version 4's storage: byte-microseconds to byte-days, rounded down once per line.
let n = if v4 && kind == source_kind::PACKAGE && unit == 3 { n / DAY_US } else { n };
if n == 0 {
continue;
}
let quantity = u64::try_from(n).map_err(|_| Overflow)?;
let price = t.price(kind, unit);
let a = n * price as u128 / t.divisor(kind, unit);
@ -1673,9 +1836,13 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
}
for f in facts {
match f {
Facts::Package(p) => figures.push((scope::PACKAGING, p.figure.clone())),
// In version 4 a package's figure is its run's, which the run's hop states (§6.6).
Facts::Package(p) if !v4 => figures.push((scope::PACKAGING, p.figure.clone())),
Facts::Ads(a) => figures.extend(a.receipts.iter().map(|r| (scope::AD_DECISIONS, r.figure.clone()))),
Facts::Job(j) => figures.extend(j.figure.iter().map(|f| (scope::JOBS, f.clone()))),
Facts::Job(j) => {
figures.extend(j.figure.iter().map(|f| (scope::JOBS, f.clone())));
figures.extend(j.unbilled.iter().map(|f| (scope::UNBILLED_HOPS, f.clone())));
}
_ => {}
}
}
@ -1683,11 +1850,11 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
Ok(Computed { records: records.into_iter().collect(), lines, energy, unmetered, amount: u64::try_from(amount).map_err(|_| Overflow)?, sums })
}
type RowsOf = (Vec<Row>, [u32; 5], BTreeMap<(u8, u8), u128>);
type RowsOf = (Vec<Row>, [u32; 6], BTreeMap<(u8, u8), u128>);
/// Rows, unmetered counts and sums per (scope, tag) of figures.
fn rows_of(figures: &[(u8, Figure)]) -> Result<RowsOf, Overflow> {
let mut unmetered = [0u32; 5];
let mut unmetered = [0u32; 6];
let mut acc: BTreeMap<(u8, u8), (u32, u128, u32)> = BTreeMap::new();
for (s, f) in figures {
match f.tag() {
@ -1976,6 +2143,7 @@ pub fn verify_sources(t: &Tariff, start: u64, end: u64, sources: &[Value], previ
facts.push(f);
}
check_across(t.version, &facts, previous).map_err(|(k, r)| (r.code(), json!(["source", k])))?;
hold(t, &mut facts, previous, start, end);
Ok(facts)
}
@ -2041,7 +2209,7 @@ pub fn accepted_json(a: &Accepted) -> Value {
.map(|(l, cited)| {
let mut v = json!({
"source_kind": source_kind::name(l.source_kind), "source": hex(&l.source),
"unit": unit_name(l.source_kind, l.unit).unwrap_or(""), "quantity": l.quantity, "price": l.price, "amount": l.amount,
"unit": unit_name_in(i.version, l.source_kind, l.unit).unwrap_or(""), "quantity": l.quantity, "price": l.price, "amount": l.amount,
"cites": cited.iter().map(|(k, id)| json!([k, hex(id)])).collect::<Vec<_>>(),
});
// A job line names its unit and capability by its job price, and in version 3 its
@ -2050,7 +2218,7 @@ pub fn accepted_json(a: &Accepted) -> Value {
let p = &a.tariff.job[l.unit as usize - 1];
v["unit"] = json!(JOB_UNITS[p.unit as usize - 1]);
v["capability"] = json!(p.capability);
if a.tariff.version == VERSION_3 {
if bills_work(a.tariff.version) {
v["operation"] = json!(p.operation);
}
}
@ -2059,7 +2227,7 @@ pub fn accepted_json(a: &Accepted) -> Value {
.collect();
let energy: Vec<Value> = i.energy.iter().map(|r| json!({"scope": scope::name(r.scope), "class": class_label(r.tag), "figures": r.figures, "joules": r.joules, "max_ppm": r.max_ppm})).collect();
let (mut unmetered, mut coverage) = (Map::new(), Map::new());
for s in 1..=if prices_jobs(i.version) { 5u8 } else { 4 } {
for s in 1..=scope::count(i.version) {
let n = i.unmetered[s as usize - 1];
let has_row = i.energy.iter().any(|r| r.scope == s);
unmetered.insert(scope::name(s).into(), n.into());
@ -2106,7 +2274,12 @@ mod tests {
start: 10,
end: 20,
currency: *b"EUR",
room: [3; ROOM_PRICES],
// Thirteen prices: a tariff of versions 1 to 3 prices no bytes out.
room: {
let mut r = [3; ROOM_PRICES];
r[ROOM_PRICES_1] = 0;
r
},
delivery: [4; 4],
package: [5; 3],
ads: [6; 2],
@ -2187,10 +2360,70 @@ mod tests {
let at = nb.windows(2).position(|w| w == [0xc3, 0xa9]).expect("the two bytes of é");
nb[at] = 0xff;
assert_eq!(Tariff::parse(&nb).unwrap_err().0, "tariff_invalid");
// A tariff of version 4 is not defined.
let mut v4 = b.clone();
v4[4] = 4;
assert_eq!(Tariff::parse(&v4).unwrap_err().0, "unknown_version");
// A tariff of version 5 is not defined.
let mut v5 = b.clone();
v5[4] = 5;
assert_eq!(Tariff::parse(&v5).unwrap_err().0, "unknown_version");
}
fn tariff_v4() -> Tariff {
let mut t = tariff_v3();
t.version = VERSION_4;
t.room[13] = 11;
t.package = [0, 0, 5];
t.job.push(JobPrice { capability: "wai.video.package".into(), operation: "package.build".into(), unit: 3, divisor: 1, price: 4 });
t.sign(&key(1), &key(2))
}
/// A tariff of version 4 is version 3's with a fourteenth room price, per 10^9 bytes out
/// (§4.1, §4.7); it reads back, every altered byte is refused, and a tariff of versions 1
/// to 3 writes thirteen, whatever its fourteenth holds.
#[test]
fn a_tariff_of_version_4_prices_bytes_out() {
let t = tariff_v4();
let b = t.to_bytes();
assert_eq!(b[4], 4);
assert_eq!(Tariff::parse(&b).unwrap(), t);
for at in 0..b.len() {
let mut m = b.clone();
m[at] ^= 0x01;
assert!(Tariff::parse(&m).is_err(), "byte {at} altered and accepted");
}
let mut v3 = tariff_v3();
v3.room[13] = 11;
assert_eq!(v3.body(), tariff_v3().body(), "version 3 states no price of bytes");
// A job price of unit 3, `work_units`, only in version 4.
let mut work = tariff_v3();
work.job.push(JobPrice { capability: "wai.video.package".into(), operation: "package.build".into(), unit: 3, divisor: 1, price: 4 });
assert_eq!(Tariff::parse(&work.sign(&key(1), &key(2)).to_bytes()).unwrap_err().0, "tariff_invalid");
let mut four = tariff_v4();
four.job[3].unit = 4;
assert_eq!(Tariff::parse(&four.sign(&key(1), &key(2)).to_bytes()).unwrap_err().0, "tariff_invalid");
let mut packed = tariff_v4();
packed.package = [7, 8, 5];
assert_eq!(packed.body(), tariff_v4().body(), "version 4 states one package price, of storage");
assert_eq!(Tariff::parse(&v3.sign(&key(1), &key(2)).to_bytes()).unwrap().room[13], 0);
assert_eq!(t.price(source_kind::ROOM, 14), 11);
assert_eq!(t.divisor(source_kind::ROOM, 14), 1_000_000_000);
assert_eq!(unit_name_in(VERSION_4, source_kind::ROOM, 14), Some("bytes_out"));
assert_eq!(unit_name_in(VERSION_3, source_kind::ROOM, 14), None);
assert_eq!(unit_name_in(VERSION_4, source_kind::PACKAGE, 1), None, "a package source bills no work in version 4");
assert_eq!(unit_name_in(VERSION_4, source_kind::PACKAGE, 2), None);
assert_eq!(unit_name_in(VERSION_4, source_kind::PACKAGE, 3), Some("byte_days"));
let price_4 = 1 + "wai.video.package".len() + 1 + "package.build".len() + 1 + 8 + 8;
assert_eq!(b.len(), tariff_v3().to_bytes().len() + 4 - 16 + price_4, "a fourteenth room price, one package price for three, and a price of work units");
assert_eq!(unit_name_in(VERSION_3, source_kind::PACKAGE, 3), Some("stored_bytes"));
}
/// What a chain of version 4 bills once (§6.9): a job's work and every record but an output
/// receipt and a package receipt, whose storage each period it is held bills (§4.7).
#[test]
fn a_chain_of_version_4_bills_a_packages_storage_each_period() {
assert!(!billed_once(VERSION_4, record_kind::PACKAGE) && billed_once(VERSION_3, record_kind::PACKAGE));
assert!(!billed_once(VERSION_4, record_kind::VIDEO_RECEIPT));
assert!(billed_once(VERSION_4, record_kind::OER_RECEIPT) && billed_once(VERSION_4, record_kind::ROUTE_RECORD) && billed_once(VERSION_4, record_kind::SESSION));
assert!(bills_work(VERSION_4) && prices_jobs(VERSION_4) && !prices_jobs(5));
assert_eq!((scope::count(VERSION), scope::count(VERSION_2), scope::count(VERSION_3), scope::count(VERSION_4)), (4, 5, 5, 6));
}
/// What a chain bills once (§6.9): in version 3, every record but an output receipt; in
@ -2202,7 +2435,7 @@ mod tests {
}
assert!(billed_once(VERSION_2, record_kind::VIDEO_RECEIPT), "version 2 bills an output once");
assert!(!billed_once(VERSION_3, record_kind::VIDEO_RECEIPT), "version 3 bills the work, and cites the output");
assert!(prices_jobs(VERSION_2) && prices_jobs(VERSION_3) && !prices_jobs(VERSION) && !prices_jobs(4));
assert!(prices_jobs(VERSION_2) && prices_jobs(VERSION_3) && !prices_jobs(VERSION) && !prices_jobs(5));
}
/// A tariff of version 2 reads back with its job prices, and every altered byte is refused;
@ -2239,7 +2472,7 @@ mod tests {
#[test]
fn an_invoice_of_version_2_states_the_jobs_scope() {
let t = tariff_v2();
let c = Computed { records: vec![], lines: vec![], energy: vec![], unmetered: [0, 0, 0, 0, 3], amount: 0, sums: BTreeMap::new() };
let c = Computed { records: vec![], lines: vec![], energy: vec![], unmetered: [0, 0, 0, 0, 3, 0], amount: 0, sums: BTreeMap::new() };
let i = invoice_of(&t, [0; 32], 10, 20, &c).sign(&key(1));
assert_eq!(Invoice::parse(&i.to_bytes()).unwrap(), i);
let mut v1 = i.clone();
@ -2278,6 +2511,22 @@ mod tests {
assert_eq!(with(k, &i3), Ok(()), "kind {k} in version 3");
}
assert_eq!(with(record_kind::RUN, &i3), Err("invoice_invalid"));
// Version 4 states a sixth count, the unbilled hops' (§6.1), and a row of scope 6.
let mut c4 = c.clone();
c4.unmetered[5] = 2;
c4.energy = vec![Row { scope: scope::UNBILLED_HOPS, tag: 4, figures: 1, joules: 9, max_ppm: 20_000 }];
let i4 = invoice_of(&tariff_v4(), [0; 32], 10, 20, &c4).sign(&key(1));
assert_eq!(i4.to_bytes().len(), i3.to_bytes().len() + 4 + ROW_LEN);
assert_eq!(Invoice::parse(&i4.to_bytes()).unwrap(), i4);
let mut row_6 = i3.clone();
row_6.energy = c4.energy.clone();
assert_eq!(Invoice::parse(&row_6.sign(&key(1)).to_bytes()).unwrap_err(), "invoice_invalid", "no scope 6 in version 3");
let mut bytes = i4.clone();
bytes.lines.push(Line { source_kind: source_kind::ROOM, source: [9; 32], unit: 14, quantity: 1, price: 1, amount: 0, cited: 1, cites: [0; 32] });
assert!(Invoice::parse(&bytes.clone().sign(&key(1)).to_bytes()).is_ok(), "bytes out, a room unit of version 4");
let mut bytes3 = i3.clone();
bytes3.lines = bytes.lines.clone();
assert_eq!(Invoice::parse(&bytes3.sign(&key(1)).to_bytes()).unwrap_err(), "invoice_invalid", "and not of version 3");
}
#[test]
@ -2314,7 +2563,7 @@ mod tests {
records: vec![(record_kind::SESSION, [7; 32]), (record_kind::HOP, [6; 32])],
lines: vec![Line { source_kind: 1, source: [9; 32], unit: 1, quantity: 1_500_000, price: 3, amount: 4, cited: 1, cites: cites_digest(&cited) }],
energy: vec![Row { scope: 1, tag: 4, figures: 1, joules: 5, max_ppm: 20_000 }],
unmetered: [0, 0, 1, 0, 0],
unmetered: [0, 0, 1, 0, 0, 0],
amount: 4,
sig: [0; 64],
}
@ -2338,8 +2587,8 @@ mod tests {
assert_eq!(Invoice::parse(&unit.sign(&key(1)).to_bytes()).unwrap_err(), "invoice_invalid");
assert_eq!(Invoice::parse(&[b.clone(), vec![0]].concat()).unwrap_err(), "malformed");
let mut v = b.clone();
v[4] = 4;
assert_eq!(Invoice::parse(&v).unwrap_err(), "unknown_version", "version 4 is not defined (2 and 3 are, §6.1)");
v[4] = 5;
assert_eq!(Invoice::parse(&v).unwrap_err(), "unknown_version", "version 5 is not defined (2 to 4 are, §6.1)");
let mut s = b;
let n = s.len();
s[n - 1] ^= 1;
@ -2406,6 +2655,6 @@ mod tests {
Row { scope: 2, tag: 2, figures: 1, joules: 7, max_ppm: 0 },
]
);
assert_eq!(unmetered, [0, 0, 1, 0, 0]);
assert_eq!(unmetered, [0, 0, 1, 0, 0, 0]);
}
}

View file

@ -9,8 +9,9 @@
//! fixed seeds, as are the tariffs and the invoices. Media jobs come from
//! `live-conformance/routes/`, and those the version-3 vectors add (a decode, a re-run, a re-run
//! replaying a hop receipt, a job claiming another's) are routed here under that corpus's own
//! seeds. [`generate`] reads those files from a `wai/` directory; [`verify`] checks every vector
//! against the outcome it states.
//! seeds, as are jobs routed under the route record's version 2 (per-operation entries, measured
//! failures, the served hop's outputs), billed under versions 3 and 4. [`generate`] reads those
//! files from a `wai/` directory; [`verify`] checks every vector against the outcome it states.
use std::collections::BTreeMap;
use std::path::Path;
@ -20,6 +21,7 @@ use serde_json::{json, Map, Value};
use super::*;
use crate::energy_class::{CounterFiltering, Uncertainty};
use crate::route_record::content_hash as rr_hash;
use crate::route_record::corpus as rr;
use crate::jwp_hash::content_hash;
use crate::live_metering::HopRecord;
@ -314,9 +316,27 @@ fn output_of(receipt_payload: &[u8], codestream: &[u8], job: &str) -> Value {
json!({"receipt": r.to_json(), "codestream": hex(codestream)})
}
/// The receipt hash of a package source (§5.2).
fn first_receipt(package: &Value) -> [u8; 32] {
let Ok(Facts::Package(p)) = verify_source(package, &[]) else { panic!("a corpus package verifies") };
p.receipt_hash
}
/// The delivery prices: millionths of a euro cent per 10^9 bytes, by object class.
const DELIVERY_PRICES: [u64; 4] = [20_000_000, 50_000_000, 80_000_000, 5_000_000];
/// The invoice of `sources` under `t`'s own rules, after `previous` (a package it holds is held
/// through the period, §4.7), computed and signed without the checks of §7.2 steps 3 to 6.
#[allow(clippy::too_many_arguments)]
fn sealed_after(t: &Tariff, previous: Option<&Invoice>, held: (u64, u64), start: u64, end: u64, sources: &[&Value], f: impl FnOnce(&mut Invoice)) -> Invoice {
let mut facts: Vec<Facts> = sources.iter().map(|s| verify_source_of(s, &signers(t), t.version).expect("a corpus source verifies")).collect();
hold(t, &mut facts, previous, held.0, held.1);
let c = compute(t, &facts).expect("figures that fit");
let mut i = invoice_of(t, previous.map_or([0; 32], Invoice::digest), start, end, &c);
f(&mut i);
i.sign(&key("seller"))
}
/// The invoice of `sources`, computed and signed without the checks of §7.2 steps 3 to 6, so a
/// control can carry a defect those steps catch.
fn sealed(t: &Tariff, prev: [u8; 32], start: u64, end: u64, sources: &[&Value], f: impl FnOnce(&mut Invoice)) -> Invoice {
@ -447,6 +467,86 @@ pub fn generate(wai: &Path) -> Result<String, String> {
vec![],
),
);
// Jobs routed under the route record's version 2 (route-record §3.4): the price book prices
// each operation of an endpoint in its own entry and states its measured failures, and the
// hop that served states its outputs by content hash, here the clip its receipt decodes. An
// encode; a failover whose failed hop was metered, it spent energy and delivered nothing; a
// decode; a hop that served past the deadline; three hops that all failed, one unmetered; a
// failover whose failed hop's figure states no class; and two jobs whose outputs are not what
// their hop delivered, another clip, and the clip twice.
let delivered = [rr_hash(&clip)];
let failed = |endpoint: &'static str, start: u64, end: u64, receipt: Value| rr::Hop { endpoint, error: Some("server_error"), start, end, receipt };
let routed_2 = |job: &str, operation: &str, hops: &[rr::Hop], outputs: &[[u8; 32]], given: Vec<Value>| routed(rr::job_document_2(job, operation, accepted, hops, outputs), given);
const ENC: &str = "media.encode";
pool.insert("job_v2_encode", routed_2("job-3001", ENC, &[served("im.alpha", rr::hop_receipt(ENC, "job-3001", "im.alpha", Some("30.25")))], &delivered, out("job-3001")));
pool.insert(
"job_v2_failover",
routed_2(
"job-3002",
ENC,
&[
failed("im.alpha", 1_000, 2_000_000, rr::failed_hop_receipt(ENC, "job-3002", "im.alpha", Some("2.25"))),
rr::Hop { endpoint: "im.eta", error: None, start: 2_000_100, end: 9_000_000, receipt: rr::hop_receipt(ENC, "job-3002", "im.eta", Some("29.5")) },
],
&delivered,
out("job-3002"),
),
);
pool.insert("job_v2_decode", routed_2("job-3003", "media.decode", &[served("im.alpha", rr::hop_receipt("media.decode", "job-3003", "im.alpha", Some("12.0")))], &delivered, out("job-3003")));
pool.insert(
"job_v2_late",
routed_2("job-3004", ENC, &[rr::Hop { endpoint: "im.alpha", error: None, start: 1_000, end: 31_000_000, receipt: rr::hop_receipt(ENC, "job-3004", "im.alpha", Some("31.0")) }], &delivered, out("job-3004")),
);
pool.insert(
"job_v2_all_failed",
routed_2(
"job-3005",
ENC,
&[
failed("im.alpha", 1_000, 2_000_000, rr::failed_hop_receipt(ENC, "job-3005", "im.alpha", Some("1.5"))),
failed("im.eta", 2_000_100, 3_000_000, rr::failed_hop_receipt(ENC, "job-3005", "im.eta", None)),
failed("im.iota", 3_000_100, 4_000_000, rr::failed_hop_receipt(ENC, "job-3005", "im.iota", Some("0.75"))),
],
&[],
vec![],
),
);
pool.insert(
"job_v2_unclassed_failure",
routed_2(
"job-3006",
ENC,
&[
failed("im.alpha", 1_000, 2_000_000, rr::without_class(rr::failed_hop_receipt(ENC, "job-3006", "im.alpha", Some("2.25")))),
rr::Hop { endpoint: "im.eta", error: None, start: 2_000_100, end: 9_000_000, receipt: rr::hop_receipt(ENC, "job-3006", "im.eta", Some("29.5")) },
],
&delivered,
out("job-3006"),
),
);
let encode_2 = pool["job_v2_encode"].clone();
let with_outputs = |outputs: Vec<Value>| {
let mut j = encode_2.clone();
j["outputs"] = Value::Array(outputs);
j
};
pool.insert("job_v2_another_output", with_outputs(vec![output_of(&other_clip, &other_clip, "job-3001")]));
pool.insert("job_v2_output_twice", with_outputs([out("job-3001"), out("job-3001")].concat()));
// Version 4's room: the lesson's receipt of version 2, its bytes out priced.
pool.insert("lesson_bytes", room_of(&metering, "bytes_out_priced")?);
// Version 4's packaging runs (§4.7): video-base-01 made by a routed run of `package.build`,
// the package its output, and made again two days later, the same package from a new run
// with new energy; and that run made again replaying the first run's hop receipt.
let base = package_of(wai, "video-base-01")?;
let base_output = json!({"package": base["receipt"], "indexes": base["indexes"]});
let base_object = [rr_hash(base["receipt"].as_str().ok_or("a package receipt")?.as_bytes())];
let pack_hop = |at: u64, receipt: Value| rr::Hop { endpoint: "pack.alpha", error: None, start: at, end: at + 3_000_000, receipt };
let first_pack = rr::hop_receipt("package.build", "pack-0001", "pack.alpha", Some("4.5"));
let packed = |accepted: u64, receipt: Value| routed(rr::package_job_document("pack-0001", accepted, &[pack_hop(1_000, receipt)], &base_object), vec![base_output.clone()]);
pool.insert("package_run", packed(accepted, first_pack.clone()));
pool.insert("package_run_again", packed(accepted + 2 * DAY_US, rr::hop_receipt("package.build", "pack-0001", "pack.alpha", Some("4.625"))));
let first_sealed: Value = serde_json::from_str(pool["package_run"]["receipts"][0].as_str().ok_or("the run's hop receipt")?).map_err(|e| e.to_string())?;
pool.insert("package_run_replayed", routed(rr::package_job_document("pack-0001", accepted + 2 * DAY_US, &[rr::Hop { endpoint: "pack.alpha", error: None, start: 1_000, end: 3_001_000, receipt: first_sealed }], &base_object), vec![base_output.clone()]));
// The rooms' prices are the lesson receipts'; the tariff's scope is every party of the
// accepted invoices.
@ -539,7 +639,7 @@ pub fn generate(wai: &Path) -> Result<String, String> {
Vector { name: "tariff_period_empty", about: "a tariff whose period is empty, signed by both", tariff: tariff_period.to_bytes(), invoice: ab.clone(), previous: None, sources: day_a.to_vec() },
// The invoice.
Vector { name: "invoice_trailing_byte", about: "an invoice with a byte after its signature", tariff: tb.clone(), invoice: [ab.clone(), vec![0]].concat(), previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_version", about: "an invoice of version 4, which this revision does not define", tariff: tb.clone(), invoice: { let mut b = ab.clone(); b[4] = 4; b }, previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_version", about: "an invoice of version 5, which this revision does not define", tariff: tb.clone(), invoice: { let mut b = ab.clone(); b[4] = 5; b }, previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_signature", about: "an invoice whose signature does not verify", tariff: tb.clone(), invoice: flip(ab.clone(), ab.len() - 1), previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_lines_unordered", about: "an invoice whose first two lines are swapped", tariff: tb.clone(), invoice: sealed(&t, [0; 32], a0, a1, &refs(&day_a), |i| i.lines.swap(0, 1)).to_bytes(), previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_row_of_no_class", about: "an energy row of tag 0: joules of no one class", tariff: tb.clone(), invoice: sealed(&t, [0; 32], a0, a1, &refs(&day_a), |i| i.energy.insert(0, Row { scope: 1, tag: 0, figures: 1, joules: 1, max_ppm: 0 })).to_bytes(), previous: None, sources: day_a.to_vec() },
@ -683,6 +783,93 @@ pub fn generate(wai: &Path) -> Result<String, String> {
Vector { name: "version_2_bills_an_output_once", about: "version 2 as before: the re-run of job-0001 in the next invoice of a version-2 chain, refused because its output receipt was billed", tariff: tjb.clone(), invoice: sealed(&t_j, inv_j_first.digest(), mid, MONTH.1, &refs(&["job_rerun"]), |_| {}).to_bytes(), previous: Some(inv_j_first.to_bytes()), sources: vec!["job_rerun"] },
]);
// Jobs routed under the route record's version 2, billed under a tariff of version 3: each
// output receipt is of an object its served hop states it delivered (§5.1); a failed hop's
// figure is not read, as version 3 states none.
let day_r = ["job_v2_encode", "job_v2_failover", "job_v2_decode", "job_v2_late", "job_v2_all_failed", "job_v2_unclassed_failure"];
let inv_r = make_invoice(&seller, &t_k, None, MONTH.0, MONTH.1, &src(&day_r)).map_err(|e| format!("invoice r: {e:?}"))?;
vectors.extend([
Vector { name: "version_2_routes_under_version_3", about: "version 3: jobs routed under the route record's version 2, per operation and ranked by their measured failures, each output receipt of an object its served hop states it delivered: an encode, a failover billed at the endpoint that served, a decode, a hop past the deadline and three that failed, billed nothing, the failed hops' figures not stated", tariff: tkb.clone(), invoice: inv_r.to_bytes(), previous: None, sources: day_r.to_vec() },
Vector { name: "an_output_the_hop_did_not_deliver", about: "version 3: a job of version 2 whose output receipt is of a clip its served hop does not state it delivered", tariff: tkb.clone(), invoice: inv_r.to_bytes(), previous: None, sources: vec!["job_v2_another_output"] },
Vector { name: "one_output_given_twice", about: "version 3: a job of version 2 giving the receipt of its one output twice, to bill its subpixels twice", tariff: tkb.clone(), invoice: inv_r.to_bytes(), previous: None, sources: vec!["job_v2_output_twice"] },
]);
// Version 4 (§4.7): a room's bytes out at a price per 10^9 bytes, a package's work once and
// its storage by byte-days for each period it is held, and every hop's energy, the hops not
// billed in a scope of their own. Its room prices are the receipt of version 2's.
let room_prices_4 = {
let Facts::Room(r) = verify_source(&pool["lesson_bytes"], &[]).map_err(|e| format!("lesson_bytes: {e:?}"))? else { unreachable!() };
let mut p = room_prices;
p[ROOM_PRICES_1] = r.tariff.bytes;
p
};
let staged_scope = first_scope("package_staged");
// A run of `package.build` priced per run and per work unit of the package it made.
let mut job_prices_4 = job_prices_3.clone();
job_prices_4.push(jp(rr::PACKAGE, "package.build", 1, 1, 300_000));
job_prices_4.push(jp(rr::PACKAGE, "package.build", 3, 1, 1_000));
let v4 = |room: [u32; ROOM_PRICES]| -> Tariff {
let mut t4 = tariff_of("buyer-4", room, vec![(source_kind::ROOM, lesson_id), package_scope, staged_scope, (source_kind::JOB, router)]);
t4.version = VERSION_4;
t4.package = [0, 0, 2_000_000];
t4.job = job_prices_4.clone();
t4.sign(&seller, &key("buyer-4"))
};
let t_4 = v4(room_prices_4);
let t4b = t_4.to_bytes();
let day_4a = ["lesson_bytes", "package_base", "package_run", "job_v2_encode", "job_v2_failover", "job_v2_decode", "job_v2_late", "job_v2_all_failed"];
let day_4b = ["package_base", "package_staged", "package_run_again"];
let inv_4a = make_invoice(&seller, &t_4, None, MONTH.0, mid, &src(&day_4a)).map_err(|e| format!("invoice 4a: {e:?}"))?;
let inv_4b = make_invoice(&seller, &t_4, Some(&inv_4a), mid, MONTH.1, &src(&day_4b)).map_err(|e| format!("invoice 4b: {e:?}"))?;
let (a4, b4) = (inv_4a.to_bytes(), inv_4b.to_bytes());
let line_of = |i: &Invoice, kind: u8, unit: u8| i.lines.iter().position(|l| l.source_kind == kind && l.unit == unit).expect("a line");
let im_encode = |unit: u8| u8::try_from(job_prices_4.iter().position(|p| p.capability == IM && p.operation == "media.encode" && p.unit == unit).expect("a price") + 1).expect("a unit");
let mut other_bytes = room_prices_4;
other_bytes[ROOM_PRICES_1] += 1;
let t_4_other = v4(other_bytes);
let base_signer = package_scope.1;
vectors.extend([
Vector { name: "version_4_by_the_byte_and_the_work", about: "version 4: a lesson room's receipt of version 2 billed by the bytes its hops sent its sessions beside its time, a package listed and not yet billed for storage, the run that made it billed per run and per work unit of the package, and jobs routed under the route record's version 2 at their own prices, every hop's energy stated: the billed hops' in the jobs' scope, the failed hops' and the late hop's in a scope of their own, one of them unmetered", tariff: t4b.clone(), invoice: a4.clone(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_storage_held_through_a_period", about: "version 4: the next invoice of the chain bills the package the first invoice holds for the byte-days of its own period, and the same package made again by a new run, its work billed for that run and its receipt cited again, beside a second package listed", tariff: t4b.clone(), invoice: b4.clone(), previous: Some(a4.clone()), sources: day_4b.to_vec() },
Vector { name: "version_4_bytes_at_another_price", about: "version 4: a tariff whose price of bytes out is not the room receipt's", tariff: t_4_other.to_bytes(), invoice: sealed_after(&t_4_other, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |_| {}).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_a_receipt_that_prices_no_bytes", about: "version 4: a room receipt of version 1 under its tariff, which states no price of bytes, billed under a tariff that prices them", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&["lesson"]), |_| {}).to_bytes(), previous: None, sources: vec!["lesson"] },
Vector { name: "version_4_bytes_out_above_the_receipt", about: "version 4: the room's bytes-out line one byte above what its receipt states", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| { let k = line_of(i, source_kind::ROOM, 14); i.lines[k].quantity += 1; }).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_storage_of_a_package_first_billed", about: "version 4: the first invoice billing a day of storage for a package it bills for the first time, which no record shows was kept before the period ended", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| {
let cited: Cited = [(record_kind::PACKAGE, first_receipt(&pool["package_base"]))].into_iter().collect();
i.lines.push(Line { source_kind: source_kind::PACKAGE, source: base_signer, unit: 3, quantity: 1, price: 2_000_000, amount: 0, cited: 1, cites: cites_digest(&cited) });
i.lines.sort_by_key(|l| (l.source_kind, l.source, l.unit));
}).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_a_package_run_replayed", about: "version 4: the next invoice bills the package made again under a new route record whose hop receipt is the first run's, which the first invoice billed", tariff: t4b.clone(), invoice: sealed_after(&t_4, Some(&inv_4a), (mid, MONTH.1), mid, MONTH.1, &refs(&["package_base", "package_staged", "package_run_replayed"]), |_| {}).to_bytes(), previous: Some(a4.clone()), sources: vec!["package_base", "package_staged", "package_run_replayed"] },
Vector { name: "version_4_one_package_run_under_two_jobs", about: "version 4: the package's first run and its replay in one invoice, one hop receipt under two route records", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, MONTH.1), MONTH.0, MONTH.1, &refs(&["package_run", "package_run_replayed"]), |_| {}).to_bytes(), previous: None, sources: vec!["package_run", "package_run_replayed"] },
Vector { name: "version_4_storage_over_the_tariffs_period", about: "version 4: the held package's storage billed over the tariff's whole period rather than the invoice's", tariff: t4b.clone(), invoice: sealed_after(&t_4, Some(&inv_4a), (MONTH.0, MONTH.1), mid, MONTH.1, &refs(&day_4b), |_| {}).to_bytes(), previous: Some(a4.clone()), sources: day_4b.to_vec() },
Vector { name: "version_4_unbilled_hops_left_out", about: "version 4: the first invoice without the failed and late hops' energy", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| { i.energy.retain(|r| r.scope != scope::UNBILLED_HOPS); i.unmetered[5] = 0; }).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_unbilled_hops_in_the_jobs_scope", about: "version 4: the failed and late hops' counter readings added to the billed hops' row", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| {
let moved: Vec<Row> = i.energy.iter().filter(|r| r.scope == scope::UNBILLED_HOPS).copied().collect();
i.energy.retain(|r| r.scope != scope::UNBILLED_HOPS);
for m in moved {
if let Some(r) = i.energy.iter_mut().find(|r| r.scope == scope::JOBS && r.tag == m.tag) {
r.figures += m.figures;
r.joules += m.joules;
}
}
i.unmetered[4] += i.unmetered[5];
i.unmetered[5] = 0;
}).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_a_failed_hop_billed_as_a_job", about: "version 4: the int_motion encode jobs line counting the failover's failed hop as a job, a routing figure billed", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| { let k = i.lines.iter().position(|l| l.source_kind == source_kind::JOB && l.unit == im_encode(1)).expect("the encode jobs line"); i.lines[k].quantity += 1; }).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_a_job_line_at_the_routers_price", about: "version 4: the int_motion encode subpixels line priced at the router's price book's list price for its endpoint, not the tariff's", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| {
let k = i.lines.iter().position(|l| l.source_kind == source_kind::JOB && l.unit == im_encode(2)).expect("the encode subpixels line");
let l = &mut i.lines[k];
i.amount -= l.amount;
l.price = 30_000;
l.amount = (l.quantity as u128 * l.price as u128 / 1_000_000) as u64;
i.amount += l.amount;
}).to_bytes(), previous: None, sources: day_4a.to_vec() },
Vector { name: "version_4_an_output_the_hop_did_not_deliver", about: "version 4: a job of version 2 whose output receipt is of a clip its served hop does not state it delivered", tariff: t4b.clone(), invoice: a4.clone(), previous: None, sources: vec!["job_v2_another_output"] },
Vector { name: "version_4_an_output_of_a_version_1_record", about: "version 4: a job whose route record of version 1 states no output, given an output receipt", tariff: t4b.clone(), invoice: a4.clone(), previous: None, sources: vec!["job_int_motion"] },
Vector { name: "version_4_unbilled_hop_with_no_class", about: "version 4: a failover whose failed hop's figure states no acquisition class, which no row can state", tariff: t4b.clone(), invoice: a4.clone(), previous: None, sources: vec!["job_v2_unclassed_failure"] },
Vector { name: "version_3_invoice_under_a_version_4_tariff", about: "an invoice of version 3 naming a tariff of version 4", tariff: t4b.clone(), invoice: sealed_after(&t_4, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_4a), |i| { i.version = VERSION_3; i.unmetered[5] = 0; i.energy.retain(|r| r.scope != scope::UNBILLED_HOPS); i.lines.retain(|l| !(l.source_kind == source_kind::ROOM && l.unit == 14)); }).to_bytes(), previous: None, sources: day_4a.to_vec() },
]);
let mut sources = Map::new();
for (n, v) in &pool {
sources.insert((*n).into(), v.clone());
@ -696,7 +883,7 @@ pub fn generate(wai: &Path) -> Result<String, String> {
"sources": v.sources, "outcome": outcome,
}));
}
let keys = json!({"seller": hex(&pk("seller")), "buyer": hex(&pk("buyer")), "buyer_m": hex(&pk("buyer-m")), "buyer_s": hex(&pk("buyer-s")), "buyer_j": hex(&pk("buyer-j")), "cdn_a": hex(&pk("cdn-a")), "cdn_b": hex(&pk("cdn-b")), "output_sink": hex(&pk("output-sink"))});
let keys = json!({"seller": hex(&pk("seller")), "buyer": hex(&pk("buyer")), "buyer_m": hex(&pk("buyer-m")), "buyer_s": hex(&pk("buyer-s")), "buyer_j": hex(&pk("buyer-j")), "buyer_4": hex(&pk("buyer-4")), "cdn_a": hex(&pk("cdn-a")), "cdn_b": hex(&pk("cdn-b")), "output_sink": hex(&pk("output-sink"))});
let doc = json!({
"corpus": CAPABILITY,
"extension": "extensions/receipt-ledger.md",

View file

@ -614,6 +614,91 @@ pub(crate) fn job_document(job: &str, operation: &str, accepted: u64, attempts:
})
}
/// The OER/2 receipt of a hop of `job` at `endpoint` for `operation` that failed, sealed and
/// bound as [`hop_receipt`] seals and binds: its figure a counter reading of `joules`, for a
/// hop that spent energy and delivered nothing, or the unmetered marker (`None`).
#[cfg(feature = "receipt_ledger")]
pub(crate) fn failed_hop_receipt(operation: &str, job: &str, endpoint: &str, joules: Option<&str>) -> Value {
let mut r = match joules {
Some(j) => receipt(operation, endpoint, -1, Some((j, counter(), "measured"))),
None => receipt(operation, endpoint, -1, None),
};
r["subject"]["inputs"] = json!([sha(&format!("{job}/{endpoint}"))]);
reseal_receipt(&mut r);
r
}
/// A hop's receipt with its figure's acquisition class removed, sealed again: a figure no
/// energy row can state (receipt-ledger §5.1).
#[cfg(feature = "receipt_ledger")]
pub(crate) fn without_class(mut r: Value) -> Value {
r["energy"].as_object_mut().expect("an energy object").remove("class");
reseal_receipt(&mut r);
r
}
/// The verification document (§7.1) of a job of version 2 (§3.4): version 2's request under
/// `job` and `operation`, routed by this corpus's router under version 2's price book, with its
/// per-operation entries and measured failures, and its policy, accepted at `accepted`, with
/// `attempts` in their order. The attempt that served states `outputs`, and its receipt is
/// sealed again naming their commitment (§6), so a caller gives a served hop's receipt as
/// [`hop_receipt`] makes it.
#[cfg(feature = "receipt_ledger")]
pub(crate) fn job_document_2(job: &str, operation: &str, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
routed_2(request_2(job, operation, no_preference()), pricebook_2(), accepted, attempts, outputs)
}
/// The capability a packaging job names: WAI's packaging (`wai.video.package`).
#[cfg(feature = "receipt_ledger")]
pub(crate) const PACKAGE: &str = "wai.video.package";
/// The verification document of a packaging job of version 2: version 2's request under `job`
/// for the operation `package.build` of [`PACKAGE`], one package, routed under a price book of
/// version 2 whose one entry, `pack.alpha`, prices that operation per package and never failed in
/// 1,000 hops; `attempts` and `outputs` as [`job_document_2`] takes them.
#[cfg(feature = "receipt_ledger")]
pub(crate) fn package_job_document(job: &str, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
let mut request = request_2(job, "package.build", no_preference());
request["capability"] = json!(PACKAGE);
request["fallback"] = Value::Null;
request["quantities"] = json!([{"unit": "packages", "quantity": 1}]);
let entry = entry_2("pack.alpha", "package.build", "prov-alpha", PACKAGE, EU_C, ("packages", 1, 400_000), Some(500), Some((4_500_000, "OnChipCounter")), Some(950), failures(1_000, 0, 0, 0));
routed_2(request, pricebook_2_of(vec![entry]), accepted, attempts, outputs)
}
#[cfg(feature = "receipt_ledger")]
fn routed_2(request: Value, pricebook: Value, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
let r = Request::read(&canonical(&request)).expect("the request reads");
let b = PriceBook::read(&canonical(&pricebook)).expect("the price book reads");
let p = Policy::read(&canonical(&policy(3))).expect("the policy reads");
let commitment = hex(&super::output_commitment(outputs));
let receipts: Vec<Value> = attempts
.iter()
.map(|h| {
let mut rc = h.receipt.clone();
if h.error.is_none() {
rc["subject"]["output"] = json!(commitment);
reseal_receipt(&mut rc);
}
rc
})
.collect();
let atts: Vec<Attempt> = attempts
.iter()
.zip(&receipts)
.map(|(h, rc)| Attempt { endpoint: h.endpoint.to_string(), error: h.error, start: accepted + h.start, end: accepted + h.end, receipt: id(rc) })
.collect();
let record = super::seal_delivered(&router(), &r, &b, &p, accepted, &atts, outputs);
json!({
"request": canonical(&request),
"pricebook": canonical(&pricebook),
"policy": canonical(&policy(3)),
"record": canonical(&record),
"receipts": receipts.iter().map(text).collect::<Vec<_>>(),
"keys": {HOP_SIGNER: hex(&hops().verifying_key().to_bytes())},
})
}
/// The corpus, as `vectors.json` holds it.
pub fn generate() -> Result<String, String> {
let mut out = Vec::new();

View file

@ -87,6 +87,42 @@ fn the_corpus_covers_every_reason_and_case() {
}
}
/// A receipt of version 2 (§4.1, §4.4, §4.6): its bytes out are its hops' ledgers' bytes out,
/// the sum of its grants'; a grant's funds count its bytes at their price; every receipt of
/// version 1 states no bytes; and the controls of version 2 are refused at their places.
#[test]
fn a_receipt_of_version_2_states_and_prices_the_bytes_out() {
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
let vectors = doc["vectors"].as_array().unwrap();
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"));
let v2 = get("bytes_out_priced");
assert_eq!(v2["outcome"], "accept");
assert_eq!(&v2["receipt"].as_str().unwrap()[8..10], "02", "a receipt of version 2");
let bytes = v2["units"]["bytes_out"].as_u64().unwrap();
assert!(bytes > 0);
let by_grant: u64 = v2["consumption"].as_array().unwrap().iter().map(|c| c["bytes_out"].as_u64().unwrap()).sum();
assert_eq!(by_grant, bytes, "the grants' bytes out sum to the receipt's");
let lesson = get("lesson");
let mut without = v2["units"].clone();
without.as_object_mut().unwrap().remove("bytes_out");
assert_eq!(without, lesson["units"], "every other unit is version 1's");
assert!(
v2["consumption"].as_array().unwrap().iter().zip(lesson["consumption"].as_array().unwrap()).any(|(a, b)| a["funds"].as_u64() > b["funds"].as_u64()),
"a grant's funds count its bytes"
);
for v in vectors.iter().filter(|v| v["outcome"] == "accept" && v["name"] != "bytes_out_priced") {
assert!(v["units"].get("bytes_out").is_none(), "{}", v["name"]);
}
for (n, why, at) in [
("bytes_out_mismatch", "unit_mismatch", serde_json::json!(["units"])),
("bytes_out_left_out_of_the_consumption", "ceiling_mismatch", serde_json::json!(["ceilings"])),
("bytes_priced_with_no_tariff", "session_invalid", serde_json::json!(["receipt"])),
("receipt_unknown_version", "unknown_version", serde_json::json!(["receipt"])),
] {
assert_eq!((&get(n)["reason"], &get(n)["at"]), (&serde_json::json!(why), &at), "{n}");
}
}
/// §9 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {

View file

@ -180,6 +180,99 @@ fn version_3_prices_each_operation_and_bills_the_work() {
}
}
/// Jobs routed under the route record's version 2 (§5.1): billed under version 3 at the endpoint
/// that served, each output receipt of an object its hop states it delivered, and refused
/// `output_not_delivered` when it is another or given twice; under version 4 a record of
/// version 1 holds its outputs to none.
#[test]
fn a_jobs_output_is_what_its_hop_delivered() {
use serde_json::json;
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
let vectors = doc["vectors"].as_array().unwrap();
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
let r = get("version_2_routes_under_version_3");
assert_eq!(r["outcome"], "accept");
let jobs: u64 = r["lines"].as_array().unwrap().iter().filter(|l| l["unit"] == "jobs").map(|l| l["quantity"].as_u64().unwrap()).sum();
assert_eq!(jobs, 4, "an encode, a failover, a decode and a failover whose failed hop states no class; the late and the failed jobs bill nothing");
assert!(r["energy"].as_array().unwrap().iter().all(|e| e["scope"] == "jobs"), "version 3 states the billed hops' energy alone");
let record = |n: &str| -> serde_json::Value { serde_json::from_str(doc["sources"][n]["record"].as_str().unwrap()).unwrap() };
assert_eq!(record("job_v2_encode")["version"], 2, "a route record of version 2");
assert!(record("job_v2_failover")["candidates"].as_array().unwrap().iter().any(|c| c["failures"].is_object()), "measured failures stated");
for (n, at) in [
("an_output_the_hop_did_not_deliver", json!(["source", 0, "outputs", 0])),
("one_output_given_twice", json!(["source", 0, "outputs", 1])),
("version_4_an_output_the_hop_did_not_deliver", json!(["source", 0, "outputs", 0])),
("version_4_an_output_of_a_version_1_record", json!(["source", 0, "outputs", 0])),
] {
let o = get(n);
assert_eq!((o["reason"].as_str(), &o["at"]), (Some("output_not_delivered"), &at), "{n}");
}
}
/// Version 4 (§4.7, §6.6): a room's bytes out at its price per 10^9 bytes; a package's work
/// billed once per run that made it, its receipt cited by each, and its storage by byte-days for
/// each period it is held; every hop's energy, the hops not billed in a scope of their own; and
/// each control refused at its place.
#[test]
fn version_4_bills_the_bytes_the_work_and_the_storage() {
use serde_json::{json, Value};
let doc: Value = serde_json::from_str(&committed()).unwrap();
let vectors = doc["vectors"].as_array().unwrap();
let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
let lines = |o: &Value, unit: &str| -> Vec<Value> { o["lines"].as_array().unwrap().iter().filter(|l| l["unit"] == unit).cloned().collect() };
let metering: Value = serde_json::from_str(&read("live-conformance/metering/vectors.json")).unwrap();
let bytes = metering["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "bytes_out_priced").unwrap()["units"]["bytes_out"].as_u64().unwrap();
let package = |n: &str| -> Value { serde_json::from_str(doc["sources"][n]["receipt"].as_str().unwrap()).unwrap() };
let base = package("package_base");
let cites = |l: &Value, kind: u64| -> Vec<String> { l["cites"].as_array().unwrap().iter().filter(|c| c[0] == kind).map(|c| c[1].as_str().unwrap().to_string()).collect() };
let a = get("version_4_by_the_byte_and_the_work");
assert_eq!(a["outcome"], "accept");
let out = lines(&a, "bytes_out");
assert_eq!(out.len(), 1);
assert_eq!(out[0]["quantity"].as_u64(), Some(bytes), "the receipt's bytes out");
assert_eq!(out[0]["amount"].as_u64().unwrap(), (bytes as u128 * out[0]["price"].as_u64().unwrap() as u128 / 1_000_000_000) as u64);
let work_a = lines(&a, "work_units");
assert_eq!((work_a.len(), &work_a[0]["quantity"], &work_a[0]["source_kind"]), (1, &base["work_units"], &json!("job")), "the run that made the package, by its work units");
assert_eq!(cites(&work_a[0], 5), vec![base["receipt_hash"].as_str().unwrap().to_string()], "the package cited as the run's output");
assert!(a["lines"].as_array().unwrap().iter().all(|l| l["source_kind"] != "package"), "a package first listed bills no storage, and a package source no work");
let scope = |o: &Value, s: &str| -> Vec<Value> { o["energy"].as_array().unwrap().iter().filter(|e| e["scope"] == s).cloned().collect() };
assert_eq!(scope(&a, "jobs").iter().map(|e| e["figures"].as_u64().unwrap()).sum::<u64>(), 4, "four billed hops, the packaging run's among them");
assert!(scope(&a, "packaging").is_empty(), "a package's energy is its run's, stated by its hop");
assert_eq!(scope(&a, "unbilled_hops").iter().map(|e| e["figures"].as_u64().unwrap()).sum::<u64>(), 4, "the failover's, the late hop's and two of the three failed hops' figures");
assert_eq!((a["unmetered"]["unbilled_hops"].as_u64(), a["coverage"]["unbilled_hops"].as_str()), (Some(1), Some("partial")), "one failed hop unmetered");
let b = get("version_4_storage_held_through_a_period");
assert_eq!(b["outcome"], "accept");
let days = lines(&b, "byte_days");
assert_eq!(days.len(), 1);
let period = b["end"].as_u64().unwrap() - b["start"].as_u64().unwrap();
assert_eq!(days[0]["quantity"].as_u64().unwrap() as u128, base["stored_bytes"].as_u64().unwrap() as u128 * period as u128 / 86_400_000_000, "its bytes over the invoice's period, in days");
let work_b = lines(&b, "work_units");
assert_eq!((work_b.len(), &work_b[0]["quantity"]), (1, &base["work_units"]), "the same package made again, its work billed for the new run");
assert_eq!(cites(&work_b[0], 5), cites(&work_a[0], 5), "the package the first invoice lists, cited again");
assert!(cites(&work_b[0], 14).iter().all(|h| !cites(&work_a[0], 14).contains(h)), "a new run's hop receipt");
for (n, reason, at) in [
("version_4_bytes_at_another_price", "tariff_conflict", json!(["source", 0])),
("version_4_a_receipt_that_prices_no_bytes", "tariff_conflict", json!(["source", 0])),
("version_4_bytes_out_above_the_receipt", "quantity_not_recorded", json!(["invoice"])),
("version_4_storage_of_a_package_first_billed", "quantity_not_recorded", json!(["invoice"])),
("version_4_a_package_run_replayed", "record_rebilled", json!(["source", 2])),
("version_4_one_package_run_under_two_jobs", "record_duplicate", json!(["source", 1])),
("version_4_storage_over_the_tariffs_period", "quantity_not_recorded", json!(["invoice"])),
("version_4_unbilled_hops_left_out", "invoice_mismatch", json!(["invoice"])),
("version_4_unbilled_hops_in_the_jobs_scope", "invoice_mismatch", json!(["invoice"])),
("version_4_a_failed_hop_billed_as_a_job", "quantity_not_recorded", json!(["invoice"])),
("version_4_a_job_line_at_the_routers_price", "invoice_mismatch", json!(["invoice"])),
("version_4_unbilled_hop_with_no_class", "job_figure_invalid", json!(["source", 0, "receipts", 0])),
("version_3_invoice_under_a_version_4_tariff", "tariff_mismatch", json!(["invoice"])),
] {
let o = get(n);
assert_eq!((o["reason"].as_str(), &o["at"]), (Some(reason), &at), "{n}");
}
}
/// §10 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {