From cf10d554acc0ebd1e41bfadddd90bd134c6a970b Mon Sep 17 00:00:00 2001 From: diego Date: Tue, 25 Feb 2025 16:38:53 +0100 Subject: [PATCH] SignedSSVMessage and SSVMessage validation --- Cargo.lock | 3 +- Cargo.toml | 1 + anchor/common/qbft/src/lib.rs | 6 - anchor/common/qbft/src/qbft_types.rs | 7 - anchor/common/qbft/src/tests.rs | 2 +- anchor/common/ssv_types/Cargo.toml | 1 + anchor/common/ssv_types/src/consensus.rs | 10 - anchor/common/ssv_types/src/message.rs | 267 ++++++++++++++--------- anchor/common/ssv_types/src/msgid.rs | 4 +- anchor/network/Cargo.toml | 2 +- anchor/network/src/network.rs | 74 ++++++- 11 files changed, 238 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 989655e8..90803293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5380,7 +5380,7 @@ dependencies = [ "ssz_types", "subnet_tracker", "task_executor", - "thiserror 1.0.69", + "thiserror 2.0.11", "tokio", "tracing", "tracing-subscriber", @@ -7518,6 +7518,7 @@ dependencies = [ "openssl", "rusqlite", "sha2 0.10.8", + "thiserror 2.0.11", "tree_hash", "tree_hash_derive", "types", diff --git a/Cargo.toml b/Cargo.toml index a8680459..a66e5327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ serde = { version = "1.0.208", features = ["derive"] } serde_yaml = "0.9" sha2 = "0.10.8" strum = { version = "0.26.3", features = ["derive"] } +thiserror = "2.0.11" tokio = { version = "1.39.2", features = [ "rt", "rt-multi-thread", diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 86d56a05..8960c7aa 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -214,12 +214,6 @@ where &self, wrapped_msg: &WrappedQbftMessage, ) -> Option<(Option>, OperatorId)> { - // Validate the wrapped message. This will validate the SignedSsvMessage and the QbftMessage - if !wrapped_msg.validate() { - warn!("Message validation unsuccessful"); - return None; - } - // Ensure that this message is for the correct round let current_round = self.current_round.get(); if (wrapped_msg.qbft_message.round < current_round as u64) diff --git a/anchor/common/qbft/src/qbft_types.rs b/anchor/common/qbft/src/qbft_types.rs index 5dee20fa..f8af9857 100644 --- a/anchor/common/qbft/src/qbft_types.rs +++ b/anchor/common/qbft/src/qbft_types.rs @@ -50,13 +50,6 @@ pub struct WrappedQbftMessage { pub qbft_message: QbftMessage, } -impl WrappedQbftMessage { - // Validate that the message is well formed - pub fn validate(&self) -> bool { - self.signed_message.validate() && self.qbft_message.validate() - } -} - /// This represents an individual round, these change on regular time intervals #[derive(Clone, Copy, Debug, Deref, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Round(NonZeroUsize); diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 2afe3ee4..c9c32ea7 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -46,7 +46,7 @@ fn convert_unsigned_to_wrapped( ) -> WrappedQbftMessage { // Create a signed message containing just this operator let signed_message = SignedSSVMessage::new( - vec![vec![0; 96]], // Test signature of 96 bytes + vec![vec![0; 256]], vec![OperatorId(*operator_id)], msg.ssv_message.clone(), msg.full_data, diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index 239eb04d..a80c0684 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -18,3 +18,4 @@ sha2 = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } types = { workspace = true } +thiserror = { workspace = true } diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index ac238e0f..edaf0fc5 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -59,16 +59,6 @@ pub struct QbftMessage { pub prepare_justification: Vec, // always without full_data } -impl QbftMessage { - /// Do QBFTMessage specific validation - pub fn validate(&self) -> bool { - if self.qbft_message_type > QbftMessageType::RoundChange { - return false; - } - true - } -} - /// Different states the QBFT Message may represent #[derive(Clone, Debug, PartialEq, PartialOrd, Copy)] pub enum QbftMessageType { diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index abe232d6..34a64166 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,10 +1,16 @@ +use crate::message::SSVMessageError::{EmptyData, SSVDataTooBig}; +use crate::message::SignedSSVMessageError::{ + DuplicatedSigner, FullDataTooLong, NoSignatures, NoSigners, + SignersAndSignaturesWithDifferentLength, SignersNotSorted, TooManyOperatorIDs, + TooManySignatures, WrongRSASignatureSize, ZeroSigner, +}; use crate::msgid::MessageId; use crate::OperatorId; use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; use std::collections::HashSet; -use std::fmt; use std::fmt::Debug; +use thiserror::Error; /// Defines the types of messages with explicit discriminant values. #[derive(Debug, Clone, PartialEq, Eq)] @@ -66,7 +72,10 @@ impl Decode for MsgType { expected: U64_SIZE, }); } - let value = u64::from_le_bytes(bytes.try_into().unwrap()); + let value = + u64::from_le_bytes(bytes.try_into().map_err(|_| { + DecodeError::BytesInvalid(format!("Invalid length: {}", bytes.len())) + })?); value.try_into() } } @@ -103,6 +112,21 @@ impl SSVMessage { } } + pub fn validate(&self) -> Result<(), SSVMessageError> { + if self.data.is_empty() { + return Err(EmptyData); + } + + if self.data.len() > SignedSSVMessage::MAX_FULL_DATA_LENGTH { + return Err(SSVDataTooBig { + got: self.data.len(), + max: SignedSSVMessage::MAX_FULL_DATA_LENGTH, + }); + } + + Ok(()) + } + /// Returns a reference to the message type. pub fn msg_type(&self) -> &MsgType { &self.msg_type @@ -122,7 +146,7 @@ impl SSVMessage { /// Represents a signed SSV Message with signatures, operator IDs, the message itself, and full data. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq)] pub struct SignedSSVMessage { - signatures: Vec>, // Vec of Vec, max 13 elements, each up to 256 bytes + signatures: Vec>, // Vec of Vec, max 13 elements, each with 256 bytes operator_ids: Vec, // Vec of OperatorID (u64), max 13 elements ssv_message: SSVMessage, // SSVMessage: Required field full_data: Vec, // Variable-length byte array, max 4,194,532 bytes @@ -131,8 +155,8 @@ pub struct SignedSSVMessage { impl SignedSSVMessage { /// Maximum allowed number of signatures and operator IDs. pub const MAX_SIGNATURES: usize = 13; - /// Maximum allowed length for each signature in bytes. - pub const MAX_SIGNATURE_LENGTH: usize = 256; + /// Length for each signature in bytes. + pub const SIGNATURE_LENGTH: usize = 256; /// Maximum allowed length for `full_data` in bytes. pub const MAX_FULL_DATA_LENGTH: usize = 4_194_532; @@ -162,44 +186,17 @@ impl SignedSSVMessage { operator_ids: Vec, ssv_message: SSVMessage, full_data: Vec, - ) -> Result { - if signatures.len() > Self::MAX_SIGNATURES { - return Err(SSVMessageError::TooManySignatures { - provided: signatures.len(), - max: Self::MAX_SIGNATURES, - }); - } - - for (i, sig) in signatures.iter().enumerate() { - if sig.len() > Self::MAX_SIGNATURE_LENGTH { - return Err(SSVMessageError::SignatureTooLong { - index: i, - length: sig.len(), - max: Self::MAX_SIGNATURE_LENGTH, - }); - } - } - - if operator_ids.len() > Self::MAX_SIGNATURES { - return Err(SSVMessageError::TooManyOperatorIDs { - provided: operator_ids.len(), - max: Self::MAX_SIGNATURES, - }); - } - - if full_data.len() > Self::MAX_FULL_DATA_LENGTH { - return Err(SSVMessageError::FullDataTooLong { - length: full_data.len(), - max: Self::MAX_FULL_DATA_LENGTH, - }); - } - - Ok(SignedSSVMessage { + ) -> Result { + let signed_ssv_message = SignedSSVMessage { signatures, operator_ids, ssv_message, full_data, - }) + }; + + signed_ssv_message.validate()?; + + Ok(signed_ssv_message) } /// Returns a reference to the signatures. @@ -249,88 +246,142 @@ impl SignedSSVMessage { } // Validate the signed message to ensure that it is well formed for qbft processing - pub fn validate(&self) -> bool { - // OperatorID must have at least one element + pub fn validate(&self) -> Result<(), SignedSSVMessageError> { + if self.signatures.len() > SignedSSVMessage::MAX_SIGNATURES { + return Err(TooManySignatures { + provided: self.signatures.len(), + max: SignedSSVMessage::MAX_SIGNATURES, + }); + } + + for (i, sig) in self.signatures.iter().enumerate() { + if sig.len() != SignedSSVMessage::SIGNATURE_LENGTH { + return Err(WrongRSASignatureSize { + index: i, + length: sig.len(), + sig_length: SignedSSVMessage::SIGNATURE_LENGTH, + }); + } + } + + if self.operator_ids.len() > SignedSSVMessage::MAX_SIGNATURES { + return Err(TooManyOperatorIDs { + provided: self.operator_ids.len(), + max: SignedSSVMessage::MAX_SIGNATURES, + }); + } + + if self.full_data.len() > SignedSSVMessage::MAX_FULL_DATA_LENGTH { + return Err(FullDataTooLong { + length: self.full_data.len(), + max: SignedSSVMessage::MAX_FULL_DATA_LENGTH, + }); + } + + // Rule: Must have at least one signer if self.operator_ids.is_empty() { - return false; + return Err(NoSigners); } - // Note: Len Signers & Operators will only be > 1 after commit aggregation + if self.signatures.is_empty() { + return Err(NoSignatures); + } - // Any OperatorID must not be 0 - if self.operator_ids.iter().any(|&id| *id == 0) { - return false; + if !self.operator_ids.is_sorted() { + return Err(SignersNotSorted); } - // The number of signatures and OperatorIDs must be the same - if self.operator_ids.len() != self.signatures.len() { - return false; + // Note: Len Signers & Operators will only be > 1 after commit aggregation + + // Rule: Signer can't be zero + if self.operator_ids.iter().any(|&id| *id == 0) { + return Err(ZeroSigner); } - // No duplicate signers + // Rule: Signers must be unique + // This check assumes that signers is sorted, so this rule should be after the check for ErrSignersNotSorted. let mut seen_ids = HashSet::with_capacity(self.operator_ids.len()); for &id in &self.operator_ids { if !seen_ids.insert(id) { - return false; + return Err(DuplicatedSigner); } } - true + + // Rule: Len(Signers) must be equal to Len(Signatures) + if self.operator_ids.len() != self.signatures.len() { + return Err(SignersAndSignaturesWithDifferentLength); + } + + self.ssv_message.validate()?; + + Ok(()) } } -/// Represents errors that can occur while creating or processing `SignedSSVMessage`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SSVMessageError { - /// Exceeded the maximum number of signatures. +/// Represents errors that can occur while creating a `SignedSSVMessage`. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum SignedSSVMessageError { + #[error("Too many signatures: provided {provided}, maximum allowed is {max}.")] TooManySignatures { provided: usize, max: usize }, - /// A signature exceeds the maximum allowed length. - SignatureTooLong { + + #[error("RSA Signature at index {index} has wrong size: {length} bytes, expected is {sig_length} bytes.")] + WrongRSASignatureSize { index: usize, length: usize, - max: usize, + sig_length: usize, }, - /// Exceeded the maximum number of operator IDs. + + #[error("Too many operator IDs: provided {provided}, maximum allowed is {max}.")] TooManyOperatorIDs { provided: usize, max: usize }, - /// `full_data` exceeds the maximum allowed length. + + #[error("Full data is too long: {length} bytes, maximum allowed is {max} bytes.")] FullDataTooLong { length: usize, max: usize }, -} -impl fmt::Display for SSVMessageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SSVMessageError::TooManySignatures { provided, max } => { - write!( - f, - "Too many signatures: provided {}, maximum allowed is {}.", - provided, max - ) - } - SSVMessageError::SignatureTooLong { index, length, max } => { - write!( - f, - "Signature at index {} is too long: {} bytes, maximum allowed is {} bytes.", - index, length, max - ) - } - SSVMessageError::TooManyOperatorIDs { provided, max } => { - write!( - f, - "Too many operator IDs: provided {}, maximum allowed is {}.", - provided, max - ) - } - SSVMessageError::FullDataTooLong { length, max } => { - write!( - f, - "Full data is too long: {} bytes, maximum allowed is {} bytes.", - length, max - ) - } - } - } + #[error("No signers were provided (must have at least one signer).")] + NoSigners, + + #[error("Signers and signatures must have the same length.")] + SignersAndSignaturesWithDifferentLength, + + #[error("At least one signer has ID = 0, which is invalid.")] + ZeroSigner, + + #[error("Signers are not sorted by their IDs.")] + SignersNotSorted, + + #[error("No signatures provided.")] + NoSignatures, + + #[error("A duplicated signer was found (all signers must be unique).")] + DuplicatedSigner, + + #[error("Invalid SSVMessage: {0}")] + SSVMessagError(#[from] SSVMessageError), } -impl std::error::Error for SSVMessageError {} +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum SSVMessageError { + #[error("SSVMessage data is empty")] + EmptyData, + + #[error("SSVMessage data too large: got {got}, max {max}")] + SSVDataTooBig { got: usize, max: usize }, + + #[error("Event message is not supported in this context")] + EventMessage, + + #[error("Unknown SSV message type: {got}")] + UnknownSSVMessageType { got: u8 }, + + #[error("Wrong domain: got {got}, expected {want}")] + WrongDomain { got: String, want: String }, + + #[error("Invalid role: {role}")] + InvalidRole { role: u8 }, + + #[error("Signer {got} not in committee: {want:?}")] + SignerNotInCommittee { got: u64, want: Vec }, +} #[cfg(test)] mod tests { @@ -410,7 +461,7 @@ mod tests { vec![1, 2, 3], ); - let signatures = vec![vec![0u8; 256], vec![1u8; 100]]; + let signatures = vec![vec![0u8; 256], vec![1u8; 256]]; let operator_ids = vec![OperatorId(1), OperatorId(2)]; let full_data = vec![255u8; 4_194_532]; @@ -443,7 +494,7 @@ mod tests { assert!(matches!( signed_msg, - Err(SSVMessageError::TooManySignatures { + Err(TooManySignatures { provided: 14, max: 13 }) @@ -465,10 +516,10 @@ mod tests { assert!(matches!( signed_msg, - Err(SSVMessageError::SignatureTooLong { + Err(WrongRSASignatureSize { index: 1, length: 257, - max: 256 + sig_length: SignedSSVMessage::SIGNATURE_LENGTH, }) )); } @@ -486,7 +537,7 @@ mod tests { assert!(matches!( signed_msg, - Err(SSVMessageError::TooManyOperatorIDs { + Err(TooManyOperatorIDs { provided: 14, max: 13 }) @@ -506,7 +557,7 @@ mod tests { assert!(matches!( signed_msg, - Err(SSVMessageError::FullDataTooLong { + Err(FullDataTooLong { length: 4_194_533, max: 4_194_532 }) @@ -522,7 +573,7 @@ mod tests { vec![100, 101, 102], ); - let signatures = vec![vec![10u8; 256], vec![20u8; 100]]; + let signatures = vec![vec![10u8; 256], vec![20u8; 256]]; let operator_ids = vec![OperatorId(1), OperatorId(2)]; let full_data = vec![200u8; 1024]; @@ -575,14 +626,18 @@ mod tests { fn test_full_data_max_length() { let full_data = vec![0u8; SignedSSVMessage::MAX_FULL_DATA_LENGTH]; let message_id = MessageId::from([0u8; 56]); - let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![0u8, 1]); let signatures = vec![vec![0u8; 256]]; let operator_ids = vec![OperatorId(1)]; let signed_msg = SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data.clone()); - assert!(signed_msg.is_ok()); + assert!( + signed_msg.is_ok(), + "Error creating SignedSSVMessage: {:?}", + signed_msg.err() + ); let signed_msg = signed_msg.unwrap(); assert_eq!(signed_msg.full_data(), &full_data); @@ -600,7 +655,7 @@ mod tests { assert!(matches!( signed_msg, - Err(SSVMessageError::FullDataTooLong { length: _, max: _ }) + Err(SignedSSVMessageError::FullDataTooLong { length: _, max: _ }) )); } } diff --git a/anchor/common/ssv_types/src/msgid.rs b/anchor/common/ssv_types/src/msgid.rs index b57f0e30..75f38fb0 100644 --- a/anchor/common/ssv_types/src/msgid.rs +++ b/anchor/common/ssv_types/src/msgid.rs @@ -26,7 +26,7 @@ impl From for [u8; 4] { } impl TryFrom<&[u8]> for Role { - type Error = (); + type Error = DecodeError; fn try_from(value: &[u8]) -> Result { match value { @@ -34,7 +34,7 @@ impl TryFrom<&[u8]> for Role { [1, 0, 0, 0] => Ok(Role::Aggregator), [2, 0, 0, 0] => Ok(Role::Proposer), [3, 0, 0, 0] => Ok(Role::SyncCommittee), - _ => Err(()), + _ => Err(DecodeError::NoMatchingVariant), } } } diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index a801978d..60d5a65c 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -34,7 +34,7 @@ ssv_types = { workspace = true } ssz_types = "0.8" subnet_tracker = { workspace = true } task_executor = { workspace = true } -thiserror = "1.0.69" +thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } version = { workspace = true } diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 51bac41d..4d1d69bd 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -7,7 +7,9 @@ use futures::StreamExt; use libp2p::core::muxing::StreamMuxerBox; use libp2p::core::transport::Boxed; use libp2p::core::ConnectedPoint; -use libp2p::gossipsub::{ConfigBuilderError, IdentTopic, MessageAuthenticity, ValidationMode}; +use libp2p::gossipsub::{ + ConfigBuilderError, IdentTopic, MessageAcceptance, MessageAuthenticity, ValidationMode, +}; use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; use libp2p::swarm::SwarmEvent; @@ -16,12 +18,12 @@ use libp2p::{ }; use lighthouse_network::discovery::DiscoveredPeers; use lighthouse_network::discv5::enr::k256::sha2::{Digest, Sha256}; -use ssv_types::message::SignedSSVMessage; +use ssv_types::message::{SSVMessageError, SignedSSVMessage, SignedSSVMessageError}; use ssz::Decode; use subnet_tracker::{SubnetEvent, SubnetId}; use task_executor::TaskExecutor; use tokio::sync::mpsc; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, trace}; use crate::behaviour::AnchorBehaviour; use crate::behaviour::AnchorBehaviourEvent; @@ -33,6 +35,7 @@ use crate::transport::build_transport; use crate::{handshake, Config, Enr}; use crate::network::NetworkError::{Gossipsub, SwarmConfig}; +use ssv_types::domain_type::DomainType; use thiserror::Error; #[derive(Debug, Error)] @@ -57,11 +60,39 @@ pub enum NetworkError { SwarmConfig(String), } +fn to_message_acceptance(value: &SignedSSVMessageError) -> MessageAcceptance { + match value { + SignedSSVMessageError::SSVMessagError(SSVMessageError::WrongDomain { got: _, want: _ }) => { + MessageAcceptance::Ignore + } + _ => MessageAcceptance::Reject, + // | ValidationFailure::NoShareMetadata + // | ValidationFailure::UnknownValidator + // | ValidationFailure::ValidatorLiquidated + // | ValidationFailure::ValidatorNotAttesting + // | ValidationFailure::EarlySlotMessage + // | ValidationFailure::LateSlotMessage + // | ValidationFailure::SlotAlreadyAdvanced + // | ValidationFailure::RoundAlreadyAdvanced + // | ValidationFailure::DecidedWithSameSigners + // | ValidationFailure::PubSubDataTooBig(_) + // | ValidationFailure::IncorrectTopic + // | ValidationFailure::NonExistentCommitteeID + // | ValidationFailure::RoundTooHigh + // | ValidationFailure::ValidatorIndexMismatch + // | ValidationFailure::TooManyDutiesPerEpoch + // | ValidationFailure::NoDuty + // | ValidationFailure::EstimatedRoundNotInAllowedSpread => MessageAcceptance::Ignore, + // _ => MessageAcceptance::Reject, + } +} + pub struct Network { swarm: Swarm, subnet_event_receiver: mpsc::Receiver, peer_id: PeerId, node_info: NodeInfo, + domain_type: DomainType, } impl Network { @@ -101,6 +132,7 @@ impl Network { subnet_event_receiver, peer_id, node_info, + domain_type: config.domain_type.clone(), }; info!(%peer_id, "Network starting"); @@ -156,9 +188,22 @@ impl Network { match SignedSSVMessage::from_ssz_bytes(&message.data) { Ok(deserialized_message) => { debug!(msg = ?deserialized_message, "SignedSSVMessage deserialized"); + if let Err(error) = self.validate_signed_ssv_message(&deserialized_message) { + trace!(?error, "Failed to validate SignedSSVMessage"); + self.gossipsub().report_message_validation_result( + &message_id, + &propagation_source, + to_message_acceptance(&error) + ); + } } - Err(e) => { - error!("error" = ?e, "Failed to deserialize SignedSSVMessage"); + Err(error) => { + trace!(?error, "Failed to deserialize SignedSSVMessage"); + self.gossipsub().report_message_validation_result( + &message_id, + &propagation_source, + MessageAcceptance::Reject + ); } } } @@ -266,6 +311,10 @@ impl Network { &mut self.swarm.behaviour_mut().peer_manager } + fn gossipsub(&mut self) -> &mut gossipsub::Behaviour { + &mut self.swarm.behaviour_mut().gossipsub + } + fn handle_handshake_result(&mut self, result: Result) { match result { Ok(handshake::Completed { @@ -280,6 +329,21 @@ impl Network { } } } + + fn validate_signed_ssv_message( + &self, + message: &SignedSSVMessage, + ) -> Result<(), SignedSSVMessageError> { + message.validate()?; + let msg_domain = message.ssv_message().msg_id().domain(); + if self.domain_type != msg_domain { + return Err(SSVMessageError::WrongDomain { + got: String::from(msg_domain), + want: String::from(self.domain_type.clone()), + })?; + }; + Ok(()) + } } fn subnet_to_topic(subnet: SubnetId) -> IdentTopic {