//! 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 for Reason { fn from(r: metering::Reason) -> Self { Reason::Metering(r) } } impl From 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 { Ok(self.take(1)?[0]) } fn u16(&mut self) -> Result { Ok(u16::from_le_bytes(self.take(2)?.try_into().expect("2 bytes"))) } fn u32(&mut self) -> Result { Ok(u32::from_le_bytes(self.take(4)?.try_into().expect("4 bytes"))) } fn u64(&mut self) -> Result { Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("8 bytes"))) } fn arr(&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 { 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, Reason> { if !b.len().is_multiple_of(ITEM_LEN) { return Err(Reason::ItemsInvalid); } let items: Vec = b .as_chunks::() .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) { 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 { 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, pub sig: [u8; 64], } impl Ack { /// The record without `sig`. pub fn body(&self) -> Vec { 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 { 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 { 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, } /// 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) -> Result { 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 { 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 { 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::, 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 { 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 { 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) { 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 { 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, pub lines: Vec, pub amount: u64, pub sig: [u8; 64], } fn read_list(r: &mut Rd) -> Result, Reason> { let n = r.u16()? as usize; (0..n).map(|_| r.arr()).collect() } fn put_list(o: &mut Vec, 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 { 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 { 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 { 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, } /// 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, 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 = 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>], 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>], 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::(); } } } 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>)], bases: &[([u8; 32], &Basis)]) -> Statement { let acks: Vec = records.iter().map(|(a, _)| a.clone()).collect(); let items: Vec>> = 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 = records.iter().map(|(_, a)| a.clone()).collect(); let hrs: Vec = 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 = 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 = 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 = 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, pub grants: Vec>, } /// What a statement verifier is given (§8.1). #[derive(Clone, Debug, Default)] pub struct Inputs { /// Records by digest. pub records: BTreeMap<[u8; 32], Vec>, /// Item lists by record digest, one per entry. pub items: BTreeMap<[u8; 32], Vec>>, /// Proof records by SHA-256. pub proofs: BTreeMap<[u8; 32], Vec>, /// Per-hop records by digest. pub hops: BTreeMap<[u8; 32], Vec>, pub rooms: Vec, /// 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, } fn grant_check(a: &Ack, e: &Entry, rooms: &[HeldRoom], proofs: &BTreeMap<[u8; 32], Vec>) -> 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 { // Step 1: the statement. let s = Statement::parse(statement).map_err(|r| (r, json!(["statement"])))?; // Step 2: the rooms. let mut rooms: Vec = 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 = Vec::with_capacity(s.records.len()); let mut item_lists: Vec>> = 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> = 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 = 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_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 = 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 = (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 = 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 = 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 = 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) -> 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) -> 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 { (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 = 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); } }