use crate::contract::ids::AdmissionProgramHash;
use serde::{Deserialize, Serialize};
pub const ADMISSION_PROGRAM_SCHEMA_VERSION: u16 = 1;
#[must_use]
pub(crate) fn ceil_log2(n: u32) -> u32 {
match n {
0 | 1 => 0,
_ => u32::BITS - (n - 1).leading_zeros(),
}
}
#[must_use]
fn index_of(id: NodeId) -> usize {
usize::try_from(id.0).expect("a NodeId fits in usize on supported targets")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Width(u16);
pub const MAX_WIDTH: u16 = 256;
impl Width {
#[must_use]
pub fn new(bits: u16) -> Option<Self> {
if (1..=MAX_WIDTH).contains(&bits) {
Some(Self(bits))
} else {
None
}
}
#[must_use]
pub const fn one() -> Self {
Self(1)
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
#[cfg(test)]
#[must_use]
pub(crate) fn from_raw(bits: u16) -> Self {
Self(bits)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct InputSlot(pub u16);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum CompareRel {
Ule,
Ult,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LookupTable {
pub key_width: Width,
pub entries: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeOp {
Constant {
bytes: Vec<u8>,
},
Input {
slot: InputSlot,
},
Eq,
Compare {
rel: CompareRel,
},
BitsetSubset,
BitsetIntersection,
And,
Or,
Not,
Select,
BoundedLookup {
table: LookupTable,
},
}
impl NodeOp {
#[must_use]
pub fn operand_count(&self) -> usize {
match self {
Self::Constant { .. } | Self::Input { .. } => 0,
Self::Not | Self::BoundedLookup { .. } => 1,
Self::Eq
| Self::Compare { .. }
| Self::BitsetSubset
| Self::BitsetIntersection
| Self::And
| Self::Or => 2,
Self::Select => 3,
}
}
#[must_use]
pub fn produces_single_bit(&self) -> bool {
match self {
Self::Eq
| Self::Compare { .. }
| Self::BitsetSubset
| Self::And
| Self::Or
| Self::Not => true,
Self::Constant { .. }
| Self::Input { .. }
| Self::BitsetIntersection
| Self::Select
| Self::BoundedLookup { .. } => false,
}
}
#[must_use]
pub fn bit_cost(&self, governing_width: Width) -> u32 {
match self {
Self::Constant { .. } | Self::Input { .. } => 0,
Self::And | Self::Or | Self::Not | Self::Select | Self::BitsetIntersection => 1,
Self::Eq | Self::Compare { .. } | Self::BitsetSubset => {
ceil_log2(u32::from(governing_width.get())) + 1
}
Self::BoundedLookup { table } => {
let entries = u32::try_from(table.entries.len()).unwrap_or(u32::MAX);
ceil_log2(entries) + 1
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NodeId(pub u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputDecl {
pub width: Width,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Node {
pub op: NodeOp,
pub operands: Vec<NodeId>,
pub width: Width,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Outputs {
pub admit: NodeId,
pub refusal_code: NodeId,
pub membranes: Vec<NodeId>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProgramError {
NodeIdOutOfRange {
at: u32,
referenced: NodeId,
},
NonCanonicalEdge {
at: u32,
referenced: NodeId,
},
ArityMismatch {
at: u32,
expected: usize,
found: usize,
},
TooManyNodes {
count: usize,
},
}
impl std::fmt::Display for ProgramError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NodeIdOutOfRange { at, referenced } => {
write!(f, "node {at} references out-of-range id {}", referenced.0)
}
Self::NonCanonicalEdge { at, referenced } => write!(
f,
"node {at} has a non-canonical (forward/self) operand {}",
referenced.0
),
Self::ArityMismatch {
at,
expected,
found,
} => write!(f, "node {at} has arity {found}, op requires {expected}"),
Self::TooManyNodes { count } => {
write!(f, "{count} nodes exceeds the u32-indexable maximum")
}
}
}
}
impl std::error::Error for ProgramError {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdmissionProgram {
schema_version: u16,
inputs: Vec<InputDecl>,
nodes: Vec<Node>,
outputs: Outputs,
}
impl AdmissionProgram {
pub fn new(
inputs: Vec<InputDecl>,
nodes: Vec<Node>,
outputs: Outputs,
) -> Result<Self, ProgramError> {
let count = nodes.len();
let node_count = u32::try_from(count).map_err(|_| ProgramError::TooManyNodes { count })?;
for (i, node) in nodes.iter().enumerate() {
let at = u32::try_from(i).map_err(|_| ProgramError::TooManyNodes { count })?;
let expected = node.op.operand_count();
if node.operands.len() != expected {
return Err(ProgramError::ArityMismatch {
at,
expected,
found: node.operands.len(),
});
}
for operand in &node.operands {
if operand.0 >= node_count {
return Err(ProgramError::NodeIdOutOfRange {
at,
referenced: *operand,
});
}
if operand.0 >= at {
return Err(ProgramError::NonCanonicalEdge {
at,
referenced: *operand,
});
}
}
}
for output in outputs
.membranes
.iter()
.chain([&outputs.admit, &outputs.refusal_code])
{
if output.0 >= node_count {
return Err(ProgramError::NodeIdOutOfRange {
at: u32::MAX,
referenced: *output,
});
}
}
Ok(Self {
schema_version: ADMISSION_PROGRAM_SCHEMA_VERSION,
inputs,
nodes,
outputs,
})
}
#[cfg(test)]
pub(crate) fn from_parts_unchecked(
schema_version: u16,
inputs: Vec<InputDecl>,
nodes: Vec<Node>,
outputs: Outputs,
) -> Self {
Self {
schema_version,
inputs,
nodes,
outputs,
}
}
#[must_use]
pub fn schema_version(&self) -> u16 {
self.schema_version
}
#[must_use]
pub fn inputs(&self) -> &[InputDecl] {
&self.inputs
}
#[must_use]
pub fn nodes(&self) -> &[Node] {
&self.nodes
}
#[must_use]
pub fn outputs(&self) -> &Outputs {
&self.outputs
}
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn max_width(&self) -> Width {
let from_inputs = self.inputs.iter().map(|d| d.width.get());
let from_nodes = self.nodes.iter().map(|n| n.width.get());
let widest = from_inputs.chain(from_nodes).max().unwrap_or(1);
Width::new(widest).unwrap_or(Width::one())
}
#[must_use]
fn governing_width(&self, node: &Node) -> Width {
match node.operands.first() {
Some(first) => self.nodes[index_of(*first)].width,
None => node.width,
}
}
#[must_use]
pub fn bit_levels(&self) -> Vec<u32> {
let mut levels: Vec<u32> = Vec::with_capacity(self.nodes.len());
for node in &self.nodes {
let cost = node.op.bit_cost(self.governing_width(node));
let parent_max = node
.operands
.iter()
.map(|operand| levels[index_of(*operand)])
.max()
.unwrap_or(0);
levels.push(cost.saturating_add(parent_max));
}
levels
}
#[must_use]
pub fn bit_depth(&self) -> u32 {
self.bit_levels().iter().copied().max().unwrap_or(0)
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
batpak::canonical::to_bytes(self)
}
pub fn digest(&self) -> Result<AdmissionProgramHash, rmp_serde::encode::Error> {
let bytes = self.canonical_bytes()?;
Ok(AdmissionProgramHash(batpak::event::hash::compute_hash(
&bytes,
)))
}
pub fn certify(&self) -> Result<ProgramCertificate, rmp_serde::encode::Error> {
let levels = self.bit_levels();
let entries = self
.nodes
.iter()
.zip(levels.iter().copied())
.map(|(node, level)| CertNode {
operands: node.operands.clone(),
width: node.width,
bit_level: level,
single_bit: node.op.produces_single_bit(),
})
.collect();
Ok(ProgramCertificate {
schema_version: self.schema_version,
node_count: self.nodes.len(),
input_width: self.max_width(),
bit_depth: levels.iter().copied().max().unwrap_or(0),
nodes: entries,
digest: self.digest()?,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CertNode {
pub operands: Vec<NodeId>,
pub width: Width,
pub bit_level: u32,
pub single_bit: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProgramCertificate {
pub schema_version: u16,
pub node_count: usize,
pub input_width: Width,
pub bit_depth: u32,
pub nodes: Vec<CertNode>,
pub digest: AdmissionProgramHash,
}
#[cfg(test)]
mod admission_tests {
use super::{
ceil_log2, AdmissionProgram, CompareRel, InputDecl, InputSlot, Node, NodeId, NodeOp,
Outputs, ProgramError, Width, ADMISSION_PROGRAM_SCHEMA_VERSION, MAX_WIDTH,
};
fn w(bits: u16) -> Width {
Width::new(bits).expect("valid width")
}
fn budget_compare_program() -> AdmissionProgram {
let nodes = vec![
Node {
op: NodeOp::Input { slot: InputSlot(0) },
operands: vec![],
width: w(64),
},
Node {
op: NodeOp::Input { slot: InputSlot(1) },
operands: vec![],
width: w(64),
},
Node {
op: NodeOp::Compare {
rel: CompareRel::Ule,
},
operands: vec![NodeId(0), NodeId(1)],
width: Width::one(),
},
];
let outputs = Outputs {
admit: NodeId(2),
refusal_code: NodeId(2),
membranes: vec![NodeId(2)],
};
AdmissionProgram::new(
vec![InputDecl { width: w(64) }, InputDecl { width: w(64) }],
nodes,
outputs,
)
.expect("well-formed")
}
#[test]
fn ceil_log2_matches_hand_values() {
assert_eq!(ceil_log2(0), 0);
assert_eq!(ceil_log2(1), 0);
assert_eq!(ceil_log2(2), 1);
assert_eq!(ceil_log2(3), 2);
assert_eq!(ceil_log2(4), 2);
assert_eq!(ceil_log2(5), 3);
assert_eq!(ceil_log2(64), 6);
assert_eq!(ceil_log2(256), 8);
}
#[test]
fn width_is_fail_closed_outside_range() {
assert!(Width::new(0).is_none());
assert!(Width::new(1).is_some());
assert!(Width::new(MAX_WIDTH).is_some());
assert!(Width::new(MAX_WIDTH + 1).is_none());
}
#[test]
fn arity_is_frozen_per_op() {
assert_eq!(NodeOp::Not.operand_count(), 1);
assert_eq!(NodeOp::And.operand_count(), 2);
assert_eq!(NodeOp::Eq.operand_count(), 2);
assert_eq!(NodeOp::Select.operand_count(), 3);
assert_eq!(NodeOp::Input { slot: InputSlot(0) }.operand_count(), 0);
}
#[test]
fn bit_cost_is_bit_level_not_word_level() {
assert_eq!(
NodeOp::Compare {
rel: CompareRel::Ule
}
.bit_cost(w(64)),
ceil_log2(64) + 1,
);
assert_eq!(NodeOp::And.bit_cost(w(64)), 1);
assert_eq!(NodeOp::BitsetIntersection.bit_cost(w(256)), 1);
assert_eq!(NodeOp::Input { slot: InputSlot(0) }.bit_cost(w(64)), 0);
}
#[test]
fn construction_enforces_canonical_topological_order() {
let program = budget_compare_program();
assert_eq!(program.schema_version(), ADMISSION_PROGRAM_SCHEMA_VERSION);
assert_eq!(program.node_count(), 3);
}
#[test]
fn forward_edge_fails_closed() {
let nodes = vec![
Node {
op: NodeOp::Not,
operands: vec![NodeId(1)],
width: Width::one(),
},
Node {
op: NodeOp::Input { slot: InputSlot(0) },
operands: vec![],
width: Width::one(),
},
];
let outputs = Outputs {
admit: NodeId(0),
refusal_code: NodeId(0),
membranes: vec![NodeId(0)],
};
let err = AdmissionProgram::new(
vec![InputDecl {
width: Width::one(),
}],
nodes,
outputs,
)
.expect_err("forward edge");
assert_eq!(
err,
ProgramError::NonCanonicalEdge {
at: 0,
referenced: NodeId(1),
}
);
}
#[test]
fn arity_mismatch_fails_closed() {
let nodes = vec![Node {
op: NodeOp::And, operands: vec![],
width: Width::one(),
}];
let outputs = Outputs {
admit: NodeId(0),
refusal_code: NodeId(0),
membranes: vec![],
};
let err = AdmissionProgram::new(vec![], nodes, outputs).expect_err("arity");
assert_eq!(
err,
ProgramError::ArityMismatch {
at: 0,
expected: 2,
found: 0,
}
);
}
#[test]
fn out_of_range_output_fails_closed() {
let nodes = vec![Node {
op: NodeOp::Input { slot: InputSlot(0) },
operands: vec![],
width: Width::one(),
}];
let outputs = Outputs {
admit: NodeId(7), refusal_code: NodeId(0),
membranes: vec![],
};
let err = AdmissionProgram::new(
vec![InputDecl {
width: Width::one(),
}],
nodes,
outputs,
)
.expect_err("oob output");
assert_eq!(
err,
ProgramError::NodeIdOutOfRange {
at: u32::MAX,
referenced: NodeId(7),
}
);
}
#[test]
fn bit_levels_accumulate_along_the_longest_path() {
let program = budget_compare_program();
let levels = program.bit_levels();
assert_eq!(levels, vec![0, 0, ceil_log2(64) + 1]);
assert_eq!(program.bit_depth(), 7);
}
#[test]
fn digest_is_stable_and_distinguishing() {
let a = budget_compare_program();
let b = budget_compare_program();
assert_eq!(a.digest().expect("a"), b.digest().expect("b"));
let mut nodes = a.nodes().to_vec();
nodes[2] = Node {
op: NodeOp::Compare {
rel: CompareRel::Ult,
},
operands: vec![NodeId(0), NodeId(1)],
width: Width::one(),
};
let c = AdmissionProgram::new(a.inputs().to_vec(), nodes, a.outputs().clone())
.expect("well-formed");
assert_ne!(a.digest().expect("a"), c.digest().expect("c"));
}
#[test]
fn certificate_recomputes_levels_and_digest() {
let program = budget_compare_program();
let cert = program.certify().expect("certify");
assert_eq!(cert.schema_version, ADMISSION_PROGRAM_SCHEMA_VERSION);
assert_eq!(cert.node_count, 3);
assert_eq!(cert.bit_depth, 7);
assert_eq!(cert.input_width, w(64));
assert_eq!(cert.digest, program.digest().expect("digest"));
assert_eq!(cert.nodes.len(), 3);
assert_eq!(cert.nodes[2].bit_level, 7);
assert!(cert.nodes[2].single_bit, "Compare yields a predicate bit");
assert!(!cert.nodes[0].single_bit, "Input yields a width lane");
}
}