use std::collections::BTreeMap;
use std::fmt;
use std::time::Duration;
use super::super::super::identity::EntityId;
pub const INTEREST_DIGEST_DOMAIN: &str = "net.sensing.interest.v1";
pub const CONSTRAINTS_DIGEST_DOMAIN: &str = "net.sensing.constraints.v1";
pub const MAX_CONSTRAINT_BYTES: usize = 1024;
macro_rules! impl_hex32_serde {
($ty:ty) => {
impl serde::Serialize for $ty {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(self.0))
} else {
serializer.serialize_bytes(&self.0)
}
}
}
impl<'de> serde::Deserialize<'de> for $ty {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Bytes32Visitor;
impl<'de> serde::de::Visitor<'de> for Bytes32Visitor {
type Value = [u8; 32];
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("32 raw bytes")
}
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<[u8; 32], E> {
v.try_into()
.map_err(|_| E::custom("expected exactly 32 bytes"))
}
fn visit_byte_buf<E: serde::de::Error>(
self,
v: Vec<u8>,
) -> Result<[u8; 32], E> {
self.visit_bytes(&v)
}
}
if deserializer.is_human_readable() {
let text = <String as serde::Deserialize>::deserialize(deserializer)?;
let decoded = hex::decode(&text).map_err(serde::de::Error::custom)?;
let bytes: [u8; 32] = decoded.try_into().map_err(|_| {
serde::de::Error::custom(concat!(
stringify!($ty),
": expected 32 bytes of hex (64 chars)"
))
})?;
Ok(Self(bytes))
} else {
deserializer.deserialize_bytes(Bytes32Visitor).map(Self)
}
}
}
};
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Digest256([u8; 32]);
impl_hex32_serde!(Digest256);
impl Digest256 {
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for Digest256 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Digest256({})", hex::encode(self.0))
}
}
#[derive(
Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct CapabilityId(String);
impl CapabilityId {
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CapabilityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum DisclosureClass {
Owner,
}
impl DisclosureClass {
pub const fn canonical_tag(self) -> u8 {
match self {
Self::Owner => 0,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct AudienceScopeCommitment([u8; 32]);
impl_hex32_serde!(AudienceScopeCommitment);
impl AudienceScopeCommitment {
pub fn owner_root(root: &EntityId) -> Self {
Self(*root.as_bytes())
}
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for AudienceScopeCommitment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AudienceScopeCommitment({})", hex::encode(self.0))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
pub struct WorkLatencyEnvelope {
pub provider_start_within: Option<Duration>,
pub first_event_after_admission: Option<Duration>,
}
impl WorkLatencyEnvelope {
pub const fn start_within(bound: Duration) -> Self {
Self {
provider_start_within: Some(bound),
first_event_after_admission: None,
}
}
pub fn canonical_bytes(&self) -> [u8; 34] {
let mut out = [0u8; 34];
for (slot, dim) in [self.provider_start_within, self.first_event_after_admission]
.into_iter()
.enumerate()
{
let base = slot * 17;
if let Some(bound) = dim {
out[base] = 1;
out[base + 1..base + 17].copy_from_slice(&bound.as_nanos().to_le_bytes());
}
}
out
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub struct ConsumerLatencyBudget {
pub end_to_end_within: Option<Duration>,
}
impl ConsumerLatencyBudget {
pub fn admits(&self, route_estimate: Duration, estimated_start: Option<Duration>) -> bool {
match self.end_to_end_within {
None => true,
Some(budget) => {
route_estimate.saturating_add(estimated_start.unwrap_or(Duration::ZERO)) <= budget
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ConstraintError {
Oversize {
len: usize,
},
Truncated,
TrailingBytes,
NonCanonicalOrder,
DuplicateKey,
InvalidUtf8,
DigestMismatch,
}
impl ConstraintError {
pub const fn is_security_relevant(self) -> bool {
matches!(self, Self::DigestMismatch)
}
}
impl fmt::Display for ConstraintError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Oversize { len } => {
write!(
f,
"canonical constraints {len} B > {MAX_CONSTRAINT_BYTES} B cap"
)
}
Self::Truncated => f.write_str("constraint bytes truncated mid-field"),
Self::TrailingBytes => f.write_str("trailing bytes after final constraint entry"),
Self::NonCanonicalOrder => f.write_str("constraint keys not strictly ascending"),
Self::DuplicateKey => f.write_str("duplicate constraint key"),
Self::InvalidUtf8 => f.write_str("constraint key/value not valid UTF-8"),
Self::DigestMismatch => {
f.write_str("inline constraint bytes do not match the claimed digest")
}
}
}
}
impl std::error::Error for ConstraintError {}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct CanonicalConstraints {
entries: BTreeMap<String, String>,
}
impl CanonicalConstraints {
pub fn from_entries<I, K, V>(entries: I) -> Result<Self, ConstraintError>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let mut map = BTreeMap::new();
for (k, v) in entries {
if map.insert(k.into(), v.into()).is_some() {
return Err(ConstraintError::DuplicateKey);
}
}
let built = Self { entries: map };
let len = built.canonical_bytes().len();
if len > MAX_CONSTRAINT_BYTES {
return Err(ConstraintError::Oversize { len });
}
Ok(built)
}
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(16 + self.entries.len() * 16);
out.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
for (k, v) in &self.entries {
for part in [k.as_str(), v.as_str()] {
out.extend_from_slice(&(part.len() as u32).to_le_bytes());
out.extend_from_slice(part.as_bytes());
}
}
out
}
pub fn parse_canonical(bytes: &[u8]) -> Result<Self, ConstraintError> {
if bytes.len() > MAX_CONSTRAINT_BYTES {
return Err(ConstraintError::Oversize { len: bytes.len() });
}
let mut cursor = bytes;
let count = read_u32(&mut cursor)?;
let mut entries = BTreeMap::new();
let mut last_key: Option<String> = None;
for _ in 0..count {
let key = read_string(&mut cursor)?;
let value = read_string(&mut cursor)?;
if let Some(prev) = &last_key {
match key.cmp(prev) {
std::cmp::Ordering::Equal => return Err(ConstraintError::DuplicateKey),
std::cmp::Ordering::Less => return Err(ConstraintError::NonCanonicalOrder),
std::cmp::Ordering::Greater => {}
}
}
last_key = Some(key.clone());
entries.insert(key, value);
}
if !cursor.is_empty() {
return Err(ConstraintError::TrailingBytes);
}
Ok(Self { entries })
}
pub fn validate_inline(bytes: &[u8], claimed: &Digest256) -> Result<Self, ConstraintError> {
let parsed = Self::parse_canonical(bytes)?;
if parsed.constraints_digest() != *claimed {
return Err(ConstraintError::DigestMismatch);
}
Ok(parsed)
}
pub fn constraints_digest(&self) -> Digest256 {
let mut hasher = blake3::Hasher::new_derive_key(CONSTRAINTS_DIGEST_DOMAIN);
hasher.update(&self.canonical_bytes());
Digest256(*hasher.finalize().as_bytes())
}
pub fn get(&self, key: &str) -> Option<&str> {
self.entries.get(key).map(String::as_str)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
fn read_u32(cursor: &mut &[u8]) -> Result<u32, ConstraintError> {
let (head, rest) = cursor
.split_first_chunk::<4>()
.ok_or(ConstraintError::Truncated)?;
*cursor = rest;
Ok(u32::from_le_bytes(*head))
}
fn read_string(cursor: &mut &[u8]) -> Result<String, ConstraintError> {
let len = read_u32(cursor)? as usize;
if cursor.len() < len {
return Err(ConstraintError::Truncated);
}
let (bytes, rest) = cursor.split_at(len);
*cursor = rest;
String::from_utf8(bytes.to_vec()).map_err(|_| ConstraintError::InvalidUtf8)
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct GroupRef([u8; 32]);
impl_hex32_serde!(GroupRef);
impl GroupRef {
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for GroupRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "GroupRef({})", hex::encode(self.0))
}
}
#[derive(
Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct TagMatch {
pub key: String,
pub value: String,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ProviderSelector {
AnyAuthorized,
Node(u64),
Nodes(Vec<u64>),
Group(GroupRef),
Tags(Vec<TagMatch>),
}
impl ProviderSelector {
pub fn is_provider_free(&self) -> bool {
matches!(self, Self::AnyAuthorized | Self::Group(_) | Self::Tags(_))
}
pub fn nodes(mut ids: Vec<u64>) -> Self {
ids.sort_unstable();
ids.dedup();
Self::Nodes(ids)
}
pub fn tags(mut matches: Vec<TagMatch>) -> Self {
matches.sort();
matches.dedup();
Self::Tags(matches)
}
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
match self {
Self::AnyAuthorized => out.push(0u8),
Self::Node(id) => {
out.push(1);
out.extend_from_slice(&id.to_le_bytes());
}
Self::Nodes(ids) => {
out.push(2);
let mut ids = ids.clone();
ids.sort_unstable();
ids.dedup();
out.extend_from_slice(&(ids.len() as u32).to_le_bytes());
for id in ids {
out.extend_from_slice(&id.to_le_bytes());
}
}
Self::Group(group) => {
out.push(3);
out.extend_from_slice(group.as_bytes());
}
Self::Tags(matches) => {
out.push(4);
let mut matches = matches.clone();
matches.sort();
matches.dedup();
out.extend_from_slice(&(matches.len() as u32).to_le_bytes());
for tag in matches {
for part in [tag.key.as_str(), tag.value.as_str()] {
out.extend_from_slice(&(part.len() as u32).to_le_bytes());
out.extend_from_slice(part.as_bytes());
}
}
}
}
out
}
}
impl PartialEq for ProviderSelector {
fn eq(&self, other: &Self) -> bool {
self.canonical_bytes() == other.canonical_bytes()
}
}
impl Eq for ProviderSelector {}
impl std::hash::Hash for ProviderSelector {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.canonical_bytes().hash(state);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
pub enum ResultMode {
Any,
TopK(u16),
Each,
Quorum(u16),
}
impl ResultMode {
pub const fn canonical_bytes(&self) -> [u8; 3] {
match self {
Self::Any => [0, 0, 0],
Self::TopK(k) => {
let b = k.to_le_bytes();
[1, b[0], b[1]]
}
Self::Each => [2, 0, 0],
Self::Quorum(k) => {
let b = k.to_le_bytes();
[3, b[0], b[1]]
}
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct InterestSpec {
pub capability_id: CapabilityId,
pub constraints: CanonicalConstraints,
pub work_latency: WorkLatencyEnvelope,
pub providers: ProviderSelector,
pub result_mode: ResultMode,
pub disclosure_class: DisclosureClass,
pub audience: AudienceScopeCommitment,
}
impl InterestSpec {
pub fn interest_digest(&self) -> Digest256 {
let mut hasher = blake3::Hasher::new_derive_key(INTEREST_DIGEST_DOMAIN);
let id_bytes = self.capability_id.as_str().as_bytes();
hasher.update(&(id_bytes.len() as u64).to_le_bytes());
hasher.update(id_bytes);
let constraint_bytes = self.constraints.canonical_bytes();
hasher.update(&(constraint_bytes.len() as u64).to_le_bytes());
hasher.update(&constraint_bytes);
hasher.update(&self.work_latency.canonical_bytes());
let selector_bytes = self.providers.canonical_bytes();
hasher.update(&(selector_bytes.len() as u64).to_le_bytes());
hasher.update(&selector_bytes);
hasher.update(&self.result_mode.canonical_bytes());
hasher.update(&[self.disclosure_class.canonical_tag()]);
hasher.update(self.audience.as_bytes());
Digest256(*hasher.finalize().as_bytes())
}
pub fn key(&self) -> CapabilityInterestKey {
CapabilityInterestKey::for_spec(self)
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct CapabilityInterestKey {
pub capability_id: CapabilityId,
pub interest_digest: Digest256,
}
impl CapabilityInterestKey {
pub fn for_spec(spec: &InterestSpec) -> Self {
Self {
capability_id: spec.capability_id.clone(),
interest_digest: spec.interest_digest(),
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct ProviderInterestKey {
pub interest: CapabilityInterestKey,
pub provider: u64,
}
impl ProviderInterestKey {
pub fn new(interest: CapabilityInterestKey, provider: u64) -> Self {
Self { interest, provider }
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct ProviderObservationKey {
pub interest: CapabilityInterestKey,
pub provider: u64,
pub capability_generation: u64,
}
impl ProviderObservationKey {
pub fn new(interest: CapabilityInterestKey, provider: u64, capability_generation: u64) -> Self {
Self {
interest,
provider,
capability_generation,
}
}
}
#[derive(Clone, Debug)]
pub struct InterestRegistration {
pub spec: InterestSpec,
pub requested_sample_interval: Duration,
pub soft_state_ttl: Duration,
pub consumer_budget: ConsumerLatencyBudget,
}
pub fn strictest_sample_interval<I>(intervals: I) -> Option<Duration>
where
I: IntoIterator<Item = Duration>,
{
intervals.into_iter().min()
}
#[cfg(test)]
mod tests {
use super::*;
fn audience(byte: u8) -> AudienceScopeCommitment {
AudienceScopeCommitment::from_bytes([byte; 32])
}
fn spec() -> InterestSpec {
InterestSpec {
capability_id: CapabilityId::new("video.transcode"),
constraints: CanonicalConstraints::from_entries([
("fps", "60"),
("resolution", "3840x2160"),
])
.unwrap(),
work_latency: WorkLatencyEnvelope::start_within(Duration::from_millis(250)),
providers: ProviderSelector::AnyAuthorized,
result_mode: ResultMode::Any,
disclosure_class: DisclosureClass::Owner,
audience: audience(0xAA),
}
}
#[test]
fn digest_is_deterministic() {
assert_eq!(spec().interest_digest(), spec().interest_digest());
}
#[test]
fn digest_domains_are_separated() {
assert_ne!(INTEREST_DIGEST_DOMAIN, CONSTRAINTS_DIGEST_DOMAIN);
assert_eq!(
super::super::CONSTRAINTS_DIGEST_DOMAIN,
CONSTRAINTS_DIGEST_DOMAIN,
);
}
#[test]
fn provider_free_discriminates_leader_routed_from_direct() {
assert!(ProviderSelector::AnyAuthorized.is_provider_free());
assert!(ProviderSelector::Group(GroupRef::from_bytes([7; 32])).is_provider_free());
assert!(ProviderSelector::tags(vec![TagMatch {
key: "gpu".into(),
value: "h100".into(),
}])
.is_provider_free());
assert!(!ProviderSelector::Node(7).is_provider_free());
assert!(!ProviderSelector::nodes(vec![7, 9]).is_provider_free());
}
#[test]
fn consumer_local_dimensions_never_split_identity() {
let strict = InterestRegistration {
spec: spec(),
requested_sample_interval: Duration::from_millis(50),
soft_state_ttl: Duration::from_secs(30),
consumer_budget: ConsumerLatencyBudget {
end_to_end_within: Some(Duration::from_millis(400)),
},
};
let loose = InterestRegistration {
spec: spec(),
requested_sample_interval: Duration::from_secs(5),
soft_state_ttl: Duration::from_secs(300),
consumer_budget: ConsumerLatencyBudget::default(),
};
assert_eq!(strict.spec.key(), loose.spec.key());
}
#[test]
fn budget_admission_is_route_relative() {
let budget = ConsumerLatencyBudget {
end_to_end_within: Some(Duration::from_millis(500)),
};
let start = Some(Duration::from_millis(300));
assert!(budget.admits(Duration::from_millis(150), start));
assert!(!budget.admits(Duration::from_millis(250), start));
assert!(ConsumerLatencyBudget::default().admits(Duration::from_secs(10), start));
assert!(budget.admits(Duration::from_millis(500), None));
}
#[test]
fn capability_generation_never_splits_interest_identity() {
let key = spec().key();
let gen10 = ProviderObservationKey::new(key.clone(), 7, 10);
let gen11 = ProviderObservationKey::new(key.clone(), 7, 11);
assert_eq!(gen10.interest, gen11.interest);
assert_ne!(gen10, gen11);
let other = ProviderObservationKey::new(key, 8, 10);
assert_eq!(gen10.interest, other.interest);
assert_ne!(gen10, other);
}
#[test]
fn provider_selector_is_identity_bearing() {
let mut node7 = spec();
node7.providers = ProviderSelector::Node(7);
let mut node8 = spec();
node8.providers = ProviderSelector::Node(8);
assert_ne!(spec().interest_digest(), node7.interest_digest());
assert_ne!(node7.interest_digest(), node8.interest_digest());
}
#[test]
fn result_mode_is_identity_bearing() {
let mut each = spec();
each.result_mode = ResultMode::Each;
let mut quorum = spec();
quorum.result_mode = ResultMode::Quorum(2);
assert_ne!(spec().interest_digest(), each.interest_digest());
assert_ne!(each.interest_digest(), quorum.interest_digest());
assert_ne!(
ResultMode::TopK(2).canonical_bytes(),
ResultMode::Quorum(2).canonical_bytes(),
);
}
#[test]
fn selectors_canonicalize_order_and_duplicates_away() {
let ab = ProviderSelector::nodes(vec![9, 3, 3, 7]);
let ba = ProviderSelector::nodes(vec![7, 9, 3]);
assert_eq!(ab.canonical_bytes(), ba.canonical_bytes());
let tag = |k: &str, v: &str| TagMatch {
key: k.into(),
value: v.into(),
};
let t1 = ProviderSelector::tags(vec![
tag("site", "factory-7"),
tag("modality", "thermal"),
tag("site", "factory-7"),
]);
let t2 = ProviderSelector::tags(vec![tag("modality", "thermal"), tag("site", "factory-7")]);
assert_eq!(t1.canonical_bytes(), t2.canonical_bytes());
let t3 = ProviderSelector::tags(vec![tag("modality", "rgb"), tag("site", "factory-7")]);
assert_ne!(t1.canonical_bytes(), t3.canonical_bytes());
let mut spec_t1 = spec();
spec_t1.providers = t1;
let mut spec_t3 = spec();
spec_t3.providers = t3;
assert_ne!(spec_t1.interest_digest(), spec_t3.interest_digest());
}
#[test]
fn selector_eq_and_hash_track_canonical_identity_not_authorship_order() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let hash = |sel: &ProviderSelector| {
let mut h = DefaultHasher::new();
sel.hash(&mut h);
h.finish()
};
let tag = |k: &str, v: &str| TagMatch {
key: k.into(),
value: v.into(),
};
let a = ProviderSelector::Nodes(vec![9, 7, 3, 3]);
let b = ProviderSelector::Nodes(vec![3, 7, 9]);
assert_eq!(a, b, "reordered node sets are one identity");
assert_eq!(hash(&a), hash(&b), "equal selectors must hash equal");
assert_eq!(a.canonical_bytes(), b.canonical_bytes());
let t1 = ProviderSelector::Tags(vec![tag("site", "f7"), tag("modality", "thermal")]);
let t2 = ProviderSelector::Tags(vec![tag("modality", "thermal"), tag("site", "f7")]);
assert_eq!(t1, t2);
assert_eq!(hash(&t1), hash(&t2));
assert_ne!(a, ProviderSelector::Nodes(vec![3, 7]));
let mut spec_raw = spec();
spec_raw.providers = ProviderSelector::Nodes(vec![9, 7, 3]);
let mut spec_ctor = spec();
spec_ctor.providers = ProviderSelector::nodes(vec![3, 9, 7]);
assert_eq!(spec_raw.providers, spec_ctor.providers);
assert_eq!(spec_raw.interest_digest(), spec_ctor.interest_digest());
}
#[test]
fn work_latency_is_identity_bearing() {
let mut other = spec();
other.work_latency.provider_start_within = Some(Duration::from_millis(251));
assert_ne!(spec().interest_digest(), other.interest_digest());
let mut absent = spec();
absent.work_latency.provider_start_within = None;
assert_ne!(spec().interest_digest(), absent.interest_digest());
let swapped = WorkLatencyEnvelope {
provider_start_within: None,
first_event_after_admission: Some(Duration::from_millis(250)),
};
assert_ne!(
WorkLatencyEnvelope::start_within(Duration::from_millis(250)).canonical_bytes(),
swapped.canonical_bytes(),
);
}
#[test]
fn capability_id_is_identity_bearing() {
let mut other = spec();
other.capability_id = CapabilityId::new("video.transcodf");
assert_ne!(spec().interest_digest(), other.interest_digest());
}
#[test]
fn audience_commitment_is_identity_bearing() {
let mut other = spec();
other.audience = audience(0xBB);
assert_ne!(spec().interest_digest(), other.interest_digest());
}
#[test]
fn constraints_are_identity_bearing() {
let mut other = spec();
other.constraints =
CanonicalConstraints::from_entries([("fps", "30"), ("resolution", "3840x2160")])
.unwrap();
assert_ne!(spec().interest_digest(), other.interest_digest());
}
#[test]
fn canonicalization_is_insertion_order_insensitive() {
let ab = CanonicalConstraints::from_entries([("a", "1"), ("b", "2")]).unwrap();
let ba = CanonicalConstraints::from_entries([("b", "2"), ("a", "1")]).unwrap();
assert_eq!(ab.canonical_bytes(), ba.canonical_bytes());
assert_eq!(ab.constraints_digest(), ba.constraints_digest());
}
#[test]
fn length_prefixes_keep_the_encoding_injective() {
let ab_c = CanonicalConstraints::from_entries([("ab", "c")]).unwrap();
let a_bc = CanonicalConstraints::from_entries([("a", "bc")]).unwrap();
assert_ne!(ab_c.canonical_bytes(), a_bc.canonical_bytes());
assert_ne!(ab_c.constraints_digest(), a_bc.constraints_digest());
}
#[test]
fn parse_round_trips_canonical_bytes() {
let original =
CanonicalConstraints::from_entries([("a", "1"), ("b", ""), ("c", "x")]).unwrap();
let parsed = CanonicalConstraints::parse_canonical(&original.canonical_bytes()).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn parse_rejects_every_non_canonical_form() {
let one = |k: &str, v: &str| {
let mut out = Vec::new();
for part in [k, v] {
out.extend_from_slice(&(part.len() as u32).to_le_bytes());
out.extend_from_slice(part.as_bytes());
}
out
};
let mut unsorted = 2u32.to_le_bytes().to_vec();
unsorted.extend(one("b", "2"));
unsorted.extend(one("a", "1"));
assert_eq!(
CanonicalConstraints::parse_canonical(&unsorted),
Err(ConstraintError::NonCanonicalOrder),
);
let mut dup = 2u32.to_le_bytes().to_vec();
dup.extend(one("a", "1"));
dup.extend(one("a", "2"));
assert_eq!(
CanonicalConstraints::parse_canonical(&dup),
Err(ConstraintError::DuplicateKey),
);
let good = CanonicalConstraints::from_entries([("a", "1")])
.unwrap()
.canonical_bytes();
assert_eq!(
CanonicalConstraints::parse_canonical(&good[..good.len() - 1]),
Err(ConstraintError::Truncated),
);
let mut trailing = good.clone();
trailing.push(0);
assert_eq!(
CanonicalConstraints::parse_canonical(&trailing),
Err(ConstraintError::TrailingBytes),
);
let mut bad_utf8 = 1u32.to_le_bytes().to_vec();
bad_utf8.extend_from_slice(&2u32.to_le_bytes());
bad_utf8.extend_from_slice(&[0xFF, 0xFE]);
bad_utf8.extend_from_slice(&0u32.to_le_bytes());
assert_eq!(
CanonicalConstraints::parse_canonical(&bad_utf8),
Err(ConstraintError::InvalidUtf8),
);
}
#[test]
fn oversize_is_rejected_on_build_and_parse() {
let big = "x".repeat(MAX_CONSTRAINT_BYTES);
assert!(matches!(
CanonicalConstraints::from_entries([("k", big.as_str())]),
Err(ConstraintError::Oversize { .. }),
));
let bytes = vec![0u8; MAX_CONSTRAINT_BYTES + 1];
assert!(matches!(
CanonicalConstraints::parse_canonical(&bytes),
Err(ConstraintError::Oversize { .. }),
));
}
#[test]
fn digest_mismatch_is_the_only_security_relevant_rejection() {
let constraints = CanonicalConstraints::from_entries([("a", "1")]).unwrap();
let bytes = constraints.canonical_bytes();
assert!(
CanonicalConstraints::validate_inline(&bytes, &constraints.constraints_digest())
.is_ok()
);
let wrong = Digest256::from_bytes([0u8; 32]);
let err = CanonicalConstraints::validate_inline(&bytes, &wrong).unwrap_err();
assert_eq!(err, ConstraintError::DigestMismatch);
assert!(err.is_security_relevant());
assert!(!ConstraintError::Truncated.is_security_relevant());
assert!(!ConstraintError::Oversize { len: 0 }.is_security_relevant());
assert!(!ConstraintError::NonCanonicalOrder.is_security_relevant());
}
#[test]
fn constraints_and_interest_digests_are_domain_separated() {
let constraints = CanonicalConstraints::from_entries([("a", "1")]).unwrap();
assert_ne!(constraints.constraints_digest(), spec().interest_digest(),);
}
#[test]
fn interest_key_binds_the_predicate_not_the_provider() {
let base = spec().key();
let mut other = spec();
other.constraints = CanonicalConstraints::from_entries([("fps", "30")]).unwrap();
assert_ne!(base, other.key());
assert_eq!(base.interest_digest, spec().interest_digest());
assert_ne!(
ProviderObservationKey::new(base.clone(), 7, 1),
ProviderObservationKey::new(base, 8, 1),
);
}
#[test]
fn strictest_sample_interval_is_min_dominance() {
assert_eq!(strictest_sample_interval([]), None);
assert_eq!(
strictest_sample_interval([
Duration::from_millis(500),
Duration::from_millis(50),
Duration::from_secs(5),
]),
Some(Duration::from_millis(50)),
);
}
}