open-standards/wai/wai-rs/src/settlement.rs
2026-10-21 20:10:17 -04:00

1685 lines
64 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Delivery settlement — `wai.delivery.settlement` ([extensions/settlement.md]).
//!
//! Delivery paid on what the party that received it signed. The downstream party of a
//! delivery, a next hop or a sink, signs an **acknowledgement** (`WDA1`) of what it received
//! in a window: per object class, the objects by content hash under jwp-receipts §2.2's tree,
//! the byte range of each, and their byte count. A hop whose downstream party does not sign
//! states that delivery in a **declaration**, the same record signed by the hop, which is
//! counted apart and billed never. A **range proof** (`WBR1`) shows that given bytes are a
//! range of an object, over BLAKE3's chunk tree, without the rest of it. A retrieval that
//! costs money is billed only to a grant whose proof of possession the request carried. Per
//! party and per period, a **settlement statement** (`WSS1`) states the acknowledged bytes
//! by object class and payer, the declared bytes, the party's energy by acquisition class
//! from its per-hop records ([`crate::live_metering`]), and the amount under a tariff in
//! integer micro-units; [`verify`] recomputes every figure from the records.
//!
//! What this module implements, by section of the extension:
//!
//! - §3, §4: the acknowledgement and the declaration, their entries, items, rules and chains;
//! - §5: the range proof ([`RangeProof`]): building, reading and verifying it, and audits;
//! - §6: the checks of a grant and its proof on an entry;
//! - §7: the statement, its lines, its tariff and its recomputation ([`figures`]), and its
//! attribution lines from a [`Basis`] (§7.8, [`attribution_lines`]);
//! - §8: verification ([`verify`]).
//!
//! The corpus `live-conformance/settlement/` is written by [`corpus::generate`] and checked
//! by [`corpus::verify`] here, and by `live-conformance/settlement_verify.py`, which shares
//! no code with this module.
//!
//! [extensions/settlement.md]: ../../extensions/settlement.md
use std::collections::BTreeMap;
use blake3::hazmat::{merge_subtrees_non_root, merge_subtrees_root, ChainingValue, HasherExt, Mode};
use ed25519_dalek::{Signer, SigningKey};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use crate::jwp_hash::{content_hash, DOMAIN_OBJECT};
use crate::live_events as events;
use crate::live_metering::{self as metering, HopRecord, Total};
use crate::live_room::{self as room, Descriptor, HeldGrant, Proof};
pub mod corpus;
/// The record this extension registers.
pub const CAPABILITY: &str = "wai.delivery.settlement";
/// The magic of an acknowledgement or a declaration (§3.1).
pub const ACK_MAGIC: [u8; 4] = *b"WDA1";
/// The magic of a range proof (§5.2).
pub const RANGE_MAGIC: [u8; 4] = *b"WBR1";
/// The magic of a settlement statement (§7.1).
pub const STATEMENT_MAGIC: [u8; 4] = *b"WSS1";
/// The version of every record in this revision.
pub const VERSION: u8 = 1;
/// The domain of a record's signature (§3.5).
pub const ACK_DOMAIN: &[u8] = b"wai:settle-ack\x01";
/// The domain of an item list's digest (§3.2).
pub const ITEMS_DOMAIN: &[u8] = b"wai:settle-items\x01";
/// The domain of a statement's signature (§7.4).
pub const STATEMENT_DOMAIN: &[u8] = b"wai:settle-statement\x01";
/// jwp-receipts §2.2's legacy leaf and node domains.
pub const LEAF_DOMAIN: &[u8] = b"moq-jwp:merkle-leaf\x01";
pub const NODE_DOMAIN: &[u8] = b"moq-jwp:merkle-node\x01";
/// An entry's length (§3.2).
pub const ENTRY_LEN: usize = 141;
/// An item's length (§3.4).
pub const ITEM_LEN: usize = 48;
/// A line's length (§7.2).
pub const LINE_LEN: usize = 90;
/// A record's fixed fields before its entries, the entry count included (§3.1).
pub const ACK_FIXED: usize = 121;
/// A range proof's fixed fields (§5.2).
pub const RANGE_FIXED: usize = 62;
/// BLAKE3's chunk (§5.1).
pub const CHUNK: u64 = 1024;
/// The length of jwp-receipts §2.1's domain, which starts `X` (§5.1).
pub const DOMAIN_LEN: u64 = 15;
/// Prices are per 10^9 bytes (§7.3).
pub const PRICE_BYTES: u128 = 1_000_000_000;
/// The object classes (§3.3).
pub const CLASSES: usize = 4;
/// A record's mode (§3.1).
pub mod mode {
/// Signed by the downstream party.
pub const ACK: u8 = 1;
/// Signed by the upstream party (§4).
pub const DECLARATION: u8 = 2;
}
/// The downstream party's role (§3.1).
pub mod role {
pub const HOP: u8 = 1;
pub const SINK: u8 = 2;
}
/// The object classes (§3.3).
pub mod class {
pub const LIVE: u8 = 1;
pub const FETCH: u8 = 2;
pub const FILL: u8 = 3;
pub const ANCILLARY: u8 = 4;
/// The class's name.
pub fn name(c: u8) -> &'static str {
match c {
LIVE => "live",
FETCH => "fetch",
FILL => "fill",
ANCILLARY => "ancillary",
_ => "unknown",
}
}
/// A retrieval class (2 or 3), billed only to a grant (§6).
pub fn is_retrieval(c: u8) -> bool {
c == FETCH || c == FILL
}
}
/// A line's kind (§7.2).
pub mod line_kind {
/// A delivery line.
pub const DELIVERY: u8 = 1;
/// An attribution line: a payee that is not a hop, paid from a basis (§7.8).
pub const ATTRIBUTION: u8 = 2;
}
// ---------------------------------------------------------------------------------------
// Reasons (§11)
// ---------------------------------------------------------------------------------------
/// Why a record, a range proof or a statement is refused (§11).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Reason {
/// A code of live-metering §9 (and, through it, of live-rooms §11 and live-events §9).
Metering(metering::Reason),
AckInvalid,
SelfAcknowledged,
ItemsInvalid,
ItemsMismatch,
AckWrongParty,
AckOutsidePeriod,
AckOverlap,
AckChainBroken,
DeclaredAndAcknowledged,
RangeInvalid,
RangeOutsideObject,
RangeProofMismatch,
RangeNotAcknowledged,
StatementInvalid,
PaidWithoutGrant,
BytesNotAcknowledged,
StatementMismatch,
HopNotParty,
SpanOutsidePeriod,
}
impl From<metering::Reason> for Reason {
fn from(r: metering::Reason) -> Self {
Reason::Metering(r)
}
}
impl From<room::Reason> for Reason {
fn from(r: room::Reason) -> Self {
Reason::Metering(metering::Reason::Room(r))
}
}
const fn ev(r: events::Reason) -> Reason {
Reason::Metering(metering::Reason::Room(room::Reason::Events(r)))
}
const fn rm(r: room::Reason) -> Reason {
Reason::Metering(metering::Reason::Room(r))
}
const TRUNCATED: Reason = ev(events::Reason::Truncated);
const MALFORMED: Reason = ev(events::Reason::Malformed);
const UNKNOWN_VERSION: Reason = ev(events::Reason::UnknownVersion);
const BAD_KEY: Reason = ev(events::Reason::BadKey);
const BAD_SIGNATURE: Reason = ev(events::Reason::BadSignature);
const RECORD_MISSING: Reason = Reason::Metering(metering::Reason::RecordMissing);
const CLASSES_SUMMED: Reason = Reason::Metering(metering::Reason::ClassesSummed);
impl Reason {
/// The codes this extension adds, in §11's order.
pub const ALL: [Reason; 19] = [
Reason::AckInvalid,
Reason::SelfAcknowledged,
Reason::ItemsInvalid,
Reason::ItemsMismatch,
Reason::AckWrongParty,
Reason::AckOutsidePeriod,
Reason::AckOverlap,
Reason::AckChainBroken,
Reason::DeclaredAndAcknowledged,
Reason::RangeInvalid,
Reason::RangeOutsideObject,
Reason::RangeProofMismatch,
Reason::RangeNotAcknowledged,
Reason::StatementInvalid,
Reason::PaidWithoutGrant,
Reason::BytesNotAcknowledged,
Reason::StatementMismatch,
Reason::HopNotParty,
Reason::SpanOutsidePeriod,
];
/// The codes of other extensions this one raises (§11's last paragraph), beside every code
/// a descriptor, a grant and a per-hop record raise.
pub const REUSED: [&'static str; 15] = [
"truncated",
"malformed",
"unknown_version",
"bad_key",
"bad_signature",
"record_missing",
"classes_summed",
"outside_grant_window",
"grant_wrong_principal",
"seek_not_granted",
"pop_malformed",
"pop_wrong_grant",
"pop_wrong_key",
"pop_bad_signature",
"pop_replayed",
];
/// The reason's code.
pub fn code(self) -> &'static str {
match self {
Reason::Metering(r) => r.code(),
Reason::AckInvalid => "ack_invalid",
Reason::SelfAcknowledged => "self_acknowledged",
Reason::ItemsInvalid => "items_invalid",
Reason::ItemsMismatch => "items_mismatch",
Reason::AckWrongParty => "ack_wrong_party",
Reason::AckOutsidePeriod => "ack_outside_period",
Reason::AckOverlap => "ack_overlap",
Reason::AckChainBroken => "ack_chain_broken",
Reason::DeclaredAndAcknowledged => "declared_and_acknowledged",
Reason::RangeInvalid => "range_invalid",
Reason::RangeOutsideObject => "range_outside_object",
Reason::RangeProofMismatch => "range_proof_mismatch",
Reason::RangeNotAcknowledged => "range_not_acknowledged",
Reason::StatementInvalid => "statement_invalid",
Reason::PaidWithoutGrant => "paid_without_grant",
Reason::BytesNotAcknowledged => "bytes_not_acknowledged",
Reason::StatementMismatch => "statement_mismatch",
Reason::HopNotParty => "hop_not_party",
Reason::SpanOutsidePeriod => "span_outside_period",
}
}
}
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
impl std::error::Error for Reason {}
// ---------------------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------------------
fn sha256(parts: &[&[u8]]) -> [u8; 32] {
let mut h = Sha256::new();
for p in parts {
h.update(p);
}
h.finalize().into()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn verify_sig(key: &[u8; 32], domain: &[u8], body: &[u8], sig: &[u8; 64]) -> Result<(), Reason> {
let mut m = domain.to_vec();
m.extend_from_slice(body);
crate::sig::verify_strict(key, &m, sig).map_err(|e| match e {
crate::sig::SigError::BadKey => BAD_KEY,
crate::sig::SigError::BadSignature => BAD_SIGNATURE,
})
}
fn sign(key: &SigningKey, domain: &[u8], body: &[u8]) -> [u8; 64] {
let mut m = domain.to_vec();
m.extend_from_slice(body);
key.sign(&m).to_bytes()
}
/// The digest of given bytes (§8.2): SHA-256 over all but their last 64.
pub fn given_digest(b: &[u8]) -> [u8; 32] {
sha256(&[&b[..b.len().saturating_sub(64)]])
}
/// The digest of a proof record (§8.2): SHA-256 over all of it.
pub fn proof_digest(b: &[u8]) -> [u8; 32] {
sha256(&[b])
}
/// A cursor; running out is `truncated`.
struct Rd<'a> {
b: &'a [u8],
p: usize,
}
impl<'a> Rd<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8], Reason> {
let e = self.p.checked_add(n).filter(|&e| e <= self.b.len()).ok_or(TRUNCATED)?;
let s = &self.b[self.p..e];
self.p = e;
Ok(s)
}
fn u8(&mut self) -> Result<u8, Reason> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, Reason> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().expect("2 bytes")))
}
fn u32(&mut self) -> Result<u32, Reason> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().expect("4 bytes")))
}
fn u64(&mut self) -> Result<u64, Reason> {
Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("8 bytes")))
}
fn arr<const N: usize>(&mut self) -> Result<[u8; N], Reason> {
Ok(self.take(N)?.try_into().expect("N bytes"))
}
fn done(&self) -> bool {
self.p == self.b.len()
}
}
/// The head every record of this extension starts with: the magic, then version 1.
fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<(), Reason> {
if b.len() < 4 {
return Err(TRUNCATED);
}
if &b[..4] != magic {
return Err(MALFORMED);
}
if b.len() < 5 {
return Err(TRUNCATED);
}
if b[4] != VERSION {
return Err(UNKNOWN_VERSION);
}
Ok(())
}
// ---------------------------------------------------------------------------------------
// §2, §3.2: the tree root and the item list
// ---------------------------------------------------------------------------------------
/// jwp-receipts §2.2's legacy leaf of a content hash.
pub fn leaf(content_hash: &[u8; 32]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(LEAF_DOMAIN);
h.update(content_hash);
*h.finalize().as_bytes()
}
fn node(l: &[u8; 32], r: &[u8; 32]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(NODE_DOMAIN);
h.update(l);
h.update(r);
*h.finalize().as_bytes()
}
/// The tree root of a list of content hashes (§2): the last node of an odd level paired with
/// itself, a single leaf its own root, and zero for no leaf.
pub fn tree_root(hashes: &[[u8; 32]]) -> [u8; 32] {
if hashes.is_empty() {
return [0; 32];
}
let mut level: Vec<[u8; 32]> = hashes.iter().map(leaf).collect();
while level.len() > 1 {
if level.len() % 2 == 1 {
level.push(*level.last().expect("non-empty"));
}
level = level.as_chunks::<2>().0.iter().map(|p| node(&p[0], &p[1])).collect();
}
level[0]
}
/// An item (§3.4): a byte range of one object.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Item {
pub content_hash: [u8; 32],
pub start: u64,
pub end: u64,
}
impl Item {
/// The whole of `envelope`.
pub fn whole(envelope: &[u8]) -> Item {
Item { content_hash: content_hash(envelope), start: 0, end: envelope.len() as u64 }
}
/// Its byte count.
pub fn bytes(&self) -> u64 {
self.end - self.start
}
}
/// An item list's bytes (§3.4).
pub fn item_list(items: &[Item]) -> Vec<u8> {
let mut o = Vec::with_capacity(ITEM_LEN * items.len());
for i in items {
o.extend_from_slice(&i.content_hash);
o.extend_from_slice(&i.start.to_le_bytes());
o.extend_from_slice(&i.end.to_le_bytes());
}
o
}
/// An item list's digest (§3.2).
pub fn items_digest(list: &[u8]) -> [u8; 32] {
sha256(&[ITEMS_DOMAIN, list])
}
/// Read an item list (§3.6): items of 48 bytes, each range not empty, no two sharing a byte
/// of one object; otherwise `items_invalid`.
pub fn read_items(b: &[u8]) -> Result<Vec<Item>, Reason> {
if !b.len().is_multiple_of(ITEM_LEN) {
return Err(Reason::ItemsInvalid);
}
let items: Vec<Item> = b
.as_chunks::<ITEM_LEN>()
.0
.iter()
.map(|c| Item {
content_hash: c[..32].try_into().expect("32 bytes"),
start: u64::from_le_bytes(c[32..40].try_into().expect("8 bytes")),
end: u64::from_le_bytes(c[40..48].try_into().expect("8 bytes")),
})
.collect();
if items.iter().any(|i| i.start >= i.end) {
return Err(Reason::ItemsInvalid);
}
let mut sorted: Vec<&Item> = items.iter().collect();
sorted.sort_by_key(|i| (i.content_hash, i.start, i.end));
if sorted.windows(2).any(|w| w[0].content_hash == w[1].content_hash && w[1].start < w[0].end) {
return Err(Reason::ItemsInvalid);
}
Ok(items)
}
// ---------------------------------------------------------------------------------------
// §3 The acknowledgement
// ---------------------------------------------------------------------------------------
/// An entry (§3.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Entry {
pub class: u8,
pub grant: [u8; 32],
pub proof: [u8; 32],
pub objects: u32,
pub bytes: u64,
pub root: [u8; 32],
pub items: [u8; 32],
}
impl Entry {
/// The entry of `items`, of `class`, under `grant` and `proof` (zero for none).
pub fn of(class: u8, grant: [u8; 32], proof: [u8; 32], items: &[Item]) -> Entry {
let hashes: Vec<[u8; 32]> = items.iter().map(|i| i.content_hash).collect();
Entry {
class,
grant,
proof,
objects: items.len() as u32,
bytes: items.iter().map(Item::bytes).sum(),
root: tree_root(&hashes),
items: items_digest(&item_list(items)),
}
}
fn encode(&self, o: &mut Vec<u8>) {
o.push(self.class);
o.extend_from_slice(&self.grant);
o.extend_from_slice(&self.proof);
o.extend_from_slice(&self.objects.to_le_bytes());
o.extend_from_slice(&self.bytes.to_le_bytes());
o.extend_from_slice(&self.root);
o.extend_from_slice(&self.items);
}
fn read(r: &mut Rd) -> Result<Entry, Reason> {
Ok(Entry { class: r.u8()?, grant: r.arr()?, proof: r.arr()?, objects: r.u32()?, bytes: r.u64()?, root: r.arr()?, items: r.arr()? })
}
fn order(&self) -> (u8, [u8; 32], [u8; 32]) {
(self.class, self.grant, self.proof)
}
/// §3.6's rules on one entry.
fn rules_hold(&self) -> bool {
let class_ok = (1..=CLASSES as u8).contains(&self.class);
let sums = self.objects >= 1 && self.bytes >= self.objects as u64;
let grant_proof = if class::is_retrieval(self.class) { (self.grant == [0; 32]) == (self.proof == [0; 32]) } else { self.proof == [0; 32] };
class_ok && sums && grant_proof
}
/// §3.6: whether `items`, read, recompute against this entry.
pub fn recomputes(&self, items: &[Item]) -> bool {
let sum: u128 = items.iter().map(|i| i.bytes() as u128).sum();
let hashes: Vec<[u8; 32]> = items.iter().map(|i| i.content_hash).collect();
items.len() as u64 == self.objects as u64
&& sum == self.bytes as u128
&& tree_root(&hashes) == self.root
&& items_digest(&item_list(items)) == self.items
}
}
/// An acknowledgement or a declaration (§3.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ack {
pub mode: u8,
pub role: u8,
pub upstream: [u8; 32],
pub downstream: [u8; 32],
pub start: u64,
pub end: u64,
pub prev: [u8; 32],
pub entries: Vec<Entry>,
pub sig: [u8; 64],
}
impl Ack {
/// The record without `sig`.
pub fn body(&self) -> Vec<u8> {
let mut o = Vec::with_capacity(ACK_FIXED + ENTRY_LEN * self.entries.len());
o.extend_from_slice(&ACK_MAGIC);
o.push(VERSION);
o.push(self.mode);
o.push(self.role);
o.extend_from_slice(&self.upstream);
o.extend_from_slice(&self.downstream);
o.extend_from_slice(&self.start.to_le_bytes());
o.extend_from_slice(&self.end.to_le_bytes());
o.extend_from_slice(&self.prev);
o.extend_from_slice(&u16::try_from(self.entries.len()).expect("at most 65535 entries").to_le_bytes());
for e in &self.entries {
e.encode(&mut o);
}
o
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut o = self.body();
o.extend_from_slice(&self.sig);
o
}
/// Its digest (§3.5).
pub fn digest(&self) -> [u8; 32] {
sha256(&[&self.body()])
}
/// The key that signs it: `downstream` in an acknowledgement, `upstream` in a declaration.
pub fn signer(&self) -> [u8; 32] {
if self.mode == mode::DECLARATION { self.upstream } else { self.downstream }
}
/// Signed with `key`, which should be [`Ack::signer`]'s.
pub fn sign(mut self, key: &SigningKey) -> Ack {
self.sig = sign(key, ACK_DOMAIN, &self.body());
self
}
/// §3.6: read a record, hold it to its rules, then its signature.
pub fn parse(b: &[u8]) -> Result<Ack, Reason> {
read_head(b, &ACK_MAGIC)?;
let mut r = Rd { b, p: 5 };
let (m, rl) = (r.u8()?, r.u8()?);
let (upstream, downstream) = (r.arr()?, r.arr()?);
let (start, end) = (r.u64()?, r.u64()?);
let prev = r.arr()?;
let n = r.u16()? as usize;
let mut entries = Vec::with_capacity(n.min(b.len() / ENTRY_LEN));
for _ in 0..n {
entries.push(Entry::read(&mut r)?);
}
let sig = r.arr()?;
if !r.done() {
return Err(MALFORMED);
}
let a = Ack { mode: m, role: rl, upstream, downstream, start, end, prev, entries, sig };
let rules = (a.mode == mode::ACK || a.mode == mode::DECLARATION)
&& (a.role == role::HOP || a.role == role::SINK)
&& a.start < a.end
&& a.upstream != [0; 32]
&& (a.mode == mode::DECLARATION || a.downstream != [0; 32])
&& !a.entries.is_empty()
&& a.entries.windows(2).all(|w| w[0].order() < w[1].order())
&& a.entries.iter().all(Entry::rules_hold);
if !rules {
return Err(Reason::AckInvalid);
}
if a.mode == mode::ACK && a.downstream == a.upstream {
return Err(Reason::SelfAcknowledged);
}
verify_sig(&a.signer(), ACK_DOMAIN, &b[..b.len() - 64], &a.sig)?;
Ok(a)
}
}
// ---------------------------------------------------------------------------------------
// §5 Range proofs
// ---------------------------------------------------------------------------------------
/// `left(n)` (§5.1): the left child of a subtree of `n` > 1024 bytes.
fn left_len(n: u64) -> u64 {
let q = (n - 1) / CHUNK;
CHUNK << (63 - q.leading_zeros())
}
/// The non-root chaining value of the subtree `x` whose first chunk is at byte offset
/// `start` of the whole input.
fn subtree_cv(x: &[u8], start: u64) -> ChainingValue {
let mut h = blake3::Hasher::new();
h.set_input_offset(start);
h.update(x);
h.finalize_non_root()
}
/// The covered chunks of a range (§5.2), as offsets into `X`: (c0, c1), for an object of
/// `envelope_len` bytes. `None` when `X`'s length does not fit in a u64.
pub fn covered(envelope_len: u64, start: u64, end: u64) -> Option<(u64, u64)> {
let l = DOMAIN_LEN.checked_add(envelope_len)?;
let c0 = (DOMAIN_LEN as u128 + start as u128) / CHUNK as u128 * CHUNK as u128;
let c1 = (DOMAIN_LEN as u128 + end as u128).div_ceil(CHUNK as u128) * CHUNK as u128;
Some((c0 as u64, c1.min(l as u128) as u64))
}
/// A range proof (§5.2).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RangeProof {
pub content_hash: [u8; 32],
pub envelope_len: u64,
pub start: u64,
pub end: u64,
pub cvs: Vec<[u8; 32]>,
/// The covered bytes that are envelope (§5.2).
pub bytes: Vec<u8>,
}
/// What a range proof that passes establishes (§5.5, §8.3).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RangeFacts {
pub start: u64,
pub end: u64,
pub envelope_len: u64,
pub covered: (u64, u64),
/// Whether the proof's last covered chunk is the object's last: a tail proof.
pub length_established: bool,
}
/// The producer's walk: the chaining values of the subtrees wholly outside [c0, c1), in the
/// order [`RangeProof::verify`] consumes them.
fn collect_cvs(x: &[u8], s: u64, n: u64, c0: u64, c1: u64, out: &mut Vec<[u8; 32]>) {
if s + n <= c0 || s >= c1 {
out.push(subtree_cv(&x[s as usize..(s + n) as usize], s));
} else if !(c0 <= s && s + n <= c1) {
let l = left_len(n);
collect_cvs(x, s, l, c0, c1, out);
collect_cvs(x, s + l, n - l, c0, c1, out);
}
}
/// §5.4's `cv(s, n)` over the covered chunks `xc`, which start at `c0` of `X`.
fn node_cv(xc: &[u8], s: u64, n: u64, c0: u64, c1: u64, it: &mut dyn Iterator<Item = [u8; 32]>) -> Result<ChainingValue, Reason> {
if s + n <= c0 || s >= c1 {
it.next().ok_or(Reason::RangeProofMismatch)
} else if c0 <= s && s + n <= c1 {
Ok(subtree_cv(&xc[(s - c0) as usize..(s - c0 + n) as usize], s))
} else if n <= CHUNK {
// A single chunk lies wholly inside or wholly outside the covered chunks (§5.4).
Err(Reason::RangeProofMismatch)
} else {
let l = left_len(n);
let left = node_cv(xc, s, l, c0, c1, it)?;
let right = node_cv(xc, s + l, n - l, c0, c1, it)?;
Ok(merge_subtrees_non_root(&left, &right, Mode::Hash))
}
}
impl RangeProof {
/// The proof of bytes `start`..`end` of `envelope`.
pub fn build(envelope: &[u8], start: u64, end: u64) -> RangeProof {
assert!(start < end && end <= envelope.len() as u64, "a range of the object");
let mut x = DOMAIN_OBJECT.to_vec();
x.extend_from_slice(envelope);
let l = x.len() as u64;
let (c0, c1) = covered(envelope.len() as u64, start, end).expect("an object in memory");
let mut cvs = Vec::new();
if !(c0 == 0 && c1 == l) {
let lt = left_len(l);
collect_cvs(&x, 0, lt, c0, c1, &mut cvs);
collect_cvs(&x, lt, l - lt, c0, c1, &mut cvs);
}
let bytes = x[c0.max(DOMAIN_LEN) as usize..c1 as usize].to_vec();
RangeProof { content_hash: content_hash(envelope), envelope_len: envelope.len() as u64, start, end, cvs, bytes }
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut o = Vec::with_capacity(RANGE_FIXED + 32 * self.cvs.len() + self.bytes.len());
o.extend_from_slice(&RANGE_MAGIC);
o.push(VERSION);
o.extend_from_slice(&self.content_hash);
o.extend_from_slice(&self.envelope_len.to_le_bytes());
o.extend_from_slice(&self.start.to_le_bytes());
o.extend_from_slice(&self.end.to_le_bytes());
o.push(u8::try_from(self.cvs.len()).expect("at most 255 chaining values"));
for cv in &self.cvs {
o.extend_from_slice(cv);
}
o.extend_from_slice(&self.bytes);
o
}
/// §5.3: read a range proof.
pub fn read(b: &[u8]) -> Result<RangeProof, Reason> {
read_head(b, &RANGE_MAGIC)?;
if b.len() < RANGE_FIXED {
return Err(TRUNCATED);
}
let mut r = Rd { b, p: 5 };
let content_hash = r.arr()?;
let (envelope_len, start, end) = (r.u64()?, r.u64()?, r.u64()?);
let n = r.u8()? as usize;
if start >= end {
return Err(Reason::RangeInvalid);
}
if end > envelope_len {
return Err(Reason::RangeOutsideObject);
}
let (c0, c1) = covered(envelope_len, start, end).ok_or(Reason::RangeOutsideObject)?;
let want = RANGE_FIXED as u128 + 32 * n as u128 + (c1 - c0.max(DOMAIN_LEN)) as u128;
if (b.len() as u128) < want {
return Err(TRUNCATED);
}
if (b.len() as u128) > want {
return Err(MALFORMED);
}
let cvs = (0..n).map(|_| r.arr()).collect::<Result<Vec<[u8; 32]>, Reason>>()?;
let bytes = b[r.p..].to_vec();
Ok(RangeProof { content_hash, envelope_len, start, end, cvs, bytes })
}
/// The covered chunks (§5.2) as offsets into `X`.
pub fn covered(&self) -> (u64, u64) {
covered(self.envelope_len, self.start, self.end).expect("checked when read")
}
/// §5.4: check this proof against the content hash `h`, and against the item it answers
/// an audit of (§5.6) where there is one.
pub fn verify(&self, h: &[u8; 32], audit: Option<&Item>) -> Result<RangeFacts, Reason> {
if self.content_hash != *h {
return Err(Reason::RangeProofMismatch);
}
if let Some(i) = audit
&& (i.content_hash != *h || self.start < i.start || self.end > i.end)
{
return Err(Reason::RangeNotAcknowledged);
}
let l = DOMAIN_LEN + self.envelope_len;
let (c0, c1) = self.covered();
let mut xc = Vec::with_capacity((c1 - c0) as usize);
if c0 == 0 {
xc.extend_from_slice(DOMAIN_OBJECT);
}
xc.extend_from_slice(&self.bytes);
let root = if c0 == 0 && c1 == l {
if !self.cvs.is_empty() {
return Err(Reason::RangeProofMismatch);
}
*blake3::hash(&xc).as_bytes()
} else {
let mut it = self.cvs.iter().copied();
let lt = left_len(l);
let left = node_cv(&xc, 0, lt, c0, c1, &mut it)?;
let right = node_cv(&xc, lt, l - lt, c0, c1, &mut it)?;
if it.next().is_some() {
return Err(Reason::RangeProofMismatch);
}
*merge_subtrees_root(&left, &right, Mode::Hash).as_bytes()
};
if root != *h {
return Err(Reason::RangeProofMismatch);
}
Ok(RangeFacts { start: self.start, end: self.end, envelope_len: self.envelope_len, covered: (c0, c1), length_established: c1 == l })
}
}
/// §5.3 then §5.4 over the bytes of a range proof.
pub fn check_range(b: &[u8], h: &[u8; 32], audit: Option<&Item>) -> Result<RangeFacts, Reason> {
RangeProof::read(b)?.verify(h, audit)
}
// ---------------------------------------------------------------------------------------
// §7 The settlement statement
// ---------------------------------------------------------------------------------------
/// A line (§7.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Line {
pub kind: u8,
pub payee: [u8; 32],
pub class: u8,
pub payer: [u8; 32],
pub objects: u64,
pub bytes: u64,
pub amount: u64,
}
impl Line {
fn encode(&self, o: &mut Vec<u8>) {
o.push(self.kind);
o.extend_from_slice(&self.payee);
o.push(self.class);
o.extend_from_slice(&self.payer);
o.extend_from_slice(&self.objects.to_le_bytes());
o.extend_from_slice(&self.bytes.to_le_bytes());
o.extend_from_slice(&self.amount.to_le_bytes());
}
fn read(r: &mut Rd) -> Result<Line, Reason> {
Ok(Line { kind: r.u8()?, payee: r.arr()?, class: r.u8()?, payer: r.arr()?, objects: r.u64()?, bytes: r.u64()?, amount: r.u64()? })
}
fn key(&self) -> (u8, [u8; 32], u8, [u8; 32]) {
(self.kind, self.payee, self.class, self.payer)
}
/// A delivery line of a retrieval class whose payer is zero (§7.2).
pub fn is_free(&self) -> bool {
self.kind == line_kind::DELIVERY && class::is_retrieval(self.class) && self.payer == [0; 32]
}
/// An attribution line (§7.8).
pub fn is_attribution(&self) -> bool {
self.kind == line_kind::ATTRIBUTION
}
}
/// A tariff (§7.1): a currency, and a price per object class in millionths of a minor unit
/// per 10^9 bytes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Tariff {
pub currency: [u8; 3],
pub prices: [u64; CLASSES],
}
impl Tariff {
/// No tariff.
pub fn none() -> Tariff {
Tariff { currency: [0; 3], prices: [0; CLASSES] }
}
/// The amount of `bytes` of class `c` (§7.3), in integers wide enough.
pub fn amount(&self, c: u8, bytes: u128) -> u128 {
bytes * self.prices[(c - 1) as usize] as u128 / PRICE_BYTES
}
}
/// A settlement statement (§7.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Statement {
pub party: [u8; 32],
pub start: u64,
pub end: u64,
pub records: Vec<[u8; 32]>,
pub hops: Vec<[u8; 32]>,
pub tariff: Tariff,
/// Acknowledged (objects, bytes) per object class.
pub classes: [(u64, u64); CLASSES],
/// Declared: (declarations, objects, bytes).
pub unacknowledged: (u32, u64, u64),
pub unmetered: u16,
pub totals: Vec<Total>,
pub lines: Vec<Line>,
pub amount: u64,
pub sig: [u8; 64],
}
fn read_list(r: &mut Rd) -> Result<Vec<[u8; 32]>, Reason> {
let n = r.u16()? as usize;
(0..n).map(|_| r.arr()).collect()
}
fn put_list(o: &mut Vec<u8>, l: &[[u8; 32]]) {
o.extend_from_slice(&u16::try_from(l.len()).expect("at most 65535").to_le_bytes());
for d in l {
o.extend_from_slice(d);
}
}
impl Statement {
/// The statement without `sig`.
pub fn body(&self) -> Vec<u8> {
let mut o = Vec::new();
o.extend_from_slice(&STATEMENT_MAGIC);
o.push(VERSION);
o.extend_from_slice(&self.party);
o.extend_from_slice(&self.start.to_le_bytes());
o.extend_from_slice(&self.end.to_le_bytes());
put_list(&mut o, &self.records);
put_list(&mut o, &self.hops);
o.extend_from_slice(&self.tariff.currency);
for p in self.tariff.prices {
o.extend_from_slice(&p.to_le_bytes());
}
for (n, b) in self.classes {
o.extend_from_slice(&n.to_le_bytes());
o.extend_from_slice(&b.to_le_bytes());
}
o.extend_from_slice(&self.unacknowledged.0.to_le_bytes());
o.extend_from_slice(&self.unacknowledged.1.to_le_bytes());
o.extend_from_slice(&self.unacknowledged.2.to_le_bytes());
o.extend_from_slice(&self.unmetered.to_le_bytes());
o.push(u8::try_from(self.totals.len()).expect("at most 255 totals"));
for t in &self.totals {
o.push(t.tag);
o.extend_from_slice(&t.figures.to_le_bytes());
o.extend_from_slice(&t.joules.to_le_bytes());
}
o.extend_from_slice(&u16::try_from(self.lines.len()).expect("at most 65535 lines").to_le_bytes());
for l in &self.lines {
l.encode(&mut o);
}
o.extend_from_slice(&self.amount.to_le_bytes());
o
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut o = self.body();
o.extend_from_slice(&self.sig);
o
}
/// Its digest (§7.4).
pub fn digest(&self) -> [u8; 32] {
sha256(&[&self.body()])
}
/// Signed with `key`; `party` is left as it is.
pub fn sign(mut self, key: &SigningKey) -> Statement {
self.sig = sign(key, STATEMENT_DOMAIN, &self.body());
self
}
/// §7.5: read a statement, hold it to its rules, then its signature.
pub fn parse(b: &[u8]) -> Result<Statement, Reason> {
read_head(b, &STATEMENT_MAGIC)?;
let mut r = Rd { b, p: 5 };
let party = r.arr()?;
let (start, end) = (r.u64()?, r.u64()?);
let records = read_list(&mut r)?;
let hops = read_list(&mut r)?;
let currency = r.arr()?;
let mut prices = [0u64; CLASSES];
for p in &mut prices {
*p = r.u64()?;
}
let mut classes = [(0u64, 0u64); CLASSES];
for c in &mut classes {
*c = (r.u64()?, r.u64()?);
}
let unacknowledged = (r.u32()?, r.u64()?, r.u64()?);
let unmetered = r.u16()?;
let m = r.u8()? as usize;
let mut totals = Vec::with_capacity(m);
for _ in 0..m {
totals.push(Total { tag: r.u8()?, figures: r.u32()?, joules: r.u64()? });
}
let p = r.u16()? as usize;
let mut lines = Vec::with_capacity(p.min(b.len() / LINE_LEN));
for _ in 0..p {
lines.push(Line::read(&mut r)?);
}
let amount = r.u64()?;
let sig = r.arr()?;
if !r.done() {
return Err(MALFORMED);
}
let s = Statement { party, start, end, records, hops, tariff: Tariff { currency, prices }, classes, unacknowledged, unmetered, totals, lines, amount, sig };
if s.totals.iter().any(|t| t.tag == 0) {
return Err(CLASSES_SUMMED);
}
let ascending = |l: &[[u8; 32]]| l.windows(2).all(|w| w[0] < w[1]);
let tariff_ok = if s.tariff.currency == [0; 3] { s.tariff.prices.iter().all(|&p| p == 0) } else { s.tariff.currency.iter().all(u8::is_ascii_uppercase) };
let rules = s.start < s.end
&& !s.records.is_empty()
&& ascending(&s.records)
&& ascending(&s.hops)
&& tariff_ok
&& s.totals.iter().all(|t| (2..=5).contains(&t.tag) && t.figures > 0)
&& s.totals.windows(2).all(|w| w[0].tag < w[1].tag)
&& s.lines.iter().all(|l| {
let kind_ok = match l.kind {
line_kind::DELIVERY => l.payee == s.party,
line_kind::ATTRIBUTION => l.payee != [0; 32] && l.payer != [0; 32],
_ => false,
};
kind_ok && (1..=CLASSES as u8).contains(&l.class) && l.objects > 0
})
&& s.lines.windows(2).all(|w| w[0].key() < w[1].key());
if !rules {
return Err(Reason::StatementInvalid);
}
verify_sig(&s.party, STATEMENT_DOMAIN, &b[..b.len() - 64], &s.sig)?;
Ok(s)
}
}
/// A basis an attribution line is paid from (§7.8; influence §6.1), as the verifier of the
/// attribution accepted it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Basis {
/// The output's content hash.
pub output: [u8; 32],
/// The whole, in units.
pub whole: u128,
/// The payable buckets.
pub payees: Vec<BasisPayee>,
}
/// A payable bucket of a basis: a key, its units and its tariff (§7.8).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BasisPayee {
pub key: [u8; 32],
pub units: u128,
/// Millionths of a minor unit of the statement's currency per delivery.
pub per_use: u64,
/// Parts per million of the revenue.
pub share_ppm: u64,
}
/// Attribution lines per (payee, class, basis): (objects, bytes, amount) (§7.8).
pub type AttributionLines = BTreeMap<([u8; 32], u8, [u8; 32]), (u128, u128, u128)>;
/// A basis's output's deliveries per (basis, class): (deliveries, bytes) (§7.8).
pub type Deliveries = BTreeMap<([u8; 32], u8), (u128, u128)>;
/// The figures of §7.6, recomputed, in integers wide enough not to overflow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Figures {
/// Per (class, payer): (objects, bytes, amount).
pub lines: BTreeMap<(u8, [u8; 32]), (u128, u128, u128)>,
/// The attribution lines (§7.8).
pub attribution: AttributionLines,
/// The deliveries of each basis's output (§7.8).
pub deliveries: Deliveries,
pub classes: [(u128, u128); CLASSES],
pub unacknowledged: (u128, u128, u128),
pub unmetered: u128,
/// Per tag: (figures, joules).
pub totals: BTreeMap<u8, (u128, u128)>,
pub amount: u128,
}
/// §7.6, §7.7: the figures of a statement whose records are `acks` and whose hop records are
/// `hops`, under `tariff`.
pub fn figures(acks: &[Ack], hops: &[HopRecord], tariff: &Tariff) -> Figures {
let mut lines: BTreeMap<(u8, [u8; 32]), (u128, u128, u128)> = BTreeMap::new();
let mut unack = (0u128, 0u128, 0u128);
for a in acks {
if a.mode == mode::DECLARATION {
unack.0 += 1;
for e in &a.entries {
unack.1 += e.objects as u128;
unack.2 += e.bytes as u128;
}
continue;
}
for e in &a.entries {
let payer = if class::is_retrieval(e.class) { e.grant } else { a.downstream };
let l = lines.entry((e.class, payer)).or_default();
l.0 += e.objects as u128;
l.1 += e.bytes as u128;
}
}
let mut classes = [(0u128, 0u128); CLASSES];
let mut amount = 0u128;
for ((c, payer), l) in lines.iter_mut() {
let free = class::is_retrieval(*c) && *payer == [0; 32];
l.2 = if free { 0 } else { tariff.amount(*c, l.1) };
classes[(*c - 1) as usize].0 += l.0;
classes[(*c - 1) as usize].1 += l.1;
amount += l.2;
}
let mut totals: BTreeMap<u8, (u128, u128)> = BTreeMap::new();
let mut unmetered = 0u128;
for h in hops {
let tag = metering::tag_of(h.class.as_ref());
if tag == 0 {
unmetered += 1;
} else {
let t = totals.entry(tag).or_default();
t.0 += 1;
t.1 += h.joules as u128;
}
}
Figures { lines, attribution: BTreeMap::new(), deliveries: BTreeMap::new(), classes, unacknowledged: unack, unmetered, totals, amount }
}
/// ⌊R × s × u / (10^6 × T)⌋ with u ≤ T, exactly: with y = R × s = a × d + r, d = 10^6 × T,
/// it is a × u + ⌊r × u / d⌋, and a × u ≤ y / 10^6 and r × u < d × T hold in a u128 for every
/// figure §7.8 admits.
pub fn share_of(revenue: u128, share_ppm: u64, units: u128, whole: u128) -> u128 {
let y = revenue * share_ppm as u128;
let d = 1_000_000 * whole;
let (a, r) = (y / d, y % d);
a * units + r * units / d
}
/// §7.8: the attribution lines of the bases `bases` (each a digest and the basis), over the
/// acknowledgements `acks` (mode 1 counts; declarations do not) whose entries' items are `items`,
/// under `tariff`; and each basis's deliveries per class.
pub fn attribution_lines(acks: &[Ack], items: &[Vec<Vec<Item>>], tariff: &Tariff, bases: &[([u8; 32], &Basis)]) -> (AttributionLines, Deliveries) {
let mut lines = BTreeMap::new();
let mut deliveries = BTreeMap::new();
for (digest, b) in bases {
for c in 1..=CLASSES as u8 {
let (mut d, mut y, mut paid) = (0u128, 0u128, 0u128);
for (a, lists) in acks.iter().zip(items) {
if a.mode != mode::ACK {
continue;
}
let mut held = false;
for (e, list) in a.entries.iter().zip(lists) {
if e.class != c {
continue;
}
let free = class::is_retrieval(c) && e.grant == [0; 32];
for it in list.iter().filter(|it| it.content_hash == b.output) {
held = true;
y += it.bytes() as u128;
if !free {
paid += it.bytes() as u128;
}
}
}
d += held as u128;
}
if d == 0 {
continue;
}
deliveries.insert((*digest, c), (d, y));
let revenue = paid * tariff.prices[(c - 1) as usize] as u128 / PRICE_BYTES;
for p in &b.payees {
let l = lines.entry((p.key, c, *digest)).or_insert((d, y, 0u128));
l.2 += p.per_use as u128 * d + share_of(revenue, p.share_ppm, p.units, b.whole);
}
}
}
(lines, deliveries)
}
/// The revenue of a basis's output in class `c` (§7.8): ⌊paid bytes × price / 10^9⌋.
pub fn revenue(acks: &[Ack], items: &[Vec<Vec<Item>>], tariff: &Tariff, output: &[u8; 32], c: u8) -> u128 {
let mut paid = 0u128;
for (a, lists) in acks.iter().zip(items) {
if a.mode != mode::ACK {
continue;
}
for (e, list) in a.entries.iter().zip(lists) {
if e.class == c && !(class::is_retrieval(c) && e.grant == [0; 32]) {
paid += list.iter().filter(|it| it.content_hash == *output).map(|it| it.bytes() as u128).sum::<u128>();
}
}
}
paid * tariff.prices[(c - 1) as usize] as u128 / PRICE_BYTES
}
/// The payers of a statement's attribution lines, in the order the lines first name them
/// (§8.2 step 6).
pub fn basis_order(s: &Statement) -> Vec<[u8; 32]> {
let mut out: Vec<[u8; 32]> = Vec::new();
for l in s.lines.iter().filter(|l| l.is_attribution()) {
if !out.contains(&l.payer) {
out.push(l.payer);
}
}
out
}
/// Add the attribution lines of `bases` to a statement computed by [`compute`] (§7.8), over its
/// records and their item lists; unsigned. Panics where a figure does not fit its field.
pub fn with_attribution(mut s: Statement, records: &[(Ack, Vec<Vec<Item>>)], bases: &[([u8; 32], &Basis)]) -> Statement {
let acks: Vec<Ack> = records.iter().map(|(a, _)| a.clone()).collect();
let items: Vec<Vec<Vec<Item>>> = records.iter().map(|(_, i)| i.clone()).collect();
let (lines, _) = attribution_lines(&acks, &items, &s.tariff, bases);
let u = |x: u128| u64::try_from(x).expect("a figure that fits");
s.lines.retain(|l| !l.is_attribution());
for ((payee, c, basis), (n, b, a)) in lines {
s.lines.push(Line { kind: line_kind::ATTRIBUTION, payee, class: c, payer: basis, objects: u(n), bytes: u(b), amount: u(a) });
}
s.lines.sort_by_key(Line::key);
s
}
/// The statement of `party` for the period, settling `records` (each with its digest) and
/// `hops`, under `tariff`; unsigned. Panics where a figure does not fit its field.
pub fn compute(party: [u8; 32], start: u64, end: u64, records: &[([u8; 32], Ack)], hops: &[([u8; 32], HopRecord)], tariff: Tariff) -> Statement {
let acks: Vec<Ack> = records.iter().map(|(_, a)| a.clone()).collect();
let hrs: Vec<HopRecord> = hops.iter().map(|(_, h)| h.clone()).collect();
let f = figures(&acks, &hrs, &tariff);
let u = |x: u128| u64::try_from(x).expect("a figure that fits");
let mut rec: Vec<[u8; 32]> = records.iter().map(|(d, _)| *d).collect();
rec.sort();
let mut hd: Vec<[u8; 32]> = hops.iter().map(|(d, _)| *d).collect();
hd.sort();
let mut lines: Vec<Line> = f
.lines
.iter()
.map(|((c, payer), (n, b, a))| Line { kind: line_kind::DELIVERY, payee: party, class: *c, payer: *payer, objects: u(*n), bytes: u(*b), amount: u(*a) })
.collect();
lines.sort_by_key(Line::key);
Statement {
party,
start,
end,
records: rec,
hops: hd,
tariff,
classes: f.classes.map(|(n, b)| (u(n), u(b))),
unacknowledged: (u32::try_from(f.unacknowledged.0).expect("fits"), u(f.unacknowledged.1), u(f.unacknowledged.2)),
unmetered: u16::try_from(f.unmetered).expect("fits"),
totals: f.totals.iter().map(|(&tag, &(n, j))| Total { tag, figures: u32::try_from(n).expect("fits"), joules: u(j) }).collect(),
lines,
amount: u(f.amount),
sig: [0; 64],
}
}
/// §7.6: compare a statement with its recomputed figures, in the order of that section.
pub fn check_figures(s: &Statement, f: &Figures) -> Result<(), Reason> {
let delivery: Vec<&Line> = s.lines.iter().filter(|l| !l.is_attribution()).collect();
let attribution: Vec<&Line> = s.lines.iter().filter(|l| l.is_attribution()).collect();
// 1. A free line bills nothing.
if s.lines.iter().any(|l| l.is_free() && l.amount != 0) {
return Err(Reason::PaidWithoutGrant);
}
// 2. No line bills what no acknowledgement covers.
for l in &delivery {
match f.lines.get(&(l.class, l.payer)) {
None => return Err(Reason::BytesNotAcknowledged),
Some(&(n, b, _)) if l.objects as u128 > n || l.bytes as u128 > b => return Err(Reason::BytesNotAcknowledged),
_ => {}
}
}
for l in &attribution {
match f.deliveries.get(&(l.payer, l.class)) {
None => return Err(Reason::BytesNotAcknowledged),
Some(&(n, b)) if l.objects as u128 > n || l.bytes as u128 > b => return Err(Reason::BytesNotAcknowledged),
_ => {}
}
}
// 3. A total summed across classes.
let classes: Vec<u8> = f.totals.keys().copied().collect();
for t in &s.totals {
if let Some(&(_, sum)) = f.totals.get(&t.tag)
&& t.joules as u128 != sum
{
let others: Vec<u8> = classes.iter().copied().filter(|&x| x != t.tag).collect();
let summed = (1u32..(1 << others.len())).any(|mask| {
let extra: u128 = others.iter().enumerate().filter(|(i, _)| mask & (1 << i) != 0).map(|(_, x)| f.totals[x].1).sum();
extra + sum == t.joules as u128
});
if summed {
return Err(CLASSES_SUMMED);
}
}
}
// 4. Anything else.
let same_lines = delivery.len() == f.lines.len()
&& delivery.iter().all(|l| f.lines.get(&(l.class, l.payer)) == Some(&(l.objects as u128, l.bytes as u128, l.amount as u128)))
&& attribution.len() == f.attribution.len()
&& attribution.iter().all(|l| f.attribution.get(&(l.payee, l.class, l.payer)) == Some(&(l.objects as u128, l.bytes as u128, l.amount as u128)));
let same_classes = s.classes.iter().zip(f.classes.iter()).all(|(a, b)| (a.0 as u128, a.1 as u128) == *b);
let u = s.unacknowledged;
let same_unack = (u.0 as u128, u.1 as u128, u.2 as u128) == f.unacknowledged;
let same_totals = s.totals.len() == f.totals.len() && s.totals.iter().all(|t| f.totals.get(&t.tag) == Some(&(t.figures as u128, t.joules as u128)));
if !(same_lines && same_classes && same_unack && s.unmetered as u128 == f.unmetered && same_totals && s.amount as u128 == f.amount) {
return Err(Reason::StatementMismatch);
}
Ok(())
}
// ---------------------------------------------------------------------------------------
// §8 Verification
// ---------------------------------------------------------------------------------------
/// A room a verifier is given (§8.1): its descriptor and its grants in order.
#[derive(Clone, Debug, Default)]
pub struct RoomInput {
pub descriptor: Vec<u8>,
pub grants: Vec<Vec<u8>>,
}
/// What a statement verifier is given (§8.1).
#[derive(Clone, Debug, Default)]
pub struct Inputs {
/// Records by digest.
pub records: BTreeMap<[u8; 32], Vec<u8>>,
/// Item lists by record digest, one per entry.
pub items: BTreeMap<[u8; 32], Vec<Vec<u8>>>,
/// Proof records by SHA-256.
pub proofs: BTreeMap<[u8; 32], Vec<u8>>,
/// Per-hop records by digest.
pub hops: BTreeMap<[u8; 32], Vec<u8>>,
pub rooms: Vec<RoomInput>,
/// The bases its attribution lines name, by digest, each verified (§8.1; influence §6.2).
pub bases: BTreeMap<[u8; 32], Basis>,
}
/// An accepted statement (§8.3).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Accepted {
pub digest: [u8; 32],
pub statement: Statement,
/// `whole`, `partial` or `none` (§7.7).
pub coverage: &'static str,
}
/// A refusal: the reason, and where (§8.2), as a JSON array.
pub type Refusal = (Reason, Value);
/// A room as a verifier holds it.
struct HeldRoom {
descriptor: Descriptor,
id: [u8; 32],
grants: Vec<HeldGrant>,
}
fn grant_check(a: &Ack, e: &Entry, rooms: &[HeldRoom], proofs: &BTreeMap<[u8; 32], Vec<u8>>) -> Result<(), Reason> {
// §6.3 step 1.
let g = rooms.iter().flat_map(|r| r.grants.iter()).find(|h| h.digest == e.grant).ok_or(RECORD_MISSING)?;
// Step 2: good throughout the window.
let (nbf, exp) = (g.grant.nbf as u128 * 1_000_000, g.grant.exp as u128 * 1_000_000);
if !(nbf <= a.start as u128 && a.end as u128 <= exp) {
return Err(rm(room::Reason::OutsideGrantWindow));
}
if !class::is_retrieval(e.class) {
// Step 3.
if g.grant.principal != a.downstream {
return Err(rm(room::Reason::GrantWrongPrincipal));
}
return Ok(());
}
// Step 4: live-rooms §10.5.4 steps 2 to 5.
let pb = proofs.get(&e.proof).ok_or(RECORD_MISSING)?;
let p = Proof::read(pb).map_err(|_| rm(room::Reason::PopMalformed))?;
if p.grant != g.digest {
return Err(rm(room::Reason::PopWrongGrant));
}
if p.key != g.grant.principal {
return Err(rm(room::Reason::PopWrongKey));
}
let mut m = room::PROOF_DOMAIN.to_vec();
m.extend_from_slice(&p.body());
crate::sig::verify_strict(&p.key, &m, &p.sig).map_err(|_| rm(room::Reason::PopBadSignature))?;
// Step 5.
if e.class == class::FETCH && g.grant.caps.powers & room::power::SEEK == 0 {
return Err(rm(room::Reason::SeekNotGranted));
}
Ok(())
}
fn overlaps(a: (u64, u64), b: (u64, u64)) -> bool {
a.0 < b.1 && b.0 < a.1
}
/// §8.2: verify a statement against its inputs.
pub fn verify(statement: &[u8], i: &Inputs) -> Result<Accepted, Refusal> {
// Step 1: the statement.
let s = Statement::parse(statement).map_err(|r| (r, json!(["statement"])))?;
// Step 2: the rooms.
let mut rooms: Vec<HeldRoom> = Vec::new();
for ri in &i.rooms {
let d = Descriptor::parse(&ri.descriptor).map_err(|r| (Reason::from(r), json!(["room", hex(&given_digest(&ri.descriptor))])))?;
let id = d.room_id();
let grants = room::check_grants(&d, &id, &ri.grants).map_err(|(k, r)| (Reason::from(r), json!(["room", hex(&id), "grant", k])))?;
rooms.push(HeldRoom { descriptor: d, id, grants });
}
// Step 3: each record.
let mut acks: Vec<Ack> = Vec::with_capacity(s.records.len());
let mut item_lists: Vec<Vec<Vec<Item>>> = Vec::with_capacity(s.records.len());
for (k, d) in s.records.iter().enumerate() {
let at = || json!(["record", k]);
let b = i.records.get(d).ok_or((RECORD_MISSING, at()))?;
let a = Ack::parse(b).map_err(|r| (r, at()))?;
if a.upstream != s.party {
return Err((Reason::AckWrongParty, at()));
}
if !(s.start <= a.start && a.end <= s.end) {
return Err((Reason::AckOutsidePeriod, at()));
}
let lists = i.items.get(d);
let mut read = Vec::with_capacity(a.entries.len());
for (j, e) in a.entries.iter().enumerate() {
let at = || json!(["record", k, "entry", j]);
let list = lists.and_then(|l| l.get(j)).ok_or((RECORD_MISSING, at()))?;
let items = read_items(list).map_err(|r| (r, at()))?;
if !e.recomputes(&items) {
return Err((Reason::ItemsMismatch, at()));
}
if e.grant != [0; 32] {
grant_check(&a, e, &rooms, &i.proofs).map_err(|r| (r, at()))?;
}
read.push(items);
}
acks.push(a);
item_lists.push(read);
}
// Step 4: the chains.
let digests = &s.records;
let mut chains: BTreeMap<(u8, [u8; 32], [u8; 32]), Vec<usize>> = BTreeMap::new();
for (k, a) in acks.iter().enumerate() {
chains.entry((a.mode, a.upstream, a.downstream)).or_default().push(k);
}
let mut before: BTreeMap<usize, usize> = BTreeMap::new();
for ks in chains.values_mut() {
ks.sort_by_key(|&k| (acks[k].start, k));
for w in ks.windows(2) {
before.insert(w[1], w[0]);
}
}
for (k, a) in acks.iter().enumerate() {
if let Some(&p) = before.get(&k) {
if acks[p].end > a.start {
return Err((Reason::AckOverlap, json!(["record", k])));
}
if a.prev != digests[p] {
return Err((Reason::AckChainBroken, json!(["record", k])));
}
}
}
let all_items = |k: usize| -> Vec<Item> { item_lists[k].iter().flatten().copied().collect() };
for (k, a) in acks.iter().enumerate() {
if a.mode != mode::DECLARATION || a.downstream == [0; 32] {
continue;
}
let declared = all_items(k);
for (q, b) in acks.iter().enumerate() {
if b.mode != mode::ACK || b.downstream != a.downstream || !overlaps((a.start, a.end), (b.start, b.end)) {
continue;
}
let acked = all_items(q);
if declared.iter().any(|x| acked.iter().any(|y| x.content_hash == y.content_hash && overlaps((x.start, x.end), (y.start, y.end)))) {
return Err((Reason::DeclaredAndAcknowledged, json!(["record", k])));
}
}
}
let mut named: BTreeMap<[u8; 32], usize> = BTreeMap::new();
for (k, a) in acks.iter().enumerate() {
for (j, e) in a.entries.iter().enumerate() {
if e.proof == [0; 32] {
continue;
}
match named.get(&e.proof) {
Some(&q) if q != k => return Err((rm(room::Reason::PopReplayed), json!(["record", k, "entry", j]))),
Some(_) => {}
None => {
named.insert(e.proof, k);
}
}
}
}
// Step 5: each hop record.
let mut hops: Vec<HopRecord> = Vec::with_capacity(s.hops.len());
for (k, d) in s.hops.iter().enumerate() {
let at = || json!(["hop", k]);
let b = i.hops.get(d).ok_or((RECORD_MISSING, at()))?;
let named_room: [u8; 32] = b.get(5..37).and_then(|x| x.try_into().ok()).unwrap_or([0; 32]);
let h = HopRecord::parse(b, &named_room).map_err(|r| (Reason::from(r), at()))?;
let r = rooms.iter().find(|r| r.id == h.room).ok_or((RECORD_MISSING, at()))?;
if h.hop != s.party {
return Err((Reason::HopNotParty, at()));
}
let t0 = r.descriptor.t0_us as u128;
if !(s.start as u128 <= t0 + h.start as u128 && t0 + h.end as u128 <= s.end as u128) {
return Err((Reason::SpanOutsidePeriod, at()));
}
hops.push(h);
}
// Step 6: the bases.
let mut bases: Vec<([u8; 32], &Basis)> = Vec::new();
for (k, d) in basis_order(&s).iter().enumerate() {
let b = i.bases.get(d).ok_or((RECORD_MISSING, json!(["basis", k])))?;
bases.push((*d, b));
}
// Step 7: the figures.
let mut f = figures(&acks, &hops, &s.tariff);
(f.attribution, f.deliveries) = attribution_lines(&acks, &item_lists, &s.tariff, &bases);
check_figures(&s, &f).map_err(|r| (r, json!(["statement"])))?;
let coverage = if hops.is_empty() {
"none"
} else if s.unmetered == 0 {
"whole"
} else {
"partial"
};
Ok(Accepted { digest: s.digest(), statement: s, coverage })
}
/// The JSON facts of an accepted statement (§8.3), as the corpus states them.
pub fn accepted_json(a: &Accepted) -> Value {
let s = &a.statement;
let currency = if s.tariff.currency == [0; 3] { Value::Null } else { Value::String(String::from_utf8(s.tariff.currency.to_vec()).expect("capitals")) };
let classes: serde_json::Map<String, Value> =
(1..=CLASSES as u8).map(|c| (class::name(c).to_string(), json!({"objects": s.classes[(c - 1) as usize].0, "bytes": s.classes[(c - 1) as usize].1}))).collect();
let lines: Vec<Value> = s
.lines
.iter()
.filter(|l| !l.is_attribution())
.map(|l| json!({"class": class::name(l.class), "payer": hex(&l.payer), "objects": l.objects, "bytes": l.bytes, "amount": l.amount, "free": l.is_free()}))
.collect();
let attribution: Vec<Value> = s
.lines
.iter()
.filter(|l| l.is_attribution())
.map(|l| json!({"payee": hex(&l.payee), "class": class::name(l.class), "basis": hex(&l.payer), "objects": l.objects, "bytes": l.bytes, "amount": l.amount}))
.collect();
let totals: Vec<Value> = s.totals.iter().map(|t| json!({"class": metering::tag_label(t.tag), "figures": t.figures, "joules": t.joules})).collect();
let mut out = json!({
"outcome": "accept",
"digest": hex(&a.digest),
"currency": currency,
"classes": classes,
"lines": lines,
"unacknowledged": {"declarations": s.unacknowledged.0, "objects": s.unacknowledged.1, "bytes": s.unacknowledged.2},
"coverage": a.coverage,
"unmetered": s.unmetered,
"totals": totals,
"amount": s.amount,
});
if !attribution.is_empty() {
out["attribution"] = Value::Array(attribution);
}
out
}
/// The JSON outcome of a statement's verification.
pub fn outcome_json(r: &Result<Accepted, Refusal>) -> Value {
match r {
Ok(a) => accepted_json(a),
Err((reason, at)) => json!({"outcome": "refuse", "reason": reason.code(), "at": at}),
}
}
/// The JSON outcome of a range proof's check (§8.3).
pub fn range_json(r: &Result<RangeFacts, Reason>) -> Value {
match r {
Ok(f) => json!({
"outcome": "accept",
"start": f.start,
"end": f.end,
"envelope_len": f.envelope_len,
"covered": [f.covered.0, f.covered.1],
"length_established": f.length_established,
}),
Err(reason) => json!({"outcome": "refuse", "reason": reason.code()}),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bytes(n: usize, seed: u8) -> Vec<u8> {
(0..n).map(|i| (i as u32).wrapping_mul(2_654_435_761).to_le_bytes()[i % 4] ^ seed).collect()
}
/// Every range of objects of many lengths, around every chunk boundary, proves and
/// verifies, and its root is the content hash BLAKE3 computes over the whole object.
#[test]
fn range_proofs_verify_against_the_content_hash() {
for n in [1usize, 15, 1008, 1009, 1010, 2033, 2034, 3000, 4096, 5121, 9000, 20_000] {
let env = bytes(n, 7);
let h = content_hash(&env);
let mut points: Vec<u64> = vec![0, 1, n as u64 / 2, n as u64];
for k in 0..=(n as u64 + 15) / CHUNK {
for d in [0i64, -1, 1] {
let p = (k * CHUNK) as i64 - 15 + d;
if p >= 0 && p <= n as i64 {
points.push(p as u64);
}
}
}
points.sort();
points.dedup();
for &s in &points {
for &e in &points {
if s >= e {
continue;
}
let p = RangeProof::build(&env, s, e);
let b = p.to_bytes();
let facts = check_range(&b, &h, None).unwrap_or_else(|r| panic!("n {n} [{s}, {e}): {r}"));
assert_eq!(facts.length_established, facts.covered.1 == 15 + n as u64);
assert_eq!(RangeProof::read(&b).unwrap(), p);
}
}
}
}
/// A changed byte, a dropped, extra or changed chaining value, and another object's
/// bytes each fail.
#[test]
fn a_tampered_range_proof_fails() {
let env = bytes(9_000, 3);
let other = bytes(9_000, 4);
let h = content_hash(&env);
let p = RangeProof::build(&env, 3_000, 4_500);
assert!(!p.cvs.is_empty());
let mut q = p.clone();
q.bytes[10] ^= 1;
assert_eq!(q.verify(&h, None), Err(Reason::RangeProofMismatch));
let mut q = p.clone();
q.cvs.pop();
assert_eq!(q.verify(&h, None), Err(Reason::RangeProofMismatch));
let mut q = p.clone();
q.cvs.push([0; 32]);
assert_eq!(q.verify(&h, None), Err(Reason::RangeProofMismatch));
let mut q = p.clone();
q.cvs[0][0] ^= 1;
assert_eq!(q.verify(&h, None), Err(Reason::RangeProofMismatch));
let mut q = p.clone();
q.bytes = RangeProof::build(&other, 3_000, 4_500).bytes;
assert_eq!(q.verify(&h, None), Err(Reason::RangeProofMismatch));
let mut b = p.to_bytes();
b.push(0);
assert_eq!(RangeProof::read(&b), Err(MALFORMED));
let b = p.to_bytes();
assert_eq!(RangeProof::read(&b[..b.len() - 1]), Err(TRUNCATED));
}
/// The tree root is jwp-receipts §2.2's: a single leaf is its own root, and an odd level
/// pairs its last node with itself.
#[test]
fn tree_root_is_the_group_tree() {
let h = |i: u8| [i; 32];
assert_eq!(tree_root(&[]), [0; 32]);
assert_eq!(tree_root(&[h(1)]), leaf(&h(1)));
let (a, b, c) = (leaf(&h(1)), leaf(&h(2)), leaf(&h(3)));
assert_eq!(tree_root(&[h(1), h(2), h(3)]), node(&node(&a, &b), &node(&c, &c)));
}
/// A record round-trips, a self-acknowledgement is refused, and a retrieval entry with a
/// grant and no proof breaks the rules.
#[test]
fn records_read_and_refuse() {
let k = SigningKey::from_bytes(&[7; 32]);
let up = SigningKey::from_bytes(&[8; 32]).verifying_key().to_bytes();
let items = [Item { content_hash: [1; 32], start: 0, end: 100 }];
let a = Ack { mode: mode::ACK, role: role::SINK, upstream: up, downstream: k.verifying_key().to_bytes(), start: 0, end: 10, prev: [0; 32], entries: vec![Entry::of(class::LIVE, [0; 32], [0; 32], &items)], sig: [0; 64] }.sign(&k);
assert_eq!(Ack::parse(&a.to_bytes()).unwrap(), a);
let me = Ack { upstream: k.verifying_key().to_bytes(), ..a.clone() }.sign(&k);
assert_eq!(Ack::parse(&me.to_bytes()), Err(Reason::SelfAcknowledged));
let bad = Ack { entries: vec![Entry::of(class::FETCH, [2; 32], [0; 32], &items)], ..a.clone() }.sign(&k);
assert_eq!(Ack::parse(&bad.to_bytes()), Err(Reason::AckInvalid));
assert_eq!(read_items(&item_list(&[items[0], Item { start: 50, end: 60, ..items[0] }])), Err(Reason::ItemsInvalid));
assert!(read_items(&item_list(&[items[0], Item { start: 100, end: 160, ..items[0] }])).is_ok());
}
/// An amount is rounded down once per line, and a free line bills nothing.
#[test]
fn amounts_round_down_per_line() {
let t = Tariff { currency: *b"USD", prices: [2_000_000, 5_000_000, 8_000_000, 1_000_000] };
assert_eq!(t.amount(class::LIVE, 1_234_567), 2_469);
assert_eq!(t.amount(class::FILL, 999), 7);
assert_eq!(Tariff::none().amount(class::FETCH, 1 << 40), 0);
}
}