1use dashmap::DashMap;
45use ed25519_dalek::Signature;
46
47use super::auth::{
48 read_32, read_u32, read_u64, SubnetAuthError, SubnetAuthorityConfig, SubnetRef,
49 SubnetRevocationFloor, MAX_TOKEN_CLOCK_SKEW_SECS,
50};
51use super::id::TopologySubnetId;
52use crate::adapter::net::channel::ChannelHash;
53use crate::adapter::net::identity::{EntityId, EntityKeypair};
54
55pub const SUBNET_DESCRIPTOR_SIG_DOMAIN: &[u8] = b"net.subnet.descriptor.v1";
57pub const SUBNET_GATEWAY_AD_SIG_DOMAIN: &[u8] = b"net.subnet.gateway-ad.v1";
59pub const SUBNET_EXPORT_POLICY_SIG_DOMAIN: &[u8] = b"net.subnet.export-policy.v1";
61
62pub const MAX_EXPORTED_CHANNELS: usize = 16;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[repr(u8)]
75pub enum SubnetFactKind {
76 Descriptor = 1,
78 GatewayAdvertisement = 2,
80 ExportPolicy = 3,
82 RevocationFloor = 4,
84}
85
86impl SubnetFactKind {
87 pub fn try_from_tag(tag: u8) -> Result<Self, SubnetAuthError> {
89 match tag {
90 1 => Ok(Self::Descriptor),
91 2 => Ok(Self::GatewayAdvertisement),
92 3 => Ok(Self::ExportPolicy),
93 4 => Ok(Self::RevocationFloor),
94 _ => Err(SubnetAuthError::InvalidFormat),
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct SubnetDescriptor {
113 pub version: u8,
115 pub scope: SubnetRef,
117 pub topology_epoch: u32,
119 pub issuer: EntityId,
121 pub revision: u64,
123 pub issued_at: u64,
126 pub signature: [u8; 64],
128}
129
130impl SubnetDescriptor {
131 pub const SIGNED_PAYLOAD_SIZE: usize = 89;
134 pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
136 const SIGNING_INPUT_SIZE: usize =
137 SUBNET_DESCRIPTOR_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
138
139 pub fn try_issue(
141 root_keypair: &EntityKeypair,
142 scope: SubnetRef,
143 topology_epoch: u32,
144 revision: u64,
145 issued_at: u64,
146 ) -> Result<Self, SubnetAuthError> {
147 let mut fact = Self {
148 version: 1,
149 scope,
150 topology_epoch,
151 issuer: root_keypair.entity_id().clone(),
152 revision,
153 issued_at,
154 signature: [0u8; 64],
155 };
156 let sig = root_keypair
157 .try_sign(&fact.signing_input())
158 .map_err(|_| SubnetAuthError::InvalidSignature)?;
159 fact.signature = sig.to_bytes();
160 Ok(fact)
161 }
162
163 fn signed_payload(&self) -> [u8; Self::SIGNED_PAYLOAD_SIZE] {
164 let mut buf = [0u8; Self::SIGNED_PAYLOAD_SIZE];
165 let mut off = 0;
166 buf[off] = self.version;
167 off += 1;
168 buf[off..off + 32].copy_from_slice(self.scope.authority.as_bytes());
169 off += 32;
170 buf[off..off + 4].copy_from_slice(&self.scope.path.raw().to_le_bytes());
171 off += 4;
172 buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
173 off += 4;
174 buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
175 off += 32;
176 buf[off..off + 8].copy_from_slice(&self.revision.to_le_bytes());
177 off += 8;
178 buf[off..off + 8].copy_from_slice(&self.issued_at.to_le_bytes());
179 buf
180 }
181
182 fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
183 let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
184 buf[..SUBNET_DESCRIPTOR_SIG_DOMAIN.len()].copy_from_slice(SUBNET_DESCRIPTOR_SIG_DOMAIN);
185 buf[SUBNET_DESCRIPTOR_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
186 buf
187 }
188
189 pub fn to_bytes(&self) -> Vec<u8> {
191 let mut out = Vec::with_capacity(Self::WIRE_SIZE);
192 out.extend_from_slice(&self.signed_payload());
193 out.extend_from_slice(&self.signature);
194 out
195 }
196
197 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
199 if bytes.len() != Self::WIRE_SIZE {
200 return Err(SubnetAuthError::InvalidFormat);
201 }
202 let mut off = 0;
203 let version = bytes[off];
204 off += 1;
205 if version != 1 {
206 return Err(SubnetAuthError::InvalidFormat);
207 }
208 let authority = EntityId::from_bytes(read_32(bytes, &mut off));
209 let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
210 let topology_epoch = read_u32(bytes, &mut off);
211 let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
212 let revision = read_u64(bytes, &mut off);
213 let issued_at = read_u64(bytes, &mut off);
214 let mut signature = [0u8; 64];
215 signature.copy_from_slice(&bytes[off..off + 64]);
216 Ok(Self {
217 version,
218 scope: SubnetRef { authority, path },
219 topology_epoch,
220 issuer,
221 revision,
222 issued_at,
223 signature,
224 })
225 }
226
227 pub fn verify(&self) -> Result<(), SubnetAuthError> {
230 let sig = Signature::from_bytes(&self.signature);
231 self.issuer
232 .verify(&self.signing_input(), &sig)
233 .map_err(|_| SubnetAuthError::InvalidSignature)
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct GatewayAdvertisement {
252 pub version: u8,
254 pub scope: SubnetRef,
256 pub topology_epoch: u32,
258 pub issuer: EntityId,
260 pub gateway: EntityId,
262 pub gateway_node: u64,
266 pub revision: u64,
268 pub not_before: u64,
270 pub not_after: u64,
272 pub signature: [u8; 64],
274}
275
276impl GatewayAdvertisement {
277 pub const SIGNED_PAYLOAD_SIZE: usize = 137;
281 pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
283 const SIGNING_INPUT_SIZE: usize =
284 SUBNET_GATEWAY_AD_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
285
286 #[expect(
289 clippy::too_many_arguments,
290 reason = "explicit wire fields; a params struct would only rename them"
291 )]
292 pub fn try_issue(
293 root_keypair: &EntityKeypair,
294 scope: SubnetRef,
295 topology_epoch: u32,
296 gateway: EntityId,
297 gateway_node: u64,
298 revision: u64,
299 not_before: u64,
300 not_after: u64,
301 ) -> Result<Self, SubnetAuthError> {
302 if not_after <= not_before {
303 return Err(SubnetAuthError::InvalidValidityWindow);
304 }
305 let mut fact = Self {
306 version: 1,
307 scope,
308 topology_epoch,
309 issuer: root_keypair.entity_id().clone(),
310 gateway,
311 gateway_node,
312 revision,
313 not_before,
314 not_after,
315 signature: [0u8; 64],
316 };
317 let sig = root_keypair
318 .try_sign(&fact.signing_input())
319 .map_err(|_| SubnetAuthError::InvalidSignature)?;
320 fact.signature = sig.to_bytes();
321 Ok(fact)
322 }
323
324 fn signed_payload(&self) -> [u8; Self::SIGNED_PAYLOAD_SIZE] {
325 let mut buf = [0u8; Self::SIGNED_PAYLOAD_SIZE];
326 let mut off = 0;
327 buf[off] = self.version;
328 off += 1;
329 buf[off..off + 32].copy_from_slice(self.scope.authority.as_bytes());
330 off += 32;
331 buf[off..off + 4].copy_from_slice(&self.scope.path.raw().to_le_bytes());
332 off += 4;
333 buf[off..off + 4].copy_from_slice(&self.topology_epoch.to_le_bytes());
334 off += 4;
335 buf[off..off + 32].copy_from_slice(self.issuer.as_bytes());
336 off += 32;
337 buf[off..off + 32].copy_from_slice(self.gateway.as_bytes());
338 off += 32;
339 buf[off..off + 8].copy_from_slice(&self.gateway_node.to_le_bytes());
340 off += 8;
341 buf[off..off + 8].copy_from_slice(&self.revision.to_le_bytes());
342 off += 8;
343 buf[off..off + 8].copy_from_slice(&self.not_before.to_le_bytes());
344 off += 8;
345 buf[off..off + 8].copy_from_slice(&self.not_after.to_le_bytes());
346 buf
347 }
348
349 fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
350 let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
351 buf[..SUBNET_GATEWAY_AD_SIG_DOMAIN.len()].copy_from_slice(SUBNET_GATEWAY_AD_SIG_DOMAIN);
352 buf[SUBNET_GATEWAY_AD_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
353 buf
354 }
355
356 pub fn to_bytes(&self) -> Vec<u8> {
358 let mut out = Vec::with_capacity(Self::WIRE_SIZE);
359 out.extend_from_slice(&self.signed_payload());
360 out.extend_from_slice(&self.signature);
361 out
362 }
363
364 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
366 if bytes.len() != Self::WIRE_SIZE {
367 return Err(SubnetAuthError::InvalidFormat);
368 }
369 let mut off = 0;
370 let version = bytes[off];
371 off += 1;
372 if version != 1 {
373 return Err(SubnetAuthError::InvalidFormat);
374 }
375 let authority = EntityId::from_bytes(read_32(bytes, &mut off));
376 let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
377 let topology_epoch = read_u32(bytes, &mut off);
378 let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
379 let gateway = EntityId::from_bytes(read_32(bytes, &mut off));
380 let gateway_node = read_u64(bytes, &mut off);
381 let revision = read_u64(bytes, &mut off);
382 let not_before = read_u64(bytes, &mut off);
383 let not_after = read_u64(bytes, &mut off);
384 if not_after <= not_before {
385 return Err(SubnetAuthError::InvalidValidityWindow);
386 }
387 let mut signature = [0u8; 64];
388 signature.copy_from_slice(&bytes[off..off + 64]);
389 Ok(Self {
390 version,
391 scope: SubnetRef { authority, path },
392 topology_epoch,
393 issuer,
394 gateway,
395 gateway_node,
396 revision,
397 not_before,
398 not_after,
399 signature,
400 })
401 }
402
403 pub fn verify(&self) -> Result<(), SubnetAuthError> {
405 let sig = Signature::from_bytes(&self.signature);
406 self.issuer
407 .verify(&self.signing_input(), &sig)
408 .map_err(|_| SubnetAuthError::InvalidSignature)
409 }
410
411 pub fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), SubnetAuthError> {
413 if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
414 return Err(SubnetAuthError::ClockSkewTooLarge);
415 }
416 if now < self.not_before.saturating_sub(skew_secs) {
417 return Err(SubnetAuthError::NotYetValid);
418 }
419 if now >= self.not_after.saturating_add(skew_secs) {
420 return Err(SubnetAuthError::Expired);
421 }
422 Ok(())
423 }
424}
425
426#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct SubnetExportPolicy {
443 pub version: u8,
445 pub scope: SubnetRef,
447 pub topology_epoch: u32,
449 pub issuer: EntityId,
451 pub exported_channels: Vec<ChannelHash>,
455 pub revision: u64,
457 pub not_before: u64,
459 pub not_after: u64,
461 pub signature: [u8; 64],
463}
464
465impl SubnetExportPolicy {
466 const FIXED_HEAD_SIZE: usize = 74;
469 const FIXED_TAIL_SIZE: usize = 24;
471
472 pub const fn wire_size(count: usize) -> usize {
474 Self::FIXED_HEAD_SIZE + count * 8 + Self::FIXED_TAIL_SIZE + 64
475 }
476
477 pub fn try_issue(
479 root_keypair: &EntityKeypair,
480 scope: SubnetRef,
481 topology_epoch: u32,
482 exported_channels: Vec<ChannelHash>,
483 revision: u64,
484 not_before: u64,
485 not_after: u64,
486 ) -> Result<Self, SubnetAuthError> {
487 if exported_channels.len() > MAX_EXPORTED_CHANNELS {
488 return Err(SubnetAuthError::InvalidFormat);
489 }
490 if not_after <= not_before {
491 return Err(SubnetAuthError::InvalidValidityWindow);
492 }
493 let mut fact = Self {
494 version: 1,
495 scope,
496 topology_epoch,
497 issuer: root_keypair.entity_id().clone(),
498 exported_channels,
499 revision,
500 not_before,
501 not_after,
502 signature: [0u8; 64],
503 };
504 let sig = root_keypair
505 .try_sign(&fact.signing_input())
506 .map_err(|_| SubnetAuthError::InvalidSignature)?;
507 fact.signature = sig.to_bytes();
508 Ok(fact)
509 }
510
511 fn signed_payload(&self) -> Vec<u8> {
514 debug_assert!(
522 self.exported_channels.len() <= MAX_EXPORTED_CHANNELS,
523 "exported_channels ({}) exceeds MAX_EXPORTED_CHANNELS — \
524 constructed around try_issue?",
525 self.exported_channels.len(),
526 );
527 let mut buf = Vec::with_capacity(Self::wire_size(self.exported_channels.len()) - 64);
528 buf.push(self.version);
529 buf.extend_from_slice(self.scope.authority.as_bytes());
530 buf.extend_from_slice(&self.scope.path.raw().to_le_bytes());
531 buf.extend_from_slice(&self.topology_epoch.to_le_bytes());
532 buf.extend_from_slice(self.issuer.as_bytes());
533 buf.push(self.exported_channels.len() as u8);
534 for hash in &self.exported_channels {
535 buf.extend_from_slice(&hash.to_le_bytes());
536 }
537 buf.extend_from_slice(&self.revision.to_le_bytes());
538 buf.extend_from_slice(&self.not_before.to_le_bytes());
539 buf.extend_from_slice(&self.not_after.to_le_bytes());
540 buf
541 }
542
543 fn signing_input(&self) -> Vec<u8> {
544 let payload = self.signed_payload();
545 let mut buf = Vec::with_capacity(SUBNET_EXPORT_POLICY_SIG_DOMAIN.len() + payload.len());
546 buf.extend_from_slice(SUBNET_EXPORT_POLICY_SIG_DOMAIN);
547 buf.extend_from_slice(&payload);
548 buf
549 }
550
551 pub fn to_bytes(&self) -> Vec<u8> {
553 let mut out = self.signed_payload();
554 out.extend_from_slice(&self.signature);
555 out
556 }
557
558 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
562 if bytes.len() < Self::wire_size(0) {
563 return Err(SubnetAuthError::InvalidFormat);
564 }
565 let mut off = 0;
566 let version = bytes[off];
567 off += 1;
568 if version != 1 {
569 return Err(SubnetAuthError::InvalidFormat);
570 }
571 let authority = EntityId::from_bytes(read_32(bytes, &mut off));
572 let path = TopologySubnetId::from_raw(read_u32(bytes, &mut off));
573 let topology_epoch = read_u32(bytes, &mut off);
574 let issuer = EntityId::from_bytes(read_32(bytes, &mut off));
575 let count = bytes[off] as usize;
576 off += 1;
577 if count > MAX_EXPORTED_CHANNELS || bytes.len() != Self::wire_size(count) {
578 return Err(SubnetAuthError::InvalidFormat);
579 }
580 let mut exported_channels = Vec::with_capacity(count);
581 for _ in 0..count {
582 exported_channels.push(read_u64(bytes, &mut off));
583 }
584 let revision = read_u64(bytes, &mut off);
585 let not_before = read_u64(bytes, &mut off);
586 let not_after = read_u64(bytes, &mut off);
587 if not_after <= not_before {
588 return Err(SubnetAuthError::InvalidValidityWindow);
589 }
590 let mut signature = [0u8; 64];
591 signature.copy_from_slice(&bytes[off..off + 64]);
592 Ok(Self {
593 version,
594 scope: SubnetRef { authority, path },
595 topology_epoch,
596 issuer,
597 exported_channels,
598 revision,
599 not_before,
600 not_after,
601 signature,
602 })
603 }
604
605 pub fn verify(&self) -> Result<(), SubnetAuthError> {
607 let sig = Signature::from_bytes(&self.signature);
608 self.issuer
609 .verify(&self.signing_input(), &sig)
610 .map_err(|_| SubnetAuthError::InvalidSignature)
611 }
612
613 pub fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), SubnetAuthError> {
615 if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
616 return Err(SubnetAuthError::ClockSkewTooLarge);
617 }
618 if now < self.not_before.saturating_sub(skew_secs) {
619 return Err(SubnetAuthError::NotYetValid);
620 }
621 if now >= self.not_after.saturating_add(skew_secs) {
622 return Err(SubnetAuthError::Expired);
623 }
624 Ok(())
625 }
626}
627
628#[derive(Debug, Clone, PartialEq, Eq)]
634pub enum SubnetControlFact {
635 Descriptor(SubnetDescriptor),
637 GatewayAdvertisement(GatewayAdvertisement),
639 ExportPolicy(SubnetExportPolicy),
641 RevocationFloor(SubnetRevocationFloor),
645}
646
647impl SubnetControlFact {
648 pub fn kind(&self) -> SubnetFactKind {
650 match self {
651 Self::Descriptor(_) => SubnetFactKind::Descriptor,
652 Self::GatewayAdvertisement(_) => SubnetFactKind::GatewayAdvertisement,
653 Self::ExportPolicy(_) => SubnetFactKind::ExportPolicy,
654 Self::RevocationFloor(_) => SubnetFactKind::RevocationFloor,
655 }
656 }
657
658 pub fn scope(&self) -> &SubnetRef {
660 match self {
661 Self::Descriptor(f) => &f.scope,
662 Self::GatewayAdvertisement(f) => &f.scope,
663 Self::ExportPolicy(f) => &f.scope,
664 Self::RevocationFloor(f) => &f.scope,
665 }
666 }
667
668 pub fn to_bytes(&self) -> Vec<u8> {
670 let body = match self {
671 Self::Descriptor(f) => f.to_bytes(),
672 Self::GatewayAdvertisement(f) => f.to_bytes(),
673 Self::ExportPolicy(f) => f.to_bytes(),
674 Self::RevocationFloor(f) => f.to_bytes(),
675 };
676 let mut out = Vec::with_capacity(1 + body.len());
677 out.push(self.kind() as u8);
678 out.extend_from_slice(&body);
679 out
680 }
681
682 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SubnetAuthError> {
685 let (&tag, body) = bytes.split_first().ok_or(SubnetAuthError::InvalidFormat)?;
686 match SubnetFactKind::try_from_tag(tag)? {
687 SubnetFactKind::Descriptor => SubnetDescriptor::from_bytes(body).map(Self::Descriptor),
688 SubnetFactKind::GatewayAdvertisement => {
689 GatewayAdvertisement::from_bytes(body).map(Self::GatewayAdvertisement)
690 }
691 SubnetFactKind::ExportPolicy => {
692 SubnetExportPolicy::from_bytes(body).map(Self::ExportPolicy)
693 }
694 SubnetFactKind::RevocationFloor => {
695 SubnetRevocationFloor::from_bytes(body).map(Self::RevocationFloor)
696 }
697 }
698 }
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
705pub struct SubnetControlOutcome {
706 pub kind: SubnetFactKind,
708 pub applied: bool,
710}
711
712type FactKey = ([u8; 32], u32, u32);
722
723#[derive(Debug, Default)]
731pub struct SubnetControlStore {
732 descriptors: DashMap<FactKey, SubnetDescriptor>,
733 gateways: DashMap<FactKey, GatewayAdvertisement>,
734 exports: DashMap<FactKey, SubnetExportPolicy>,
735}
736
737impl SubnetControlStore {
738 pub fn new() -> Self {
740 Self::default()
741 }
742
743 pub fn apply(
764 &self,
765 fact: &SubnetControlFact,
766 config: &SubnetAuthorityConfig,
767 now: u64,
768 skew_secs: u64,
769 ) -> Result<bool, SubnetAuthError> {
770 if fact.scope().authority != config.authority {
771 return Err(SubnetAuthError::WrongAuthority);
772 }
773 if config.roots.is_empty() {
774 return Err(SubnetAuthError::UnknownAuthority);
775 }
776 match fact {
777 SubnetControlFact::Descriptor(f) => {
778 if !config.roots.contains(&f.issuer) {
779 return Err(SubnetAuthError::IssuerNotAuthorized);
780 }
781 f.verify()?;
782 Ok(Self::apply_monotonic(
783 &self.descriptors,
784 key_of(&f.scope, f.topology_epoch),
785 f,
786 |s| s.revision,
787 ))
788 }
789 SubnetControlFact::GatewayAdvertisement(f) => {
790 if !config.roots.contains(&f.issuer) {
791 return Err(SubnetAuthError::IssuerNotAuthorized);
792 }
793 f.verify()?;
794 f.check_time_bounds_at(now, skew_secs)?;
795 Ok(Self::apply_monotonic(
796 &self.gateways,
797 key_of(&f.scope, f.topology_epoch),
798 f,
799 |s| s.revision,
800 ))
801 }
802 SubnetControlFact::ExportPolicy(f) => {
803 if !config.roots.contains(&f.issuer) {
804 return Err(SubnetAuthError::IssuerNotAuthorized);
805 }
806 f.verify()?;
807 f.check_time_bounds_at(now, skew_secs)?;
808 Ok(Self::apply_monotonic(
809 &self.exports,
810 key_of(&f.scope, f.topology_epoch),
811 f,
812 |s| s.revision,
813 ))
814 }
815 SubnetControlFact::RevocationFloor(_) => Err(SubnetAuthError::InvalidFormat),
816 }
817 }
818
819 fn apply_monotonic<T: Clone>(
823 map: &DashMap<FactKey, T>,
824 key: FactKey,
825 fact: &T,
826 revision_of: impl Fn(&T) -> u64,
827 ) -> bool {
828 let mut changed = false;
829 map.entry(key)
830 .and_modify(|stored| {
831 if revision_of(fact) > revision_of(stored) {
832 *stored = fact.clone();
833 changed = true;
834 }
835 })
836 .or_insert_with(|| {
837 changed = true;
838 fact.clone()
839 });
840 changed
841 }
842
843 pub fn descriptor_for(
845 &self,
846 authority: &EntityId,
847 topology_epoch: u32,
848 path: TopologySubnetId,
849 ) -> Option<SubnetDescriptor> {
850 self.descriptors
851 .get(&(*authority.as_bytes(), topology_epoch, path.raw()))
852 .map(|e| e.clone())
853 }
854
855 pub fn gateway_for(
860 &self,
861 authority: &EntityId,
862 topology_epoch: u32,
863 path: TopologySubnetId,
864 now: u64,
865 skew_secs: u64,
866 ) -> Option<GatewayAdvertisement> {
867 self.gateways
868 .get(&(*authority.as_bytes(), topology_epoch, path.raw()))
869 .filter(|e| e.check_time_bounds_at(now, skew_secs).is_ok())
870 .map(|e| e.clone())
871 }
872
873 pub fn export_policy_for(
876 &self,
877 authority: &EntityId,
878 topology_epoch: u32,
879 path: TopologySubnetId,
880 now: u64,
881 skew_secs: u64,
882 ) -> Option<SubnetExportPolicy> {
883 self.exports
884 .get(&(*authority.as_bytes(), topology_epoch, path.raw()))
885 .filter(|e| e.check_time_bounds_at(now, skew_secs).is_ok())
886 .map(|e| e.clone())
887 }
888
889 pub fn purge_stale_epochs(&self, current_epoch: u32) -> usize {
894 let before = self.descriptors.len() + self.gateways.len() + self.exports.len();
895 self.descriptors.retain(|k, _| k.1 >= current_epoch);
896 self.gateways.retain(|k, _| k.1 >= current_epoch);
897 self.exports.retain(|k, _| k.1 >= current_epoch);
898 before - (self.descriptors.len() + self.gateways.len() + self.exports.len())
899 }
900}
901
902fn key_of(scope: &SubnetRef, topology_epoch: u32) -> FactKey {
903 (
904 *scope.authority.as_bytes(),
905 topology_epoch,
906 scope.path.raw(),
907 )
908}
909
910#[cfg(test)]
911mod tests {
912 #![allow(clippy::unwrap_used)]
913
914 use super::*;
915
916 fn root() -> EntityKeypair {
917 EntityKeypair::generate()
918 }
919
920 fn scope_of(authority: &EntityKeypair, path: u32) -> SubnetRef {
921 SubnetRef {
922 authority: authority.entity_id().clone(),
923 path: TopologySubnetId::from_raw(path),
924 }
925 }
926
927 fn config_of(authority: &EntityKeypair, roots: &[&EntityKeypair]) -> SubnetAuthorityConfig {
928 SubnetAuthorityConfig {
929 authority: authority.entity_id().clone(),
930 roots: roots.iter().map(|r| r.entity_id().clone()).collect(),
931 maximum_grant_lifetime_secs: 3600,
932 }
933 }
934
935 const NOW: u64 = 1_700_000_000;
936 const SKEW: u64 = 30;
937
938 fn descriptor(root: &EntityKeypair, path: u32, revision: u64) -> SubnetControlFact {
939 SubnetControlFact::Descriptor(
940 SubnetDescriptor::try_issue(root, scope_of(root, path), 1, revision, NOW).unwrap(),
941 )
942 }
943
944 fn gateway_ad(root: &EntityKeypair, path: u32, revision: u64) -> SubnetControlFact {
945 SubnetControlFact::GatewayAdvertisement(
946 GatewayAdvertisement::try_issue(
947 root,
948 scope_of(root, path),
949 1,
950 EntityKeypair::generate().entity_id().clone(),
951 0xBEEF,
952 revision,
953 NOW - 10,
954 NOW + 3600,
955 )
956 .unwrap(),
957 )
958 }
959
960 fn export_policy(
961 root: &EntityKeypair,
962 path: u32,
963 revision: u64,
964 channels: Vec<ChannelHash>,
965 ) -> SubnetControlFact {
966 SubnetControlFact::ExportPolicy(
967 SubnetExportPolicy::try_issue(
968 root,
969 scope_of(root, path),
970 1,
971 channels,
972 revision,
973 NOW - 10,
974 NOW + 3600,
975 )
976 .unwrap(),
977 )
978 }
979
980 #[test]
981 fn every_kind_round_trips_through_the_tagged_wire() {
982 let root = root();
983 let facts = [
984 descriptor(&root, 0x0101, 7),
985 gateway_ad(&root, 0x0101, 7),
986 export_policy(&root, 0x0101, 7, vec![0xAAAA, 0xBBBB]),
987 SubnetControlFact::RevocationFloor(
988 SubnetRevocationFloor::try_issue(&root, scope_of(&root, 0x0101), 1, 3, 7, NOW)
989 .unwrap(),
990 ),
991 ];
992 for fact in &facts {
993 let bytes = fact.to_bytes();
994 let decoded = SubnetControlFact::from_bytes(&bytes).unwrap();
995 assert_eq!(&decoded, fact);
996 }
997 }
998
999 #[test]
1000 fn unknown_tags_versions_and_lengths_fail_closed() {
1001 let root = root();
1002 let good = descriptor(&root, 1, 1).to_bytes();
1003
1004 let mut bad_tag = good.clone();
1006 bad_tag[0] = 9;
1007 assert_eq!(
1008 SubnetControlFact::from_bytes(&bad_tag),
1009 Err(SubnetAuthError::InvalidFormat)
1010 );
1011 let mut bad_version = good.clone();
1013 bad_version[1] = 2;
1014 assert_eq!(
1015 SubnetControlFact::from_bytes(&bad_version),
1016 Err(SubnetAuthError::InvalidFormat)
1017 );
1018 assert_eq!(
1020 SubnetControlFact::from_bytes(&good[..good.len() - 1]),
1021 Err(SubnetAuthError::InvalidFormat)
1022 );
1023 let mut trailing = good.clone();
1024 trailing.push(0);
1025 assert_eq!(
1026 SubnetControlFact::from_bytes(&trailing),
1027 Err(SubnetAuthError::InvalidFormat)
1028 );
1029 assert_eq!(
1031 SubnetControlFact::from_bytes(&[]),
1032 Err(SubnetAuthError::InvalidFormat)
1033 );
1034 }
1035
1036 #[test]
1037 fn an_export_count_disagreeing_with_the_buffer_fails_closed() {
1038 let root = root();
1039 let bytes = export_policy(&root, 1, 1, vec![0xAAAA, 0xBBBB]).to_bytes();
1040 let mut shrunk = bytes.clone();
1042 shrunk[1 + SubnetExportPolicy::FIXED_HEAD_SIZE - 1] = 1;
1043 assert_eq!(
1044 SubnetControlFact::from_bytes(&shrunk),
1045 Err(SubnetAuthError::InvalidFormat)
1046 );
1047 let mut oversized = bytes;
1049 oversized[1 + SubnetExportPolicy::FIXED_HEAD_SIZE - 1] = (MAX_EXPORTED_CHANNELS + 1) as u8;
1050 assert_eq!(
1051 SubnetControlFact::from_bytes(&oversized),
1052 Err(SubnetAuthError::InvalidFormat)
1053 );
1054 }
1055
1056 #[test]
1057 fn an_unsigned_or_tampered_fact_changes_no_state() {
1058 let root = root();
1059 let store = SubnetControlStore::new();
1060 let config = config_of(&root, &[&root]);
1061
1062 let SubnetControlFact::Descriptor(mut plain) = descriptor(&root, 1, 1) else {
1064 unreachable!()
1065 };
1066 plain.signature = [0u8; 64];
1067 assert_eq!(
1068 store.apply(&SubnetControlFact::Descriptor(plain), &config, NOW, SKEW),
1069 Err(SubnetAuthError::InvalidSignature)
1070 );
1071
1072 let SubnetControlFact::Descriptor(mut tampered) = descriptor(&root, 1, 1) else {
1074 unreachable!()
1075 };
1076 tampered.revision = 99;
1077 assert_eq!(
1078 store.apply(&SubnetControlFact::Descriptor(tampered), &config, NOW, SKEW),
1079 Err(SubnetAuthError::InvalidSignature)
1080 );
1081
1082 assert!(store
1083 .descriptor_for(&config.authority, 1, TopologySubnetId::from_raw(1))
1084 .is_none());
1085 }
1086
1087 #[test]
1088 fn a_wrong_authority_or_non_root_issuer_is_inert() {
1089 let root_a = root();
1090 let root_b = root();
1091 let store = SubnetControlStore::new();
1092 let config_a = config_of(&root_a, &[&root_a]);
1093
1094 assert_eq!(
1096 store.apply(&descriptor(&root_b, 1, 1), &config_a, NOW, SKEW),
1097 Err(SubnetAuthError::WrongAuthority)
1098 );
1099
1100 let outsider = root();
1104 let fact = SubnetDescriptor::try_issue(&outsider, scope_of(&root_a, 1), 1, 1, NOW).unwrap();
1105 assert_eq!(
1106 store.apply(&SubnetControlFact::Descriptor(fact), &config_a, NOW, SKEW),
1107 Err(SubnetAuthError::IssuerNotAuthorized)
1108 );
1109
1110 let empty = SubnetAuthorityConfig {
1112 authority: root_a.entity_id().clone(),
1113 roots: vec![],
1114 maximum_grant_lifetime_secs: 3600,
1115 };
1116 assert_eq!(
1117 store.apply(&descriptor(&root_a, 1, 1), &empty, NOW, SKEW),
1118 Err(SubnetAuthError::UnknownAuthority)
1119 );
1120
1121 assert!(store
1122 .descriptor_for(root_a.entity_id(), 1, TopologySubnetId::from_raw(1))
1123 .is_none());
1124 }
1125
1126 #[test]
1127 fn revisions_are_monotonic_per_scope_and_kind() {
1128 let root = root();
1129 let store = SubnetControlStore::new();
1130 let config = config_of(&root, &[&root]);
1131
1132 assert!(store
1133 .apply(&descriptor(&root, 1, 5), &config, NOW, SKEW)
1134 .unwrap());
1135 assert!(!store
1137 .apply(&descriptor(&root, 1, 5), &config, NOW, SKEW)
1138 .unwrap());
1139 assert!(!store
1140 .apply(&descriptor(&root, 1, 4), &config, NOW, SKEW)
1141 .unwrap());
1142 assert!(store
1144 .apply(&descriptor(&root, 1, 6), &config, NOW, SKEW)
1145 .unwrap());
1146 assert_eq!(
1147 store
1148 .descriptor_for(&config.authority, 1, TopologySubnetId::from_raw(1))
1149 .unwrap()
1150 .revision,
1151 6
1152 );
1153 assert!(store
1155 .apply(&descriptor(&root, 2, 1), &config, NOW, SKEW)
1156 .unwrap());
1157 }
1158
1159 #[test]
1160 fn a_newer_gateway_fact_does_not_suppress_an_export_policy() {
1161 let root = root();
1162 let store = SubnetControlStore::new();
1163 let config = config_of(&root, &[&root]);
1164
1165 assert!(store
1166 .apply(
1167 &export_policy(&root, 1, 1, vec![0xAAAA]),
1168 &config,
1169 NOW,
1170 SKEW
1171 )
1172 .unwrap());
1173 assert!(store
1175 .apply(&gateway_ad(&root, 1, 99), &config, NOW, SKEW)
1176 .unwrap());
1177
1178 let policy = store
1180 .export_policy_for(
1181 &config.authority,
1182 1,
1183 TopologySubnetId::from_raw(1),
1184 NOW,
1185 SKEW,
1186 )
1187 .unwrap();
1188 assert_eq!(policy.exported_channels, vec![0xAAAA]);
1189 assert!(store
1191 .apply(
1192 &export_policy(&root, 1, 2, vec![0xBBBB]),
1193 &config,
1194 NOW,
1195 SKEW
1196 )
1197 .unwrap());
1198 }
1199
1200 #[test]
1201 fn replay_and_reorder_converge_to_max_revision_state() {
1202 let root = root();
1203 let config = config_of(&root, &[&root]);
1204 let facts = [
1205 descriptor(&root, 1, 3),
1206 descriptor(&root, 1, 1),
1207 descriptor(&root, 1, 2),
1208 gateway_ad(&root, 1, 2),
1209 gateway_ad(&root, 1, 1),
1210 export_policy(&root, 1, 2, vec![0xCC]),
1211 export_policy(&root, 1, 1, vec![0xDD]),
1212 ];
1213 for order in [[0usize, 1, 2, 3, 4, 5, 6], [6, 5, 4, 3, 2, 1, 0]] {
1215 let store = SubnetControlStore::new();
1216 for &i in &order {
1217 let _ = store.apply(&facts[i], &config, NOW, SKEW).unwrap();
1218 }
1219 for &i in &order {
1220 assert!(
1221 !store.apply(&facts[i], &config, NOW, SKEW).unwrap(),
1222 "a full replay must change nothing"
1223 );
1224 }
1225 let path = TopologySubnetId::from_raw(1);
1226 assert_eq!(
1227 store
1228 .descriptor_for(&config.authority, 1, path)
1229 .unwrap()
1230 .revision,
1231 3
1232 );
1233 assert_eq!(
1234 store
1235 .gateway_for(&config.authority, 1, path, NOW, SKEW)
1236 .unwrap()
1237 .revision,
1238 2
1239 );
1240 assert_eq!(
1241 store
1242 .export_policy_for(&config.authority, 1, path, NOW, SKEW)
1243 .unwrap()
1244 .exported_channels,
1245 vec![0xCC]
1246 );
1247 }
1248 }
1249
1250 #[test]
1251 fn windowed_kinds_expire_at_read_and_refuse_at_apply() {
1252 let root = root();
1253 let store = SubnetControlStore::new();
1254 let config = config_of(&root, &[&root]);
1255
1256 assert!(store
1257 .apply(&gateway_ad(&root, 1, 1), &config, NOW, SKEW)
1258 .unwrap());
1259 let path = TopologySubnetId::from_raw(1);
1260 assert!(store
1261 .gateway_for(&config.authority, 1, path, NOW, SKEW)
1262 .is_some());
1263 assert!(store
1265 .gateway_for(&config.authority, 1, path, NOW + 7200, SKEW)
1266 .is_none());
1267 assert_eq!(
1269 store.apply(&gateway_ad(&root, 2, 1), &config, NOW + 7200, SKEW),
1270 Err(SubnetAuthError::Expired)
1271 );
1272 }
1273
1274 #[test]
1275 fn floors_are_routed_to_the_registry_not_stored_here() {
1276 let root = root();
1277 let store = SubnetControlStore::new();
1278 let config = config_of(&root, &[&root]);
1279 let floor = SubnetControlFact::RevocationFloor(
1280 SubnetRevocationFloor::try_issue(&root, scope_of(&root, 1), 1, 3, 1, NOW).unwrap(),
1281 );
1282 assert_eq!(
1283 store.apply(&floor, &config, NOW, SKEW),
1284 Err(SubnetAuthError::InvalidFormat),
1285 "the store must not become a second revocation authority"
1286 );
1287 }
1288
1289 #[test]
1290 fn purging_stale_epochs_keeps_current_and_future_facts() {
1291 let root = root();
1292 let store = SubnetControlStore::new();
1293 let config = config_of(&root, &[&root]);
1294
1295 let at_epoch = |epoch: u32, path: u32| {
1296 SubnetControlFact::Descriptor(
1297 SubnetDescriptor::try_issue(&root, scope_of(&root, path), epoch, 1, NOW).unwrap(),
1298 )
1299 };
1300 assert!(store.apply(&at_epoch(1, 1), &config, NOW, SKEW).unwrap());
1301 assert!(store.apply(&at_epoch(2, 2), &config, NOW, SKEW).unwrap());
1302 assert!(store.apply(&at_epoch(3, 3), &config, NOW, SKEW).unwrap());
1303
1304 assert_eq!(store.purge_stale_epochs(2), 1);
1305 assert!(store
1306 .descriptor_for(&config.authority, 1, TopologySubnetId::from_raw(1))
1307 .is_none());
1308 assert!(store
1309 .descriptor_for(&config.authority, 2, TopologySubnetId::from_raw(2))
1310 .is_some());
1311 assert!(store
1312 .descriptor_for(&config.authority, 3, TopologySubnetId::from_raw(3))
1313 .is_some());
1314 }
1315}