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
20 changed files with 3441 additions and 274 deletions
|
|
@ -6,6 +6,102 @@ wire format may change and such changes are recorded here.
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
### 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
|
- **Tariff version 3** ([extensions/receipt-ledger.md](extensions/receipt-ledger.md) §4.6): each job price
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
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
|
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,
|
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
|
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
|
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
|
`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
|
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
|
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 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
|
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
|
receipt or a settlement statement that states a tariff must state this one, and the seller signs an
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@
|
||||||
> the **room-session receipt**, `WMR1`, which names the per-hop records, the takes and the
|
> 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,
|
> composition receipts it aggregates, and states the session's units (publisher-seconds,
|
||||||
> subscriber-seconds by layer class, composition-seconds, events sent and delivered,
|
> 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
|
> recorded-seconds, and in version 2 the bytes the hops sent), the joules of every hop with
|
||||||
> one across classes, and each grant's consumption against its joule and funds ceilings.
|
> 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.
|
> 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
|
> 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
|
> 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 |
|
| field | size | meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `magic` | 4 | `WMR1` (ASCII) |
|
| `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 |
|
| `room` | 32 | the room id |
|
||||||
| `signer` | 32 | the key that signs the receipt |
|
| `signer` | 32 | the key that signs the receipt |
|
||||||
| `start`, `end` | 8 each | u64: the room session, [`start`, `end`) |
|
| `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 |
|
| `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) |
|
| `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) |
|
| `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 |
|
| `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) |
|
| `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` |
|
| `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) |
|
| `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 |
|
| `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
|
### 4.2 Digest, signature and rules
|
||||||
|
|
||||||
The **digest** of a receipt is SHA-256 over its bytes before `sig`; its **signature** is
|
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 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;
|
2 to 5 and its `figures` above 0;
|
||||||
- `compose`'s digest is zero exactly when its count is, and so is `qoe`'s;
|
- `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
|
- with no tariff (`currency` zero) every price is 0, the price of bytes among them; otherwise
|
||||||
`A`–`Z`;
|
`currency` is three capitals `A`–`Z`;
|
||||||
- each past grant's `flags` is 1, 2 or 3;
|
- 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.
|
- `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
|
+ *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
|
take with no held commitment records nothing. What a device captured and did not commit
|
||||||
while capturing is not counted.
|
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.
|
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
|
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
|
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;
|
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,
|
- **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*] ×
|
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>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
|
*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,
|
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,
|
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),
|
`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,
|
`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),
|
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**
|
`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,
|
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
|
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:
|
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
|
- **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
|
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
|
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
|
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
|
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
|
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
|
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
|
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`
|
the corpus states. `wai_live_metering_vectors verify` and the test `tests/live_metering.rs`
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,14 @@
|
||||||
> receipt of its output, in the units those receipts state. A tariff of version 3 prices a
|
> 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
|
> 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,
|
> 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
|
> 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
|
> `receipt_ledger` module of `wai-rs` (feature `receipt_ledger`) and the service
|
||||||
> `wai/wai-ledger`; the corpus is [`live-conformance/ledger/`](../live-conformance/ledger/),
|
> `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:
|
consistent with what the records say:
|
||||||
|
|
||||||
1. **A record that states a tariff states this one.** A room-session receipt of a source either
|
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
|
states no tariff (currency zero) or the tariff's currency and its room prices, in
|
||||||
live-metering §4.1's order; a settlement statement either states no tariff or the tariff's
|
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
|
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
|
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
|
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
|
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
|
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.
|
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
|
3. **Units are never converted.** A room is billed in its own units (in version 4, its bytes
|
||||||
package in packages, entries and stored bytes, an ad in impressions, a job in jobs or in
|
out too), a delivery in bytes, a package in packages, entries and stored bytes (in version
|
||||||
the subpixels its output's receipt states (§4.5). No line turns one unit
|
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
|
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.
|
its lines (§6.6), per class, and is not an amount.
|
||||||
|
|
||||||
|
|
@ -115,16 +124,16 @@ records recomputes all three.
|
||||||
| field | size | meaning |
|
| field | size | meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `magic` | 4 | `WLT1` (ASCII) |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `start`, `end` | 8 each | u64: the period the tariff is good for |
|
||||||
| `currency` | 3 | three capitals `A`–`Z` (ISO 4217) |
|
| `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) |
|
| `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 |
|
| `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`) |
|
| `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 |
|
| `seller_sig` | 64 | §4.2 |
|
||||||
| `buyer_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
|
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`,
|
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`.
|
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:
|
It then refuses as `tariff_invalid` a tariff that breaks any of these:
|
||||||
|
|
||||||
- `start` is below `end`; `currency` is three capitals `A`–`Z`;
|
- `start` is below `end`; `currency` is three capitals `A`–`Z`;
|
||||||
- `seller` and `buyer` are not zero, and not the same key;
|
- `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
|
- `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
|
- 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,
|
order of (`capability`'s bytes, `unit`); in versions 3 and 4, at most 255, each as §4.6
|
||||||
in strictly ascending order of (`capability`'s bytes, `operation`'s bytes, `unit`).
|
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`.
|
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` |
|
| 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` |
|
| 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 |
|
| 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
|
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.
|
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 |
|
| 1 | `jobs` | 1 |
|
||||||
| 2 | `subpixels` | the sum of its output receipts' `work_pixel_ops` (integer-payloads §15.1): the subpixels its output decodes to |
|
| 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
|
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),
|
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).
|
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.
|
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. Sources
|
||||||
|
|
||||||
### 5.1 Kinds and their documents
|
### 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 |
|
| `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 |
|
| `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) |
|
| `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
|
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
|
**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
|
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
|
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.
|
(§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
|
### 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` |
|
| 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 |
|
| 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` |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 and 3): each output receipt |
|
| 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 and 3): its record |
|
| 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
|
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
|
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
|
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
|
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.
|
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,
|
Under a tariff of version 3 or 4 it is not: an output receipt is the evidence of what a job
|
||||||
not the work, and the same request run again on a deterministic codec delivers the same output
|
delivered, not the work, and the same request run again on a deterministic codec delivers the
|
||||||
under the same receipt from a new hop that did the work again (§6.9). Two lines of one invoice
|
same object, of the same content hash, under the same receipt from a new hop that did the work
|
||||||
that cite one output receipt therefore cite different hop receipts, whichever lines they are.
|
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
|
### 5.3 Placement in time
|
||||||
|
|
||||||
|
|
@ -296,14 +382,15 @@ A source is **in** a period when:
|
||||||
- `delivery`: the statement's period lies within it;
|
- `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;
|
- `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
|
- `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.
|
- `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
|
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
|
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:
|
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
|
the later is refused `record_overlap`. Under a tariff of version 3 or 4, two route records of one
|
||||||
are two runs of it, each billed from its own hop, and no hop twice (§5.2): a job's id names what
|
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.
|
was asked, not the work, and a request run again is worked again.
|
||||||
|
|
||||||
## 6. The invoice
|
## 6. The invoice
|
||||||
|
|
@ -313,7 +400,7 @@ was asked, not the work, and a request run again is worked again.
|
||||||
| field | size | meaning |
|
| field | size | meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `magic` | 4 | `WLI1` (ASCII) |
|
| `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 |
|
| `seller`, `buyer` | 32 each | the tariff's |
|
||||||
| `tariff` | 32 | the tariff's digest |
|
| `tariff` | 32 | the tariff's digest |
|
||||||
| `prev` | 32 | the digest of the invoice before it in its chain (§6.9); zero for the first |
|
| `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`) |
|
| `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`) |
|
| `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`) |
|
| `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 |
|
| `amount` | 8 | u64: the sum of the lines' amounts |
|
||||||
| `sig` | 64 | §6.7 |
|
| `sig` | 64 | §6.7 |
|
||||||
|
|
||||||
|
|
@ -337,7 +424,7 @@ A line is 94 bytes:
|
||||||
|
|
||||||
| field | size | meaning |
|
| 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 |
|
| `source` | 32 | the room id, the delivering party, the packager, the ad decider or the router |
|
||||||
| `unit` | 1 | the unit (below) |
|
| `unit` | 1 | the unit (below) |
|
||||||
| `quantity` | 8 | u64: the quantity of the unit the records state |
|
| `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 | 11 | `composition_us` | their `composition_us` | `room` 10 | 10^6 |
|
||||||
| 1 | 12 | `recorded_us` | their `recorded_us` | `room` 11 | 10^6 |
|
| 1 | 12 | `recorded_us` | their `recorded_us` | `room` 11 | 10^6 |
|
||||||
| 1 | 13 | `events_sent` | their `events_sent` | `room` 12 | 1 |
|
| 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 |
|
| 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 | 1 | `packages` | the number of package receipts | `package` 0 | 1 |
|
||||||
| 3 | 2 | `entries` | their `total_entries` | `package` 1 | 1 |
|
| 3 | 2 | `entries` | their `total_entries` | `package` 1 | 1 |
|
||||||
| 3 | 3 | `stored_bytes` | their `stored_bytes` | `package` 2 | 10^9 |
|
| 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 | 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 |
|
| 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
|
In version 4 a package source bills only its `byte_days`, and its work is its run's job line.
|
||||||
none for a quantity of 0. A delivery line's bytes are those its party's downstream parties signed
|
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
|
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:
|
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
|
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,
|
- 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
|
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);
|
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,
|
- 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
|
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;
|
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,
|
- 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,
|
for `subpixels`, each of its output receipts whose `work_pixel_ops` is above 0. In versions 3
|
||||||
an output receipt that two jobs of the line delivered is cited once and its `work_pixel_ops`
|
and 4, an output receipt that two jobs of the line delivered is cited once and its
|
||||||
counted for each (§6.9).
|
`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
|
`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`,
|
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
|
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
|
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,
|
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).
|
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) |
|
| 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 |
|
| 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 |
|
| 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
|
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
|
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`;
|
- `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
|
- 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);
|
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 and 3), its `unit` a unit of its kind
|
- each line's `source_kind` is 1 to 4 (1 to 5 in versions 2 to 4), its `unit` a unit of its kind
|
||||||
(§6.3; for kind 5, at least 1), its `quantity` and `cited` above 0;
|
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), its `tag` 2 to 5 and its `figures`
|
- 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
|
||||||
above 0.
|
and its `figures` above 0.
|
||||||
|
|
||||||
Then the signature (§6.7): `bad_key`, `bad_signature`.
|
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
|
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
|
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
|
the `records` of two invoices of a chain, but, under a tariff of version 3 or 4, an output receipt
|
||||||
(kind 15): a record billed once is billed never again, whatever period a later invoice states, and
|
(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.
|
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**.
|
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
|
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
|
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 records a chain bills once for a job are its route record and its hops' OER/2 receipts,
|
||||||
the billed hop's among them;
|
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
|
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
|
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
|
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.
|
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
|
## 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
|
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
|
(`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,
|
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
|
`record_overlap`); and a record that is in the previous invoice's `records`, in versions 3 and
|
||||||
output receipt excepted (§6.9, `record_rebilled`).
|
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
|
7. **The figures**, at `invoice`, recomputed from the sources as §6.2 to §6.6 compute them and
|
||||||
compared in this order:
|
compared in this order:
|
||||||
1. a line whose source and unit no source gives, or that states a larger quantity than the
|
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.
|
- **refuse**: the reason and where.
|
||||||
- **accept**: the invoice's digest; the tariff's digest; the currency and the period; the number of
|
- **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
|
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
|
price's `capability`, and in versions 3 and 4 its `operation`), quantity, price, amount and its
|
||||||
records; each energy row by scope and class label with its figures, joules and `max_ppm`; each
|
cited records; each energy row by scope and class label with its figures, joules and `max_ppm`;
|
||||||
scope's unmetered figures and coverage (scopes 1 to 4, and in versions 2 and 3 scope 5); the
|
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.
|
amount; and the reports of §8.
|
||||||
|
|
||||||
## 8. Reports
|
## 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 |
|
| `private_kind` | §5.2, §6.8, §7.2 step 2 |
|
||||||
| `output_invalid` | §5.1, §7.2 step 5 |
|
| `output_invalid` | §5.1, §7.2 step 5 |
|
||||||
| `job_figure_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
|
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;
|
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
|
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.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
|
`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
|
held once, by name, and each vector is a verification document (§7.1) naming its sources, with the
|
||||||
outcome §7.3 gives it:
|
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
|
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
|
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
|
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
|
- **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 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
|
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
|
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
|
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 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
|
`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
|
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
|
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
|
`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
|
`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
|
citation, row and report itself, gives every vector the same outcome, and requires every mutated
|
||||||
copy of the corpus to fail.
|
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
|
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;
|
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.
|
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
|
- **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,
|
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
|
as settlement §13 says; a room line rests on per-hop records and the room's events; an ad line
|
||||||
|
|
|
||||||
|
|
@ -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,
|
- `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;
|
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
|
- `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
|
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.
|
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
|
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
|
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`,
|
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
|
[`settlement/`](../settlement/), ad rooms from [`ads/`](../ads/) (`live_break`, `gap`) and
|
||||||
[`presentation/`](../presentation/) (`video_rule`, `ceiling_exceeded_by_presented`), streams
|
[`presentation/`](../presentation/) (`video_rule`, `ceiling_exceeded_by_presented`), streams
|
||||||
stitched by others from [`stitched/`](../stitched/) (`hls_stitched`, `form1_prefetch`,
|
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`,
|
`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
|
`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
|
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
|
packages `video-base-01` and `video-staged-01` of
|
||||||
[`pack-conformance/packs/`](../../pack-conformance/packs/). When one of those corpora is
|
[`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.
|
regenerated, so is this one. Nothing here was fetched from outside the repository.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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
|
- 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
|
rules (version 2's DVR figures held to its entries), the size bound of its version, its
|
||||||
ledger and the ledger's root;
|
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
|
- the room's replay (§5.3): who publishes, the grants in effect, the media tracks
|
||||||
published;
|
published;
|
||||||
- the units (§4.4), the joules per hop and per class (§4.5), each grant's consumption and
|
- 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_MAGIC = b"WMH1"
|
||||||
HOP_VERSIONS = (1, 2)
|
HOP_VERSIONS = (1, 2)
|
||||||
SESSION_MAGIC = b"WMR1"
|
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")
|
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"
|
HOP_DOMAIN = b"wai:metering-hop\x01"
|
||||||
SESSION_DOMAIN = b"wai:metering-session\x01"
|
SESSION_DOMAIN = b"wai:metering-session\x01"
|
||||||
|
|
@ -310,9 +312,9 @@ def ledger_root(entries):
|
||||||
|
|
||||||
def read_receipt(b, room):
|
def read_receipt(b, room):
|
||||||
"""§4.2: the receipt, `classes_summed` for a total of tag 0, its rules, its room, its signature."""
|
"""§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)
|
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
|
x["hops"], classes_ok = [], True
|
||||||
for _ in range(r.u8()):
|
for _ in range(r.u8()):
|
||||||
h = {"record": r.take(32), "hop": r.take(32), "role": r.u8(), "joules": r.u64()}
|
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["takes"] = [r.take(32) for _ in range(r.u8())]
|
||||||
x["compose_count"], x["compose_digest"] = r.u32(), r.take(32)
|
x["compose_count"], x["compose_digest"] = r.u32(), r.take(32)
|
||||||
x["qoe_count"], x["qoe_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["unmetered"] = r.u8()
|
||||||
x["totals"] = [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]
|
x["totals"] = [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]
|
||||||
x["currency"] = r.take(3)
|
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["grants"], x["root"] = r.u32(), r.take(32)
|
||||||
x["past"] = [(r.take(32), r.u8()) for _ in range(r.u32())]
|
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()]
|
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):
|
def units_of(v):
|
||||||
return {"publisher_us": v[0], "subscriber_us": v[1:10], "sessions": v[10], "composition_us": v[11],
|
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]}
|
"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):
|
def cell_dict(c):
|
||||||
|
|
@ -595,7 +601,7 @@ def u64s(*v):
|
||||||
return b"".join(struct.pack("<Q", x) for x in 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."""
|
"""§5.1 steps 4 to 7 and the computations of §4: what the session gives, or Stop."""
|
||||||
room = ctx.id
|
room = ctx.id
|
||||||
published = set()
|
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.
|
# §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,
|
units = {"publisher_us": 0, "subscriber_us": [0] * CLASSES, "sessions": 0, "composition_us": 0, "events_sent": 0,
|
||||||
"events_delivered": 0, "recorded_us": 0}
|
"events_delivered": 0, "recorded_us": 0}
|
||||||
|
if version == 2:
|
||||||
|
units["bytes_out"] = 0 # §4.4: the ledgers' bytes out, version 2
|
||||||
per = {}
|
per = {}
|
||||||
|
|
||||||
def grant(g):
|
def grant(g):
|
||||||
if g not in per:
|
if g not in per:
|
||||||
jc, fc, cur = ctx.ceilings(g) or (NO_CEILING, NO_CEILING, b"\x00\x00\x00")
|
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,
|
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]
|
return per[g]
|
||||||
for st, a, z in ctx.over(s, e):
|
for st, a, z in ctx.over(s, e):
|
||||||
for p in publishers(st):
|
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"])
|
g = grant(l["grant"])
|
||||||
for c in range(CLASSES):
|
for c in range(CLASSES):
|
||||||
g["sub"][c] += l["sub"][c]
|
g["sub"][c] += l["sub"][c]
|
||||||
|
if version == 2:
|
||||||
|
units["bytes_out"] += l["out"]
|
||||||
|
g["out"] += l["out"]
|
||||||
b = l["in"] + l["out"]
|
b = l["in"] + l["out"]
|
||||||
if b == 0:
|
if b == 0:
|
||||||
continue
|
continue
|
||||||
|
|
@ -747,7 +758,10 @@ def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, cu
|
||||||
if priced:
|
if priced:
|
||||||
n = c["pub"] * prices[0] + sum(c["sub"][k] * prices[1 + k] for k in range(CLASSES)) + c["comp"] * prices[10] \
|
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
|
+ 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:
|
if c["fc"] == NO_CEILING:
|
||||||
fv = 0
|
fv = 0
|
||||||
elif not priced:
|
elif not priced:
|
||||||
|
|
@ -756,9 +770,10 @@ def evaluate(ctx, s, e, hops, takes, compositions, descriptors, logs, prices, cu
|
||||||
fv = 2
|
fv = 2
|
||||||
else:
|
else:
|
||||||
fv = 1
|
fv = 1
|
||||||
raw = (g + u64s(c["jc"], c["fc"]) + c["currency"] + u64s(c["pub"], *c["sub"], c["comp"], c["rec"], c["ev"], *c["attr"])
|
out = u64s(c["out"]) if version == 2 else b""
|
||||||
+ bytes([1 if c["unmetered"] else 0]) + u64s(funds) + bytes([jv, fv]))
|
raw = (g + u64s(c["jc"], c["fc"]) + c["currency"] + u64s(c["pub"], *c["sub"], c["comp"], c["rec"], c["ev"]) + out
|
||||||
assert len(raw) == 198
|
+ 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))
|
c.update(funds=funds, jv=jv, fv=fv, leaf=sha(CONSUMPTION_DOMAIN, raw))
|
||||||
consumption.append(c)
|
consumption.append(c)
|
||||||
return {"records": records, "m": m, "units": units, "totals": totals, "takes": take_digests, "compose_digests": digests,
|
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"]
|
s, e = rc["start"], rc["end"]
|
||||||
try:
|
try:
|
||||||
if len(comps) != rc["compose_count"] or list_digest([given_digest(b) for b in comps]) != rc["compose_digest"]:
|
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"])
|
raise Stop("record_missing", ["compose"])
|
||||||
named_logs = None
|
named_logs = None
|
||||||
if rc["qoe_count"] != 0:
|
if rc["qoe_count"] != 0:
|
||||||
if logs is None or sha(logs) != rc["qoe_digest"]:
|
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"])
|
raise Stop("record_missing", ["qoe"])
|
||||||
named_logs = logs
|
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"]:
|
if len(comp["metrics"]) != rc["qoe_count"]:
|
||||||
raise Stop("unit_mismatch", ["qoe"])
|
raise Stop("unit_mismatch", ["qoe"])
|
||||||
if comp["units"] != rc["units"]:
|
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)},
|
"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"],
|
"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(),
|
"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"]],
|
} for c in comp["consumption"]],
|
||||||
"consumption_root": root.hex(),
|
"consumption_root": root.hex(),
|
||||||
"past": [{"grant": g.hex(), "flags": f} for g, f in past],
|
"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))
|
h.update(record=flip(h["record"], n - 64 - 48 + k))
|
||||||
|
|
||||||
lesson_units = ["lesson", "signed_by_authority"]
|
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 [
|
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: 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: 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)),
|
mutate("lesson: recorded-seconds stated one less", ["lesson"], lambda d: vec(d, "lesson")["units"].update(recorded_us=vec(d, "lesson")["units"]["recorded_us"] - 1)),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# live-conformance/metering — live metering
|
# live-conformance/metering — live metering
|
||||||
|
|
||||||
The corpus of [`extensions/live-metering.md`](../../extensions/live-metering.md) §10: rooms,
|
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
|
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
|
`WCR1`; the take manifests, `WTM1`; and the logs document of QoE observations), and the
|
||||||
outcome a verifier gives it.
|
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
|
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
|
`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`,
|
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,
|
`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
|
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`.
|
flags; and the `qoe_cell`.
|
||||||
|
|
||||||
A verifier that reaches another outcome, reason, place or fact does not conform.
|
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
|
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
|
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);
|
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`,
|
- refused, one defect each: the receipt (`receipt_bad_signature`, `receipt_truncated`,
|
||||||
`receipt_wrong_room`, `session_invalid`, `signer_not_authorised`, `signer_revoked`,
|
`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`,
|
`record_invalid`, `energy_invalid`, `record_wrong_room`, `record_bad_signature`,
|
||||||
`entry_restated_wrong`, `hop_not_authorised`, `hop_class_not_authorised`, `hop_revoked`,
|
`entry_restated_wrong`, `hop_not_authorised`, `hop_class_not_authorised`, `hop_revoked`,
|
||||||
`span_outside_session`, `track_unknown`, `ledger_invalid`, `ledger_ceiling_mismatch`,
|
`span_outside_session`, `track_unknown`, `ledger_invalid`, `ledger_ceiling_mismatch`,
|
||||||
|
|
|
||||||
|
|
@ -2712,6 +2712,531 @@
|
||||||
"vst_ms": 0
|
"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",
|
"name": "receipt_bad_signature",
|
||||||
"about": "the receipt's signature with one bit flipped",
|
"about": "the receipt's signature with one bit flipped",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
_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
|
- 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
|
- 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
|
- what each source contributes (§5.2), its placement in time (§5.3), and the checks across
|
||||||
sources (§7.2 step 6);
|
sources (§7.2 step 6);
|
||||||
- every line, citation, energy row and amount recomputed (§6.2 to §6.6), compared in §7.2 step
|
- 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 route_record_verify as RRV # noqa: E402 (it puts ../oer-conformance on the path)
|
||||||
import oer2_verify as OER # noqa: E402
|
import oer2_verify as OER # noqa: E402
|
||||||
import _int_motion_receipt as IMR # 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
|
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")
|
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
|
PRIVATE = 128 # §5.2: 128 to 255, private and provisional use
|
||||||
KIND_NAMES = {ROOM: "room", DELIVERY: "delivery", PACKAGE: "package", ADS: "ads", JOB: "job"}
|
KIND_NAMES = {ROOM: "room", DELIVERY: "delivery", PACKAGE: "package", ADS: "ads", JOB: "job"}
|
||||||
STITCHED = "stitched" # §5.1: a source kind billed as ads
|
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
|
SCOPES = {1: "room_hops", 2: "delivery_hops", 3: "packaging", 4: "ad_decisions", 5: "jobs", 6: "unbilled_hops"} # §6.6
|
||||||
JOB_UNITS = ["jobs", "subpixels"] # §4.5
|
CONTENT_DOMAIN = b"moq-jwp:object\x01" # route-record §2, jwp-receipts §2.1
|
||||||
JOB_VERSIONS = (2, 3) # §4.5, §4.6: the versions that price jobs
|
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")
|
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
|
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"}
|
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)],
|
PACKAGE: [("packages", 1), ("entries", 1), ("stored_bytes", 10 ** 9)],
|
||||||
ADS: [("presented_impressions", 1), ("billable_impressions", 1)],
|
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()
|
SKIP = set()
|
||||||
|
|
||||||
|
|
@ -134,7 +157,7 @@ def head(b, magic):
|
||||||
return "malformed"
|
return "malformed"
|
||||||
if len(b) < 5:
|
if len(b) < 5:
|
||||||
return "truncated"
|
return "truncated"
|
||||||
if b[4] not in (1, 2, 3):
|
if b[4] not in (1, 2, 3, 4):
|
||||||
return "unknown_version"
|
return "unknown_version"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -160,16 +183,18 @@ def read_tariff(b):
|
||||||
r = R(b, 5)
|
r = R(b, 5)
|
||||||
try:
|
try:
|
||||||
t = {"seller": r.take(32), "buyer": r.take(32), "start": r.u(8), "end": r.u(8), "currency": r.take(3)}
|
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["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["ads"] = [r.u(8) for _ in range(2)]
|
||||||
t["version"] = b[4]
|
t["version"] = b[4]
|
||||||
t["job"] = [] # (capability, operation, unit, divisor, price)
|
t["job"] = [] # (capability, operation, unit, divisor, price)
|
||||||
if t["version"] in JOB_VERSIONS: # §4.5, §4.6
|
if t["version"] in JOB_VERSIONS: # §4.5, §4.6
|
||||||
for _ in range(r.u(2)):
|
for _ in range(r.u(2)):
|
||||||
cap = r.take(r.u(1))
|
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["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))]
|
t["scope"] = [(r.u(1), r.take(32)) for _ in range(r.u(2))]
|
||||||
body_end = r.p
|
body_end = r.p
|
||||||
|
|
@ -183,8 +208,9 @@ def read_tariff(b):
|
||||||
cap, op = j[0].decode("utf-8"), j[1].decode("utf-8")
|
cap, op = j[0].decode("utf-8"), j[1].decode("utf-8")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return False
|
return False
|
||||||
named = OPERATION.match(op) is not None and len(j[1]) <= 128 if t["version"] == 3 else op == ""
|
named = OPERATION.match(op) is not None and len(j[1]) <= 128 if t["version"] in WORK_VERSIONS else op == ""
|
||||||
return CAPABILITY.match(cap) is not None and named and 1 <= j[2] <= len(JOB_UNITS) and j[3] >= 1
|
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
|
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
|
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"])
|
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):
|
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
|
# ---------------------------------------------------------------- §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["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]
|
i["version"] = b[4]
|
||||||
v2 = i["version"] in JOB_VERSIONS
|
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)
|
i["amount"] = r.u(8)
|
||||||
sig = r.take(64)
|
sig = r.take(64)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
@ -246,12 +273,13 @@ def read_invoice(b, at):
|
||||||
def unit_ok(x):
|
def unit_ok(x):
|
||||||
if x["kind"] == JOB:
|
if x["kind"] == JOB:
|
||||||
return v2 and x["unit"] >= 1
|
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"])
|
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(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 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 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:
|
if not rules and "invoice-rules" not in SKIP:
|
||||||
raise Refuse("invoice_invalid", at)
|
raise Refuse("invoice_invalid", at)
|
||||||
bad = verify_sig(i["seller"], INVOICE_DOMAIN, b[:-64], sig)
|
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"]))
|
d = LRV.read_descriptor(bytes.fromhex(s["room"]["descriptor"]))
|
||||||
rc = LMV.read_receipt(bytes.fromhex(s["receipt"]), d["room"])
|
rc = LMV.read_receipt(bytes.fromhex(s["receipt"]), d["room"])
|
||||||
u = rc["units"]
|
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"],
|
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"]]}
|
"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"]]
|
tag = TAGS[j["energy_provenance"]]
|
||||||
cls = (tag, j["energy_uncertainty"]["relative_ppm"] if tag in (4, 5) else 0)
|
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"]),
|
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):
|
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)
|
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"} \
|
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):
|
or s["receipts"] is None or s["keys"] is None or not isinstance(s["outputs"], list):
|
||||||
raise Refuse("malformed", [])
|
raise Refuse("malformed", [])
|
||||||
|
|
@ -465,21 +503,62 @@ def job_source(s):
|
||||||
raise Refuse("job_figure_invalid", ["receipts", j])
|
raise Refuse("job_figure_invalid", ["receipts", j])
|
||||||
billed = (cap.encode("utf-8"), oer_identity(a["receipt"]))
|
billed = (cap.encode("utf-8"), oer_identity(a["receipt"]))
|
||||||
break
|
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"]):
|
for i, x in enumerate(s["outputs"]):
|
||||||
if not isinstance(x, dict) or set(x) != {"receipt", "codestream"} or not isinstance(x["receipt"], str) \
|
if version == V4 and isinstance(x, dict) and set(x) == {"package", "indexes"}:
|
||||||
or not isinstance(x["codestream"], str) or not re.fullmatch(r"(?:[0-9a-f]{2})*", x["codestream"]):
|
# §4.7: a package a run of `package.build` made, its object its receipt's text.
|
||||||
raise Refuse("malformed", [])
|
if not isinstance(x["package"], str) or not isinstance(x["indexes"], list) \
|
||||||
try:
|
or not all(isinstance(h, str) and hexs.fullmatch(h) for h in x["indexes"]):
|
||||||
r = IMR.read(x["receipt"])
|
raise Refuse("malformed", [])
|
||||||
except IMR.Bad:
|
try:
|
||||||
raise Refuse("output_invalid", ["outputs", i])
|
pr = PV.read_receipt(x["package"].encode("utf-8"))
|
||||||
if "output" not in SKIP and IMR.rederived(r, bytes.fromhex(x["codestream"])) is not None:
|
PV.verify_receipt_v2(pr, [bytes.fromhex(h) for h in x["indexes"]])
|
||||||
raise Refuse("output_invalid", ["outputs", i])
|
except PV.Refused:
|
||||||
outputs.append((IMR.receipt_hash(r), r["work"]))
|
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"],
|
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,
|
"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 = {}
|
_SOURCES = {}
|
||||||
|
|
@ -488,7 +567,8 @@ _SOURCES = {}
|
||||||
def verify_source(s, signers, version=1):
|
def verify_source(s, signers, version=1):
|
||||||
"""§5.1: the source verified by its own extension, a stitched source with `signers`, a job
|
"""§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."""
|
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:
|
if key not in _SOURCES:
|
||||||
try:
|
try:
|
||||||
_SOURCES[key] = read_source(s, signers, version)
|
_SOURCES[key] = read_source(s, signers, version)
|
||||||
|
|
@ -506,7 +586,7 @@ def read_source(s, signers, version):
|
||||||
if s.get("kind") == "stitched":
|
if s.get("kind") == "stitched":
|
||||||
return stitched_source(s, signers)
|
return stitched_source(s, signers)
|
||||||
if s.get("kind") == "job" and version in JOB_VERSIONS:
|
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}
|
readers = {"room": room_source, "delivery": delivery_source, "package": package_source, "ads": ads_source}
|
||||||
if s.get("kind") not in readers:
|
if s.get("kind") not in readers:
|
||||||
raise Refuse("malformed", [])
|
raise Refuse("malformed", [])
|
||||||
|
|
@ -541,6 +621,7 @@ def contributed(f):
|
||||||
out |= {(STITCHED_CLAIM, c) for c in f["claims"]}
|
out |= {(STITCHED_CLAIM, c) for c in f["claims"]}
|
||||||
elif f["kind"] == JOB:
|
elif f["kind"] == JOB:
|
||||||
out |= {(OER_RECEIPT, h) for h in f["hops"]} | {(VIDEO_RECEIPT, h) for h, _ in f["outputs"]}
|
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
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -555,7 +636,9 @@ def check_source(t, start, end, f):
|
||||||
if f["kind"] == ROOM:
|
if f["kind"] == ROOM:
|
||||||
if not in_scope(t, 1, f["room"]):
|
if not in_scope(t, 1, f["room"]):
|
||||||
return "outside_scope"
|
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"
|
return "tariff_conflict"
|
||||||
if not within(f["t0"] + f["start"], f["t0"] + f["end"]):
|
if not within(f["t0"] + f["start"], f["t0"] + f["end"]):
|
||||||
return "outside_period"
|
return "outside_period"
|
||||||
|
|
@ -607,10 +690,23 @@ def bills_again(f, g, version):
|
||||||
|
|
||||||
|
|
||||||
def billed_once(version, kind):
|
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
|
"""§6.9: whether a chain bills a record of `kind` once. In versions 3 and 4 a job's output
|
||||||
the evidence of what it delivered, which a re-run delivers again; its work, the route record and
|
receipt is the evidence of what it delivered, which a re-run delivers again; its work, the route
|
||||||
the hop receipts, is billed once."""
|
record and the hop receipts, is billed once. In version 4 a package receipt is listed again by
|
||||||
return not (kind == VIDEO_RECEIPT and (version == 3 or "output-once" in SKIP))
|
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):
|
def check_across(facts, previous, version=1):
|
||||||
|
|
@ -660,7 +756,7 @@ def compute(t, facts):
|
||||||
e[1] |= set(cites)
|
e[1] |= set(cites)
|
||||||
for f in facts:
|
for f in facts:
|
||||||
if f["kind"] == ROOM:
|
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"])])
|
add(ROOM, f["room"], unit, f["units"][unit - 1], [(SESSION, f["digest"])])
|
||||||
elif f["kind"] == DELIVERY:
|
elif f["kind"] == DELIVERY:
|
||||||
st = f["statement"]
|
st = f["statement"]
|
||||||
|
|
@ -672,6 +768,9 @@ def compute(t, facts):
|
||||||
cites += [(ACK, a["digest"]) for a in f["acks"]
|
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"])]
|
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)
|
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:
|
elif f["kind"] == PACKAGE:
|
||||||
cite = [(PACK, f["digest"])]
|
cite = [(PACK, f["digest"])]
|
||||||
add(PACKAGE, f["signer"], 1, 1, cite)
|
add(PACKAGE, f["signer"], 1, 1, cite)
|
||||||
|
|
@ -686,14 +785,17 @@ def compute(t, facts):
|
||||||
continue
|
continue
|
||||||
cap, hop = f["billed"]
|
cap, hop = f["billed"]
|
||||||
for k, (pcap, pop, unit, _, _) in enumerate(t["job"]):
|
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
|
continue
|
||||||
cites = [(ROUTE_RECORD, f["digest"]), (OER_RECEIPT, hop)]
|
cites = [(ROUTE_RECORD, f["digest"]), (OER_RECEIPT, hop)]
|
||||||
if unit == 1:
|
if unit == 1:
|
||||||
n = 1
|
n = 1
|
||||||
else:
|
elif unit == 2:
|
||||||
cites += [(VIDEO_RECEIPT, h) for h, w in f["outputs"] if w > 0]
|
cites += [(VIDEO_RECEIPT, h) for h, w in f["outputs"] if w > 0]
|
||||||
n = sum(w for _, w in f["outputs"])
|
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)
|
add(JOB, f["router"], k + 1, n, cites)
|
||||||
else:
|
else:
|
||||||
unit = 1 if f["claims_required"] else 2
|
unit = 1 if f["claims_required"] else 2
|
||||||
|
|
@ -704,6 +806,10 @@ def compute(t, facts):
|
||||||
lines, total = [], 0
|
lines, total = [], 0
|
||||||
for (kind, source, unit) in sorted(q):
|
for (kind, source, unit) in sorted(q):
|
||||||
n, cited = q[(kind, source, unit)]
|
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)
|
p = price(t, kind, unit)
|
||||||
a = n * p // divisor(t, kind, unit)
|
a = n * p // divisor(t, kind, unit)
|
||||||
if n > U64 or a > U64:
|
if n > U64 or a > U64:
|
||||||
|
|
@ -725,12 +831,14 @@ def compute(t, facts):
|
||||||
seen.add(d)
|
seen.add(d)
|
||||||
figs.append((2, fig))
|
figs.append((2, fig))
|
||||||
for f in facts:
|
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"]))
|
figs.append((3, f["figure"]))
|
||||||
elif f["kind"] == ADS:
|
elif f["kind"] == ADS:
|
||||||
figs += [(4, r["figure"]) for r in f["receipts"]]
|
figs += [(4, r["figure"]) for r in f["receipts"]]
|
||||||
elif f["kind"] == JOB and f["figure"] is not None:
|
elif f["kind"] == JOB:
|
||||||
figs.append((5, f["figure"]))
|
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)
|
rows, unmetered, sums = rows_of(figs)
|
||||||
if rows is None or total > U64:
|
if rows is None or total > U64:
|
||||||
return None
|
return None
|
||||||
|
|
@ -738,7 +846,7 @@ def compute(t, facts):
|
||||||
|
|
||||||
|
|
||||||
def rows_of(figs):
|
def rows_of(figs):
|
||||||
unmetered = [0, 0, 0, 0, 0]
|
unmetered = [0, 0, 0, 0, 0, 0]
|
||||||
acc = {}
|
acc = {}
|
||||||
for s, (tag, joules, ppm) in figs:
|
for s, (tag, joules, ppm) in figs:
|
||||||
if tag is None:
|
if tag is None:
|
||||||
|
|
@ -925,6 +1033,7 @@ def verify(doc):
|
||||||
across = check_across(facts, prev, t["version"]) # step 6
|
across = check_across(facts, prev, t["version"]) # step 6
|
||||||
if across:
|
if across:
|
||||||
raise Refuse(across[1], ["source", across[0]])
|
raise Refuse(across[1], ["source", across[0]])
|
||||||
|
hold(t, facts, prev, inv["start"], inv["end"])
|
||||||
c = compute(t, facts) # step 7
|
c = compute(t, facts) # step 7
|
||||||
if c is None:
|
if c is None:
|
||||||
raise Refuse("invoice_mismatch", ["invoice"])
|
raise Refuse("invoice_mismatch", ["invoice"])
|
||||||
|
|
@ -939,19 +1048,19 @@ def verify(doc):
|
||||||
def line_json(x, t):
|
def line_json(x, t):
|
||||||
"""§7.3: a line, a job line naming its job price's unit and capability."""
|
"""§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(),
|
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"],
|
"quantity": x["quantity"], "price": x["price"], "amount": x["amount"],
|
||||||
"cites": [[k, i.hex()] for k, i in x["cited_records"]]}
|
"cites": [[k, i.hex()] for k, i in x["cited_records"]]}
|
||||||
if x["kind"] == JOB:
|
if x["kind"] == JOB:
|
||||||
out["capability"] = t["job"][x["unit"] - 1][0].decode("utf-8")
|
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")
|
out["operation"] = t["job"][x["unit"] - 1][1].decode("utf-8")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def accepted(inv, facts, c, t):
|
def accepted(inv, facts, c, t):
|
||||||
unmetered, coverage = {}, {}
|
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]
|
n = inv["unmetered"][s - 1]
|
||||||
unmetered[SCOPES[s]] = n
|
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"
|
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 = copy.deepcopy(doc)
|
||||||
m["vectors"][k]["sources"] = ["job_rerun_replays_its_hop"]
|
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))
|
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"]):
|
for k, v in enumerate(doc["vectors"]):
|
||||||
if v["outcome"]["outcome"] == "refuse":
|
if v["outcome"]["outcome"] == "refuse":
|
||||||
m = copy.deepcopy(doc)
|
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",
|
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",
|
"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):
|
def main(corpus_dir):
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,15 @@ impl Chain {
|
||||||
fn billed(&self, h: &Held) -> bool {
|
fn billed(&self, h: &Held) -> bool {
|
||||||
h.facts.heads().iter().all(|r| self.billed.contains(r))
|
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 {
|
pub struct Ledger {
|
||||||
|
|
@ -100,7 +109,7 @@ impl Ledger {
|
||||||
for (_, p) in numbered(&d.join("sources"), "json")? {
|
for (_, p) in numbered(&d.join("sources"), "json")? {
|
||||||
let text = std::fs::read_to_string(&p).map_err(|e| format!("{}: {e}", p.display()))?;
|
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 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 });
|
chain.sources.push(Held { doc, facts });
|
||||||
}
|
}
|
||||||
let mut prev = [0u8; 32];
|
let mut prev = [0u8; 32];
|
||||||
|
|
@ -148,7 +157,7 @@ impl Ledger {
|
||||||
/// and checked against everything held under the tariff (step 6), then kept.
|
/// 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> {
|
pub fn add_source(&mut self, tariff: &[u8; 32], doc: Value) -> Result<Vec<(u8, [u8; 32])>, Error> {
|
||||||
let c = self.chain(tariff)?;
|
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")];
|
let mut a = vec![json!("source")];
|
||||||
a.extend(at.as_array().cloned().unwrap_or_default());
|
a.extend(at.as_array().cloned().unwrap_or_default());
|
||||||
Error::Refused((r, Value::Array(a)))
|
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();
|
let mut all: Vec<Facts> = c.sources.iter().map(|h| h.facts.clone()).collect();
|
||||||
all.push(facts.clone());
|
all.push(facts.clone());
|
||||||
rl::check_across(c.tariff.version, &all, None).map_err(|(_, r)| refuse(r.code(), json!(["source"])))?;
|
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
|
// A record the chain billed is billed never again; under a tariff of version 3 or 4, a
|
||||||
// output receipt is cited again by a re-run, whose hop and route record are new (§6.9).
|
// job's output receipt is cited again by a re-run, whose hop and route record are new
|
||||||
if facts.records().iter().any(|r| rl::billed_once(c.tariff.version, r.0) && c.billed.contains(r)) {
|
// (§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"])));
|
return Err(refuse(Reason::RecordRebilled.code(), json!(["source"])));
|
||||||
}
|
}
|
||||||
let n = c.sources.len() as u64;
|
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> {
|
pub fn invoice(&mut self, tariff: &[u8; 32], start: u64, end: u64) -> Result<Issued, Error> {
|
||||||
let c = self.chain(tariff)?;
|
let c = self.chain(tariff)?;
|
||||||
let t = &c.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 prev = c.invoices.last().map(|(i, _, _)| i);
|
||||||
let inv = rl::make_invoice(&self.key, t, prev, start, end, &selected)?;
|
let inv = rl::make_invoice(&self.key, t, prev, start, end, &selected)?;
|
||||||
let bytes = inv.to_bytes();
|
let bytes = inv.to_bytes();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
//! The service's ledger against the committed corpus (`live-conformance/ledger/`): given the
|
//! 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
|
//! 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
|
//! 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.
|
//! one process holds its directory.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
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);
|
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]
|
#[test]
|
||||||
fn one_process_holds_the_directory_and_tariffs_are_its_sellers() {
|
fn one_process_holds_the_directory_and_tariffs_are_its_sellers() {
|
||||||
let doc = corpus();
|
let doc = corpus();
|
||||||
|
|
|
||||||
|
|
@ -940,6 +940,7 @@ mod tests {
|
||||||
fn session_receipt(totals: Vec<crate::live_metering::Total>, unmetered: u8) -> crate::live_metering::SessionReceipt {
|
fn session_receipt(totals: Vec<crate::live_metering::Total>, unmetered: u8) -> crate::live_metering::SessionReceipt {
|
||||||
use crate::live_metering::*;
|
use crate::live_metering::*;
|
||||||
SessionReceipt {
|
SessionReceipt {
|
||||||
|
version: RECORD_VERSION,
|
||||||
room: [7; 32],
|
room: [7; 32],
|
||||||
signer: [8; 32],
|
signer: [8; 32],
|
||||||
start: 0,
|
start: 0,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
//!
|
//!
|
||||||
//! - §3: the per-hop record in versions 1 and 2, its track entries, its ledger, version 2's
|
//! - §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;
|
//! 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
|
//! - §5: verification, over the room ([`crate::live_room`]), the takes
|
||||||
//! ([`crate::capture_take`]) and the composition receipts ([`crate::live_compose`]);
|
//! ([`crate::capture_take`]) and the composition receipts ([`crate::live_compose`]);
|
||||||
//! - §6: the QoE observations' CMCD event-timeline record and `application/cmcd` batch.
|
//! - §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;
|
pub const RECORD_VERSION: u8 = 1;
|
||||||
/// The version of a per-hop record with DVR figures (§3.1, §3.9).
|
/// The version of a per-hop record with DVR figures (§3.1, §3.9).
|
||||||
pub const HOP_VERSION_DVR: u8 = 2;
|
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).
|
/// The domain a per-hop record's signature is made over (§3.5).
|
||||||
pub const HOP_DOMAIN: &[u8] = b"wai:metering-hop\x01";
|
pub const HOP_DOMAIN: &[u8] = b"wai:metering-hop\x01";
|
||||||
/// The domain a receipt's signature is made over (§4.2).
|
/// 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;
|
pub const ENTRY_LEN: usize = 90;
|
||||||
/// A ledger entry's length (§3.4).
|
/// A ledger entry's length (§3.4).
|
||||||
pub const LEDGER_ENTRY_LEN: usize = 143;
|
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;
|
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).
|
/// The longest calibration reference a record carries (§3.6).
|
||||||
pub const MAX_CALIBRATION_REF: usize = 255;
|
pub const MAX_CALIBRATION_REF: usize = 255;
|
||||||
/// The fixed fields of a version 1 record (§3.7), and its longest class evidence.
|
/// 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
|
/// 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)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct Tariff {
|
pub struct Tariff {
|
||||||
pub currency: [u8; 3],
|
pub currency: [u8; 3],
|
||||||
|
|
@ -822,6 +831,20 @@ pub struct Tariff {
|
||||||
pub composition: u32,
|
pub composition: u32,
|
||||||
pub recorded: u32,
|
pub recorded: u32,
|
||||||
pub event: 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).
|
/// The session's units (§4.4).
|
||||||
|
|
@ -834,20 +857,27 @@ pub struct Units {
|
||||||
pub events_sent: u64,
|
pub events_sent: u64,
|
||||||
pub events_delivered: u64,
|
pub events_delivered: u64,
|
||||||
pub recorded_us: 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 {
|
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());
|
o.extend_from_slice(&self.publisher_us.to_le_bytes());
|
||||||
put_u64s(o, &self.subscriber_us);
|
put_u64s(o, &self.subscriber_us);
|
||||||
put_u64s(o, &[self.sessions, self.composition_us, self.events_sent, self.events_delivered, self.recorded_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 publisher_us = r.u64()?;
|
||||||
let subscriber_us = r.u64s()?;
|
let subscriber_us = r.u64s()?;
|
||||||
let [sessions, composition_us, events_sent, events_delivered, recorded_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).
|
/// A room-session receipt (§4.1).
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct SessionReceipt {
|
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 room: [u8; 32],
|
||||||
pub signer: [u8; 32],
|
pub signer: [u8; 32],
|
||||||
pub start: u64,
|
pub start: u64,
|
||||||
|
|
@ -924,7 +956,7 @@ impl SessionReceipt {
|
||||||
pub fn body(&self) -> Vec<u8> {
|
pub fn body(&self) -> Vec<u8> {
|
||||||
let mut o = Vec::new();
|
let mut o = Vec::new();
|
||||||
o.extend_from_slice(&SESSION_MAGIC);
|
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.room);
|
||||||
o.extend_from_slice(&self.signer);
|
o.extend_from_slice(&self.signer);
|
||||||
o.extend_from_slice(&self.start.to_le_bytes());
|
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.compose_digest);
|
||||||
o.extend_from_slice(&self.qoe_count.to_le_bytes());
|
o.extend_from_slice(&self.qoe_count.to_le_bytes());
|
||||||
o.extend_from_slice(&self.qoe_digest);
|
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(self.unmetered);
|
||||||
o.push(u8::try_from(self.totals.len()).expect("at most 255 totals"));
|
o.push(u8::try_from(self.totals.len()).expect("at most 255 totals"));
|
||||||
for t in &self.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(&t.joules.to_le_bytes());
|
||||||
}
|
}
|
||||||
o.extend_from_slice(&self.tariff.currency);
|
o.extend_from_slice(&self.tariff.currency);
|
||||||
let t = &self.tariff;
|
for p in self.tariff.prices(self.version) {
|
||||||
for p in [t.publisher].iter().chain(&t.subscriber).chain(&[t.composition, t.recorded, t.event]) {
|
|
||||||
o.extend_from_slice(&p.to_le_bytes());
|
o.extend_from_slice(&p.to_le_bytes());
|
||||||
}
|
}
|
||||||
o.extend_from_slice(&self.grants.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
|
/// 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.
|
/// total of tag 0, its rules, its room, its signature.
|
||||||
pub fn parse(b: &[u8], room_id: &[u8; 32]) -> Result<SessionReceipt, Reason> {
|
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 mut r = Rd { b, p: 5 };
|
||||||
let (room, signer, start, end) = (r.arr()?, r.arr()?, r.u64()?, r.u64()?);
|
let (room, signer, start, end) = (r.arr()?, r.arr()?, r.u64()?, r.u64()?);
|
||||||
let n = r.u8()? as usize;
|
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 takes = (0..k).map(|_| r.arr()).collect::<Result<Vec<[u8; 32]>, _>>()?;
|
||||||
let (compose_count, compose_digest) = (r.u32()?, r.arr()?);
|
let (compose_count, compose_digest) = (r.u32()?, r.arr()?);
|
||||||
let (qoe_count, qoe_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 unmetered = r.u8()?;
|
||||||
let m = r.u8()? as usize;
|
let m = r.u8()? as usize;
|
||||||
let mut totals = Vec::with_capacity(m);
|
let mut totals = Vec::with_capacity(m);
|
||||||
|
|
@ -1014,6 +1045,7 @@ impl SessionReceipt {
|
||||||
}
|
}
|
||||||
let currency = r.arr()?;
|
let currency = r.arr()?;
|
||||||
let prices: [u32; 13] = r.u32s()?;
|
let prices: [u32; 13] = r.u32s()?;
|
||||||
|
let bytes = if version == SESSION_VERSION_BYTES { r.u32()? } else { 0 };
|
||||||
let tariff = Tariff {
|
let tariff = Tariff {
|
||||||
currency,
|
currency,
|
||||||
publisher: prices[0],
|
publisher: prices[0],
|
||||||
|
|
@ -1021,6 +1053,7 @@ impl SessionReceipt {
|
||||||
composition: prices[10],
|
composition: prices[10],
|
||||||
recorded: prices[11],
|
recorded: prices[11],
|
||||||
event: prices[12],
|
event: prices[12],
|
||||||
|
bytes,
|
||||||
};
|
};
|
||||||
let (grants, consumption_root) = (r.u32()?, r.arr()?);
|
let (grants, consumption_root) = (r.u32()?, r.arr()?);
|
||||||
let p = r.u32()? as usize;
|
let p = r.u32()? as usize;
|
||||||
|
|
@ -1036,7 +1069,7 @@ impl SessionReceipt {
|
||||||
if totals.iter().any(|t| t.tag == 0) {
|
if totals.iter().any(|t| t.tag == 0) {
|
||||||
return Err(Reason::ClassesSummed);
|
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
|
let rules = start < end
|
||||||
&& hops.windows(2).all(|w| w[0].record < w[1].record)
|
&& hops.windows(2).all(|w| w[0].record < w[1].record)
|
||||||
&& hops.iter().all(|h| (1..=3).contains(&h.role))
|
&& 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)?;
|
verify_sig(&signer, SESSION_DOMAIN, &b[..b.len() - 64], &sig)?;
|
||||||
Ok(SessionReceipt {
|
Ok(SessionReceipt {
|
||||||
|
version,
|
||||||
room,
|
room,
|
||||||
signer,
|
signer,
|
||||||
start,
|
start,
|
||||||
|
|
@ -1084,6 +1118,8 @@ impl SessionReceipt {
|
||||||
/// A grant's consumption entry (§4.6).
|
/// A grant's consumption entry (§4.6).
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Consumption {
|
pub struct Consumption {
|
||||||
|
/// Its receipt's version: an entry of version 2 states `bytes_out`.
|
||||||
|
pub version: u8,
|
||||||
pub grant: [u8; 32],
|
pub grant: [u8; 32],
|
||||||
pub joules_ceiling: u64,
|
pub joules_ceiling: u64,
|
||||||
pub funds_ceiling: u64,
|
pub funds_ceiling: u64,
|
||||||
|
|
@ -1093,6 +1129,8 @@ pub struct Consumption {
|
||||||
pub composition_us: u64,
|
pub composition_us: u64,
|
||||||
pub recorded_us: u64,
|
pub recorded_us: u64,
|
||||||
pub events_sent: 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.
|
/// The attributed joules of tags 2, 3, 4 and 5.
|
||||||
pub attributed: [u64; 4],
|
pub attributed: [u64; 4],
|
||||||
/// Its share is unmetered at some hop.
|
/// Its share is unmetered at some hop.
|
||||||
|
|
@ -1104,8 +1142,9 @@ pub struct Consumption {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Consumption {
|
impl Consumption {
|
||||||
pub fn encode(&self) -> [u8; CONSUMPTION_LEN] {
|
/// Its bytes: [`CONSUMPTION_LEN`] of them in version 1, [`CONSUMPTION_LEN_2`] in version 2.
|
||||||
let mut o = Vec::with_capacity(CONSUMPTION_LEN);
|
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.grant);
|
||||||
o.extend_from_slice(&self.joules_ceiling.to_le_bytes());
|
o.extend_from_slice(&self.joules_ceiling.to_le_bytes());
|
||||||
o.extend_from_slice(&self.funds_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());
|
o.extend_from_slice(&self.publisher_us.to_le_bytes());
|
||||||
put_u64s(&mut o, &self.subscriber_us);
|
put_u64s(&mut o, &self.subscriber_us);
|
||||||
put_u64s(&mut o, &[self.composition_us, self.recorded_us, self.events_sent]);
|
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);
|
put_u64s(&mut o, &self.attributed);
|
||||||
o.push(self.unmetered as u8);
|
o.push(self.unmetered as u8);
|
||||||
o.extend_from_slice(&self.funds.to_le_bytes());
|
o.extend_from_slice(&self.funds.to_le_bytes());
|
||||||
o.push(self.joules_verdict);
|
o.push(self.joules_verdict);
|
||||||
o.push(self.funds_verdict);
|
o.push(self.funds_verdict);
|
||||||
o.try_into().expect("198 bytes")
|
o
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The entry's leaf (§4.6).
|
/// 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).
|
/// What a session gives, computed from its records (§4.4–§4.7).
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Computed {
|
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 hops: Vec<HopFacts>,
|
||||||
pub media_tracks: u64,
|
pub media_tracks: u64,
|
||||||
pub units: Units,
|
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
|
/// §5.1 steps 4 to 7 and the computations of §4: every refusal those steps make, and what
|
||||||
/// the session gives.
|
/// 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;
|
let room_id = ctx.vr.id;
|
||||||
// The media tracks the room published in the session (§5.3).
|
// 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();
|
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 entry = |g: [u8; 32]| -> Consumption {
|
||||||
let (jc, fc, cur) = ctx.ceilings(&g).unwrap_or((NO_CEILING, NO_CEILING, [0; 3]));
|
let (jc, fc, cur) = ctx.ceilings(&g).unwrap_or((NO_CEILING, NO_CEILING, [0; 3]));
|
||||||
Consumption {
|
Consumption {
|
||||||
|
version,
|
||||||
grant: g,
|
grant: g,
|
||||||
joules_ceiling: jc,
|
joules_ceiling: jc,
|
||||||
funds_ceiling: fc,
|
funds_ceiling: fc,
|
||||||
|
|
@ -1455,6 +1500,7 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
|
||||||
composition_us: 0,
|
composition_us: 0,
|
||||||
recorded_us: 0,
|
recorded_us: 0,
|
||||||
events_sent: 0,
|
events_sent: 0,
|
||||||
|
bytes_out: 0,
|
||||||
attributed: [0; 4],
|
attributed: [0; 4],
|
||||||
unmetered: false,
|
unmetered: false,
|
||||||
funds: NO_FUNDS,
|
funds: NO_FUNDS,
|
||||||
|
|
@ -1462,6 +1508,8 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
|
||||||
funds_verdict: 0,
|
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 (st, a, b) in ctx.over(start, end) {
|
||||||
for p in st.publishers() {
|
for p in st.publishers() {
|
||||||
units.publisher_us += b - a;
|
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 {
|
for k in 0..CLASSES {
|
||||||
c.subscriber_us[k] += e.subscriber_us[k];
|
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 {
|
if e.moved() == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1568,6 +1620,7 @@ fn evaluate(ctx: &Ctx, start: u64, end: u64, named: &Named, tariff: &Tariff) ->
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(Computed {
|
Ok(Computed {
|
||||||
|
version,
|
||||||
hops,
|
hops,
|
||||||
media_tracks: m,
|
media_tracks: m,
|
||||||
units,
|
units,
|
||||||
|
|
@ -1635,7 +1688,9 @@ fn committed_capture(m: &take::Manifest, f: &take::FoundTake, start: u64, end: u
|
||||||
.sum()
|
.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 {
|
fn funds_of(c: &Consumption, t: &Tariff) -> u64 {
|
||||||
let mut n: u128 = c.publisher_us as u128 * t.publisher as u128;
|
let mut n: u128 = c.publisher_us as u128 * t.publisher as u128;
|
||||||
for k in 0..CLASSES {
|
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.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;
|
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))
|
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 {
|
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).
|
// The hop entries and the takes come first (steps 4 and 5).
|
||||||
let named = Named { hops, takes, compositions: vec![], descriptors: i.descriptors, logs: None };
|
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(at, reason);
|
||||||
}
|
}
|
||||||
return refuse(Place::Compose(None), Reason::RecordMissing);
|
return refuse(Place::Compose(None), Reason::RecordMissing);
|
||||||
|
|
@ -1744,7 +1803,7 @@ pub fn verify_session(i: &SessionInputs) -> SessionOutcome {
|
||||||
Some(b) => Some(b),
|
Some(b) => Some(b),
|
||||||
None => {
|
None => {
|
||||||
let named = Named { hops, takes, compositions, descriptors: i.descriptors, logs: 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(at, reason);
|
||||||
}
|
}
|
||||||
return refuse(Place::Qoe, Reason::RecordMissing);
|
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 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,
|
Ok(c) => c,
|
||||||
Err((at, reason)) => return refuse(at, reason),
|
Err((at, reason)) => return refuse(at, reason),
|
||||||
};
|
};
|
||||||
|
|
@ -1797,6 +1856,8 @@ pub struct SessionPlan<'a> {
|
||||||
pub takes: &'a [Vec<u8>],
|
pub takes: &'a [Vec<u8>],
|
||||||
pub logs: Option<&'a [u8]>,
|
pub logs: Option<&'a [u8]>,
|
||||||
pub tariff: Tariff,
|
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.
|
/// 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,
|
descriptors: p.descriptors,
|
||||||
logs: p.logs,
|
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 {
|
let r = SessionReceipt {
|
||||||
|
version: p.version,
|
||||||
room: ctx.vr.id,
|
room: ctx.vr.id,
|
||||||
signer,
|
signer,
|
||||||
start: p.start,
|
start: p.start,
|
||||||
|
|
@ -2058,6 +2120,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn classes_summed_is_told_from_a_wrong_total() {
|
fn classes_summed_is_told_from_a_wrong_total() {
|
||||||
let mut c = Computed {
|
let mut c = Computed {
|
||||||
|
version: RECORD_VERSION,
|
||||||
hops: vec![],
|
hops: vec![],
|
||||||
media_tracks: 0,
|
media_tracks: 0,
|
||||||
units: Units::default(),
|
units: Units::default(),
|
||||||
|
|
@ -2072,6 +2135,7 @@ mod tests {
|
||||||
consumption: vec![],
|
consumption: vec![],
|
||||||
};
|
};
|
||||||
let r = |totals: Vec<Total>| SessionReceipt {
|
let r = |totals: Vec<Total>| SessionReceipt {
|
||||||
|
version: RECORD_VERSION,
|
||||||
room: [0; 32],
|
room: [0; 32],
|
||||||
signer: [0; 32],
|
signer: [0; 32],
|
||||||
start: 0,
|
start: 0,
|
||||||
|
|
@ -2105,6 +2169,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn funds_round_up() {
|
fn funds_round_up() {
|
||||||
let mut c = Consumption {
|
let mut c = Consumption {
|
||||||
|
version: RECORD_VERSION,
|
||||||
grant: [1; 32],
|
grant: [1; 32],
|
||||||
joules_ceiling: NO_CEILING,
|
joules_ceiling: NO_CEILING,
|
||||||
funds_ceiling: 10,
|
funds_ceiling: 10,
|
||||||
|
|
@ -2114,6 +2179,7 @@ mod tests {
|
||||||
composition_us: 0,
|
composition_us: 0,
|
||||||
recorded_us: 0,
|
recorded_us: 0,
|
||||||
events_sent: 0,
|
events_sent: 0,
|
||||||
|
bytes_out: 0,
|
||||||
attributed: [0; 4],
|
attributed: [0; 4],
|
||||||
unmetered: false,
|
unmetered: false,
|
||||||
funds: NO_FUNDS,
|
funds: NO_FUNDS,
|
||||||
|
|
@ -2129,6 +2195,24 @@ mod tests {
|
||||||
c.events_sent = u64::MAX;
|
c.events_sent = u64::MAX;
|
||||||
let t = Tariff { currency: *b"EUR", event: u32::MAX, ..Tariff::default() };
|
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");
|
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 {
|
fn obs(v: serde_json::Value) -> Observation {
|
||||||
|
|
|
||||||
|
|
@ -960,7 +960,7 @@ fn logs(n: usize) -> Vec<u8> {
|
||||||
// ---- the receipts ----
|
// ---- the receipts ----
|
||||||
|
|
||||||
fn tariff() -> Tariff {
|
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.
|
/// 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,
|
takes: &g.takes,
|
||||||
logs: g.logs.as_deref(),
|
logs: g.logs.as_deref(),
|
||||||
tariff: tariff(),
|
tariff: tariff(),
|
||||||
|
version: RECORD_VERSION,
|
||||||
};
|
};
|
||||||
let (r, _) = aggregate(&plan, pk(signer)).unwrap_or_else(|e| panic!("the corpus's session aggregates: {e:?}"));
|
let (r, _) = aggregate(&plan, pk(signer)).unwrap_or_else(|e| panic!("the corpus's session aggregates: {e:?}"));
|
||||||
r.sign(&key(signer))
|
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 ----
|
// ---- the vectors ----
|
||||||
|
|
||||||
/// What a vector is written to reach.
|
/// What a vector is written to reach.
|
||||||
|
|
@ -1088,6 +1112,39 @@ fn vectors(rooms: &[(&'static str, WrittenRoom)]) -> Vec<Vector> {
|
||||||
dvr_given.clone(),
|
dvr_given.clone(),
|
||||||
Intent::Accept,
|
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 ----
|
// ---- refused: the receipt ----
|
||||||
let mut flipped = base.to_bytes();
|
let mut flipped = base.to_bytes();
|
||||||
|
|
@ -1275,6 +1332,7 @@ fn vectors(rooms: &[(&'static str, WrittenRoom)]) -> Vec<Vector> {
|
||||||
takes: &dvr_given.takes,
|
takes: &dvr_given.takes,
|
||||||
logs: dvr_given.logs.as_deref(),
|
logs: dvr_given.logs.as_deref(),
|
||||||
tariff: tariff(),
|
tariff: tariff(),
|
||||||
|
version: RECORD_VERSION,
|
||||||
};
|
};
|
||||||
let m = aggregate(&plan, pk(METER)).expect("the DVR relay's session aggregates").1.media_tracks;
|
let m = aggregate(&plan, pk(METER)).expect("the DVR relay's session aggregates").1.media_tracks;
|
||||||
let big = dvr_with(&|r| {
|
let big = dvr_with(&|r| {
|
||||||
|
|
@ -1433,12 +1491,17 @@ fn funds_verdict(v: u8) -> &'static str {
|
||||||
["none", "within", "past", "no_tariff"][v as usize]
|
["none", "within", "past", "no_tariff"][v as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn units_json(u: &Units) -> Value {
|
/// The units (§4.4): a receipt of version 2 states its bytes out too.
|
||||||
json!({
|
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,
|
"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,
|
"composition_us": u.composition_us, "events_sent": u.events_sent, "events_delivered": u.events_delivered,
|
||||||
"recorded_us": u.recorded_us,
|
"recorded_us": u.recorded_us,
|
||||||
})
|
});
|
||||||
|
if version == SESSION_VERSION_BYTES {
|
||||||
|
v["bytes_out"] = u.bytes_out.into();
|
||||||
|
}
|
||||||
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cell_json(c: &QoeCell) -> Value {
|
fn cell_json(c: &QoeCell) -> Value {
|
||||||
|
|
@ -1451,7 +1514,7 @@ fn cell_json(c: &QoeCell) -> Value {
|
||||||
|
|
||||||
fn consumption_json(c: &Consumption) -> 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();
|
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),
|
"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() },
|
"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,
|
"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() },
|
"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),
|
"joules_verdict": joules_verdict(c.joules_verdict), "funds_verdict": funds_verdict(c.funds_verdict),
|
||||||
"leaf": hex(&c.leaf()),
|
"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.
|
/// 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("digest".into(), hex(digest).into());
|
||||||
m.insert("media_tracks".into(), c.media_tracks.into());
|
m.insert("media_tracks".into(), c.media_tracks.into());
|
||||||
m.insert("records".into(), c.hops.iter().map(record_json).collect());
|
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("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("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());
|
m.insert("coverage".into(), if c.whole() { "whole" } else { "partial" }.into());
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,14 @@
|
||||||
//! What this module implements, by section of the extension:
|
//! What this module implements, by section of the extension:
|
||||||
//!
|
//!
|
||||||
//! - §4: the tariff ([`Tariff`]), its two signatures and its scope, and its job prices: per
|
//! - §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`]);
|
//! - §5: the sources ([`verify_source`]) and what each contributes ([`Facts`]);
|
||||||
//! - §6: the invoice ([`Invoice`]), its lines, citations, energy and chain ([`compute`]), and
|
//! - §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
|
//! what a chain bills once ([`billed_once`]): in versions 3 and 4, a job's work, never its
|
||||||
//! receipt;
|
//! 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.
|
//! - §7: verification ([`verify`]); §8: the reports.
|
||||||
//!
|
//!
|
||||||
//! The corpus `live-conformance/ledger/` is written by [`corpus::generate`] and checked by
|
//! 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
|
/// The version of a tariff that prices jobs per operation and bills a job's work, and of its
|
||||||
/// invoices (§4.6, §6.9).
|
/// invoices (§4.6, §6.9).
|
||||||
pub const VERSION_3: u8 = 3;
|
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 TARIFF_DOMAIN: &[u8] = b"wai:ledger-tariff\x01";
|
||||||
pub const ACCEPT_DOMAIN: &[u8] = b"wai:ledger-accept\x01";
|
pub const ACCEPT_DOMAIN: &[u8] = b"wai:ledger-accept\x01";
|
||||||
pub const INVOICE_DOMAIN: &[u8] = b"wai:ledger-invoice\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;
|
pub const LINE_LEN: usize = 94;
|
||||||
/// An energy row's length (§6.1).
|
/// An energy row's length (§6.1).
|
||||||
pub const ROW_LEN: usize = 18;
|
pub const ROW_LEN: usize = 18;
|
||||||
/// The room prices of a tariff, as live-metering §4.1 orders them.
|
/// The room prices of a tariff, as live-metering §4.1 orders them in a receipt of version 2:
|
||||||
pub const ROOM_PRICES: usize = 13;
|
/// 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).
|
/// The kinds of a source and of a line (§6.3).
|
||||||
pub mod source_kind {
|
pub mod source_kind {
|
||||||
|
|
@ -116,21 +127,42 @@ pub mod scope {
|
||||||
pub const AD_DECISIONS: u8 = 4;
|
pub const AD_DECISIONS: u8 = 4;
|
||||||
/// Version 2 (§6.6).
|
/// Version 2 (§6.6).
|
||||||
pub const JOBS: u8 = 5;
|
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 {
|
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> {
|
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",
|
"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 {
|
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::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::PACKAGE => &["packages", "entries", "stored_bytes"],
|
||||||
source_kind::ADS => &["presented_impressions", "billable_impressions"],
|
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 {
|
pub fn divisor(kind: u8, unit: u8) -> u128 {
|
||||||
match (kind, unit) {
|
match (kind, unit) {
|
||||||
(source_kind::ROOM, 13) => 1,
|
(source_kind::ROOM, 13) => 1,
|
||||||
|
(source_kind::ROOM, 14) => 1_000_000_000,
|
||||||
(source_kind::ROOM, _) => 1_000_000,
|
(source_kind::ROOM, _) => 1_000_000,
|
||||||
(source_kind::DELIVERY, _) => 1_000_000_000,
|
(source_kind::DELIVERY, _) => 1_000_000_000,
|
||||||
(source_kind::PACKAGE, 3) => 1_000_000_000,
|
(source_kind::PACKAGE, 3) => 1_000_000_000,
|
||||||
|
|
@ -174,11 +207,12 @@ pub enum Reason {
|
||||||
PrivateKind,
|
PrivateKind,
|
||||||
OutputInvalid,
|
OutputInvalid,
|
||||||
JobFigureInvalid,
|
JobFigureInvalid,
|
||||||
|
OutputNotDelivered,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reason {
|
impl Reason {
|
||||||
/// §10's order.
|
/// §10's order.
|
||||||
pub const ALL: [Reason; 18] = [
|
pub const ALL: [Reason; 19] = [
|
||||||
Reason::TariffInvalid,
|
Reason::TariffInvalid,
|
||||||
Reason::InvoiceInvalid,
|
Reason::InvoiceInvalid,
|
||||||
Reason::TariffMismatch,
|
Reason::TariffMismatch,
|
||||||
|
|
@ -197,6 +231,7 @@ impl Reason {
|
||||||
Reason::PrivateKind,
|
Reason::PrivateKind,
|
||||||
Reason::OutputInvalid,
|
Reason::OutputInvalid,
|
||||||
Reason::JobFigureInvalid,
|
Reason::JobFigureInvalid,
|
||||||
|
Reason::OutputNotDelivered,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// The codes of other extensions this one raises itself, besides those of its sources.
|
/// 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::PrivateKind => "private_kind",
|
||||||
Reason::OutputInvalid => "output_invalid",
|
Reason::OutputInvalid => "output_invalid",
|
||||||
Reason::JobFigureInvalid => "job_figure_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> {
|
fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<u8, &'static str> {
|
||||||
if b.len() < 4 {
|
if b.len() < 4 {
|
||||||
return Err(TRUNCATED);
|
return Err(TRUNCATED);
|
||||||
|
|
@ -326,14 +362,25 @@ fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<u8, &'static str> {
|
||||||
}
|
}
|
||||||
match b.get(4) {
|
match b.get(4) {
|
||||||
None => Err(TRUNCATED),
|
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),
|
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 {
|
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
|
/// 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,
|
pub price: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The job units, in unit order (§4.5).
|
/// The job units, in unit order (§4.5): `work_units`, the work its output packages state, in
|
||||||
pub const JOB_UNITS: [&str; 2] = ["jobs", "subpixels"];
|
/// 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 {
|
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'.')
|
(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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Tariff {
|
pub struct Tariff {
|
||||||
/// 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and
|
/// 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 version: u8,
|
||||||
pub seller: [u8; 32],
|
pub seller: [u8; 32],
|
||||||
pub buyer: [u8; 32],
|
pub buyer: [u8; 32],
|
||||||
pub start: u64,
|
pub start: u64,
|
||||||
pub end: u64,
|
pub end: u64,
|
||||||
pub currency: [u8; 3],
|
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],
|
pub room: [u32; ROOM_PRICES],
|
||||||
/// Settlement §7.1's four prices, in class order.
|
/// Settlement §7.1's four prices, in class order.
|
||||||
pub delivery: [u64; 4],
|
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],
|
pub package: [u64; 3],
|
||||||
/// Per presented impression, per billable impression.
|
/// Per presented impression, per billable impression.
|
||||||
pub ads: [u64; 2],
|
pub ads: [u64; 2],
|
||||||
|
|
@ -410,10 +466,11 @@ impl Tariff {
|
||||||
o.extend_from_slice(&self.start.to_le_bytes());
|
o.extend_from_slice(&self.start.to_le_bytes());
|
||||||
o.extend_from_slice(&self.end.to_le_bytes());
|
o.extend_from_slice(&self.end.to_le_bytes());
|
||||||
o.extend_from_slice(&self.currency);
|
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());
|
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());
|
o.extend_from_slice(&p.to_le_bytes());
|
||||||
}
|
}
|
||||||
if prices_jobs(self.version) {
|
if prices_jobs(self.version) {
|
||||||
|
|
@ -421,7 +478,7 @@ impl Tariff {
|
||||||
for j in &self.job {
|
for j in &self.job {
|
||||||
o.push(u8::try_from(j.capability.len()).expect("a capability of at most 255 bytes"));
|
o.push(u8::try_from(j.capability.len()).expect("a capability of at most 255 bytes"));
|
||||||
o.extend_from_slice(j.capability.as_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.push(u8::try_from(j.operation.len()).expect("an operation of at most 255 bytes"));
|
||||||
o.extend_from_slice(j.operation.as_bytes());
|
o.extend_from_slice(j.operation.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
@ -469,7 +526,7 @@ impl Tariff {
|
||||||
let (start, end) = (r.u64()?, r.u64()?);
|
let (start, end) = (r.u64()?, r.u64()?);
|
||||||
let currency = r.arr()?;
|
let currency = r.arr()?;
|
||||||
let mut room = [0u32; ROOM_PRICES];
|
let mut room = [0u32; ROOM_PRICES];
|
||||||
for p in &mut room {
|
for p in &mut room[..room_prices(version)] {
|
||||||
*p = r.u32()?;
|
*p = r.u32()?;
|
||||||
}
|
}
|
||||||
let mut delivery = [0u64; 4];
|
let mut delivery = [0u64; 4];
|
||||||
|
|
@ -477,7 +534,7 @@ impl Tariff {
|
||||||
*p = r.u64()?;
|
*p = r.u64()?;
|
||||||
}
|
}
|
||||||
let mut package = [0u64; 3];
|
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()?;
|
*p = r.u64()?;
|
||||||
}
|
}
|
||||||
let ads = [r.u64()?, r.u64()?];
|
let ads = [r.u64()?, r.u64()?];
|
||||||
|
|
@ -490,7 +547,7 @@ impl Tariff {
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let len = r.u8()? as usize;
|
let len = r.u8()? as usize;
|
||||||
let capability = text(r.take(len)?);
|
let capability = text(r.take(len)?);
|
||||||
let operation = if version == VERSION_3 {
|
let operation = if bills_work(version) {
|
||||||
let len = r.u8()? as usize;
|
let len = r.u8()? as usize;
|
||||||
text(r.take(len)?)
|
text(r.take(len)?)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -522,8 +579,8 @@ impl Tariff {
|
||||||
&& t.job.len() <= 255
|
&& 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.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| {
|
&& t.job.iter().all(|j| {
|
||||||
let operation = if t.version == VERSION_3 { job_operation(&j.operation) } else { j.operation.is_empty() };
|
let operation = if bills_work(t.version) { 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
|
job_capability(&j.capability) && operation && (1..=job_units(t.version)).contains(&j.unit) && j.divisor >= 1
|
||||||
});
|
});
|
||||||
if !rules {
|
if !rules {
|
||||||
return Err((Reason::TariffInvalid.code(), at()));
|
return Err((Reason::TariffInvalid.code(), at()));
|
||||||
|
|
@ -593,7 +650,7 @@ pub struct Row {
|
||||||
/// An invoice (§6.1).
|
/// An invoice (§6.1).
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Invoice {
|
pub struct Invoice {
|
||||||
/// Its tariff's version, 1, 2 or 3.
|
/// Its tariff's version, 1 to 4.
|
||||||
pub version: u8,
|
pub version: u8,
|
||||||
pub seller: [u8; 32],
|
pub seller: [u8; 32],
|
||||||
pub buyer: [u8; 32],
|
pub buyer: [u8; 32],
|
||||||
|
|
@ -605,8 +662,9 @@ pub struct Invoice {
|
||||||
pub records: Vec<(u8, [u8; 32])>,
|
pub records: Vec<(u8, [u8; 32])>,
|
||||||
pub lines: Vec<Line>,
|
pub lines: Vec<Line>,
|
||||||
pub energy: Vec<Row>,
|
pub energy: Vec<Row>,
|
||||||
/// Scopes 1 to 4, and in versions 2 and 3 scope 5; a version-1 invoice states no fifth.
|
/// Scopes 1 to 4, in versions 2 and 3 scope 5, and in version 4 scope 6 (§6.1); an invoice
|
||||||
pub unmetered: [u32; 5],
|
/// states only its version's.
|
||||||
|
pub unmetered: [u32; 6],
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub sig: [u8; 64],
|
pub sig: [u8; 64],
|
||||||
}
|
}
|
||||||
|
|
@ -647,8 +705,7 @@ impl Invoice {
|
||||||
o.extend_from_slice(&e.joules.to_le_bytes());
|
o.extend_from_slice(&e.joules.to_le_bytes());
|
||||||
o.extend_from_slice(&e.max_ppm.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[..scope::count(self.version) as usize] {
|
||||||
for u in &self.unmetered[..scopes] {
|
|
||||||
o.extend_from_slice(&u.to_le_bytes());
|
o.extend_from_slice(&u.to_le_bytes());
|
||||||
}
|
}
|
||||||
o.extend_from_slice(&self.amount.to_le_bytes());
|
o.extend_from_slice(&self.amount.to_le_bytes());
|
||||||
|
|
@ -704,7 +761,10 @@ impl Invoice {
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
energy.push(Row { scope: r.u8()?, tag: r.u8()?, figures: r.u32()?, joules: r.u64()?, max_ppm: r.u32()? });
|
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 amount = r.u64()?;
|
||||||
let sig = r.arr()?;
|
let sig = r.arr()?;
|
||||||
if !r.done() {
|
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)))
|
&& i.records.iter().all(|(k, _)| (1..=9).contains(k) || (v2 && (record_kind::OER_RECEIPT..=record_kind::ROUTE_RECORD).contains(k)))
|
||||||
&& ascending(&keys)
|
&& ascending(&keys)
|
||||||
&& i.lines.iter().all(|l| {
|
&& 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
|
unit_ok && l.quantity > 0 && l.cited > 0
|
||||||
})
|
})
|
||||||
&& ascending(&rows)
|
&& 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 {
|
if !rules {
|
||||||
return Err(Reason::InvoiceInvalid.code());
|
return Err(Reason::InvoiceInvalid.code());
|
||||||
}
|
}
|
||||||
|
|
@ -801,7 +861,9 @@ impl RoomFacts {
|
||||||
2..=10 => u.subscriber_us[unit as usize - 2],
|
2..=10 => u.subscriber_us[unit as usize - 2],
|
||||||
11 => u.composition_us,
|
11 => u.composition_us,
|
||||||
12 => u.recorded_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 total_entries: u64,
|
||||||
pub stored_bytes: u64,
|
pub stored_bytes: u64,
|
||||||
pub figure: Figure,
|
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).
|
/// One impression of an ad receipt, as the report states it (§8.1).
|
||||||
|
|
@ -905,6 +972,13 @@ pub struct JobFacts {
|
||||||
pub figure: Option<Figure>,
|
pub figure: Option<Figure>,
|
||||||
/// Each output receipt: its receipt hash and its `work_pixel_ops`.
|
/// Each output receipt: its receipt hash and its `work_pixel_ops`.
|
||||||
pub outputs: Vec<([u8; 32], u64)>,
|
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).
|
/// What a verified source gives (§5.2).
|
||||||
|
|
@ -946,6 +1020,7 @@ impl Facts {
|
||||||
Facts::Job(j) => {
|
Facts::Job(j) => {
|
||||||
s.extend(j.hops.iter().map(|h| (record_kind::OER_RECEIPT, *h)));
|
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.outputs.iter().map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
|
||||||
|
s.extend(j.packages.iter().map(|(h, _)| (record_kind::PACKAGE, *h)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s
|
s
|
||||||
|
|
@ -1142,6 +1217,7 @@ fn package_source(o: &Map<String, Value>) -> Result<Facts, Refusal> {
|
||||||
total_entries: r.total_entries,
|
total_entries: r.total_entries,
|
||||||
stored_bytes: r.stored_bytes,
|
stored_bytes: r.stored_bytes,
|
||||||
figure: Figure { joules: r.joules_micro, class: r.energy_provenance.clone() },
|
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) })
|
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;
|
use crate::route_record as rr;
|
||||||
if !has_exactly(o, &["kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"]) {
|
if !has_exactly(o, &["kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"]) {
|
||||||
return Err(bad_source());
|
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 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 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 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);
|
let (mut billed, mut figure) = (None, None);
|
||||||
if let Some(a) = attempts.iter().find(|a| a["billed"] == true) {
|
if let Some(a) = attempts.iter().find(|a| a["billed"] == true) {
|
||||||
let endpoint = a["endpoint"].as_str().expect("a token");
|
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 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 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])))?);
|
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")));
|
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() {
|
for (i, x) in outputs.iter().enumerate() {
|
||||||
let bad = || (Reason::OutputInvalid.code(), json!(["outputs", i]));
|
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 x = x.as_object().ok_or_else(bad_source)?;
|
||||||
let (Some(text), Some(code)) = (x["receipt"].as_str(), bytes_of(&x["codestream"])) else { return Err(bad_source()) };
|
// Each output's object: an int_motion codestream, or in version 4 a package's receipt,
|
||||||
let r = crate::video_receipt::VideoReceipt::from_json(text).map_err(|_| bad())?;
|
// the package a run of `package.build` made (§4.7), verified against its indexes.
|
||||||
if !r.verify_reconstruction_wiv1(&code) {
|
let object = if version == VERSION_4 && has_exactly(x, &["package", "indexes"]) {
|
||||||
return Err(bad());
|
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 {
|
Ok(Facts::Job(JobFacts {
|
||||||
record: rr::digest(&record),
|
record: rr::digest(&record),
|
||||||
|
|
@ -1386,17 +1510,20 @@ fn job_source(o: &Map<String, Value>) -> Result<Facts, Refusal> {
|
||||||
billed,
|
billed,
|
||||||
figure,
|
figure,
|
||||||
outputs: outs,
|
outputs: outs,
|
||||||
|
packages,
|
||||||
|
unbilled,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// §5.1: verify one source by its extension, a stitched source with `signers` (§4.4); a job
|
/// §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
|
/// source is read here as a tariff of version 3 reads it, and only a tariff of version 2 to 4
|
||||||
/// refusal's place is the extension's own, relative to the source.
|
/// 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> {
|
pub fn verify_source(v: &Value, signers: &[[u8; 32]]) -> Result<Facts, Refusal> {
|
||||||
verify_source_of(v, signers, VERSION_3)
|
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)?;
|
let o = v.as_object().ok_or_else(bad_source)?;
|
||||||
match o.get("kind").and_then(Value::as_str) {
|
match o.get("kind").and_then(Value::as_str) {
|
||||||
Some("room") => room_source(o),
|
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("package") => package_source(o),
|
||||||
Some("ads") => ads_source(o),
|
Some("ads") => ads_source(o),
|
||||||
Some("stitched") => stitched_source(o, signers),
|
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()),
|
_ => 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) {
|
if !t.in_scope(source_kind::ROOM, &r.room) {
|
||||||
return Err(Reason::OutsideScope);
|
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;
|
let rt = &r.tariff;
|
||||||
if rt.currency != [0; 3] {
|
if rt.currency != [0; 3] && (rt.currency != t.currency || rt.prices(metering::SESSION_VERSION_BYTES) != t.room) {
|
||||||
let prices: Vec<u32> = std::iter::once(rt.publisher).chain(rt.subscriber).chain([rt.composition, rt.recorded, rt.event]).collect();
|
return Err(Reason::TariffConflict);
|
||||||
if rt.currency != t.currency || prices != t.room {
|
|
||||||
return Err(Reason::TariffConflict);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !within(r.t0 as u128 + r.start as u128, r.t0 as u128 + r.end as u128) {
|
if !within(r.t0 as u128 + r.start as u128, r.t0 as u128 + r.end as u128) {
|
||||||
return Err(Reason::OutsidePeriod);
|
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
|
/// 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
|
/// record but, in versions 3 and 4, a job's output receipt, and in version 4 a package receipt.
|
||||||
/// hops' OER/2 receipts, is new on every run and billed once; its output receipt is the
|
/// A job's work, its route record and its hops' OER/2 receipts, is new on every run and billed
|
||||||
/// evidence of what it delivered, which a deterministic re-run delivers again.
|
/// 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 {
|
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
|
/// §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.
|
/// Each line, and the records it cites.
|
||||||
pub lines: Vec<(Line, Cited)>,
|
pub lines: Vec<(Line, Cited)>,
|
||||||
pub energy: Vec<Row>,
|
pub energy: Vec<Row>,
|
||||||
pub unmetered: [u32; 5],
|
pub unmetered: [u32; 6],
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
/// Per scope and tag, the sum of its figures, for the summed-classes check.
|
/// Per scope and tag, the sum of its figures, for the summed-classes check.
|
||||||
pub sums: BTreeMap<(u8, u8), u128>,
|
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]
|
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> {
|
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();
|
let mut records: BTreeSet<(u8, [u8; 32])> = BTreeSet::new();
|
||||||
for f in facts {
|
for f in facts {
|
||||||
records.extend(f.records());
|
records.extend(f.records());
|
||||||
|
|
@ -1576,7 +1720,7 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
|
||||||
for f in facts {
|
for f in facts {
|
||||||
match f {
|
match f {
|
||||||
Facts::Room(r) => {
|
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)]);
|
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);
|
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) => {
|
Facts::Package(p) => {
|
||||||
let cite = [(record_kind::PACKAGE, p.receipt_hash)];
|
let cite = [(record_kind::PACKAGE, p.receipt_hash)];
|
||||||
add(source_kind::PACKAGE, p.signer, 1, 1, &cite);
|
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).
|
// cited once and counted for each (§6.5).
|
||||||
Facts::Job(j) => {
|
Facts::Job(j) => {
|
||||||
let Some((cap, hop)) = &j.billed else { continue };
|
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)) {
|
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 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 mut cites = vec![(record_kind::ROUTE_RECORD, j.record), (record_kind::OER_RECEIPT, *hop)];
|
||||||
let n = if p.unit == 1 {
|
let n = match p.unit {
|
||||||
1
|
1 => 1,
|
||||||
} else {
|
2 => {
|
||||||
cites.extend(j.outputs.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
|
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()
|
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);
|
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 lines = Vec::with_capacity(q.len());
|
||||||
let mut amount: u128 = 0;
|
let mut amount: u128 = 0;
|
||||||
for ((kind, source, unit), (n, cited)) in q {
|
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 quantity = u64::try_from(n).map_err(|_| Overflow)?;
|
||||||
let price = t.price(kind, unit);
|
let price = t.price(kind, unit);
|
||||||
let a = n * price as u128 / t.divisor(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 {
|
for f in facts {
|
||||||
match f {
|
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::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 })
|
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.
|
/// Rows, unmetered counts and sums per (scope, tag) of figures.
|
||||||
fn rows_of(figures: &[(u8, Figure)]) -> Result<RowsOf, Overflow> {
|
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();
|
let mut acc: BTreeMap<(u8, u8), (u32, u128, u32)> = BTreeMap::new();
|
||||||
for (s, f) in figures {
|
for (s, f) in figures {
|
||||||
match f.tag() {
|
match f.tag() {
|
||||||
|
|
@ -1976,6 +2143,7 @@ pub fn verify_sources(t: &Tariff, start: u64, end: u64, sources: &[Value], previ
|
||||||
facts.push(f);
|
facts.push(f);
|
||||||
}
|
}
|
||||||
check_across(t.version, &facts, previous).map_err(|(k, r)| (r.code(), json!(["source", k])))?;
|
check_across(t.version, &facts, previous).map_err(|(k, r)| (r.code(), json!(["source", k])))?;
|
||||||
|
hold(t, &mut facts, previous, start, end);
|
||||||
Ok(facts)
|
Ok(facts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2041,7 +2209,7 @@ pub fn accepted_json(a: &Accepted) -> Value {
|
||||||
.map(|(l, cited)| {
|
.map(|(l, cited)| {
|
||||||
let mut v = json!({
|
let mut v = json!({
|
||||||
"source_kind": source_kind::name(l.source_kind), "source": hex(&l.source),
|
"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<_>>(),
|
"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
|
// 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];
|
let p = &a.tariff.job[l.unit as usize - 1];
|
||||||
v["unit"] = json!(JOB_UNITS[p.unit as usize - 1]);
|
v["unit"] = json!(JOB_UNITS[p.unit as usize - 1]);
|
||||||
v["capability"] = json!(p.capability);
|
v["capability"] = json!(p.capability);
|
||||||
if a.tariff.version == VERSION_3 {
|
if bills_work(a.tariff.version) {
|
||||||
v["operation"] = json!(p.operation);
|
v["operation"] = json!(p.operation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2059,7 +2227,7 @@ pub fn accepted_json(a: &Accepted) -> Value {
|
||||||
.collect();
|
.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 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());
|
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 n = i.unmetered[s as usize - 1];
|
||||||
let has_row = i.energy.iter().any(|r| r.scope == s);
|
let has_row = i.energy.iter().any(|r| r.scope == s);
|
||||||
unmetered.insert(scope::name(s).into(), n.into());
|
unmetered.insert(scope::name(s).into(), n.into());
|
||||||
|
|
@ -2106,7 +2274,12 @@ mod tests {
|
||||||
start: 10,
|
start: 10,
|
||||||
end: 20,
|
end: 20,
|
||||||
currency: *b"EUR",
|
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],
|
delivery: [4; 4],
|
||||||
package: [5; 3],
|
package: [5; 3],
|
||||||
ads: [6; 2],
|
ads: [6; 2],
|
||||||
|
|
@ -2187,10 +2360,70 @@ mod tests {
|
||||||
let at = nb.windows(2).position(|w| w == [0xc3, 0xa9]).expect("the two bytes of é");
|
let at = nb.windows(2).position(|w| w == [0xc3, 0xa9]).expect("the two bytes of é");
|
||||||
nb[at] = 0xff;
|
nb[at] = 0xff;
|
||||||
assert_eq!(Tariff::parse(&nb).unwrap_err().0, "tariff_invalid");
|
assert_eq!(Tariff::parse(&nb).unwrap_err().0, "tariff_invalid");
|
||||||
// A tariff of version 4 is not defined.
|
// A tariff of version 5 is not defined.
|
||||||
let mut v4 = b.clone();
|
let mut v5 = b.clone();
|
||||||
v4[4] = 4;
|
v5[4] = 5;
|
||||||
assert_eq!(Tariff::parse(&v4).unwrap_err().0, "unknown_version");
|
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
|
/// 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_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!(!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;
|
/// A tariff of version 2 reads back with its job prices, and every altered byte is refused;
|
||||||
|
|
@ -2239,7 +2472,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn an_invoice_of_version_2_states_the_jobs_scope() {
|
fn an_invoice_of_version_2_states_the_jobs_scope() {
|
||||||
let t = tariff_v2();
|
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));
|
let i = invoice_of(&t, [0; 32], 10, 20, &c).sign(&key(1));
|
||||||
assert_eq!(Invoice::parse(&i.to_bytes()).unwrap(), i);
|
assert_eq!(Invoice::parse(&i.to_bytes()).unwrap(), i);
|
||||||
let mut v1 = i.clone();
|
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(k, &i3), Ok(()), "kind {k} in version 3");
|
||||||
}
|
}
|
||||||
assert_eq!(with(record_kind::RUN, &i3), Err("invoice_invalid"));
|
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]
|
#[test]
|
||||||
|
|
@ -2314,7 +2563,7 @@ mod tests {
|
||||||
records: vec![(record_kind::SESSION, [7; 32]), (record_kind::HOP, [6; 32])],
|
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) }],
|
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 }],
|
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,
|
amount: 4,
|
||||||
sig: [0; 64],
|
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(&unit.sign(&key(1)).to_bytes()).unwrap_err(), "invoice_invalid");
|
||||||
assert_eq!(Invoice::parse(&[b.clone(), vec![0]].concat()).unwrap_err(), "malformed");
|
assert_eq!(Invoice::parse(&[b.clone(), vec![0]].concat()).unwrap_err(), "malformed");
|
||||||
let mut v = b.clone();
|
let mut v = b.clone();
|
||||||
v[4] = 4;
|
v[4] = 5;
|
||||||
assert_eq!(Invoice::parse(&v).unwrap_err(), "unknown_version", "version 4 is not defined (2 and 3 are, §6.1)");
|
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 mut s = b;
|
||||||
let n = s.len();
|
let n = s.len();
|
||||||
s[n - 1] ^= 1;
|
s[n - 1] ^= 1;
|
||||||
|
|
@ -2406,6 +2655,6 @@ mod tests {
|
||||||
Row { scope: 2, tag: 2, figures: 1, joules: 7, max_ppm: 0 },
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@
|
||||||
//! fixed seeds, as are the tariffs and the invoices. Media jobs come from
|
//! 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
|
//! `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
|
//! 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
|
//! seeds, as are jobs routed under the route record's version 2 (per-operation entries, measured
|
||||||
//! against the outcome it states.
|
//! 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::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -20,6 +21,7 @@ use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::energy_class::{CounterFiltering, Uncertainty};
|
use crate::energy_class::{CounterFiltering, Uncertainty};
|
||||||
|
use crate::route_record::content_hash as rr_hash;
|
||||||
use crate::route_record::corpus as rr;
|
use crate::route_record::corpus as rr;
|
||||||
use crate::jwp_hash::content_hash;
|
use crate::jwp_hash::content_hash;
|
||||||
use crate::live_metering::HopRecord;
|
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)})
|
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.
|
/// 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];
|
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
|
/// 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.
|
/// 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 {
|
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![],
|
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
|
// The rooms' prices are the lesson receipts'; the tariff's scope is every party of the
|
||||||
// accepted invoices.
|
// 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() },
|
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.
|
// 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_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_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_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() },
|
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"] },
|
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();
|
let mut sources = Map::new();
|
||||||
for (n, v) in &pool {
|
for (n, v) in &pool {
|
||||||
sources.insert((*n).into(), v.clone());
|
sources.insert((*n).into(), v.clone());
|
||||||
|
|
@ -696,7 +883,7 @@ pub fn generate(wai: &Path) -> Result<String, String> {
|
||||||
"sources": v.sources, "outcome": outcome,
|
"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!({
|
let doc = json!({
|
||||||
"corpus": CAPABILITY,
|
"corpus": CAPABILITY,
|
||||||
"extension": "extensions/receipt-ledger.md",
|
"extension": "extensions/receipt-ledger.md",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// The corpus, as `vectors.json` holds it.
|
||||||
pub fn generate() -> Result<String, String> {
|
pub fn generate() -> Result<String, String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// §9 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_extension_lists_the_reasons() {
|
fn the_extension_lists_the_reasons() {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// §10 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_extension_lists_the_reasons() {
|
fn the_extension_lists_the_reasons() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue