use dashmap::DashMap;
use ed25519_dalek::Signature;
use super::auth::{
read_32, read_u32, read_u64, SubnetAuthError, SubnetAuthorityConfig, SubnetRef,
SubnetRevocationFloor, MAX_TOKEN_CLOCK_SKEW_SECS,
};
use super::id::TopologySubnetId;
use crate::adapter::net::channel::ChannelHash;
use crate::adapter::net::identity::{EntityId, EntityKeypair};
pub const SUBNET_DESCRIPTOR_SIG_DOMAIN: &[u8] = b"net.subnet.descriptor.v1";
pub const SUBNET_GATEWAY_AD_SIG_DOMAIN: &[u8] = b"net.subnet.gateway-ad.v1";
pub const SUBNET_EXPORT_POLICY_SIG_DOMAIN: &[u8] = b"net.subnet.export-policy.v1";
pub const MAX_EXPORTED_CHANNELS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum SubnetFactKind {
Descriptor = 1,
GatewayAdvertisement = 2,
ExportPolicy = 3,
RevocationFloor = 4,
}
impl SubnetFactKind {
pub fn try_from_tag(tag: u8) -> Result<Self, SubnetAuthError> {
match tag {
1 => Ok(Self::Descriptor),
2 => Ok(Self::GatewayAdvertisement),
3 => Ok(Self::ExportPolicy),
4 => Ok(Self::RevocationFloor),
_ => Err(SubnetAuthError::InvalidFormat),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetDescriptor {
pub version: u8,
pub scope: SubnetRef,
pub topology_epoch: u32,
pub issuer: EntityId,
pub revision: u64,
pub issued_at: u64,
pub signature: [u8; 64],
}
impl SubnetDescriptor {
pub const SIGNED_PAYLOAD_SIZE: usize = 89;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
const SIGNING_INPUT_SIZE: usize =
SUBNET_DESCRIPTOR_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
pub fn try_issue(
root_keypair: &EntityKeypair,
scope: SubnetRef,
topology_epoch: u32,
revision: u64,
issued_at: u64,
) -> Result<Self, SubnetAuthError> {
let mut fact = Self {
version: 1,
scope,
topology_epoch,
issuer: root_keypair.entity_id().clone(),
revision,
issued_at,
signature: [0u8; 64],
};
let sig = root_keypair
.try_sign(&fact.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
fact.signature = sig.to_bytes();
Ok(fact)
}
fn signed_payload(&self) -> [u8; Self::SIGNED_PAYLOAD_SIZE] {
let mut buf = [0u8; Self::SIGNED_PAYLOAD_SIZE];
let mut off = 0;
buf[off] = self.version;
off += 1;
buf[off..off + 32].copy_from_slice(self.scope.authority.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.scope.path.raw().to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
off += 4;
buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
off += 32;
buf[off..off + 8].copy_from_slice(&self.revision.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.issued_at.to_le_bytes());
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..SUBNET_DESCRIPTOR_SIG_DOMAIN.len()].copy_from_slice(SUBNET_DESCRIPTOR_SIG_DOMAIN);
buf[SUBNET_DESCRIPTOR_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::WIRE_SIZE);
out.extend_from_slice(&self.signed_payload());
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() != Self::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let authority = EntityId::from_bytes(read_32(bytes, &mut off));
let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let topology_epoch = read_u32(bytes, &mut off);
let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
let revision = read_u64(bytes, &mut off);
let issued_at = read_u64(bytes, &mut off);
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
Ok(Self {
version,
scope: SubnetRef { authority, path },
topology_epoch,
issuer,
revision,
issued_at,
signature,
})
}
pub fn verify(&self) -> Result<(), SubnetAuthError> {
let sig = Signature::from_bytes(&self.signature);
self.issuer
.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayAdvertisement {
pub version: u8,
pub scope: SubnetRef,
pub topology_epoch: u32,
pub issuer: EntityId,
pub gateway: EntityId,
pub gateway_node: u64,
pub revision: u64,
pub not_before: u64,
pub not_after: u64,
pub signature: [u8; 64],
}
impl GatewayAdvertisement {
pub const SIGNED_PAYLOAD_SIZE: usize = 137;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
const SIGNING_INPUT_SIZE: usize =
SUBNET_GATEWAY_AD_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
#[expect(
clippy::too_many_arguments,
reason = "explicit wire fields; a params struct would only rename them"
)]
pub fn try_issue(
root_keypair: &EntityKeypair,
scope: SubnetRef,
topology_epoch: u32,
gateway: EntityId,
gateway_node: u64,
revision: u64,
not_before: u64,
not_after: u64,
) -> Result<Self, SubnetAuthError> {
if not_after <= not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
let mut fact = Self {
version: 1,
scope,
topology_epoch,
issuer: root_keypair.entity_id().clone(),
gateway,
gateway_node,
revision,
not_before,
not_after,
signature: [0u8; 64],
};
let sig = root_keypair
.try_sign(&fact.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
fact.signature = sig.to_bytes();
Ok(fact)
}
fn signed_payload(&self) -> [u8; Self::SIGNED_PAYLOAD_SIZE] {
let mut buf = [0u8; Self::SIGNED_PAYLOAD_SIZE];
let mut off = 0;
buf[off] = self.version;
off += 1;
buf[off..off + 32].copy_from_slice(self.scope.authority.as_bytes());
off += 32;
buf[off..off + 4].copy_from_slice(&self.scope.path.raw().to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
off += 4;
buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
off += 32;
buf[off..off + 32].copy_from_slice(self.gateway.as_bytes());
off += 32;
buf[off..off + 8].copy_from_slice(&self.gateway_node.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.revision.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.not_before.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.not_after.to_le_bytes());
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..SUBNET_GATEWAY_AD_SIG_DOMAIN.len()].copy_from_slice(SUBNET_GATEWAY_AD_SIG_DOMAIN);
buf[SUBNET_GATEWAY_AD_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::WIRE_SIZE);
out.extend_from_slice(&self.signed_payload());
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() != Self::WIRE_SIZE {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let authority = EntityId::from_bytes(read_32(bytes, &mut off));
let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let topology_epoch = read_u32(bytes, &mut off);
let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
let gateway = EntityId::from_bytes(read_32(bytes, &mut off));
let gateway_node = read_u64(bytes, &mut off);
let revision = read_u64(bytes, &mut off);
let not_before = read_u64(bytes, &mut off);
let not_after = read_u64(bytes, &mut off);
if not_after <= not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
Ok(Self {
version,
scope: SubnetRef { authority, path },
topology_epoch,
issuer,
gateway,
gateway_node,
revision,
not_before,
not_after,
signature,
})
}
pub fn verify(&self) -> Result<(), SubnetAuthError> {
let sig = Signature::from_bytes(&self.signature);
self.issuer
.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
pub fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), SubnetAuthError> {
if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(SubnetAuthError::ClockSkewTooLarge);
}
if now < self.not_before.saturating_sub(skew_secs) {
return Err(SubnetAuthError::NotYetValid);
}
if now >= self.not_after.saturating_add(skew_secs) {
return Err(SubnetAuthError::Expired);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetExportPolicy {
pub version: u8,
pub scope: SubnetRef,
pub topology_epoch: u32,
pub issuer: EntityId,
pub exported_channels: Vec<ChannelHash>,
pub revision: u64,
pub not_before: u64,
pub not_after: u64,
pub signature: [u8; 64],
}
impl SubnetExportPolicy {
const FIXED_HEAD_SIZE: usize = 74;
const FIXED_TAIL_SIZE: usize = 24;
pub const fn wire_size(count: usize) -> usize {
Self::FIXED_HEAD_SIZE + count * 8 + Self::FIXED_TAIL_SIZE + 64
}
pub fn try_issue(
root_keypair: &EntityKeypair,
scope: SubnetRef,
topology_epoch: u32,
exported_channels: Vec<ChannelHash>,
revision: u64,
not_before: u64,
not_after: u64,
) -> Result<Self, SubnetAuthError> {
if exported_channels.len() > MAX_EXPORTED_CHANNELS {
return Err(SubnetAuthError::InvalidFormat);
}
if not_after <= not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
let mut fact = Self {
version: 1,
scope,
topology_epoch,
issuer: root_keypair.entity_id().clone(),
exported_channels,
revision,
not_before,
not_after,
signature: [0u8; 64],
};
let sig = root_keypair
.try_sign(&fact.signing_input())
.map_err(|_| SubnetAuthError::InvalidSignature)?;
fact.signature = sig.to_bytes();
Ok(fact)
}
fn signed_payload(&self) -> Vec<u8> {
debug_assert!(
self.exported_channels.len() <= MAX_EXPORTED_CHANNELS,
"exported_channels ({}) exceeds MAX_EXPORTED_CHANNELS — \
constructed around try_issue?",
self.exported_channels.len(),
);
let mut buf = Vec::with_capacity(Self::wire_size(self.exported_channels.len()) - 64);
buf.push(self.version);
buf.extend_from_slice(self.scope.authority.as_bytes());
buf.extend_from_slice(&self.scope.path.raw().to_le_bytes());
buf.extend_from_slice(&self.topology_epoch.to_le_bytes());
buf.extend_from_slice(self.issuer.as_bytes());
buf.push(self.exported_channels.len() as u8);
for hash in &self.exported_channels {
buf.extend_from_slice(&hash.to_le_bytes());
}
buf.extend_from_slice(&self.revision.to_le_bytes());
buf.extend_from_slice(&self.not_before.to_le_bytes());
buf.extend_from_slice(&self.not_after.to_le_bytes());
buf
}
fn signing_input(&self) -> Vec<u8> {
let payload = self.signed_payload();
let mut buf = Vec::with_capacity(SUBNET_EXPORT_POLICY_SIG_DOMAIN.len() + payload.len());
buf.extend_from_slice(SUBNET_EXPORT_POLICY_SIG_DOMAIN);
buf.extend_from_slice(&payload);
buf
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = self.signed_payload();
out.extend_from_slice(&self.signature);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
if bytes.len() < Self::wire_size(0) {
return Err(SubnetAuthError::InvalidFormat);
}
let mut off = 0;
let version = bytes[off];
off += 1;
if version != 1 {
return Err(SubnetAuthError::InvalidFormat);
}
let authority = EntityId::from_bytes(read_32(bytes, &mut off));
let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
let topology_epoch = read_u32(bytes, &mut off);
let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
let count = bytes[off] as usize;
off += 1;
if count > MAX_EXPORTED_CHANNELS || bytes.len() != Self::wire_size(count) {
return Err(SubnetAuthError::InvalidFormat);
}
let mut exported_channels = Vec::with_capacity(count);
for _ in 0..count {
exported_channels.push(read_u64(bytes, &mut off));
}
let revision = read_u64(bytes, &mut off);
let not_before = read_u64(bytes, &mut off);
let not_after = read_u64(bytes, &mut off);
if not_after <= not_before {
return Err(SubnetAuthError::InvalidValidityWindow);
}
let mut signature = [0u8; 64];
signature.copy_from_slice(&bytes[off..off + 64]);
Ok(Self {
version,
scope: SubnetRef { authority, path },
topology_epoch,
issuer,
exported_channels,
revision,
not_before,
not_after,
signature,
})
}
pub fn verify(&self) -> Result<(), SubnetAuthError> {
let sig = Signature::from_bytes(&self.signature);
self.issuer
.verify(&self.signing_input(), &sig)
.map_err(|_| SubnetAuthError::InvalidSignature)
}
pub fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), SubnetAuthError> {
if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(SubnetAuthError::ClockSkewTooLarge);
}
if now < self.not_before.saturating_sub(skew_secs) {
return Err(SubnetAuthError::NotYetValid);
}
if now >= self.not_after.saturating_add(skew_secs) {
return Err(SubnetAuthError::Expired);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubnetControlFact {
Descriptor(SubnetDescriptor),
GatewayAdvertisement(GatewayAdvertisement),
ExportPolicy(SubnetExportPolicy),
RevocationFloor(SubnetRevocationFloor),
}
impl SubnetControlFact {
pub fn kind(&self) -> SubnetFactKind {
match self {
Self::Descriptor(_) => SubnetFactKind::Descriptor,
Self::GatewayAdvertisement(_) => SubnetFactKind::GatewayAdvertisement,
Self::ExportPolicy(_) => SubnetFactKind::ExportPolicy,
Self::RevocationFloor(_) => SubnetFactKind::RevocationFloor,
}
}
pub fn scope(&self) -> &SubnetRef {
match self {
Self::Descriptor(f) => &f.scope,
Self::GatewayAdvertisement(f) => &f.scope,
Self::ExportPolicy(f) => &f.scope,
Self::RevocationFloor(f) => &f.scope,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let body = match self {
Self::Descriptor(f) => f.to_bytes(),
Self::GatewayAdvertisement(f) => f.to_bytes(),
Self::ExportPolicy(f) => f.to_bytes(),
Self::RevocationFloor(f) => f.to_bytes(),
};
let mut out = Vec::with_capacity(1 + body.len());
out.push(self.kind() as u8);
out.extend_from_slice(&body);
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
let (&tag, body) = bytes.split_first().ok_or(SubnetAuthError::InvalidFormat)?;
match SubnetFactKind::try_from_tag(tag)? {
SubnetFactKind::Descriptor => SubnetDescriptor::from_bytes(body).map(Self::Descriptor),
SubnetFactKind::GatewayAdvertisement => {
GatewayAdvertisement::from_bytes(body).map(Self::GatewayAdvertisement)
}
SubnetFactKind::ExportPolicy => {
SubnetExportPolicy::from_bytes(body).map(Self::ExportPolicy)
}
SubnetFactKind::RevocationFloor => {
SubnetRevocationFloor::from_bytes(body).map(Self::RevocationFloor)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubnetControlOutcome {
pub kind: SubnetFactKind,
pub applied: bool,
}
type FactKey = ([u8; 32], u32, u32);
#[derive(Debug, Default)]
pub struct SubnetControlStore {
descriptors: DashMap<FactKey, SubnetDescriptor>,
gateways: DashMap<FactKey, GatewayAdvertisement>,
exports: DashMap<FactKey, SubnetExportPolicy>,
}
impl SubnetControlStore {
pub fn new() -> Self {
Self::default()
}
pub fn apply(
&self,
fact: &SubnetControlFact,
config: &SubnetAuthorityConfig,
now: u64,
skew_secs: u64,
) -> Result<bool, SubnetAuthError> {
if fact.scope().authority != config.authority {
return Err(SubnetAuthError::WrongAuthority);
}
if config.roots.is_empty() {
return Err(SubnetAuthError::UnknownAuthority);
}
match fact {
SubnetControlFact::Descriptor(f) => {
if !config.roots.contains(&f.issuer) {
return Err(SubnetAuthError::IssuerNotAuthorized);
}
f.verify()?;
Ok(Self::apply_monotonic(
&self.descriptors,
key_of(&f.scope, f.topology_epoch),
f,
|s| s.revision,
))
}
SubnetControlFact::GatewayAdvertisement(f) => {
if !config.roots.contains(&f.issuer) {
return Err(SubnetAuthError::IssuerNotAuthorized);
}
f.verify()?;
f.check_time_bounds_at(now, skew_secs)?;
Ok(Self::apply_monotonic(
&self.gateways,
key_of(&f.scope, f.topology_epoch),
f,
|s| s.revision,
))
}
SubnetControlFact::ExportPolicy(f) => {
if !config.roots.contains(&f.issuer) {
return Err(SubnetAuthError::IssuerNotAuthorized);
}
f.verify()?;
f.check_time_bounds_at(now, skew_secs)?;
Ok(Self::apply_monotonic(
&self.exports,
key_of(&f.scope, f.topology_epoch),
f,
|s| s.revision,
))
}
SubnetControlFact::RevocationFloor(_) => Err(SubnetAuthError::InvalidFormat),
}
}
fn apply_monotonic<T: Clone>(
map: &DashMap<FactKey, T>,
key: FactKey,
fact: &T,
revision_of: impl Fn(&T) -> u64,
) -> bool {
let mut changed = false;
map.entry(key)
.and_modify(|stored| {
if revision_of(fact) > revision_of(stored) {
*stored = fact.clone();
changed = true;
}
})
.or_insert_with(|| {
changed = true;
fact.clone()
});
changed
}
pub fn descriptor_for(
&self,
authority: &EntityId,
topology_epoch: u32,
path: TopologySubnetId,
) -> Option<SubnetDescriptor> {
self.descriptors
.get(&(*authority.as_bytes(), topology_epoch, path.raw()))
.map(|e| e.clone())
}
pub fn gateway_for(
&self,
authority: &EntityId,
topology_epoch: u32,
path: TopologySubnetId,
now: u64,
skew_secs: u64,
) -> Option<GatewayAdvertisement> {
self.gateways
.get(&(*authority.as_bytes(), topology_epoch, path.raw()))
.filter(|e| e.check_time_bounds_at(now, skew_secs).is_ok())
.map(|e| e.clone())
}
pub fn export_policy_for(
&self,
authority: &EntityId,
topology_epoch: u32,
path: TopologySubnetId,
now: u64,
skew_secs: u64,
) -> Option<SubnetExportPolicy> {
self.exports
.get(&(*authority.as_bytes(), topology_epoch, path.raw()))
.filter(|e| e.check_time_bounds_at(now, skew_secs).is_ok())
.map(|e| e.clone())
}
pub fn purge_stale_epochs(&self, current_epoch: u32) -> usize {
let before = self.descriptors.len() + self.gateways.len() + self.exports.len();
self.descriptors.retain(|k, _| k.1 >= current_epoch);
self.gateways.retain(|k, _| k.1 >= current_epoch);
self.exports.retain(|k, _| k.1 >= current_epoch);
before - (self.descriptors.len() + self.gateways.len() + self.exports.len())
}
}
fn key_of(scope: &SubnetRef, topology_epoch: u32) -> FactKey {
(
*scope.authority.as_bytes(),
topology_epoch,
scope.path.raw(),
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
fn root() -> EntityKeypair {
EntityKeypair::generate()
}
fn scope_of(authority: &EntityKeypair, path: u32) -> SubnetRef {
SubnetRef {
authority: authority.entity_id().clone(),
path: TopologySubnetId::from_raw(path),
}
}
fn config_of(authority: &EntityKeypair, roots: &[&EntityKeypair]) -> SubnetAuthorityConfig {
SubnetAuthorityConfig {
authority: authority.entity_id().clone(),
roots: roots.iter().map(|r| r.entity_id().clone()).collect(),
maximum_grant_lifetime_secs: 3600,
}
}
const NOW: u64 = 1_700_000_000;
const SKEW: u64 = 30;
fn descriptor(root: &EntityKeypair, path: u32, revision: u64) -> SubnetControlFact {
SubnetControlFact::Descriptor(
SubnetDescriptor::try_issue(root, scope_of(root, path), 1, revision, NOW).unwrap(),
)
}
fn gateway_ad(root: &EntityKeypair, path: u32, revision: u64) -> SubnetControlFact {
SubnetControlFact::GatewayAdvertisement(
GatewayAdvertisement::try_issue(
root,
scope_of(root, path),
1,
EntityKeypair::generate().entity_id().clone(),
0xBEEF,
revision,
NOW - 10,
NOW + 3600,
)
.unwrap(),
)
}
fn export_policy(
root: &EntityKeypair,
path: u32,
revision: u64,
channels: Vec<ChannelHash>,
) -> SubnetControlFact {
SubnetControlFact::ExportPolicy(
SubnetExportPolicy::try_issue(
root,
scope_of(root, path),
1,
channels,
revision,
NOW - 10,
NOW + 3600,
)
.unwrap(),
)
}
#[test]
fn every_kind_round_trips_through_the_tagged_wire() {
let root = root();
let facts = [
descriptor(&root, 0x0101, 7),
gateway_ad(&root, 0x0101, 7),
export_policy(&root, 0x0101, 7, vec![0xAAAA, 0xBBBB]),
SubnetControlFact::RevocationFloor(
SubnetRevocationFloor::try_issue(&root, scope_of(&root, 0x0101), 1, 3, 7, NOW)
.unwrap(),
),
];
for fact in &facts {
let bytes = fact.to_bytes();
let decoded = SubnetControlFact::from_bytes(&bytes).unwrap();
assert_eq!(&decoded, fact);
}
}
#[test]
fn unknown_tags_versions_and_lengths_fail_closed() {
let root = root();
let good = descriptor(&root, 1, 1).to_bytes();
let mut bad_tag = good.clone();
bad_tag[0] = 9;
assert_eq!(
SubnetControlFact::from_bytes(&bad_tag),
Err(SubnetAuthError::InvalidFormat)
);
let mut bad_version = good.clone();
bad_version[1] = 2;
assert_eq!(
SubnetControlFact::from_bytes(&bad_version),
Err(SubnetAuthError::InvalidFormat)
);
assert_eq!(
SubnetControlFact::from_bytes(&good[..good.len() - 1]),
Err(SubnetAuthError::InvalidFormat)
);
let mut trailing = good.clone();
trailing.push(0);
assert_eq!(
SubnetControlFact::from_bytes(&trailing),
Err(SubnetAuthError::InvalidFormat)
);
assert_eq!(
SubnetControlFact::from_bytes(&[]),
Err(SubnetAuthError::InvalidFormat)
);
}
#[test]
fn an_export_count_disagreeing_with_the_buffer_fails_closed() {
let root = root();
let bytes = export_policy(&root, 1, 1, vec![0xAAAA, 0xBBBB]).to_bytes();
let mut shrunk = bytes.clone();
shrunk[1 + SubnetExportPolicy::FIXED_HEAD_SIZE - 1] = 1;
assert_eq!(
SubnetControlFact::from_bytes(&shrunk),
Err(SubnetAuthError::InvalidFormat)
);
let mut oversized = bytes;
oversized[1 + SubnetExportPolicy::FIXED_HEAD_SIZE - 1] = (MAX_EXPORTED_CHANNELS + 1) as u8;
assert_eq!(
SubnetControlFact::from_bytes(&oversized),
Err(SubnetAuthError::InvalidFormat)
);
}
#[test]
fn an_unsigned_or_tampered_fact_changes_no_state() {
let root = root();
let store = SubnetControlStore::new();
let config = config_of(&root, &[&root]);
let SubnetControlFact::Descriptor(mut plain) = descriptor(&root, 1, 1) else {
unreachable!()
};
plain.signature = [0u8; 64];
assert_eq!(
store.apply(&SubnetControlFact::Descriptor(plain), &config, NOW, SKEW),
Err(SubnetAuthError::InvalidSignature)
);
let SubnetControlFact::Descriptor(mut tampered) = descriptor(&root, 1, 1) else {
unreachable!()
};
tampered.revision = 99;
assert_eq!(
store.apply(&SubnetControlFact::Descriptor(tampered), &config, NOW, SKEW),
Err(SubnetAuthError::InvalidSignature)
);
assert!(store
.descriptor_for(&config.authority, 1, TopologySubnetId::from_raw(1))
.is_none());
}
#[test]
fn a_wrong_authority_or_non_root_issuer_is_inert() {
let root_a = root();
let root_b = root();
let store = SubnetControlStore::new();
let config_a = config_of(&root_a, &[&root_a]);
assert_eq!(
store.apply(&descriptor(&root_b, 1, 1), &config_a, NOW, SKEW),
Err(SubnetAuthError::WrongAuthority)
);
let outsider = root();
let fact = SubnetDescriptor::try_issue(&outsider, scope_of(&root_a, 1), 1, 1, NOW).unwrap();
assert_eq!(
store.apply(&SubnetControlFact::Descriptor(fact), &config_a, NOW, SKEW),
Err(SubnetAuthError::IssuerNotAuthorized)
);
let empty = SubnetAuthorityConfig {
authority: root_a.entity_id().clone(),
roots: vec![],
maximum_grant_lifetime_secs: 3600,
};
assert_eq!(
store.apply(&descriptor(&root_a, 1, 1), &empty, NOW, SKEW),
Err(SubnetAuthError::UnknownAuthority)
);
assert!(store
.descriptor_for(root_a.entity_id(), 1, TopologySubnetId::from_raw(1))
.is_none());
}
#[test]
fn revisions_are_monotonic_per_scope_and_kind() {
let root = root();
let store = SubnetControlStore::new();
let config = config_of(&root, &[&root]);
assert!(store
.apply(&descriptor(&root, 1, 5), &config, NOW, SKEW)
.unwrap());
assert!(!store
.apply(&descriptor(&root, 1, 5), &config, NOW, SKEW)
.unwrap());
assert!(!store
.apply(&descriptor(&root, 1, 4), &config, NOW, SKEW)
.unwrap());
assert!(store
.apply(&descriptor(&root, 1, 6), &config, NOW, SKEW)
.unwrap());
assert_eq!(
store
.descriptor_for(&config.authority, 1, TopologySubnetId::from_raw(1))
.unwrap()
.revision,
6
);
assert!(store
.apply(&descriptor(&root, 2, 1), &config, NOW, SKEW)
.unwrap());
}
#[test]
fn a_newer_gateway_fact_does_not_suppress_an_export_policy() {
let root = root();
let store = SubnetControlStore::new();
let config = config_of(&root, &[&root]);
assert!(store
.apply(
&export_policy(&root, 1, 1, vec![0xAAAA]),
&config,
NOW,
SKEW
)
.unwrap());
assert!(store
.apply(&gateway_ad(&root, 1, 99), &config, NOW, SKEW)
.unwrap());
let policy = store
.export_policy_for(
&config.authority,
1,
TopologySubnetId::from_raw(1),
NOW,
SKEW,
)
.unwrap();
assert_eq!(policy.exported_channels, vec![0xAAAA]);
assert!(store
.apply(
&export_policy(&root, 1, 2, vec![0xBBBB]),
&config,
NOW,
SKEW
)
.unwrap());
}
#[test]
fn replay_and_reorder_converge_to_max_revision_state() {
let root = root();
let config = config_of(&root, &[&root]);
let facts = [
descriptor(&root, 1, 3),
descriptor(&root, 1, 1),
descriptor(&root, 1, 2),
gateway_ad(&root, 1, 2),
gateway_ad(&root, 1, 1),
export_policy(&root, 1, 2, vec![0xCC]),
export_policy(&root, 1, 1, vec![0xDD]),
];
for order in [[0usize, 1, 2, 3, 4, 5, 6], [6, 5, 4, 3, 2, 1, 0]] {
let store = SubnetControlStore::new();
for &i in &order {
let _ = store.apply(&facts[i], &config, NOW, SKEW).unwrap();
}
for &i in &order {
assert!(
!store.apply(&facts[i], &config, NOW, SKEW).unwrap(),
"a full replay must change nothing"
);
}
let path = TopologySubnetId::from_raw(1);
assert_eq!(
store
.descriptor_for(&config.authority, 1, path)
.unwrap()
.revision,
3
);
assert_eq!(
store
.gateway_for(&config.authority, 1, path, NOW, SKEW)
.unwrap()
.revision,
2
);
assert_eq!(
store
.export_policy_for(&config.authority, 1, path, NOW, SKEW)
.unwrap()
.exported_channels,
vec![0xCC]
);
}
}
#[test]
fn windowed_kinds_expire_at_read_and_refuse_at_apply() {
let root = root();
let store = SubnetControlStore::new();
let config = config_of(&root, &[&root]);
assert!(store
.apply(&gateway_ad(&root, 1, 1), &config, NOW, SKEW)
.unwrap());
let path = TopologySubnetId::from_raw(1);
assert!(store
.gateway_for(&config.authority, 1, path, NOW, SKEW)
.is_some());
assert!(store
.gateway_for(&config.authority, 1, path, NOW + 7200, SKEW)
.is_none());
assert_eq!(
store.apply(&gateway_ad(&root, 2, 1), &config, NOW + 7200, SKEW),
Err(SubnetAuthError::Expired)
);
}
#[test]
fn floors_are_routed_to_the_registry_not_stored_here() {
let root = root();
let store = SubnetControlStore::new();
let config = config_of(&root, &[&root]);
let floor = SubnetControlFact::RevocationFloor(
SubnetRevocationFloor::try_issue(&root, scope_of(&root, 1), 1, 3, 1, NOW).unwrap(),
);
assert_eq!(
store.apply(&floor, &config, NOW, SKEW),
Err(SubnetAuthError::InvalidFormat),
"the store must not become a second revocation authority"
);
}
#[test]
fn purging_stale_epochs_keeps_current_and_future_facts() {
let root = root();
let store = SubnetControlStore::new();
let config = config_of(&root, &[&root]);
let at_epoch = |epoch: u32, path: u32| {
SubnetControlFact::Descriptor(
SubnetDescriptor::try_issue(&root, scope_of(&root, path), epoch, 1, NOW).unwrap(),
)
};
assert!(store.apply(&at_epoch(1, 1), &config, NOW, SKEW).unwrap());
assert!(store.apply(&at_epoch(2, 2), &config, NOW, SKEW).unwrap());
assert!(store.apply(&at_epoch(3, 3), &config, NOW, SKEW).unwrap());
assert_eq!(store.purge_stale_epochs(2), 1);
assert!(store
.descriptor_for(&config.authority, 1, TopologySubnetId::from_raw(1))
.is_none());
assert!(store
.descriptor_for(&config.authority, 2, TopologySubnetId::from_raw(2))
.is_some());
assert!(store
.descriptor_for(&config.authority, 3, TopologySubnetId::from_raw(3))
.is_some());
}
}