ad-presentation (on main) registers WPC1 for a player's presentation claim, and co-performance registered WPC1 again for its commitment, so the registry's library test (wire_identifiers_are_in_their_documents) refuses the train: "WPC1 (magic) is listed twice". The commitment takes WPK1, unused anywhere: the extension's magic table and §7.5 layout, wai-rs's constant, the relay's floor reader, the independent verifier, the SPEC §5 paragraph, the changelog entry and two comments. The corpus is regenerated by its generator (only the commitments' bytes and what signs or digests them change) and the registry export with it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
2340 lines
87 KiB
Rust
2340 lines
87 KiB
Rust
//! Co-performance — `wai.live.coperformance` ([extensions/co-performance.md]).
|
||
//!
|
||
//! People and AI participants perform together in a room, and an audience joins in. A
|
||
//! participant announces an **intent** (`WPI1`): the material it means to play next, ahead of
|
||
//! the instant it plays, by digest or as a symbolic form (notes, a text line, a camera move);
|
||
//! a pinned rule compares what played with what was announced, and a room key's **fidelity
|
||
//! statement** (`WPF1`) states per participant how many intents it kept, which this module
|
||
//! recomputes. A **turn schedule** (`WPT1`) gives the floor in turns of microseconds or of
|
||
//! bars at a tempo, solo or as call-and-response pairs; a **handover** (`WPH1`) passes a turn;
|
||
//! an object out of turn is refused or marked (`WPM1`) as the schedule says, and an object
|
||
//! later than its grant's latency budget is marked. An audience member whose grant carries
|
||
//! `audience_input` sends **inputs** (`WPA1`) to a **window** (`WPW1`); the clock **commits**
|
||
//! (`WPK1`) to the eligible inputs once it closes, and a **selection** (`WPS1`) recomputes
|
||
//! from the commitment under the window's rule, with a credit for every input it picks. The
|
||
//! room authority signs the room's **co-performance policy** (`WPP1`).
|
||
//!
|
||
//! What this module implements, by section of the extension:
|
||
//!
|
||
//! - §3: the policy, its rules and its chain;
|
||
//! - §4: the statements, who signs each, and the two grant attributes;
|
||
//! - §5: intents, what played, the pinned rules and the fidelity statement;
|
||
//! - §6: schedules, turns and pairs, handovers, the floor, latency budgets and marks;
|
||
//! - §7: windows, inputs, moderation of inputs, the commitment, the three rules, the
|
||
//! selection and its credits;
|
||
//! - §9: verification over a room the live-rooms verifier replays.
|
||
//!
|
||
//! The corpus `live-conformance/coperformance/` is written by [`corpus::generate`] and checked
|
||
//! by [`corpus::verify`], and by `live-conformance/co_performance_verify.py`, which shares no
|
||
//! code with this module.
|
||
//!
|
||
//! [extensions/co-performance.md]: ../../extensions/co-performance.md
|
||
|
||
use std::collections::{BTreeMap, BTreeSet};
|
||
|
||
use ed25519_dalek::{Signer, SigningKey};
|
||
use sha2::{Digest, Sha256};
|
||
|
||
use crate::live_events::{self as events, kind, Event, Payload};
|
||
use crate::live_room::{self as room, power, Descriptor, Detail};
|
||
|
||
pub mod corpus;
|
||
|
||
/// The capability string this extension registers.
|
||
pub const CAPABILITY: &str = "wai.live.coperformance";
|
||
/// The version of every record this revision defines.
|
||
pub const RECORD_VERSION: u8 = 1;
|
||
/// The magic of the co-performance policy (§3.1).
|
||
pub const POLICY_MAGIC: [u8; 4] = *b"WPP1";
|
||
/// The magics of the statements (§4.1).
|
||
pub mod magic {
|
||
pub const INTENT: [u8; 4] = *b"WPI1";
|
||
pub const FIDELITY: [u8; 4] = *b"WPF1";
|
||
pub const SCHEDULE: [u8; 4] = *b"WPT1";
|
||
pub const HANDOVER: [u8; 4] = *b"WPH1";
|
||
pub const MARK: [u8; 4] = *b"WPM1";
|
||
pub const WINDOW: [u8; 4] = *b"WPW1";
|
||
pub const INPUT: [u8; 4] = *b"WPA1";
|
||
pub const COMMIT: [u8; 4] = *b"WPK1";
|
||
pub const SELECTION: [u8; 4] = *b"WPS1";
|
||
/// Every statement magic, in §4.1's order.
|
||
pub const ALL: [[u8; 4]; 9] = [INTENT, FIDELITY, SCHEDULE, HANDOVER, MARK, WINDOW, INPUT, COMMIT, SELECTION];
|
||
}
|
||
/// The domain a policy's signature is made under (§3.2).
|
||
pub const POLICY_DOMAIN: &[u8] = b"wai:co-performance-policy\x01";
|
||
/// The domain of a draw's seed commitment (§7.2).
|
||
pub const SEED_DOMAIN: &[u8] = b"wai:co-performance-seed\x01";
|
||
/// The domain of a draw's order (§7.6).
|
||
pub const DRAW_DOMAIN: &[u8] = b"wai:co-performance-draw\x01";
|
||
/// The domain of a commitment's root (§7.5).
|
||
pub const COMMIT_DOMAIN: &[u8] = b"wai:co-performance-commit\x01";
|
||
/// The domain of a direct credit's voters (§7.8).
|
||
pub const VOTERS_DOMAIN: &[u8] = b"wai:co-performance-voters\x01";
|
||
/// The domain of a direct credit's option (§7.8).
|
||
pub const OPTION_DOMAIN: &[u8] = b"wai:co-performance-option\x01";
|
||
/// The grant attribute that carries the audience-input power (§4.4).
|
||
pub const AUDIENCE_ATTR: &str = "audience_input";
|
||
/// The grant attribute that carries the latency budget (§4.4).
|
||
pub const BUDGET_ATTR: &str = "latency_budget_ms";
|
||
/// The largest budget (§4.4).
|
||
pub const MAX_BUDGET_MS: u32 = 600_000;
|
||
|
||
/// An intent's form (§5.2).
|
||
pub mod form {
|
||
pub const OBJECT: u8 = 1;
|
||
pub const NOTES: u8 = 2;
|
||
pub const TEXT: u8 = 3;
|
||
pub const CAMERA: u8 = 4;
|
||
}
|
||
|
||
/// A schedule's mode (§6.5).
|
||
pub mod mode {
|
||
pub const REFUSE: u8 = 1;
|
||
pub const MARK: u8 = 2;
|
||
}
|
||
|
||
/// A mark's flags (§6.7).
|
||
pub mod flag {
|
||
pub const OUT_OF_TURN: u8 = 1;
|
||
pub const LATE: u8 = 2;
|
||
}
|
||
|
||
/// A window's rule (§7.6).
|
||
pub mod rule {
|
||
pub const PLURALITY: u8 = 1;
|
||
pub const REACTIONS: u8 = 2;
|
||
pub const DRAW: u8 = 3;
|
||
}
|
||
|
||
/// An input's type (§7.3), and its bit in a window's `types` (bit `type` − 1).
|
||
pub mod input_type {
|
||
pub const PROMPT: u8 = 1;
|
||
pub const SKETCH: u8 = 2;
|
||
pub const VOTE: u8 = 3;
|
||
pub const MOVEMENT: u8 = 4;
|
||
}
|
||
|
||
/// A credit's step (§7.8).
|
||
pub mod step {
|
||
pub const AUTHOR: u8 = 1;
|
||
pub const DIRECT: u8 = 2;
|
||
}
|
||
|
||
/// Why a policy, a grant, a statement or an object is refused (§10).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||
pub enum Reason {
|
||
/// A code of live-rooms §11, or through it of live-events §9.
|
||
Room(room::Reason),
|
||
CoPolicyMalformed,
|
||
CoPolicyUnlinked,
|
||
CoNoPolicy,
|
||
CoAttrMalformed,
|
||
CoStatementMalformed,
|
||
CoNotPermitted,
|
||
IntentNotAhead,
|
||
IntentBeyondHorizon,
|
||
PayloadMissing,
|
||
FidelityMismatch,
|
||
ScheduleRetroactive,
|
||
ModeNotAllowed,
|
||
HandoverInvalid,
|
||
HandoverNotHolder,
|
||
TurnViolated,
|
||
TurnUnmarked,
|
||
LateUnflagged,
|
||
MarkUnfounded,
|
||
InputNotPermitted,
|
||
WindowUnknown,
|
||
InputTypeMismatch,
|
||
InputOverLimit,
|
||
StatementRepeated,
|
||
CommitEarly,
|
||
CommitMismatch,
|
||
SelectionUncommitted,
|
||
SeedMismatch,
|
||
InputLate,
|
||
SelectionMismatch,
|
||
InputUncredited,
|
||
CreditMismatch,
|
||
}
|
||
|
||
impl Reason {
|
||
/// The codes this extension adds, in §10's order.
|
||
pub const ALL: [Reason; 31] = [
|
||
Reason::CoPolicyMalformed,
|
||
Reason::CoPolicyUnlinked,
|
||
Reason::CoNoPolicy,
|
||
Reason::CoAttrMalformed,
|
||
Reason::CoStatementMalformed,
|
||
Reason::CoNotPermitted,
|
||
Reason::IntentNotAhead,
|
||
Reason::IntentBeyondHorizon,
|
||
Reason::PayloadMissing,
|
||
Reason::FidelityMismatch,
|
||
Reason::ScheduleRetroactive,
|
||
Reason::ModeNotAllowed,
|
||
Reason::HandoverInvalid,
|
||
Reason::HandoverNotHolder,
|
||
Reason::TurnViolated,
|
||
Reason::TurnUnmarked,
|
||
Reason::LateUnflagged,
|
||
Reason::MarkUnfounded,
|
||
Reason::InputNotPermitted,
|
||
Reason::WindowUnknown,
|
||
Reason::InputTypeMismatch,
|
||
Reason::InputOverLimit,
|
||
Reason::StatementRepeated,
|
||
Reason::CommitEarly,
|
||
Reason::CommitMismatch,
|
||
Reason::SelectionUncommitted,
|
||
Reason::SeedMismatch,
|
||
Reason::InputLate,
|
||
Reason::SelectionMismatch,
|
||
Reason::InputUncredited,
|
||
Reason::CreditMismatch,
|
||
];
|
||
|
||
/// The reason's code.
|
||
pub fn code(self) -> &'static str {
|
||
match self {
|
||
Reason::Room(r) => r.code(),
|
||
Reason::CoPolicyMalformed => "co_policy_malformed",
|
||
Reason::CoPolicyUnlinked => "co_policy_unlinked",
|
||
Reason::CoNoPolicy => "co_no_policy",
|
||
Reason::CoAttrMalformed => "co_attr_malformed",
|
||
Reason::CoStatementMalformed => "co_statement_malformed",
|
||
Reason::CoNotPermitted => "co_not_permitted",
|
||
Reason::IntentNotAhead => "intent_not_ahead",
|
||
Reason::IntentBeyondHorizon => "intent_beyond_horizon",
|
||
Reason::PayloadMissing => "payload_missing",
|
||
Reason::FidelityMismatch => "fidelity_mismatch",
|
||
Reason::ScheduleRetroactive => "schedule_retroactive",
|
||
Reason::ModeNotAllowed => "mode_not_allowed",
|
||
Reason::HandoverInvalid => "handover_invalid",
|
||
Reason::HandoverNotHolder => "handover_not_holder",
|
||
Reason::TurnViolated => "turn_violated",
|
||
Reason::TurnUnmarked => "turn_unmarked",
|
||
Reason::LateUnflagged => "late_unflagged",
|
||
Reason::MarkUnfounded => "mark_unfounded",
|
||
Reason::InputNotPermitted => "input_not_permitted",
|
||
Reason::WindowUnknown => "window_unknown",
|
||
Reason::InputTypeMismatch => "input_type_mismatch",
|
||
Reason::InputOverLimit => "input_over_limit",
|
||
Reason::StatementRepeated => "statement_repeated",
|
||
Reason::CommitEarly => "commit_early",
|
||
Reason::CommitMismatch => "commit_mismatch",
|
||
Reason::SelectionUncommitted => "selection_uncommitted",
|
||
Reason::SeedMismatch => "seed_mismatch",
|
||
Reason::InputLate => "input_late",
|
||
Reason::SelectionMismatch => "selection_mismatch",
|
||
Reason::InputUncredited => "input_uncredited",
|
||
Reason::CreditMismatch => "credit_mismatch",
|
||
}
|
||
}
|
||
}
|
||
|
||
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 {}
|
||
|
||
fn ev(r: events::Reason) -> Reason {
|
||
Reason::Room(room::Reason::Events(r))
|
||
}
|
||
|
||
fn sha256(parts: &[&[u8]]) -> [u8; 32] {
|
||
let mut h = Sha256::new();
|
||
for p in parts {
|
||
h.update(p);
|
||
}
|
||
h.finalize().into()
|
||
}
|
||
|
||
const ZERO: [u8; 32] = [0; 32];
|
||
|
||
/// A cursor that reports running out as `None`.
|
||
struct Rd<'a> {
|
||
b: &'a [u8],
|
||
p: usize,
|
||
}
|
||
|
||
impl<'a> Rd<'a> {
|
||
fn new(b: &'a [u8]) -> Self {
|
||
Rd { b, p: 0 }
|
||
}
|
||
fn take(&mut self, n: usize) -> Option<&'a [u8]> {
|
||
let end = self.p.checked_add(n)?;
|
||
let s = self.b.get(self.p..end)?;
|
||
self.p = end;
|
||
Some(s)
|
||
}
|
||
fn u8(&mut self) -> Option<u8> {
|
||
Some(self.take(1)?[0])
|
||
}
|
||
fn u16(&mut self) -> Option<u16> {
|
||
Some(u16::from_le_bytes(self.take(2)?.try_into().ok()?))
|
||
}
|
||
fn u32(&mut self) -> Option<u32> {
|
||
Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
|
||
}
|
||
fn i32(&mut self) -> Option<i32> {
|
||
Some(i32::from_le_bytes(self.take(4)?.try_into().ok()?))
|
||
}
|
||
fn u64(&mut self) -> Option<u64> {
|
||
Some(u64::from_le_bytes(self.take(8)?.try_into().ok()?))
|
||
}
|
||
fn d32(&mut self) -> Option<[u8; 32]> {
|
||
self.take(32)?.try_into().ok()
|
||
}
|
||
/// A text field (§2): its bytes, which must be UTF-8.
|
||
fn text(&mut self) -> Option<String> {
|
||
let n = self.u16()? as usize;
|
||
String::from_utf8(self.take(n)?.to_vec()).ok()
|
||
}
|
||
fn done(&self) -> bool {
|
||
self.p == self.b.len()
|
||
}
|
||
}
|
||
|
||
fn put_text(o: &mut Vec<u8>, s: &str) {
|
||
assert!(s.len() <= u16::MAX as usize, "a text field is at most 65535 bytes");
|
||
o.extend_from_slice(&(s.len() as u16).to_le_bytes());
|
||
o.extend_from_slice(s.as_bytes());
|
||
}
|
||
|
||
fn ascending<T: Ord>(v: &[T]) -> bool {
|
||
v.windows(2).all(|w| w[0] < w[1])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// §3 The co-performance policy
|
||
// ---------------------------------------------------------------------------------------
|
||
|
||
/// The co-performance policy (§3.1).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct CoPolicy {
|
||
pub room: [u8; 32],
|
||
pub prev: [u8; 32],
|
||
pub effective: u64,
|
||
pub horizon_ms: u32,
|
||
pub window_ms: u32,
|
||
pub time_tol_us: u32,
|
||
pub angle_tol_mdeg: u32,
|
||
pub modes: u8,
|
||
pub sig: [u8; 64],
|
||
}
|
||
|
||
impl CoPolicy {
|
||
/// The record without `sig`: what the digest covers.
|
||
pub fn body(&self) -> Vec<u8> {
|
||
let mut o = Vec::with_capacity(94);
|
||
o.extend_from_slice(&POLICY_MAGIC);
|
||
o.push(RECORD_VERSION);
|
||
o.extend_from_slice(&self.room);
|
||
o.extend_from_slice(&self.prev);
|
||
o.extend_from_slice(&self.effective.to_le_bytes());
|
||
o.extend_from_slice(&self.horizon_ms.to_le_bytes());
|
||
o.extend_from_slice(&self.window_ms.to_le_bytes());
|
||
o.extend_from_slice(&self.time_tol_us.to_le_bytes());
|
||
o.extend_from_slice(&self.angle_tol_mdeg.to_le_bytes());
|
||
o.push(self.modes);
|
||
o
|
||
}
|
||
|
||
pub fn to_bytes(&self) -> Vec<u8> {
|
||
let mut o = self.body();
|
||
o.extend_from_slice(&self.sig);
|
||
o
|
||
}
|
||
|
||
/// Its digest (§3.2).
|
||
pub fn digest(&self) -> [u8; 32] {
|
||
sha256(&[&self.body()])
|
||
}
|
||
|
||
/// Signed by the room authority (§3.2).
|
||
pub fn sign(mut self, authority: &SigningKey) -> CoPolicy {
|
||
let mut m = POLICY_DOMAIN.to_vec();
|
||
m.extend_from_slice(&self.body());
|
||
self.sig = authority.sign(&m).to_bytes();
|
||
self
|
||
}
|
||
|
||
/// Read a policy and hold it to its rules (§3.3), before its room, signature and link.
|
||
pub fn parse(b: &[u8]) -> Result<CoPolicy, Reason> {
|
||
read_head(b, &POLICY_MAGIC)?;
|
||
let mut r = Rd::new(b);
|
||
r.take(5);
|
||
let tr = || ev(events::Reason::Truncated);
|
||
let p = CoPolicy {
|
||
room: r.d32().ok_or_else(tr)?,
|
||
prev: r.d32().ok_or_else(tr)?,
|
||
effective: r.u64().ok_or_else(tr)?,
|
||
horizon_ms: r.u32().ok_or_else(tr)?,
|
||
window_ms: r.u32().ok_or_else(tr)?,
|
||
time_tol_us: r.u32().ok_or_else(tr)?,
|
||
angle_tol_mdeg: r.u32().ok_or_else(tr)?,
|
||
modes: r.u8().ok_or_else(tr)?,
|
||
sig: r.take(64).ok_or_else(tr)?.try_into().expect("64 bytes"),
|
||
};
|
||
if !r.done() {
|
||
return Err(ev(events::Reason::Malformed));
|
||
}
|
||
if p.horizon_ms == 0 || p.window_ms == 0 || !(1..=3).contains(&p.modes) {
|
||
return Err(Reason::CoPolicyMalformed);
|
||
}
|
||
Ok(p)
|
||
}
|
||
|
||
/// The match window in µs (§5.4).
|
||
pub fn window_us(&self) -> u64 {
|
||
self.window_ms as u64 * 1000
|
||
}
|
||
}
|
||
|
||
/// §3.3 step 1: the magic and the version.
|
||
fn read_head(b: &[u8], m: &[u8; 4]) -> Result<(), Reason> {
|
||
if b.len() < 4 {
|
||
return Err(ev(events::Reason::Truncated));
|
||
}
|
||
if b[..4] != *m {
|
||
return Err(ev(events::Reason::Malformed));
|
||
}
|
||
if b.len() < 5 {
|
||
return Err(ev(events::Reason::Truncated));
|
||
}
|
||
if b[4] != RECORD_VERSION {
|
||
return Err(ev(events::Reason::UnknownVersion));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
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 => ev(events::Reason::BadKey),
|
||
crate::sig::SigError::BadSignature => ev(events::Reason::BadSignature),
|
||
})
|
||
}
|
||
|
||
fn policy_at(ps: &[CoPolicy], t: u64) -> Option<&CoPolicy> {
|
||
ps.iter().rev().find(|p| p.effective <= t)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// §4 Attributes
|
||
// ---------------------------------------------------------------------------------------
|
||
|
||
/// The two attributes this extension reads of a grant (§4.4).
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub struct Attrs {
|
||
/// The audience-input power: the most inputs to one window.
|
||
pub audience_input: Option<u8>,
|
||
/// The latency budget in ms.
|
||
pub latency_budget_ms: Option<u32>,
|
||
}
|
||
|
||
/// A decimal integer of §4.4: 1 to 6 digits, the first not `0`.
|
||
fn decimal(s: &str) -> Option<u32> {
|
||
let b = s.as_bytes();
|
||
if b.is_empty() || b.len() > 6 || b[0] == b'0' || !b.iter().all(u8::is_ascii_digit) {
|
||
return None;
|
||
}
|
||
s.parse().ok()
|
||
}
|
||
|
||
/// Read a grant's two attributes (§4.4).
|
||
pub fn read_attrs(g: &room::Grant) -> Result<Attrs, Reason> {
|
||
let mut a = Attrs::default();
|
||
for (k, v) in &g.attrs {
|
||
if k == AUDIENCE_ATTR {
|
||
let n = decimal(v).filter(|n| (1..=255).contains(n)).ok_or(Reason::CoAttrMalformed)?;
|
||
a.audience_input = Some(n as u8);
|
||
} else if k == BUDGET_ATTR {
|
||
a.latency_budget_ms = Some(decimal(v).filter(|n| (1..=MAX_BUDGET_MS).contains(n)).ok_or(Reason::CoAttrMalformed)?);
|
||
}
|
||
}
|
||
Ok(a)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// §5–§7 Statements
|
||
// ---------------------------------------------------------------------------------------
|
||
|
||
/// One note of a notes body (§5.2).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Note {
|
||
pub onset: u32,
|
||
pub duration: u32,
|
||
pub pitch: u8,
|
||
pub velocity: u8,
|
||
}
|
||
|
||
/// A camera body (§5.2).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Camera {
|
||
pub pan: i32,
|
||
pub tilt: i32,
|
||
pub zoom: u32,
|
||
}
|
||
|
||
/// What an intent plans (§5.2).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum Plan {
|
||
Object([u8; 32]),
|
||
Notes(Vec<Note>),
|
||
Text(String),
|
||
Camera(Camera),
|
||
}
|
||
|
||
impl Plan {
|
||
pub fn form(&self) -> u8 {
|
||
match self {
|
||
Plan::Object(_) => form::OBJECT,
|
||
Plan::Notes(_) => form::NOTES,
|
||
Plan::Text(_) => form::TEXT,
|
||
Plan::Camera(_) => form::CAMERA,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The name of a form, as the corpus states it.
|
||
pub fn form_name(f: u8) -> &'static str {
|
||
match f {
|
||
form::OBJECT => "object",
|
||
form::NOTES => "notes",
|
||
form::TEXT => "text",
|
||
_ => "camera",
|
||
}
|
||
}
|
||
|
||
/// A notes body (§5.2): the count and the notes.
|
||
pub fn notes_body(notes: &[Note]) -> Vec<u8> {
|
||
let mut o = vec![notes.len() as u8];
|
||
for n in notes {
|
||
o.extend_from_slice(&n.onset.to_le_bytes());
|
||
o.extend_from_slice(&n.duration.to_le_bytes());
|
||
o.push(n.pitch);
|
||
o.push(n.velocity);
|
||
}
|
||
o
|
||
}
|
||
|
||
/// A camera body (§5.2): 12 bytes.
|
||
pub fn camera_body(c: &Camera) -> Vec<u8> {
|
||
let mut o = Vec::with_capacity(12);
|
||
o.extend_from_slice(&c.pan.to_le_bytes());
|
||
o.extend_from_slice(&c.tilt.to_le_bytes());
|
||
o.extend_from_slice(&c.zoom.to_le_bytes());
|
||
o
|
||
}
|
||
|
||
fn read_notes(r: &mut Rd) -> Option<Vec<Note>> {
|
||
let n = r.u8()?;
|
||
if !(1..=64).contains(&n) {
|
||
return None;
|
||
}
|
||
let mut v = Vec::with_capacity(n as usize);
|
||
for _ in 0..n {
|
||
let x = Note { onset: r.u32()?, duration: r.u32()?, pitch: r.u8()?, velocity: r.u8()? };
|
||
if x.duration == 0 || x.pitch > 127 || !(1..=127).contains(&x.velocity) {
|
||
return None;
|
||
}
|
||
v.push(x);
|
||
}
|
||
if !v.windows(2).all(|w| (w[0].onset, w[0].pitch) < (w[1].onset, w[1].pitch)) {
|
||
return None;
|
||
}
|
||
Some(v)
|
||
}
|
||
|
||
fn read_camera(r: &mut Rd) -> Option<Camera> {
|
||
let c = Camera { pan: r.i32()?, tilt: r.i32()?, zoom: r.u32()? };
|
||
((-180_000..=180_000).contains(&c.pan) && (-90_000..=90_000).contains(&c.tilt) && c.zoom > 0).then_some(c)
|
||
}
|
||
|
||
/// A symbolic payload read as a notes body, whole (§5.2, §5.5).
|
||
pub fn parse_notes(b: &[u8]) -> Option<Vec<Note>> {
|
||
let mut r = Rd::new(b);
|
||
let v = read_notes(&mut r)?;
|
||
r.done().then_some(v)
|
||
}
|
||
|
||
/// A symbolic payload read as a camera body, whole (§5.2, §5.5).
|
||
pub fn parse_camera(b: &[u8]) -> Option<Camera> {
|
||
let mut r = Rd::new(b);
|
||
let c = read_camera(&mut r)?;
|
||
r.done().then_some(c)
|
||
}
|
||
|
||
/// An intent (§5.1).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Intent {
|
||
pub track: [u8; 32],
|
||
pub at: u64,
|
||
pub span: u32,
|
||
pub plan: Plan,
|
||
}
|
||
|
||
/// One entry of a fidelity statement (§5.6), and a participant's fidelity (§9.3).
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub struct Fidelity {
|
||
pub announced: u32,
|
||
pub kept: u32,
|
||
pub diverged: u32,
|
||
pub missed: u32,
|
||
}
|
||
|
||
/// Each participant's fidelity, in ascending order of its key (§5.6).
|
||
pub type Entries = Vec<([u8; 32], Fidelity)>;
|
||
|
||
/// A fidelity statement (§5.6).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct FidelityStatement {
|
||
pub through: u64,
|
||
pub entries: Entries,
|
||
}
|
||
|
||
/// A segment of a schedule (§6.1).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Segment {
|
||
pub start: u64,
|
||
pub length: u32,
|
||
pub turns: u16,
|
||
pub pairs: u8,
|
||
pub holders: Vec<[u8; 32]>,
|
||
}
|
||
|
||
/// A turn schedule (§6.1).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Schedule {
|
||
pub t0: u64,
|
||
pub from: u64,
|
||
pub mode: u8,
|
||
pub beat_us: u32,
|
||
pub beats_per_bar: u8,
|
||
pub segments: Vec<Segment>,
|
||
}
|
||
|
||
impl Schedule {
|
||
/// A segment's turn length in µs (§6.1), when it fits a u64.
|
||
pub fn turn_len(&self, s: &Segment) -> Option<u64> {
|
||
let l = if self.beat_us == 0 { s.length as u128 } else { s.length as u128 * self.beats_per_bar as u128 * self.beat_us as u128 };
|
||
u64::try_from(l).ok()
|
||
}
|
||
|
||
/// A segment's end: `start` + `turns` × L, when it fits a u64.
|
||
pub fn end(&self, s: &Segment) -> Option<u64> {
|
||
let e = s.start as u128 + s.turns as u128 * self.turn_len(s)? as u128;
|
||
u64::try_from(e).ok()
|
||
}
|
||
|
||
/// The turn running at `x` (§6.2): its segment, its index, its start and its end.
|
||
pub fn turn_at(&self, x: u64) -> Option<(usize, u64, u64, u64)> {
|
||
for (i, s) in self.segments.iter().enumerate() {
|
||
let l = self.turn_len(s)?;
|
||
let end = self.end(s)?;
|
||
if s.start <= x && x < end {
|
||
let n = (x - s.start) / l;
|
||
return Some((i, n, s.start + n * l, s.start + (n + 1) * l));
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The structural rules of §6.1 that the record alone fixes.
|
||
fn well_formed(&self) -> bool {
|
||
if !(self.mode == mode::REFUSE || self.mode == mode::MARK) {
|
||
return false;
|
||
}
|
||
if (self.beat_us == 0) != (self.beats_per_bar == 0) || self.beats_per_bar > 32 {
|
||
return false;
|
||
}
|
||
if !(1..=4).contains(&self.segments.len()) {
|
||
return false;
|
||
}
|
||
let mut floor = self.from;
|
||
for s in &self.segments {
|
||
if !(1..=1024).contains(&s.turns) || s.pairs > 1 || !(1..=4).contains(&s.holders.len()) {
|
||
return false;
|
||
}
|
||
match self.turn_len(s) {
|
||
Some(l) if l > 0 => {}
|
||
_ => return false,
|
||
}
|
||
let Some(end) = self.end(s) else { return false };
|
||
if s.start < floor {
|
||
return false;
|
||
}
|
||
let distinct: BTreeSet<&[u8; 32]> = s.holders.iter().collect();
|
||
if distinct.len() != s.holders.len() {
|
||
return false;
|
||
}
|
||
if s.pairs == 1 && (s.turns % 2 == 1 || s.holders.len() < 2) {
|
||
return false;
|
||
}
|
||
floor = end;
|
||
}
|
||
true
|
||
}
|
||
}
|
||
|
||
/// A handover (§6.3).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Handover {
|
||
pub at: u64,
|
||
pub to: [u8; 32],
|
||
}
|
||
|
||
/// A mark (§6.7).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Mark {
|
||
pub track: [u8; 32],
|
||
pub group: u64,
|
||
pub object: u64,
|
||
pub flags: u8,
|
||
}
|
||
|
||
/// A selection window (§7.2).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Window {
|
||
pub rule: u8,
|
||
pub types: u8,
|
||
pub options: u8,
|
||
pub k: u8,
|
||
pub close: u64,
|
||
pub seed_commit: [u8; 32],
|
||
pub question: String,
|
||
}
|
||
|
||
impl Window {
|
||
fn well_formed(&self) -> bool {
|
||
let rule_ok = match self.rule {
|
||
rule::PLURALITY => self.types == 1 << (input_type::VOTE - 1) && (2..=32).contains(&self.options),
|
||
rule::REACTIONS | rule::DRAW => self.types != 0 && self.types & !0b1011 == 0 && self.options == 0,
|
||
_ => false,
|
||
};
|
||
rule_ok && (1..=8).contains(&self.k) && (self.rule == rule::DRAW || self.seed_commit == ZERO) && self.question.len() <= 500
|
||
}
|
||
}
|
||
|
||
/// An input's body (§7.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum InputBody {
|
||
Prompt(String),
|
||
Sketch([u8; 32]),
|
||
Vote(u8),
|
||
Movement([u8; 32]),
|
||
}
|
||
|
||
impl InputBody {
|
||
pub fn input_type(&self) -> u8 {
|
||
match self {
|
||
InputBody::Prompt(_) => input_type::PROMPT,
|
||
InputBody::Sketch(_) => input_type::SKETCH,
|
||
InputBody::Vote(_) => input_type::VOTE,
|
||
InputBody::Movement(_) => input_type::MOVEMENT,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// An audience input (§7.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct AudienceInput {
|
||
pub window: [u8; 32],
|
||
pub body: InputBody,
|
||
}
|
||
|
||
/// A commitment (§7.5).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Commit {
|
||
pub window: [u8; 32],
|
||
pub count: u32,
|
||
pub root: [u8; 32],
|
||
}
|
||
|
||
/// A selection's picks (§7.7).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum Picks {
|
||
Options(Vec<u8>),
|
||
Inputs(Vec<[u8; 32]>),
|
||
}
|
||
|
||
impl Picks {
|
||
pub fn len(&self) -> usize {
|
||
match self {
|
||
Picks::Options(v) => v.len(),
|
||
Picks::Inputs(v) => v.len(),
|
||
}
|
||
}
|
||
|
||
pub fn is_empty(&self) -> bool {
|
||
self.len() == 0
|
||
}
|
||
}
|
||
|
||
/// A credit (§7.8).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Credit {
|
||
pub step: u8,
|
||
pub party: [u8; 32],
|
||
pub reference: [u8; 32],
|
||
pub count: u32,
|
||
}
|
||
|
||
/// A selection (§7.7).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct Selection {
|
||
pub rule: u8,
|
||
pub window: [u8; 32],
|
||
pub commit: [u8; 32],
|
||
pub seed: [u8; 32],
|
||
pub picks: Picks,
|
||
pub credits: Vec<Credit>,
|
||
}
|
||
|
||
/// A statement of this extension (§4.1).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum Statement {
|
||
Intent(Intent),
|
||
Fidelity(FidelityStatement),
|
||
Schedule(Schedule),
|
||
Handover(Handover),
|
||
Mark(Mark),
|
||
Window(Window),
|
||
Input(AudienceInput),
|
||
Commit(Commit),
|
||
Selection(Selection),
|
||
}
|
||
|
||
impl Statement {
|
||
/// Whether a `meta` payload is a statement of this extension (§4.1): its first four
|
||
/// bytes are one of the magics.
|
||
pub fn is_statement(payload: &[u8]) -> bool {
|
||
payload.len() >= 4 && magic::ALL.iter().any(|m| payload[..4] == m[..])
|
||
}
|
||
|
||
/// The statement as a `meta` payload.
|
||
pub fn encode(&self) -> Vec<u8> {
|
||
let mut o = Vec::new();
|
||
let head = |o: &mut Vec<u8>, m: &[u8; 4]| {
|
||
o.extend_from_slice(m);
|
||
o.push(RECORD_VERSION);
|
||
};
|
||
match self {
|
||
Statement::Intent(i) => {
|
||
head(&mut o, &magic::INTENT);
|
||
o.extend_from_slice(&i.track);
|
||
o.extend_from_slice(&i.at.to_le_bytes());
|
||
o.extend_from_slice(&i.span.to_le_bytes());
|
||
o.push(i.plan.form());
|
||
match &i.plan {
|
||
Plan::Object(d) => o.extend_from_slice(d),
|
||
Plan::Notes(n) => o.extend_from_slice(¬es_body(n)),
|
||
Plan::Text(s) => put_text(&mut o, s),
|
||
Plan::Camera(c) => o.extend_from_slice(&camera_body(c)),
|
||
}
|
||
}
|
||
Statement::Fidelity(f) => {
|
||
head(&mut o, &magic::FIDELITY);
|
||
o.extend_from_slice(&f.through.to_le_bytes());
|
||
o.push(f.entries.len() as u8);
|
||
for (p, e) in &f.entries {
|
||
o.extend_from_slice(p);
|
||
for n in [e.announced, e.kept, e.diverged, e.missed] {
|
||
o.extend_from_slice(&n.to_le_bytes());
|
||
}
|
||
}
|
||
}
|
||
Statement::Schedule(s) => {
|
||
head(&mut o, &magic::SCHEDULE);
|
||
o.extend_from_slice(&s.t0.to_le_bytes());
|
||
o.extend_from_slice(&s.from.to_le_bytes());
|
||
o.push(s.mode);
|
||
o.extend_from_slice(&s.beat_us.to_le_bytes());
|
||
o.push(s.beats_per_bar);
|
||
o.push(s.segments.len() as u8);
|
||
for g in &s.segments {
|
||
o.extend_from_slice(&g.start.to_le_bytes());
|
||
o.extend_from_slice(&g.length.to_le_bytes());
|
||
o.extend_from_slice(&g.turns.to_le_bytes());
|
||
o.push(g.pairs);
|
||
o.push(g.holders.len() as u8);
|
||
for h in &g.holders {
|
||
o.extend_from_slice(h);
|
||
}
|
||
}
|
||
}
|
||
Statement::Handover(h) => {
|
||
head(&mut o, &magic::HANDOVER);
|
||
o.extend_from_slice(&h.at.to_le_bytes());
|
||
o.extend_from_slice(&h.to);
|
||
}
|
||
Statement::Mark(m) => {
|
||
head(&mut o, &magic::MARK);
|
||
o.extend_from_slice(&m.track);
|
||
o.extend_from_slice(&m.group.to_le_bytes());
|
||
o.extend_from_slice(&m.object.to_le_bytes());
|
||
o.push(m.flags);
|
||
}
|
||
Statement::Window(w) => {
|
||
head(&mut o, &magic::WINDOW);
|
||
o.push(w.rule);
|
||
o.push(w.types);
|
||
o.push(w.options);
|
||
o.push(w.k);
|
||
o.extend_from_slice(&w.close.to_le_bytes());
|
||
o.extend_from_slice(&w.seed_commit);
|
||
put_text(&mut o, &w.question);
|
||
}
|
||
Statement::Input(a) => {
|
||
head(&mut o, &magic::INPUT);
|
||
o.extend_from_slice(&a.window);
|
||
o.push(a.body.input_type());
|
||
match &a.body {
|
||
InputBody::Prompt(s) => put_text(&mut o, s),
|
||
InputBody::Sketch(d) | InputBody::Movement(d) => o.extend_from_slice(d),
|
||
InputBody::Vote(v) => o.push(*v),
|
||
}
|
||
}
|
||
Statement::Commit(c) => {
|
||
head(&mut o, &magic::COMMIT);
|
||
o.extend_from_slice(&c.window);
|
||
o.extend_from_slice(&c.count.to_le_bytes());
|
||
o.extend_from_slice(&c.root);
|
||
}
|
||
Statement::Selection(s) => {
|
||
head(&mut o, &magic::SELECTION);
|
||
o.push(s.rule);
|
||
o.extend_from_slice(&s.window);
|
||
o.extend_from_slice(&s.commit);
|
||
o.extend_from_slice(&s.seed);
|
||
o.push(s.picks.len() as u8);
|
||
match &s.picks {
|
||
Picks::Options(v) => o.extend_from_slice(v),
|
||
Picks::Inputs(v) => {
|
||
for d in v {
|
||
o.extend_from_slice(d);
|
||
}
|
||
}
|
||
}
|
||
o.push(s.credits.len() as u8);
|
||
for c in &s.credits {
|
||
o.push(c.step);
|
||
o.extend_from_slice(&c.party);
|
||
o.extend_from_slice(&c.reference);
|
||
o.extend_from_slice(&c.count.to_le_bytes());
|
||
}
|
||
}
|
||
}
|
||
o
|
||
}
|
||
|
||
/// Read a statement (§4.2): every failure is `co_statement_malformed`.
|
||
pub fn parse(b: &[u8]) -> Result<Statement, Reason> {
|
||
Statement::read(b).ok_or(Reason::CoStatementMalformed)
|
||
}
|
||
|
||
fn read(b: &[u8]) -> Option<Statement> {
|
||
let mut r = Rd::new(b);
|
||
let m: [u8; 4] = r.take(4)?.try_into().ok()?;
|
||
if r.u8()? != RECORD_VERSION {
|
||
return None;
|
||
}
|
||
let s = match m {
|
||
magic::INTENT => {
|
||
let track = r.d32()?;
|
||
let at = r.u64()?;
|
||
let span = r.u32()?;
|
||
let f = r.u8()?;
|
||
let plan = match f {
|
||
form::OBJECT => Plan::Object(r.d32()?),
|
||
form::NOTES => Plan::Notes(read_notes(&mut r)?),
|
||
form::TEXT => {
|
||
let s = r.text()?;
|
||
if s.is_empty() || s.len() > 500 {
|
||
return None;
|
||
}
|
||
Plan::Text(s)
|
||
}
|
||
form::CAMERA => Plan::Camera(read_camera(&mut r)?),
|
||
_ => return None,
|
||
};
|
||
if span == 0 || (track == ZERO) != (f == form::TEXT) {
|
||
return None;
|
||
}
|
||
if let Plan::Notes(n) = &plan
|
||
&& n.iter().any(|x| x.onset >= span)
|
||
{
|
||
return None;
|
||
}
|
||
Statement::Intent(Intent { track, at, span, plan })
|
||
}
|
||
magic::FIDELITY => {
|
||
let through = r.u64()?;
|
||
let n = r.u8()?;
|
||
let mut entries = Vec::with_capacity(n as usize);
|
||
for _ in 0..n {
|
||
let p = r.d32()?;
|
||
let e = Fidelity { announced: r.u32()?, kept: r.u32()?, diverged: r.u32()?, missed: r.u32()? };
|
||
entries.push((p, e));
|
||
}
|
||
let keys: Vec<[u8; 32]> = entries.iter().map(|(p, _)| *p).collect();
|
||
if !ascending(&keys) {
|
||
return None;
|
||
}
|
||
Statement::Fidelity(FidelityStatement { through, entries })
|
||
}
|
||
magic::SCHEDULE => {
|
||
let mut s = Schedule { t0: r.u64()?, from: r.u64()?, mode: r.u8()?, beat_us: r.u32()?, beats_per_bar: r.u8()?, segments: vec![] };
|
||
let n = r.u8()?;
|
||
for _ in 0..n {
|
||
let start = r.u64()?;
|
||
let length = r.u32()?;
|
||
let turns = r.u16()?;
|
||
let pairs = r.u8()?;
|
||
let h = r.u8()?;
|
||
let mut holders = Vec::new();
|
||
for _ in 0..h {
|
||
holders.push(r.d32()?);
|
||
}
|
||
s.segments.push(Segment { start, length, turns, pairs, holders });
|
||
}
|
||
if !s.well_formed() {
|
||
return None;
|
||
}
|
||
Statement::Schedule(s)
|
||
}
|
||
magic::HANDOVER => Statement::Handover(Handover { at: r.u64()?, to: r.d32()? }),
|
||
magic::MARK => {
|
||
let m = Mark { track: r.d32()?, group: r.u64()?, object: r.u64()?, flags: r.u8()? };
|
||
if m.flags == 0 || m.flags & !0b11 != 0 {
|
||
return None;
|
||
}
|
||
Statement::Mark(m)
|
||
}
|
||
magic::WINDOW => {
|
||
let w = Window { rule: r.u8()?, types: r.u8()?, options: r.u8()?, k: r.u8()?, close: r.u64()?, seed_commit: r.d32()?, question: r.text()? };
|
||
if !w.well_formed() {
|
||
return None;
|
||
}
|
||
Statement::Window(w)
|
||
}
|
||
magic::INPUT => {
|
||
let window = r.d32()?;
|
||
let body = match r.u8()? {
|
||
input_type::PROMPT => {
|
||
let s = r.text()?;
|
||
if s.is_empty() || s.len() > 500 {
|
||
return None;
|
||
}
|
||
InputBody::Prompt(s)
|
||
}
|
||
input_type::SKETCH => InputBody::Sketch(r.d32()?),
|
||
input_type::VOTE => InputBody::Vote(r.u8()?),
|
||
input_type::MOVEMENT => InputBody::Movement(r.d32()?),
|
||
_ => return None,
|
||
};
|
||
Statement::Input(AudienceInput { window, body })
|
||
}
|
||
magic::COMMIT => Statement::Commit(Commit { window: r.d32()?, count: r.u32()?, root: r.d32()? }),
|
||
magic::SELECTION => {
|
||
let rl = r.u8()?;
|
||
if !(1..=3).contains(&rl) {
|
||
return None;
|
||
}
|
||
let window = r.d32()?;
|
||
let commit = r.d32()?;
|
||
let seed = r.d32()?;
|
||
let n = r.u8()?;
|
||
let picks = if rl == rule::PLURALITY {
|
||
Picks::Options(r.take(n as usize)?.to_vec())
|
||
} else {
|
||
let mut v = Vec::new();
|
||
for _ in 0..n {
|
||
v.push(r.d32()?);
|
||
}
|
||
Picks::Inputs(v)
|
||
};
|
||
let m = r.u8()?;
|
||
let mut credits = Vec::new();
|
||
for _ in 0..m {
|
||
let c = Credit { step: r.u8()?, party: r.d32()?, reference: r.d32()?, count: r.u32()? };
|
||
if !(1..=2).contains(&c.step) {
|
||
return None;
|
||
}
|
||
credits.push(c);
|
||
}
|
||
Statement::Selection(Selection { rule: rl, window, commit, seed, picks, credits })
|
||
}
|
||
_ => return None,
|
||
};
|
||
r.done().then_some(s)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// The helpers a selection is recomputed with (§7.2, §7.5–§7.8)
|
||
// ---------------------------------------------------------------------------------------
|
||
|
||
/// A draw's seed commitment (§7.2).
|
||
pub fn seed_commit(seed: &[u8; 32]) -> [u8; 32] {
|
||
sha256(&[SEED_DOMAIN, seed])
|
||
}
|
||
|
||
/// A commitment's root over the eligible inputs' digests, in the merged order (§7.5).
|
||
pub fn commit_root(window: &[u8; 32], inputs: &[[u8; 32]]) -> [u8; 32] {
|
||
let mut h = Sha256::new();
|
||
h.update(COMMIT_DOMAIN);
|
||
h.update(window);
|
||
h.update((inputs.len() as u32).to_le_bytes());
|
||
for d in inputs {
|
||
h.update(d);
|
||
}
|
||
h.finalize().into()
|
||
}
|
||
|
||
/// A draw's order key for one input (§7.6).
|
||
pub fn draw_key(seed: &[u8; 32], input: &[u8; 32]) -> [u8; 32] {
|
||
sha256(&[DRAW_DOMAIN, seed, input])
|
||
}
|
||
|
||
/// A direct credit's `party`: the voters' keys in ascending order (§7.8).
|
||
pub fn voters_party(keys: &BTreeSet<[u8; 32]>) -> [u8; 32] {
|
||
let mut h = Sha256::new();
|
||
h.update(VOTERS_DOMAIN);
|
||
for k in keys {
|
||
h.update(k);
|
||
}
|
||
h.finalize().into()
|
||
}
|
||
|
||
/// A direct credit's `ref`: the window and the option (§7.8).
|
||
pub fn option_ref(window: &[u8; 32], option: u8) -> [u8; 32] {
|
||
sha256(&[OPTION_DOMAIN, window, &[option]])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// §9 Verification
|
||
// ---------------------------------------------------------------------------------------
|
||
|
||
/// A media object the relay forwarded (§9.1).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct Object {
|
||
pub principal: [u8; 32],
|
||
pub track: [u8; 32],
|
||
pub group: u64,
|
||
pub object: u64,
|
||
pub digest: [u8; 32],
|
||
pub arrival: u64,
|
||
pub due: u64,
|
||
}
|
||
|
||
/// What a co-performance verifier is given (§9.1).
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct Input<'a> {
|
||
pub descriptor: &'a [u8],
|
||
pub grants: &'a [Vec<u8>],
|
||
pub head: Option<&'a str>,
|
||
pub chains: &'a [Vec<Vec<u8>>],
|
||
pub policies: &'a [Vec<u8>],
|
||
pub objects: &'a [Object],
|
||
pub payloads: &'a [Vec<u8>],
|
||
}
|
||
|
||
/// Where a verification stopped (§9.2).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum At {
|
||
Room(room::At),
|
||
Policy(usize),
|
||
Grant(usize),
|
||
Record { chain: usize, record: usize },
|
||
Object(usize),
|
||
}
|
||
|
||
/// The outcome of a verification (§9.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub enum Outcome {
|
||
Accept,
|
||
Gap(Vec<events::Gap>),
|
||
Refuse { at: At, reason: Reason },
|
||
}
|
||
|
||
/// What became of an intent (§5.4).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Kept {
|
||
Kept,
|
||
Diverged,
|
||
Missed,
|
||
}
|
||
|
||
impl Kept {
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Kept::Kept => "kept",
|
||
Kept::Diverged => "diverged",
|
||
Kept::Missed => "missed",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// What played for an intent (§5.4).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Played {
|
||
Object { track: [u8; 32], group: u64, object: u64 },
|
||
Event([u8; 32]),
|
||
}
|
||
|
||
/// An intent, as a verification states it (§9.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct IntentFacts {
|
||
pub digest: [u8; 32],
|
||
pub principal: [u8; 32],
|
||
pub form: u8,
|
||
pub at: u64,
|
||
pub outcome: Kept,
|
||
pub played: Option<Played>,
|
||
}
|
||
|
||
/// A turn's role (§6.2).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Role {
|
||
Solo,
|
||
Call,
|
||
Response,
|
||
}
|
||
|
||
impl Role {
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Role::Solo => "solo",
|
||
Role::Call => "call",
|
||
Role::Response => "response",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A turn of a schedule (§9.3).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct TurnFacts {
|
||
pub segment: usize,
|
||
pub index: u64,
|
||
pub start: u64,
|
||
pub end: u64,
|
||
pub holder: [u8; 32],
|
||
pub role: Role,
|
||
}
|
||
|
||
/// A schedule, as a verification states it (§9.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct ScheduleFacts {
|
||
pub digest: [u8; 32],
|
||
pub mode: u8,
|
||
pub from: u64,
|
||
pub turns: Vec<TurnFacts>,
|
||
}
|
||
|
||
/// A handover, as a verification states it (§9.3).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct HandoverFacts {
|
||
pub digest: [u8; 32],
|
||
pub at: u64,
|
||
pub to: [u8; 32],
|
||
pub schedule: [u8; 32],
|
||
pub segment: usize,
|
||
pub turn: u64,
|
||
}
|
||
|
||
/// An object's place on the floor (§6.4).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum Floor {
|
||
Open,
|
||
InTurn,
|
||
OutOfTurn,
|
||
}
|
||
|
||
impl Floor {
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Floor::Open => "open",
|
||
Floor::InTurn => "in_turn",
|
||
Floor::OutOfTurn => "out_of_turn",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// An object, as a verification states it (§9.3).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct ObjectFacts {
|
||
pub floor: Floor,
|
||
/// The mode of the schedule that governs it, when one does.
|
||
pub mode: Option<u8>,
|
||
pub late: bool,
|
||
/// µs after its due instant it arrived; 0 when not after.
|
||
pub lateness: u64,
|
||
/// The flags of the marks naming it, together.
|
||
pub marks: u8,
|
||
}
|
||
|
||
/// Each eligible input's score under the reactions rule, in the merged order (§7.6).
|
||
pub type Scores = Vec<([u8; 32], u32)>;
|
||
|
||
/// A window, as a verification states it (§9.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct WindowFacts {
|
||
pub digest: [u8; 32],
|
||
pub rule: u8,
|
||
pub close: u64,
|
||
pub eligible: Vec<[u8; 32]>,
|
||
pub commit: Option<[u8; 32]>,
|
||
pub picks: Option<Picks>,
|
||
pub credits: Vec<Credit>,
|
||
/// Plurality: each option's count.
|
||
pub tally: Option<Vec<u32>>,
|
||
/// Reactions: each eligible input's score.
|
||
pub scores: Option<Scores>,
|
||
}
|
||
|
||
/// What a co-performance verification holds at its end.
|
||
#[derive(Clone, Debug)]
|
||
pub struct Run {
|
||
pub outcome: Outcome,
|
||
pub room: Option<[u8; 32]>,
|
||
pub policies: Vec<[u8; 32]>,
|
||
pub intents: Vec<IntentFacts>,
|
||
pub fidelity: Entries,
|
||
pub schedules: Vec<ScheduleFacts>,
|
||
pub handovers: Vec<HandoverFacts>,
|
||
pub objects: Vec<ObjectFacts>,
|
||
pub windows: Vec<WindowFacts>,
|
||
}
|
||
|
||
impl Run {
|
||
fn refuse(mut self, at: At, reason: Reason) -> Run {
|
||
self.outcome = Outcome::Refuse { at, reason };
|
||
self
|
||
}
|
||
}
|
||
|
||
/// One event the room accepted, in the merged order.
|
||
struct Held {
|
||
chain: usize,
|
||
record: usize,
|
||
ev: Event,
|
||
payload: Payload,
|
||
digest: [u8; 32],
|
||
/// The grant it was verified under, `None` for a room key's.
|
||
under: Option<[u8; 32]>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct SchedHeld {
|
||
digest: [u8; 32],
|
||
s: Schedule,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct HandHeld {
|
||
k: usize,
|
||
digest: [u8; 32],
|
||
h: Handover,
|
||
schedule: [u8; 32],
|
||
segment: usize,
|
||
turn: u64,
|
||
}
|
||
|
||
struct IntentHeld {
|
||
k: usize,
|
||
digest: [u8; 32],
|
||
i: Intent,
|
||
window_us: u64,
|
||
time_tol_us: u32,
|
||
angle_tol_mdeg: u32,
|
||
}
|
||
|
||
struct WinHeld {
|
||
digest: [u8; 32],
|
||
w: Window,
|
||
/// The commitment: its event's index and digest, and the eligible inputs it fixed.
|
||
commit: Option<(usize, [u8; 32])>,
|
||
eligible: Vec<usize>,
|
||
selection: Option<Selection>,
|
||
}
|
||
|
||
#[derive(Default)]
|
||
struct St {
|
||
floor: FloorBook,
|
||
intents: Vec<IntentHeld>,
|
||
windows: Vec<WinHeld>,
|
||
marks: Vec<(usize, Mark)>,
|
||
/// Each input: its event's index and the statement.
|
||
inputs: Vec<(usize, AudienceInput)>,
|
||
}
|
||
|
||
impl St {
|
||
fn window(&self, d: &[u8; 32]) -> Option<usize> {
|
||
self.windows.iter().position(|w| w.digest == *d)
|
||
}
|
||
}
|
||
|
||
/// The floor as a room's schedules and handovers make it (§6.1 to §6.4), in the merged order
|
||
/// they were held in: what a verification replays, and what a room clock holds to mark the
|
||
/// objects it forwards (§6.5, §6.7) and to know who holds a turn (§6.3).
|
||
#[derive(Clone, Debug, Default)]
|
||
pub struct FloorBook {
|
||
schedules: Vec<SchedHeld>,
|
||
handovers: Vec<HandHeld>,
|
||
}
|
||
|
||
/// The turn running at an instant, as a [`FloorBook`] holds it (§6.2, §6.3).
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct TurnNow {
|
||
/// The digest of the schedule in effect.
|
||
pub schedule: [u8; 32],
|
||
pub mode: u8,
|
||
pub segment: usize,
|
||
pub index: u64,
|
||
pub start: u64,
|
||
pub end: u64,
|
||
/// Its scheduled holder, and its holder at the instant (a handover's `to`, §6.3).
|
||
pub scheduled: [u8; 32],
|
||
pub holder: [u8; 32],
|
||
/// The holders its segment names.
|
||
pub holders: Vec<[u8; 32]>,
|
||
pub role: Role,
|
||
}
|
||
|
||
impl FloorBook {
|
||
/// The schedule in effect at `x` among those held (§6.1).
|
||
fn schedule_at(&self, x: u64) -> Option<&SchedHeld> {
|
||
self.schedules.iter().rev().find(|s| s.s.from <= x)
|
||
}
|
||
|
||
/// The holder of turn (`schedule`, `segment`, `turn`) at `x` (§6.3).
|
||
fn holder_at(&self, sh: &SchedHeld, segment: usize, turn: u64, x: u64) -> [u8; 32] {
|
||
let seg = &sh.s.segments[segment];
|
||
let scheduled = seg.holders[(turn % seg.holders.len() as u64) as usize];
|
||
self.handovers
|
||
.iter()
|
||
.filter(|h| h.schedule == sh.digest && h.segment == segment && h.turn == turn && h.h.at <= x)
|
||
.max_by_key(|h| (h.h.at, h.k))
|
||
.map_or(scheduled, |h| h.h.to)
|
||
}
|
||
|
||
/// The floor for a principal's object arriving at `x` (§6.4), and the governing mode.
|
||
pub fn place(&self, p: &[u8; 32], x: u64) -> (Floor, Option<u8>) {
|
||
let Some(sh) = self.schedule_at(x) else { return (Floor::Open, None) };
|
||
let Some((seg, turn, _, _)) = sh.s.turn_at(x) else { return (Floor::Open, None) };
|
||
if !sh.s.segments[seg].holders.contains(p) {
|
||
return (Floor::Open, None);
|
||
}
|
||
let f = if self.holder_at(sh, seg, turn, x) == *p { Floor::InTurn } else { Floor::OutOfTurn };
|
||
(f, Some(sh.s.mode))
|
||
}
|
||
|
||
/// The turn running at `x` under the schedule in effect at `x` (§6.2), with its holder at
|
||
/// `x` (§6.3); `None` when none runs.
|
||
pub fn turn_at(&self, x: u64) -> Option<TurnNow> {
|
||
let sh = self.schedule_at(x)?;
|
||
let (segment, index, start, end) = sh.s.turn_at(x)?;
|
||
let seg = &sh.s.segments[segment];
|
||
let role = match (seg.pairs, index % 2) {
|
||
(0, _) => Role::Solo,
|
||
(_, 0) => Role::Call,
|
||
_ => Role::Response,
|
||
};
|
||
Some(TurnNow {
|
||
schedule: sh.digest,
|
||
mode: sh.s.mode,
|
||
segment,
|
||
index,
|
||
start,
|
||
end,
|
||
scheduled: seg.holders[(index % seg.holders.len() as u64) as usize],
|
||
holder: self.holder_at(sh, segment, index, x),
|
||
holders: seg.holders.clone(),
|
||
role,
|
||
})
|
||
}
|
||
|
||
/// Whether it holds no schedule.
|
||
pub fn is_empty(&self) -> bool {
|
||
self.schedules.is_empty()
|
||
}
|
||
}
|
||
|
||
struct Ctx<'a> {
|
||
d: Descriptor,
|
||
held: Vec<Held>,
|
||
grants: Vec<room::HeldGrant>,
|
||
attrs: BTreeMap<[u8; 32], Attrs>,
|
||
objects: &'a [Object],
|
||
payloads: BTreeMap<[u8; 32], &'a [u8]>,
|
||
/// Each change of a principal's grant from the lifecycle: (t, principal, grant or none).
|
||
life: Vec<(u64, [u8; 32], Option<[u8; 32]>)>,
|
||
}
|
||
|
||
impl Ctx<'_> {
|
||
fn at(&self, k: usize) -> At {
|
||
At::Record { chain: self.held[k].chain, record: self.held[k].record }
|
||
}
|
||
|
||
fn grant(&self, digest: &[u8; 32]) -> Option<&room::Grant> {
|
||
self.grants.iter().find(|g| g.digest == *digest).map(|g| &g.grant)
|
||
}
|
||
|
||
/// The powers of the author of a held event: every power for a room key (§4.3).
|
||
fn powers(&self, h: &Held) -> u8 {
|
||
match h.under {
|
||
None => 0xff,
|
||
Some(g) => self.grant(&g).map_or(0, |g| g.caps.powers),
|
||
}
|
||
}
|
||
|
||
fn attrs_of(&self, h: &Held) -> Attrs {
|
||
h.under.and_then(|g| self.attrs.get(&g).copied()).unwrap_or_default()
|
||
}
|
||
|
||
/// The grant in effect for `p` at `x`, for an object (§9.1).
|
||
fn grant_at(&self, p: &[u8; 32], x: u64) -> Option<[u8; 32]> {
|
||
self.life.iter().rev().find(|(t, q, _)| q == p && *t <= x).and_then(|(_, _, g)| *g)
|
||
}
|
||
|
||
/// Is the item `target` restricted by the decisions before index `before` (§7.4)?
|
||
fn restricted(&self, target: &[u8; 32], before: usize) -> bool {
|
||
self.held[..before]
|
||
.iter()
|
||
.rev()
|
||
.find_map(|h| match &h.payload {
|
||
Payload::Decision { verdict, target: t, .. } if t == target => Some(*verdict),
|
||
_ => None,
|
||
})
|
||
.is_some_and(|v| v == 2 || v == 3)
|
||
}
|
||
|
||
/// What played for an intent (§5.4), as of (`events before`, `arrival at or below`).
|
||
fn played(&self, ih: &IntentHeld, as_of: Option<(usize, u64)>) -> Result<(Kept, Option<Played>), Reason> {
|
||
let author = self.held[ih.k].ev.principal;
|
||
let i = &ih.i;
|
||
let w = ih.window_us;
|
||
if let Plan::Text(text) = &i.plan {
|
||
let limit = as_of.map_or(self.held.len(), |(k, _)| k);
|
||
let mut best: Option<(u64, usize)> = None;
|
||
for (k, h) in self.held[..limit].iter().enumerate() {
|
||
if h.ev.principal != author || h.ev.t.abs_diff(i.at) > w {
|
||
continue;
|
||
}
|
||
if !matches!(h.payload, Payload::Chat { .. } | Payload::Caption(_)) {
|
||
continue;
|
||
}
|
||
let dist = h.ev.t.abs_diff(i.at);
|
||
if best.is_none_or(|(b, _)| dist < b) {
|
||
best = Some((dist, k));
|
||
}
|
||
}
|
||
let Some((_, k)) = best else { return Ok((Kept::Missed, None)) };
|
||
let said = match &self.held[k].payload {
|
||
Payload::Chat { text, .. } => text.clone(),
|
||
Payload::Caption(c) => c.text.clone(),
|
||
_ => unreachable!("a chat or a caption"),
|
||
};
|
||
let kept = if said.as_bytes() == text.as_bytes() { Kept::Kept } else { Kept::Diverged };
|
||
return Ok((kept, Some(Played::Event(self.held[k].digest))));
|
||
}
|
||
let mut best: Option<(u64, usize)> = None;
|
||
for (n, o) in self.objects.iter().enumerate() {
|
||
if o.principal != author || o.track != i.track || o.due.abs_diff(i.at) > w {
|
||
continue;
|
||
}
|
||
if as_of.is_some_and(|(_, t)| o.arrival > t) {
|
||
continue;
|
||
}
|
||
let dist = o.due.abs_diff(i.at);
|
||
if best.is_none_or(|(b, _)| dist < b) {
|
||
best = Some((dist, n));
|
||
}
|
||
}
|
||
let Some((_, n)) = best else { return Ok((Kept::Missed, None)) };
|
||
let o = &self.objects[n];
|
||
let played = Some(Played::Object { track: o.track, group: o.group, object: o.object });
|
||
let kept = match &i.plan {
|
||
Plan::Object(d) => o.digest == *d,
|
||
Plan::Notes(want) => {
|
||
let b = self.payloads.get(&o.digest).ok_or(Reason::PayloadMissing)?;
|
||
parse_notes(b).is_some_and(|got| {
|
||
got.len() == want.len()
|
||
&& want.iter().zip(&got).all(|(a, b)| {
|
||
let ta = i.at as i128 + a.onset as i128;
|
||
let tb = o.due as i128 + b.onset as i128;
|
||
a.pitch == b.pitch
|
||
&& (ta - tb).unsigned_abs() <= ih.time_tol_us as u128
|
||
&& (a.duration as i128 - b.duration as i128).unsigned_abs() <= ih.time_tol_us as u128
|
||
})
|
||
})
|
||
}
|
||
Plan::Camera(want) => {
|
||
let b = self.payloads.get(&o.digest).ok_or(Reason::PayloadMissing)?;
|
||
parse_camera(b).is_some_and(|got| {
|
||
(got.pan as i64 - want.pan as i64).unsigned_abs() <= ih.angle_tol_mdeg as u64
|
||
&& (got.tilt as i64 - want.tilt as i64).unsigned_abs() <= ih.angle_tol_mdeg as u64
|
||
&& got.zoom == want.zoom
|
||
})
|
||
}
|
||
Plan::Text(_) => unreachable!("handled above"),
|
||
};
|
||
Ok((if kept { Kept::Kept } else { Kept::Diverged }, played))
|
||
}
|
||
|
||
/// The fidelity of every author over `intents` (§5.6), in ascending order of key.
|
||
fn fidelity(&self, intents: &[&IntentHeld], as_of: Option<(usize, u64)>) -> Result<Entries, (At, Reason)> {
|
||
let mut by: BTreeMap<[u8; 32], Fidelity> = BTreeMap::new();
|
||
for ih in intents {
|
||
let (kept, _) = self.played(ih, as_of).map_err(|r| (self.at(ih.k), r))?;
|
||
let e = by.entry(self.held[ih.k].ev.principal).or_default();
|
||
e.announced += 1;
|
||
match kept {
|
||
Kept::Kept => e.kept += 1,
|
||
Kept::Diverged => e.diverged += 1,
|
||
Kept::Missed => e.missed += 1,
|
||
}
|
||
}
|
||
Ok(by.into_iter().collect())
|
||
}
|
||
|
||
/// The eligible inputs of a window at a commitment of index `ck` (§7.5).
|
||
fn eligible(&self, st: &St, w: &WinHeld, ck: usize) -> Vec<usize> {
|
||
st.inputs
|
||
.iter()
|
||
.filter(|(k, a)| *k < ck && a.window == w.digest && self.held[*k].ev.t < w.w.close && !self.restricted(&self.held[*k].digest, ck))
|
||
.map(|(k, _)| *k)
|
||
.collect()
|
||
}
|
||
|
||
/// Plurality (§7.6): each option's count, its voters, and the picks.
|
||
fn plurality(&self, st: &St, w: &WinHeld, eligible: &[usize]) -> (Vec<u32>, Vec<BTreeSet<[u8; 32]>>, Vec<u8>) {
|
||
let mut last: BTreeMap<[u8; 32], u8> = BTreeMap::new();
|
||
for &k in eligible {
|
||
if let Some((_, a)) = st.inputs.iter().find(|(x, _)| *x == k)
|
||
&& let InputBody::Vote(o) = a.body
|
||
{
|
||
last.insert(self.held[k].ev.principal, o);
|
||
}
|
||
}
|
||
let n = w.w.options as usize;
|
||
let mut voters = vec![BTreeSet::new(); n];
|
||
for (p, o) in &last {
|
||
voters[*o as usize].insert(*p);
|
||
}
|
||
let tally: Vec<u32> = voters.iter().map(|v| v.len() as u32).collect();
|
||
let mut order: Vec<u8> = (0..n as u8).filter(|o| tally[*o as usize] > 0).collect();
|
||
order.sort_by(|a, b| tally[*b as usize].cmp(&tally[*a as usize]).then(a.cmp(b)));
|
||
order.truncate(w.w.k as usize);
|
||
(tally, voters, order)
|
||
}
|
||
|
||
/// Reactions (§7.6): each eligible input's score, and the picks.
|
||
fn reactions(&self, w: &WinHeld, eligible: &[usize], ck: usize) -> (Scores, Vec<[u8; 32]>) {
|
||
let mut scores = Vec::new();
|
||
for &k in eligible {
|
||
let x = self.held[k].digest;
|
||
let mut last: BTreeMap<[u8; 32], usize> = BTreeMap::new();
|
||
for (j, h) in self.held[..ck].iter().enumerate() {
|
||
if let Payload::Reaction(r) = &h.payload
|
||
&& r.target == Some(x)
|
||
&& h.ev.t < w.w.close
|
||
{
|
||
last.insert(h.ev.principal, j);
|
||
}
|
||
}
|
||
let score = last
|
||
.values()
|
||
.filter(|&&j| {
|
||
let Payload::Reaction(r) = &self.held[j].payload else { unreachable!("a reaction") };
|
||
!r.remove && !self.restricted(&self.held[j].digest, ck)
|
||
})
|
||
.count() as u32;
|
||
scores.push((x, score, k));
|
||
}
|
||
let mut order = scores.clone();
|
||
order.sort_by(|a, b| b.1.cmp(&a.1).then(a.2.cmp(&b.2)));
|
||
let picks = order.iter().take(w.w.k as usize).map(|(x, _, _)| *x).collect();
|
||
(scores.into_iter().map(|(x, s, _)| (x, s)).collect(), picks)
|
||
}
|
||
|
||
/// A draw (§7.6): the picks.
|
||
fn draw(&self, w: &WinHeld, eligible: &[usize], seed: &[u8; 32]) -> Vec<[u8; 32]> {
|
||
let mut v: Vec<([u8; 32], [u8; 32])> = eligible.iter().map(|&k| (draw_key(seed, &self.held[k].digest), self.held[k].digest)).collect();
|
||
v.sort();
|
||
v.into_iter().take(w.w.k as usize).map(|(_, d)| d).collect()
|
||
}
|
||
|
||
/// The selection a window's rule makes over its committed inputs (§7.6), a commitment at
|
||
/// held index `ck`, and the credit each pick gives (§7.8), in the picks' order. `seed` is a
|
||
/// draw's.
|
||
fn rule_outcome(&self, st: &St, w: &WinHeld, ck: usize, seed: &[u8; 32]) -> (Picks, Vec<Credit>) {
|
||
match w.w.rule {
|
||
rule::PLURALITY => {
|
||
let (_, voters, picks) = self.plurality(st, w, &w.eligible);
|
||
let credits = picks
|
||
.iter()
|
||
.map(|&o| Credit { step: step::DIRECT, party: voters_party(&voters[o as usize]), reference: option_ref(&w.digest, o), count: voters[o as usize].len() as u32 })
|
||
.collect::<Vec<_>>();
|
||
(Picks::Options(picks), credits)
|
||
}
|
||
r => {
|
||
let picks = if r == rule::REACTIONS { self.reactions(w, &w.eligible, ck).1 } else { self.draw(w, &w.eligible, seed) };
|
||
let credits = picks
|
||
.iter()
|
||
.map(|x| {
|
||
let j = w.eligible.iter().copied().find(|&j| self.held[j].digest == *x).expect("a pick among the eligible");
|
||
Credit { step: step::AUTHOR, party: self.held[j].ev.principal, reference: *x, count: 1 }
|
||
})
|
||
.collect::<Vec<_>>();
|
||
(Picks::Inputs(picks), credits)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The room as §9.2 steps 1 to 4 leave it: the room replayed, the policies and the grants'
|
||
/// attributes read, and every statement held in the merged order. [`verify`] goes on from it;
|
||
/// a room clock reads from it what it signs ([`Replay`]).
|
||
struct Replayed<'a> {
|
||
cx: Ctx<'a>,
|
||
st: St,
|
||
policies: Vec<CoPolicy>,
|
||
gaps: Vec<events::Gap>,
|
||
}
|
||
|
||
/// Steps 1 to 4 (§9.2) into `run`: the replay, or `None` with `run` refused.
|
||
fn replay_into<'a>(inp: &Input<'a>, run: &mut Run) -> Option<Replayed<'a>> {
|
||
let refuse = |run: &mut Run, at: At, reason: Reason| {
|
||
run.outcome = Outcome::Refuse { at, reason };
|
||
None
|
||
};
|
||
// Step 1: the room.
|
||
let rr = room::verify_room(inp.descriptor, inp.grants, inp.head, inp.chains);
|
||
if let room::RoomOutcome::Refuse { at, reason } = rr.outcome {
|
||
return refuse(run, At::Room(at), Reason::Room(reason));
|
||
}
|
||
let d = rr.descriptor.clone().expect("a room that did not refuse has its descriptor");
|
||
let room_id = d.room_id();
|
||
run.room = Some(room_id);
|
||
let gaps: Vec<events::Gap> = match &rr.outcome {
|
||
room::RoomOutcome::Gap(g) => g.clone(),
|
||
_ => Vec::new(),
|
||
};
|
||
let gapped = !gaps.is_empty();
|
||
// Step 2: the policies.
|
||
let mut policies: Vec<CoPolicy> = Vec::new();
|
||
for (i, b) in inp.policies.iter().enumerate() {
|
||
let at = At::Policy(i);
|
||
let p = match CoPolicy::parse(b) {
|
||
Ok(p) => p,
|
||
Err(r) => return refuse(run, at, r),
|
||
};
|
||
if p.room != room_id {
|
||
return refuse(run, at, ev(events::Reason::WrongRoom));
|
||
}
|
||
if let Err(r) = verify_sig(&d.authority, POLICY_DOMAIN, &p.body(), &p.sig) {
|
||
return refuse(run, at, r);
|
||
}
|
||
let linked = match policies.last() {
|
||
None => p.prev == ZERO && p.effective == 0,
|
||
Some(q) => p.prev == q.digest() && p.effective > q.effective,
|
||
};
|
||
if !linked {
|
||
return refuse(run, at, Reason::CoPolicyUnlinked);
|
||
}
|
||
policies.push(p);
|
||
}
|
||
run.policies = policies.iter().map(CoPolicy::digest).collect();
|
||
// Step 3: the grants' attributes.
|
||
let mut attrs: BTreeMap<[u8; 32], Attrs> = BTreeMap::new();
|
||
for (i, g) in rr.grants.iter().enumerate() {
|
||
match read_attrs(&g.grant) {
|
||
Ok(a) => {
|
||
attrs.insert(g.digest, a);
|
||
}
|
||
Err(r) => return refuse(run, At::Grant(i), r),
|
||
}
|
||
}
|
||
// The events the room accepted, once each, in the merged order.
|
||
let mut held: Vec<Held> = Vec::new();
|
||
let mut seen = BTreeSet::new();
|
||
for &(c, i, under) in &rr.order {
|
||
let e = Event::parse(&inp.chains[c][i]).expect("a record the room accepted reads");
|
||
let digest = e.digest();
|
||
if !seen.insert(digest) {
|
||
continue;
|
||
}
|
||
let payload = events::parse_payload(e.kind, &e.payload, u16::MAX).expect("a payload the room accepted reads");
|
||
held.push(Held { chain: c, record: i, ev: e, payload, digest, under });
|
||
}
|
||
let mut life = Vec::new();
|
||
for h in &held {
|
||
if let Payload::Lifecycle { kind: k, subject, detail } = &h.payload
|
||
&& d.is_room_key(&h.ev.principal)
|
||
{
|
||
match Detail::parse(*k, detail) {
|
||
Ok(Detail::Join { grant }) => life.push((h.ev.t, *subject, Some(grant))),
|
||
Ok(Detail::GrantExchange { new, .. }) => life.push((h.ev.t, *subject, Some(new))),
|
||
Ok(Detail::Leave { .. }) => life.push((h.ev.t, *subject, None)),
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
let payloads: BTreeMap<[u8; 32], &[u8]> = inp.payloads.iter().map(|b| (sha256(&[b]), b.as_slice())).collect();
|
||
let cx = Ctx { d, held, grants: rr.grants, attrs, objects: inp.objects, payloads, life };
|
||
let mut st = St::default();
|
||
// Step 4: the statements.
|
||
for k in 0..cx.held.len() {
|
||
let h = &cx.held[k];
|
||
if h.ev.kind != kind::META || !Statement::is_statement(&h.ev.payload) {
|
||
continue;
|
||
}
|
||
if let Err((at, r)) = statement(&cx, &mut st, &policies, k, gapped) {
|
||
return refuse(run, at, r);
|
||
}
|
||
}
|
||
Some(Replayed { cx, st, policies, gaps })
|
||
}
|
||
|
||
fn empty_run() -> Run {
|
||
Run {
|
||
outcome: Outcome::Accept,
|
||
room: None,
|
||
policies: Vec::new(),
|
||
intents: Vec::new(),
|
||
fidelity: Vec::new(),
|
||
schedules: Vec::new(),
|
||
handovers: Vec::new(),
|
||
objects: Vec::new(),
|
||
windows: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Verify a room's co-performance (§9).
|
||
pub fn verify(inp: &Input) -> Run {
|
||
let mut run = empty_run();
|
||
let Some(Replayed { cx, st, gaps, .. }) = replay_into(inp, &mut run) else { return run };
|
||
let gapped = !gaps.is_empty();
|
||
let held = &cx.held;
|
||
let attrs = &cx.attrs;
|
||
// Step 5: the intents.
|
||
let mut finals = Vec::new();
|
||
for ih in &st.intents {
|
||
match cx.played(ih, None) {
|
||
Ok((kept, played)) => {
|
||
finals.push(ih);
|
||
run.intents.push(IntentFacts { digest: ih.digest, principal: held[ih.k].ev.principal, form: ih.i.plan.form(), at: ih.i.at, outcome: kept, played });
|
||
}
|
||
Err(r) => return run.refuse(cx.at(ih.k), r),
|
||
}
|
||
}
|
||
run.fidelity = cx.fidelity(&finals, None).expect("every intent compared above");
|
||
// Step 6: the objects.
|
||
for (n, o) in inp.objects.iter().enumerate() {
|
||
let (floor, m) = st.floor.place(&o.principal, o.arrival);
|
||
let marks = st.marks.iter().filter(|(_, x)| x.track == o.track && x.group == o.group && x.object == o.object).fold(0, |a, (_, x)| a | x.flags);
|
||
let lateness = o.arrival.saturating_sub(o.due);
|
||
let budget = cx.grant_at(&o.principal, o.arrival).and_then(|g| attrs.get(&g)).and_then(|a| a.latency_budget_ms);
|
||
let late = budget.is_some_and(|b| lateness > b as u64 * 1000);
|
||
let at = At::Object(n);
|
||
if floor == Floor::OutOfTurn && m == Some(mode::REFUSE) {
|
||
return run.refuse(at, Reason::TurnViolated);
|
||
}
|
||
if floor == Floor::OutOfTurn && marks & flag::OUT_OF_TURN == 0 {
|
||
return run.refuse(at, Reason::TurnUnmarked);
|
||
}
|
||
if late && marks & flag::LATE == 0 {
|
||
return run.refuse(at, Reason::LateUnflagged);
|
||
}
|
||
run.objects.push(ObjectFacts { floor, mode: m, late, lateness, marks });
|
||
}
|
||
// Step 7: the marks.
|
||
for (k, m) in &st.marks {
|
||
let found = inp.objects.iter().zip(&run.objects).find(|(o, _)| o.track == m.track && o.group == m.group && o.object == m.object);
|
||
let founded = found.is_some_and(|(_, f)| {
|
||
(m.flags & flag::OUT_OF_TURN == 0 || f.floor == Floor::OutOfTurn) && (m.flags & flag::LATE == 0 || f.late)
|
||
});
|
||
if !founded {
|
||
return run.refuse(cx.at(*k), Reason::MarkUnfounded);
|
||
}
|
||
}
|
||
// What the verification states.
|
||
for sh in &st.floor.schedules {
|
||
let mut turns = Vec::new();
|
||
for (si, seg) in sh.s.segments.iter().enumerate() {
|
||
let l = sh.s.turn_len(seg).expect("a well-formed schedule");
|
||
for n in 0..seg.turns as u64 {
|
||
let role = match (seg.pairs, n % 2) {
|
||
(0, _) => Role::Solo,
|
||
(_, 0) => Role::Call,
|
||
_ => Role::Response,
|
||
};
|
||
turns.push(TurnFacts {
|
||
segment: si,
|
||
index: n,
|
||
start: seg.start + n * l,
|
||
end: seg.start + (n + 1) * l,
|
||
holder: seg.holders[(n % seg.holders.len() as u64) as usize],
|
||
role,
|
||
});
|
||
}
|
||
}
|
||
run.schedules.push(ScheduleFacts { digest: sh.digest, mode: sh.s.mode, from: sh.s.from, turns });
|
||
}
|
||
run.handovers = st.floor.handovers.iter().map(|h| HandoverFacts { digest: h.digest, at: h.h.at, to: h.h.to, schedule: h.schedule, segment: h.segment, turn: h.turn }).collect();
|
||
for w in &st.windows {
|
||
let eligible: Vec<[u8; 32]> = w.eligible.iter().map(|&k| held[k].digest).collect();
|
||
let (tally, scores) = match (w.w.rule, w.commit) {
|
||
(rule::PLURALITY, Some(_)) => (Some(cx.plurality(&st, w, &w.eligible).0), None),
|
||
(rule::REACTIONS, Some((ck, _))) => (None, Some(cx.reactions(w, &w.eligible, ck).0)),
|
||
_ => (None, None),
|
||
};
|
||
run.windows.push(WindowFacts {
|
||
digest: w.digest,
|
||
rule: w.w.rule,
|
||
close: w.w.close,
|
||
eligible,
|
||
commit: w.commit.map(|(_, c)| c),
|
||
picks: w.selection.as_ref().map(|s| s.picks.clone()),
|
||
credits: w.selection.as_ref().map(|s| s.credits.clone()).unwrap_or_default(),
|
||
tally,
|
||
scores,
|
||
});
|
||
}
|
||
if gapped {
|
||
run.outcome = Outcome::Gap(gaps);
|
||
}
|
||
run
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// What a room clock signs (§8)
|
||
// ---------------------------------------------------------------------------------------
|
||
|
||
/// A window as a room holds it (§7.2), for the room clock that commits to it and selects
|
||
/// from it.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct WindowHeld {
|
||
pub digest: [u8; 32],
|
||
pub window: Window,
|
||
/// The `t` of the event that opened it.
|
||
pub opened: u64,
|
||
/// The digest of its commitment's event, once one is held.
|
||
pub commit: Option<[u8; 32]>,
|
||
pub selected: bool,
|
||
}
|
||
|
||
/// A room replayed through §9.2 step 4 ([`replay`]): what a room clock reads to sign the
|
||
/// statements a room key alone signs (§4.1) as a verifier recomputes them, the commitments,
|
||
/// the selections, the fidelity statements and the marks (§8).
|
||
pub struct Replay<'a> {
|
||
r: Replayed<'a>,
|
||
room: [u8; 32],
|
||
}
|
||
|
||
/// Replay a room through §9.2 steps 1 to 4, with what [`verify`] is given: the refusal and
|
||
/// where, for a room that does not get that far. A room with a gap replays: a clock signs from
|
||
/// what it holds, and [`Replay::gaps`] says what that lacks.
|
||
pub fn replay<'a>(inp: &Input<'a>) -> Result<Replay<'a>, (At, Reason)> {
|
||
let mut run = empty_run();
|
||
match replay_into(inp, &mut run) {
|
||
Some(r) => Ok(Replay { room: run.room.expect("a replayed room has its id"), r }),
|
||
None => match run.outcome {
|
||
Outcome::Refuse { at, reason } => Err((at, reason)),
|
||
_ => unreachable!("a replay stops only at a refusal"),
|
||
},
|
||
}
|
||
}
|
||
|
||
impl Replay<'_> {
|
||
/// The room id.
|
||
pub fn room(&self) -> [u8; 32] {
|
||
self.room
|
||
}
|
||
|
||
/// The chains' gaps (live-events §6.2): empty when every record of every chain is held.
|
||
pub fn gaps(&self) -> &[events::Gap] {
|
||
&self.r.gaps
|
||
}
|
||
|
||
/// The floor the schedules and handovers held make (§6).
|
||
pub fn floor(&self) -> &FloorBook {
|
||
&self.r.st.floor
|
||
}
|
||
|
||
/// The co-performance policy in effect at `t` (§3.4).
|
||
pub fn policy_at(&self, t: u64) -> Option<&CoPolicy> {
|
||
policy_at(&self.r.policies, t)
|
||
}
|
||
|
||
/// The windows held, in the merged order.
|
||
pub fn windows(&self) -> Vec<WindowHeld> {
|
||
self.r
|
||
.st
|
||
.windows
|
||
.iter()
|
||
.map(|w| WindowHeld {
|
||
digest: w.digest,
|
||
window: w.w.clone(),
|
||
opened: self.r.cx.held.iter().find(|h| h.digest == w.digest).map_or(0, |h| h.ev.t),
|
||
commit: w.commit.map(|(_, d)| d),
|
||
selected: w.selection.is_some(),
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// How many inputs `principal` has sent to window `w` that the room accepted (§7.3 step 4).
|
||
pub fn inputs_sent(&self, w: &[u8; 32], principal: &[u8; 32]) -> usize {
|
||
self.r.st.inputs.iter().filter(|(k, a)| a.window == *w && self.r.cx.held[*k].ev.principal == *principal).count()
|
||
}
|
||
|
||
/// Where an event of a room key at `t` falls in the merged order (live-rooms §9.2): after
|
||
/// every event held at an earlier `t`, and after the room keys' events at `t`.
|
||
fn position(&self, t: u64) -> usize {
|
||
self.r.cx.held.partition_point(|h| h.ev.t < t || (h.ev.t == t && h.under.is_none()))
|
||
}
|
||
|
||
/// The commitment a room key signs at `t` to window `w` (§7.5): the eligible inputs before
|
||
/// an event at `t`, counted and rooted. `None` for a window not held, one already
|
||
/// committed, or a `t` before its `close` (`commit_early`).
|
||
pub fn commitment(&self, w: &[u8; 32], t: u64) -> Option<Commit> {
|
||
let wh = &self.r.st.windows[self.r.st.window(w)?];
|
||
if wh.commit.is_some() || t < wh.w.close {
|
||
return None;
|
||
}
|
||
let eligible = self.r.cx.eligible(&self.r.st, wh, self.position(t));
|
||
let digests: Vec<[u8; 32]> = eligible.iter().map(|&j| self.r.cx.held[j].digest).collect();
|
||
Some(Commit { window: *w, count: digests.len() as u32, root: commit_root(w, &digests) })
|
||
}
|
||
|
||
/// The selection a room key signs from window `w` once its commitment is held (§7.6 to
|
||
/// §7.8): its rule's picks over the committed inputs, each credited. `seed` is the seed a
|
||
/// draw's `seed_commit` commits to (§7.2), and zero for the other rules. `None` for a
|
||
/// window not held, not committed, already selected, or a draw whose seed is not the one
|
||
/// committed to (`seed_mismatch`).
|
||
pub fn selection(&self, w: &[u8; 32], seed: [u8; 32]) -> Option<Selection> {
|
||
let wh = &self.r.st.windows[self.r.st.window(w)?];
|
||
let (ck, commit) = wh.commit?;
|
||
if wh.selection.is_some() {
|
||
return None;
|
||
}
|
||
let seed = if wh.w.rule == rule::DRAW {
|
||
if seed_commit(&seed) != wh.w.seed_commit {
|
||
return None;
|
||
}
|
||
seed
|
||
} else {
|
||
ZERO
|
||
};
|
||
let (picks, credits) = self.r.cx.rule_outcome(&self.r.st, wh, ck, &seed);
|
||
Some(Selection { rule: wh.w.rule, window: *w, commit, seed, picks, credits })
|
||
}
|
||
|
||
/// The fidelity statement a room key signs at `t`, stated through `through` (§5.6): the
|
||
/// intents before it whose `at` and match window end at or before `through`, read as of
|
||
/// `t`. `payload_missing` for an intent whose played object's payload is not given (§5.4).
|
||
pub fn fidelity(&self, through: u64, t: u64) -> Result<FidelityStatement, Reason> {
|
||
let k = self.position(t);
|
||
let covered: Vec<&IntentHeld> = self.r.st.intents.iter().filter(|ih| ih.k < k && ih.i.at as u128 + ih.window_us as u128 <= through as u128).collect();
|
||
let entries = self.r.cx.fidelity(&covered, Some((k, t))).map_err(|(_, r)| r)?;
|
||
Ok(FidelityStatement { through, entries })
|
||
}
|
||
|
||
/// The latency budget of `principal`'s grant in effect at `x`, in ms (§6.6, §9.1).
|
||
pub fn budget_at(&self, principal: &[u8; 32], x: u64) -> Option<u32> {
|
||
self.r.cx.grant_at(principal, x).and_then(|g| self.r.cx.attrs.get(&g)).and_then(|a| a.latency_budget_ms)
|
||
}
|
||
|
||
/// The flags a mark of `o` sets (§6.5 to §6.7): out of turn under a schedule that marks,
|
||
/// and late past its principal's budget; 0 when it needs none. An object out of turn
|
||
/// under a schedule that refuses is the relay's to hold back (§6.5), and no mark is for it.
|
||
pub fn marks_for(&self, o: &Object) -> u8 {
|
||
let (floor, m) = self.r.st.floor.place(&o.principal, o.arrival);
|
||
let late = self.budget_at(&o.principal, o.arrival).is_some_and(|b| o.arrival.saturating_sub(o.due) > b as u64 * 1000);
|
||
let mut f = 0;
|
||
if floor == Floor::OutOfTurn && m == Some(mode::MARK) {
|
||
f |= flag::OUT_OF_TURN;
|
||
}
|
||
if late {
|
||
f |= flag::LATE;
|
||
}
|
||
f
|
||
}
|
||
}
|
||
|
||
/// §9.2 step 4 over one statement, at held index `k`.
|
||
fn statement(cx: &Ctx, st: &mut St, policies: &[CoPolicy], k: usize, gapped: bool) -> Result<(), (At, Reason)> {
|
||
let h = &cx.held[k];
|
||
let at = cx.at(k);
|
||
let fail = |r: Reason| Err((at, r));
|
||
let s = Statement::parse(&h.ev.payload).map_err(|r| (at, r))?;
|
||
let room_key = cx.d.is_room_key(&h.ev.principal);
|
||
let direct = cx.powers(h) & power::DIRECT != 0;
|
||
let t = h.ev.t;
|
||
// §4.2 reading, with what the record's context fixes.
|
||
match &s {
|
||
Statement::Schedule(x) if x.t0 != cx.d.t0_us => return fail(Reason::CoStatementMalformed),
|
||
Statement::Window(x) if x.close <= t => return fail(Reason::CoStatementMalformed),
|
||
Statement::Fidelity(x) if x.through > t => return fail(Reason::CoStatementMalformed),
|
||
_ => {}
|
||
}
|
||
// §3.4: a policy in effect.
|
||
let Some(pol) = policy_at(policies, t) else { return fail(Reason::CoNoPolicy) };
|
||
// §4.3: who signs.
|
||
let allowed = match &s {
|
||
Statement::Intent(_) | Statement::Handover(_) | Statement::Input(_) => true,
|
||
Statement::Schedule(_) | Statement::Window(_) => room_key || direct,
|
||
Statement::Fidelity(_) | Statement::Mark(_) | Statement::Commit(_) | Statement::Selection(_) => room_key,
|
||
};
|
||
if !allowed {
|
||
return fail(Reason::CoNotPermitted);
|
||
}
|
||
match s {
|
||
Statement::Intent(i) => {
|
||
if i.at <= t {
|
||
return fail(Reason::IntentNotAhead);
|
||
}
|
||
if i.at as u128 + i.span as u128 - t as u128 > pol.horizon_ms as u128 * 1000 {
|
||
return fail(Reason::IntentBeyondHorizon);
|
||
}
|
||
st.intents.push(IntentHeld { k, digest: h.digest, i, window_us: pol.window_us(), time_tol_us: pol.time_tol_us, angle_tol_mdeg: pol.angle_tol_mdeg });
|
||
}
|
||
Statement::Fidelity(f) => {
|
||
if !gapped {
|
||
let covered: Vec<&IntentHeld> = st.intents.iter().filter(|ih| ih.i.at as u128 + ih.window_us as u128 <= f.through as u128).collect();
|
||
let want = cx.fidelity(&covered, Some((k, t)))?;
|
||
if want != f.entries {
|
||
return fail(Reason::FidelityMismatch);
|
||
}
|
||
}
|
||
}
|
||
Statement::Schedule(x) => {
|
||
if x.from < t {
|
||
return fail(Reason::ScheduleRetroactive);
|
||
}
|
||
if pol.modes & (1 << (x.mode - 1)) == 0 {
|
||
return fail(Reason::ModeNotAllowed);
|
||
}
|
||
st.floor.schedules.push(SchedHeld { digest: h.digest, s: x });
|
||
}
|
||
Statement::Handover(x) => {
|
||
if x.at < t {
|
||
return fail(Reason::HandoverInvalid);
|
||
}
|
||
let Some(sh) = st.floor.schedule_at(x.at) else { return fail(Reason::HandoverInvalid) };
|
||
let Some((seg, turn, _, _)) = sh.s.turn_at(x.at) else { return fail(Reason::HandoverInvalid) };
|
||
if !sh.s.segments[seg].holders.contains(&x.to) {
|
||
return fail(Reason::HandoverInvalid);
|
||
}
|
||
let holder = st.floor.holder_at(sh, seg, turn, x.at);
|
||
if !(room_key || direct || holder == h.ev.principal) {
|
||
return fail(Reason::HandoverNotHolder);
|
||
}
|
||
let schedule = sh.digest;
|
||
st.floor.handovers.push(HandHeld { k, digest: h.digest, h: x, schedule, segment: seg, turn });
|
||
}
|
||
Statement::Mark(m) => st.marks.push((k, m)),
|
||
Statement::Window(w) => st.windows.push(WinHeld { digest: h.digest, w, commit: None, eligible: vec![], selection: None }),
|
||
Statement::Input(a) => {
|
||
let limit = cx.attrs_of(h).audience_input;
|
||
let Some(limit) = limit.filter(|_| !room_key) else { return fail(Reason::InputNotPermitted) };
|
||
let Some(wi) = st.window(&a.window) else { return fail(Reason::WindowUnknown) };
|
||
let w = &st.windows[wi].w;
|
||
let ty = a.body.input_type();
|
||
let fits = w.types & (1 << (ty - 1)) != 0 && !matches!(a.body, InputBody::Vote(o) if o >= w.options);
|
||
if !fits {
|
||
return fail(Reason::InputTypeMismatch);
|
||
}
|
||
let sent = st.inputs.iter().filter(|(j, b)| b.window == a.window && cx.held[*j].ev.principal == h.ev.principal).count();
|
||
if sent + 1 > limit as usize {
|
||
return fail(Reason::InputOverLimit);
|
||
}
|
||
st.inputs.push((k, a));
|
||
}
|
||
Statement::Commit(c) => {
|
||
let Some(wi) = st.window(&c.window) else { return fail(Reason::WindowUnknown) };
|
||
if t < st.windows[wi].w.close {
|
||
return fail(Reason::CommitEarly);
|
||
}
|
||
if st.windows[wi].commit.is_some() {
|
||
return fail(Reason::StatementRepeated);
|
||
}
|
||
let eligible = cx.eligible(st, &st.windows[wi], k);
|
||
let digests: Vec<[u8; 32]> = eligible.iter().map(|&j| cx.held[j].digest).collect();
|
||
if !gapped && (c.count as usize != digests.len() || c.root != commit_root(&c.window, &digests)) {
|
||
return fail(Reason::CommitMismatch);
|
||
}
|
||
st.windows[wi].commit = Some((k, h.digest));
|
||
st.windows[wi].eligible = eligible;
|
||
}
|
||
Statement::Selection(s) => {
|
||
let Some(wi) = st.window(&s.window) else { return fail(Reason::WindowUnknown) };
|
||
let w = &st.windows[wi];
|
||
if w.selection.is_some() {
|
||
return fail(Reason::StatementRepeated);
|
||
}
|
||
let Some((ck, cd)) = w.commit else { return fail(Reason::SelectionUncommitted) };
|
||
if s.commit != cd {
|
||
return fail(Reason::SelectionUncommitted);
|
||
}
|
||
if w.w.rule == rule::DRAW {
|
||
if seed_commit(&s.seed) != w.w.seed_commit {
|
||
return fail(Reason::SeedMismatch);
|
||
}
|
||
} else if s.seed != ZERO {
|
||
return fail(Reason::CoStatementMalformed);
|
||
}
|
||
if !gapped {
|
||
if let Picks::Inputs(v) = &s.picks {
|
||
for x in v {
|
||
let late = st.inputs.iter().any(|(j, a)| cx.held[*j].digest == *x && a.window == w.digest && cx.held[*j].ev.t >= w.w.close);
|
||
if late {
|
||
return fail(Reason::InputLate);
|
||
}
|
||
}
|
||
}
|
||
let (want, credits) = cx.rule_outcome(st, w, ck, &s.seed);
|
||
if s.rule != w.w.rule || s.picks != want {
|
||
return fail(Reason::SelectionMismatch);
|
||
}
|
||
for (n, c) in credits.iter().enumerate() {
|
||
match s.credits.get(n) {
|
||
None => return fail(Reason::InputUncredited),
|
||
Some(x) if x != c => return fail(Reason::CreditMismatch),
|
||
_ => {}
|
||
}
|
||
}
|
||
if s.credits.len() > credits.len() {
|
||
return fail(Reason::CreditMismatch);
|
||
}
|
||
}
|
||
st.windows[wi].selection = Some(s);
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn policy() -> CoPolicy {
|
||
CoPolicy { room: [7; 32], prev: ZERO, effective: 0, horizon_ms: 8_000, window_ms: 120, time_tol_us: 15_000, angle_tol_mdeg: 500, modes: 3, sig: [0; 64] }
|
||
.sign(&SigningKey::from_bytes(&[1; 32]))
|
||
}
|
||
|
||
#[test]
|
||
fn a_policy_round_trips_and_its_rules_refuse() {
|
||
let p = policy();
|
||
assert_eq!(CoPolicy::parse(&p.to_bytes()), Ok(p.clone()));
|
||
let b = p.to_bytes();
|
||
assert_eq!(CoPolicy::parse(&b[..3]), Err(ev(events::Reason::Truncated)));
|
||
assert_eq!(CoPolicy::parse(&b[..b.len() - 1]), Err(ev(events::Reason::Truncated)));
|
||
let mut long = b.clone();
|
||
long.push(0);
|
||
assert_eq!(CoPolicy::parse(&long), Err(ev(events::Reason::Malformed)));
|
||
for bad in [CoPolicy { window_ms: 0, ..p.clone() }, CoPolicy { horizon_ms: 0, ..p.clone() }, CoPolicy { modes: 0, ..p.clone() }, CoPolicy { modes: 4, ..p.clone() }] {
|
||
assert_eq!(CoPolicy::parse(&bad.to_bytes()), Err(Reason::CoPolicyMalformed));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn statements_round_trip() {
|
||
let k = [3u8; 32];
|
||
let all = vec![
|
||
Statement::Intent(Intent { track: k, at: 9, span: 5, plan: Plan::Object(k) }),
|
||
Statement::Intent(Intent { track: k, at: 9, span: 5, plan: Plan::Notes(vec![Note { onset: 0, duration: 1, pitch: 60, velocity: 90 }, Note { onset: 2, duration: 1, pitch: 64, velocity: 90 }]) }),
|
||
Statement::Intent(Intent { track: ZERO, at: 9, span: 5, plan: Plan::Text("a line".into()) }),
|
||
Statement::Intent(Intent { track: k, at: 9, span: 5, plan: Plan::Camera(Camera { pan: -1, tilt: 2, zoom: 65_536 }) }),
|
||
Statement::Fidelity(FidelityStatement { through: 4, entries: vec![([1; 32], Fidelity { announced: 2, kept: 1, diverged: 1, missed: 0 }), ([2; 32], Fidelity::default())] }),
|
||
Statement::Schedule(Schedule { t0: 1, from: 2, mode: mode::MARK, beat_us: 500_000, beats_per_bar: 4, segments: vec![Segment { start: 2, length: 2, turns: 4, pairs: 1, holders: vec![[1; 32], [2; 32]] }] }),
|
||
Statement::Handover(Handover { at: 4, to: k }),
|
||
Statement::Mark(Mark { track: k, group: 1, object: 2, flags: 3 }),
|
||
Statement::Window(Window { rule: rule::PLURALITY, types: 4, options: 3, k: 1, close: 9, seed_commit: ZERO, question: "?".into() }),
|
||
Statement::Input(AudienceInput { window: k, body: InputBody::Prompt("fog".into()) }),
|
||
Statement::Input(AudienceInput { window: k, body: InputBody::Vote(2) }),
|
||
Statement::Commit(Commit { window: k, count: 2, root: k }),
|
||
Statement::Selection(Selection { rule: rule::DRAW, window: k, commit: k, seed: k, picks: Picks::Inputs(vec![k]), credits: vec![Credit { step: 1, party: k, reference: k, count: 1 }] }),
|
||
Statement::Selection(Selection { rule: rule::PLURALITY, window: k, commit: k, seed: ZERO, picks: Picks::Options(vec![2]), credits: vec![] }),
|
||
];
|
||
for s in all {
|
||
assert_eq!(Statement::parse(&s.encode()), Ok(s.clone()), "{s:?}");
|
||
assert!(Statement::is_statement(&s.encode()));
|
||
let mut long = s.encode();
|
||
long.push(0);
|
||
assert_eq!(Statement::parse(&long), Err(Reason::CoStatementMalformed));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn statement_rules_refuse() {
|
||
let k = [3u8; 32];
|
||
let bad = vec![
|
||
// A text intent on a track, and an object intent on none.
|
||
Statement::Intent(Intent { track: k, at: 9, span: 5, plan: Plan::Text("x".into()) }),
|
||
Statement::Intent(Intent { track: ZERO, at: 9, span: 5, plan: Plan::Object(k) }),
|
||
// A note past the span, and notes out of order.
|
||
Statement::Intent(Intent { track: k, at: 9, span: 5, plan: Plan::Notes(vec![Note { onset: 5, duration: 1, pitch: 60, velocity: 1 }]) }),
|
||
Statement::Intent(Intent { track: k, at: 9, span: 9, plan: Plan::Notes(vec![Note { onset: 2, duration: 1, pitch: 60, velocity: 1 }, Note { onset: 1, duration: 1, pitch: 60, velocity: 1 }]) }),
|
||
Statement::Intent(Intent { track: k, at: 9, span: 5, plan: Plan::Camera(Camera { pan: 180_001, tilt: 0, zoom: 1 }) }),
|
||
// A pair segment with an odd count, and one before `from`.
|
||
Statement::Schedule(Schedule { t0: 1, from: 2, mode: mode::MARK, beat_us: 0, beats_per_bar: 0, segments: vec![Segment { start: 2, length: 2, turns: 3, pairs: 1, holders: vec![[1; 32], [2; 32]] }] }),
|
||
Statement::Schedule(Schedule { t0: 1, from: 5, mode: mode::MARK, beat_us: 0, beats_per_bar: 0, segments: vec![Segment { start: 2, length: 2, turns: 2, pairs: 0, holders: vec![[1; 32]] }] }),
|
||
Statement::Schedule(Schedule { t0: 1, from: 2, mode: 3, beat_us: 0, beats_per_bar: 0, segments: vec![Segment { start: 2, length: 2, turns: 2, pairs: 0, holders: vec![[1; 32]] }] }),
|
||
Statement::Schedule(Schedule { t0: 1, from: 2, mode: 1, beat_us: 1, beats_per_bar: 0, segments: vec![Segment { start: 2, length: 2, turns: 2, pairs: 0, holders: vec![[1; 32]] }] }),
|
||
Statement::Schedule(Schedule { t0: 1, from: 2, mode: 1, beat_us: 0, beats_per_bar: 0, segments: vec![Segment { start: 2, length: 2, turns: 2, pairs: 0, holders: vec![[1; 32], [1; 32]] }] }),
|
||
Statement::Mark(Mark { track: k, group: 1, object: 2, flags: 4 }),
|
||
Statement::Window(Window { rule: rule::PLURALITY, types: 1, options: 3, k: 1, close: 9, seed_commit: ZERO, question: String::new() }),
|
||
Statement::Window(Window { rule: rule::REACTIONS, types: 4, options: 0, k: 1, close: 9, seed_commit: ZERO, question: String::new() }),
|
||
Statement::Window(Window { rule: rule::REACTIONS, types: 1, options: 0, k: 9, close: 9, seed_commit: ZERO, question: String::new() }),
|
||
Statement::Window(Window { rule: rule::REACTIONS, types: 1, options: 0, k: 1, close: 9, seed_commit: k, question: String::new() }),
|
||
Statement::Fidelity(FidelityStatement { through: 4, entries: vec![([2; 32], Fidelity::default()), ([1; 32], Fidelity::default())] }),
|
||
];
|
||
for s in bad {
|
||
assert_eq!(Statement::parse(&s.encode()), Err(Reason::CoStatementMalformed), "{s:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn attributes_read_as_decimal_integers() {
|
||
assert_eq!(decimal("1"), Some(1));
|
||
assert_eq!(decimal("600000"), Some(600_000));
|
||
for s in ["", "0", "01", "1000000", "-1", "+1", " 1", "1 "] {
|
||
assert_eq!(decimal(s), None, "{s:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_schedule_finds_its_turns() {
|
||
let s = Schedule { t0: 0, from: 0, mode: mode::MARK, beat_us: 500_000, beats_per_bar: 4, segments: vec![Segment { start: 4_000_000, length: 2, turns: 4, pairs: 1, holders: vec![[1; 32], [2; 32]] }] };
|
||
assert_eq!(s.turn_len(&s.segments[0]), Some(4_000_000));
|
||
assert_eq!(s.turn_at(3_999_999), None);
|
||
assert_eq!(s.turn_at(4_000_000), Some((0, 0, 4_000_000, 8_000_000)));
|
||
assert_eq!(s.turn_at(8_000_000), Some((0, 1, 8_000_000, 12_000_000)));
|
||
assert_eq!(s.turn_at(19_999_999), Some((0, 3, 16_000_000, 20_000_000)));
|
||
assert_eq!(s.turn_at(20_000_000), None);
|
||
// A turn length that does not fit a u64 is no schedule.
|
||
let huge = Schedule { beat_us: u32::MAX, beats_per_bar: 32, segments: vec![Segment { start: 0, length: u32::MAX, turns: 1024, pairs: 0, holders: vec![[1; 32]] }], ..s };
|
||
assert!(!huge.well_formed());
|
||
}
|
||
|
||
#[test]
|
||
fn the_symbolic_bodies_read_whole() {
|
||
let n = vec![Note { onset: 0, duration: 10, pitch: 60, velocity: 64 }];
|
||
assert_eq!(parse_notes(¬es_body(&n)), Some(n.clone()));
|
||
let mut b = notes_body(&n);
|
||
b.push(0);
|
||
assert_eq!(parse_notes(&b), None);
|
||
let c = Camera { pan: 1, tilt: -1, zoom: 1 };
|
||
assert_eq!(parse_camera(&camera_body(&c)), Some(c));
|
||
assert_eq!(parse_camera(&camera_body(&c)[..11]), None);
|
||
}
|
||
}
|