diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml index c238f89a4f2..33b04640358 100644 --- a/rust/rbac-registration/Cargo.toml +++ b/rust/rbac-registration/Cargo.toml @@ -34,5 +34,5 @@ thiserror = "2.0.11" c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "c509-certificate-v0.0.3" } cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" } -cardano-blockchain-types = { version = "0.0.8", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cardano-blockchain-types/v0.0.8" } +cardano-blockchain-types = { version = "0.0.9", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cardano-blockchain-types/v0.0.9" } catalyst-types = { version = "0.0.10", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.10" } diff --git a/rust/rbac-registration/src/cardano/cip509/cip509.rs b/rust/rbac-registration/src/cardano/cip509/cip509.rs index 18d21213ff3..840de2c7482 100644 --- a/rust/rbac-registration/src/cardano/cip509/cip509.rs +++ b/rust/rbac-registration/src/cardano/cip509/cip509.rs @@ -22,6 +22,7 @@ use catalyst_types::{ uuid::UuidV4, }; use cbork_utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; +use ed25519_dalek::VerifyingKey; use minicbor::{ decode::{self}, Decode, Decoder, @@ -32,6 +33,7 @@ use uuid::Uuid; use crate::cardano::cip509::{ decode_context::DecodeContext, + extract_key, rbac::Cip509RbacMetadata, types::{PaymentHistory, TxInputHash, ValidationSignature}, utils::Cip0134UriSet, @@ -40,7 +42,7 @@ use crate::cardano::cip509::{ validate_txn_inputs_hash, }, x509_chunks::X509Chunks, - Payment, PointTxnIdx, RoleData, + C509Cert, LocalRefInt, Payment, PointTxnIdx, RoleData, SimplePublicKeyType, X509DerCert, }; /// A x509 metadata envelope. @@ -227,6 +229,48 @@ impl Cip509 { self.metadata.as_ref().and_then(|m| m.role_data.get(&role)) } + /// Returns signing public key for a role. + /// Would return only signing public keys for the present certificates, + /// if certificate marked as deleted or undefined it would be skipped. + #[must_use] + pub(crate) fn signing_pk_for_role( + &self, + role: RoleId, + ) -> Option { + self.metadata.as_ref().and_then(|m| { + let key_ref = m.role_data.get(&role).and_then(|d| d.signing_key())?; + match key_ref.local_ref { + LocalRefInt::X509Certs => { + m.x509_certs.get(key_ref.key_offset).and_then(|c| { + if let X509DerCert::X509Cert(c) = c { + extract_key::x509_key(c).ok() + } else { + None + } + }) + }, + LocalRefInt::C509Certs => { + m.c509_certs.get(key_ref.key_offset).and_then(|c| { + if let C509Cert::C509Certificate(c) = c { + extract_key::c509_key(c).ok() + } else { + None + } + }) + }, + LocalRefInt::PubKeys => { + m.pub_keys.get(key_ref.key_offset).and_then(|c| { + if let SimplePublicKeyType::Ed25519(c) = c { + Some(*c) + } else { + None + } + }) + }, + } + }) + } + /// Returns a purpose of this registration. #[must_use] pub fn purpose(&self) -> Option { @@ -313,26 +357,10 @@ impl Cip509 { .unwrap_or_default() } - /// Returns `Cip509` fields consuming the structure if it was successfully decoded and - /// validated otherwise return the problem report that contains all the encountered - /// issues. - /// - /// # Errors - /// - /// - `Err(ProblemReport)` - pub fn consume(self) -> Result<(UuidV4, Cip509RbacMetadata, PaymentHistory), ProblemReport> { - match ( - self.purpose, - self.txn_inputs_hash, - self.metadata, - self.validation_signature, - ) { - (Some(purpose), Some(_), Some(metadata), Some(_)) if !self.report.is_problematic() => { - Ok((purpose, metadata, self.payment_history)) - }, - - _ => Err(self.report), - } + /// Returns a payment history map. + #[must_use] + pub fn payment_history(&self) -> &PaymentHistory { + &self.payment_history } } diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs index 44ba89af91a..1a4f5c4786a 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs @@ -39,6 +39,8 @@ struct Cip0134UriSetInner { x_uris: UrisMap, /// URIs from c509 certificates. c_uris: UrisMap, + /// URIs which are taken by another certificates. + taken: HashSet, } impl Cip0134UriSet { @@ -51,7 +53,12 @@ impl Cip0134UriSet { ) -> Self { let x_uris = extract_x509_uris(x509_certs, report); let c_uris = extract_c509_uris(c509_certs, report); - Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris })) + let taken_uris = HashSet::new(); + Self(Arc::new(Cip0134UriSetInner { + x_uris, + c_uris, + taken: taken_uris, + })) } /// Returns a mapping from the x509 certificate index to URIs contained within. @@ -66,6 +73,15 @@ impl Cip0134UriSet { &self.0.c_uris } + /// Returns an iterator over of active (without taken) `Cip0134Uri`. + pub(crate) fn values(&self) -> impl Iterator { + self.x_uris() + .values() + .chain(self.c_uris().values()) + .flat_map(|uris| uris.iter()) + .filter(|v| !self.0.taken.contains(v)) + } + /// Returns `true` if both x509 and c509 certificate maps are empty. #[must_use] pub fn is_empty(&self) -> bool { @@ -90,7 +106,7 @@ impl Cip0134UriSet { result } - /// Returns a list of stake addresses by the given role. + /// Returns a set of stake addresses by the given role. #[must_use] pub fn role_stake_addresses( &self, @@ -107,13 +123,10 @@ impl Cip0134UriSet { .collect() } - /// Returns a list of all stake addresses. + /// Returns a set of all stake addresses. #[must_use] pub fn stake_addresses(&self) -> HashSet { - self.x_uris() - .values() - .chain(self.c_uris().values()) - .flat_map(|uris| uris.iter()) + self.values() .filter_map(|uri| { match uri.address() { Address::Stake(a) => Some(a.clone().into()), @@ -154,6 +167,7 @@ impl Cip0134UriSet { let Cip0134UriSetInner { mut x_uris, mut c_uris, + taken: mut taken_uris, } = Arc::unwrap_or_clone(self.0); for (index, cert) in metadata.x509_certs.iter().enumerate() { @@ -166,6 +180,9 @@ impl Cip0134UriSet { }, X509DerCert::X509Cert(_) => { if let Some(uris) = metadata.certificate_uris.x_uris().get(&index) { + uris.iter().for_each(|v| { + taken_uris.remove(v); + }); x_uris.insert(index, uris.clone()); } }, @@ -185,13 +202,53 @@ impl Cip0134UriSet { }, C509Cert::C509Certificate(_) => { if let Some(uris) = metadata.certificate_uris.c_uris().get(&index) { + uris.iter().for_each(|v| { + taken_uris.remove(v); + }); c_uris.insert(index, uris.clone()); } }, } } - Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris })) + Self(Arc::new(Cip0134UriSetInner { + x_uris, + c_uris, + taken: taken_uris, + })) + } + + /// Return the updated URIs set where the provided URIs were taken by other + /// registration chains. + /// + /// Updates the current URI set by removing the taken URIs from it. + #[must_use] + pub fn update_taken_uris( + self, + reg: &Cip509RbacMetadata, + ) -> Self { + let current_uris_set = self.values().collect::>(); + + let latest_taken_uris = reg + .certificate_uris + .values() + .filter(|v| current_uris_set.contains(v)) + .cloned() + .collect::>(); + + let Cip0134UriSetInner { + x_uris, + c_uris, + taken: mut taken_uris, + } = Arc::unwrap_or_clone(self.0); + + taken_uris.extend(latest_taken_uris); + + Self(Arc::new(Cip0134UriSetInner { + x_uris, + c_uris, + taken: taken_uris, + })) } } diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index 158a889fefd..8484f5bf514 100644 --- a/rust/rbac-registration/src/cardano/cip509/validation.rs +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -157,10 +157,7 @@ fn extract_stake_addresses(uris: Option<&Cip0134UriSet>) -> Vec<(VKeyHash, Strin return Vec::new(); }; - uris.x_uris() - .iter() - .chain(uris.c_uris()) - .flat_map(|(_index, uris)| uris.iter()) + uris.values() .filter_map(|uri| { if let Address::Stake(a) = uri.address() { let bech32 = uri.address().to_string(); @@ -185,10 +182,7 @@ fn extract_payment_addresses(uris: Option<&Cip0134UriSet>) -> Vec<(VKeyHash, Str return Vec::new(); }; - uris.x_uris() - .iter() - .chain(uris.c_uris()) - .flat_map(|(_index, uris)| uris.iter()) + uris.values() .filter_map(|uri| { if let Address::Shelley(a) = uri.address() { match a.payment() { @@ -593,8 +587,7 @@ mod tests { assert_eq!(origin.txn_index(), data.txn_index); assert_eq!(origin.point().slot_or_default(), data.slot); - // The consume function must return the problem report contained within the registration. - let report = registration.consume().unwrap_err(); + let report = registration.report(); assert!(report.is_problematic()); let report = format!("{report:?}"); assert!(report.contains("is not present in the transaction witness set, and can not be verified as owned and spendable")); @@ -616,7 +609,7 @@ mod tests { assert_eq!(origin.txn_index(), data.txn_index); assert_eq!(origin.point().slot_or_default(), data.slot); - let report = registration.consume().unwrap_err(); + let report = registration.report(); assert!(report.is_problematic()); let report = format!("{report:?}"); assert!(report @@ -637,8 +630,7 @@ mod tests { assert_eq!(origin.txn_index(), data.txn_index); assert_eq!(origin.point().slot_or_default(), data.slot); - // The consume function must return the problem report contained within the registration. - let report = registration.consume().unwrap_err(); + let report = registration.report(); assert!(report.is_problematic()); let report = format!("{report:?}"); assert!(report.contains("Unknown role found: 4")); diff --git a/rust/rbac-registration/src/cardano/mod.rs b/rust/rbac-registration/src/cardano/mod.rs index 8af2182b83a..11784792f49 100644 --- a/rust/rbac-registration/src/cardano/mod.rs +++ b/rust/rbac-registration/src/cardano/mod.rs @@ -1,3 +1,4 @@ //! Cardano module pub mod cip509; +pub mod state; diff --git a/rust/rbac-registration/src/cardano/state.rs b/rust/rbac-registration/src/cardano/state.rs new file mode 100644 index 00000000000..42d4e29020e --- /dev/null +++ b/rust/rbac-registration/src/cardano/state.rs @@ -0,0 +1,54 @@ +//! Cardano RBAC state traits, which are used during different stateful validation +//! procedures. + +use std::future::Future; + +use cardano_blockchain_types::{hashes::TransactionId, StakeAddress}; +use catalyst_types::catalyst_id::CatalystId; +use ed25519_dalek::VerifyingKey; + +use crate::registration::cardano::RegistrationChain; + +/// RBAC chains state trait +pub trait RBACState { + /// Returns RBAC chain for the given Catalyst ID. + fn chain( + &self, + id: &CatalystId, + ) -> impl Future>> + Send; + + /// Returns `true` if a RBAC chain with the given Catalyst ID already exists. + fn is_chain_known( + &self, + id: &CatalystId, + ) -> impl Future> + Send; + + /// Returns a current valid RBAC chain Catalyst ID corresponding to the given stake + /// address. + fn chain_catalyst_id_from_stake_address( + &self, + address: &StakeAddress, + ) -> impl Future>> + Send; + + /// Returns a corresponding to the RBAC chain's Catalyst ID corresponding by the given + /// public key. + fn chain_catalyst_id_from_public_key( + &self, + key: &VerifyingKey, + ) -> impl Future>> + Send; + + /// Returns a corresponding to the RBAC chain's Catalyst ID corresponding by the given + /// transaction hash. + fn chain_catalyst_id_from_txn_id( + &self, + txn_id: &TransactionId, + ) -> impl Future>> + Send; + + /// Update the update by "taking" the given `StakeAddress` for the corresponding RBAC + /// chain's by the given `CatalystId`. + fn take_stake_address_from_chain( + &mut self, + id: &CatalystId, + address: &StakeAddress, + ) -> impl Future> + Send; +} diff --git a/rust/rbac-registration/src/lib.rs b/rust/rbac-registration/src/lib.rs index b140584362f..6e35f516568 100644 --- a/rust/rbac-registration/src/lib.rs +++ b/rust/rbac-registration/src/lib.rs @@ -1,7 +1,6 @@ //! This crate provides functionalities for RBAC registration. pub mod cardano; -pub mod providers; pub mod registration; mod utils; diff --git a/rust/rbac-registration/src/providers.rs b/rust/rbac-registration/src/providers.rs deleted file mode 100644 index 271f7ccd75c..00000000000 --- a/rust/rbac-registration/src/providers.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Providers traits, which are used during different validation procedures. - -use std::future::Future; - -use cardano_blockchain_types::{hashes::TransactionId, StakeAddress}; -use catalyst_types::catalyst_id::CatalystId; -use ed25519_dalek::VerifyingKey; - -use crate::registration::cardano::RegistrationChain; - -/// `RegistrationChain` Provider trait -pub trait RbacRegistrationProvider { - /// Returns registration chain - /// for the given Catalyst ID. - fn chain( - &self, - id: CatalystId, - ) -> impl Future>> + Send; - - /// Returns `true` if a chain with the given Catalyst ID already exists. - /// - /// This function behaves in the same way as `latest_rbac_chain(...).is_some()` but - /// the implementation is more optimized because we don't need to build the whole - /// chain. - fn is_chain_known( - &self, - id: CatalystId, - ) -> impl Future> + Send; - - /// Returns a Catalyst ID corresponding to the given stake address. - fn catalyst_id_from_stake_address( - &self, - address: &StakeAddress, - ) -> impl Future>> + Send; - - /// Returns a Catalyst ID corresponding to the given public key. - fn catalyst_id_from_public_key( - &self, - key: VerifyingKey, - ) -> impl Future>> + Send; - - /// Returns a Catalyst ID corresponding to the given transaction hash. - fn catalyst_id_from_txn_id( - &self, - txn_id: TransactionId, - ) -> impl Future>> + Send; -} diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index 4c9d1df1721..45e784a98fb 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -22,12 +22,12 @@ use update_rbac::{ }; use x509_cert::certificate::Certificate as X509Certificate; -use crate::{ - cardano::cip509::{ - CertKeyHash, CertOrPk, Cip0134UriSet, Cip509, PaymentHistory, PointData, RoleData, - RoleDataRecord, ValidationSignature, +use crate::cardano::{ + cip509::{ + CertKeyHash, CertOrPk, Cip0134UriSet, Cip509, PaymentHistory, PointData, PointTxnIdx, + RoleData, RoleDataRecord, ValidationSignature, }, - providers::RbacRegistrationProvider, + state::RBACState, }; /// Registration chains. @@ -40,32 +40,117 @@ pub struct RegistrationChain { } impl RegistrationChain { + /// Attempts to initialize a new RBAC registration chain + /// from a given CIP-509 registration, ensuring uniqueness of Catalyst ID, stake + /// addresses, and associated public keys. + /// + /// # Errors + /// - Propagates any I/O or provider-level errors encountered while checking key + /// ownership (e.g., database lookup failures). + pub async fn new( + cip509: &Cip509, + state: &mut State, + ) -> anyhow::Result> + where + State: RBACState, + { + let Some(new_chain) = Self::new_stateless(cip509) else { + return Ok(None); + }; + + // Verify that a Catalyst ID of this chain is unique. + { + let cat_id = new_chain.catalyst_id(); + if state.is_chain_known(cat_id).await? { + cip509.report().functional_validation( + &format!("{} is already used", cat_id.as_short_id()), + "It isn't allowed to use same Catalyst ID (certificate subject public key) in multiple registration chains", + ); + } + + check_signing_pk(cat_id, cip509, state).await?; + } + + if cip509.report().is_problematic() { + return Ok(None); + } + + for address in &cip509.stake_addresses() { + if let Some(id) = state.chain_catalyst_id_from_stake_address(address).await? { + state.take_stake_address_from_chain(&id, address).await?; + } + } + + Ok(Some(new_chain)) + } + /// Create a new instance of registration chain. /// The first new value should be the chain root. - /// - /// # Arguments - /// - `cip509` - The CIP509. #[must_use] - pub fn new_stateless(cip509: Cip509) -> Option { - let inner = RegistrationChainInner::new_stateless(cip509)?; + fn new_stateless(cip509: &Cip509) -> Option { + let inner = RegistrationChainInner::new(cip509)?; Some(Self { inner: Arc::new(inner), }) } + /// Attempts to update an existing RBAC registration chain + /// with a new CIP-509 registration, validating address and key usage consistency. + /// + /// # Errors + /// - Propagates any I/O or provider-level errors encountered while checking key + /// ownership (e.g., database lookup failures). + pub async fn update( + &self, + cip509: &Cip509, + state: &State, + ) -> anyhow::Result> + where + State: RBACState, + { + let Some(new_chain) = self.update_stateless(cip509) else { + return Ok(None); + }; + + // Check that addresses from the new registration aren't used in other chains. + let previous_addresses = self.stake_addresses(); + let reg_addresses = cip509.stake_addresses(); + let new_addresses: Vec<_> = reg_addresses.difference(&previous_addresses).collect(); + for address in &new_addresses { + if state + .chain_catalyst_id_from_stake_address(address) + .await? + .is_some() + { + cip509.report().functional_validation( + &format!("{address} stake addresses is already used"), + "It isn't allowed to use same stake address in multiple registration chains, if its not a new chain", + ); + } + } + + check_signing_pk(self.catalyst_id(), cip509, state).await?; + + if cip509.report().is_problematic() { + Ok(None) + } else { + Ok(Some(new_chain)) + } + } + /// Update the registration chain. /// /// # Arguments /// - `cip509` - The CIP509. #[must_use] - pub fn update_stateless( + fn update_stateless( &self, - cip509: Cip509, + cip509: &Cip509, ) -> Option { let latest_signing_pk = self.get_latest_signing_pk_for_role(RoleId::Role0); let new_inner = if let Some((signing_pk, _)) = latest_signing_pk { - self.inner.update_stateless(cip509, signing_pk)? + self.inner.update(cip509, signing_pk)? } else { cip509.report().missing_field( "latest signing key for role 0", @@ -78,29 +163,6 @@ impl RegistrationChain { }) } - /// Creates or updates an RBAC registration chain from a CIP-509 registration. - /// - /// If the given registration references a previous transaction, it attempts - /// to update the existing chain using that previous transaction. - /// Otherwise, it starts a new chain from the provided registration. - pub async fn update( - &self, - reg: Cip509, - provider: &Provider, - ) -> Option - where - Provider: RbacRegistrationProvider, - { - let new_inner = if reg.previous_transaction().is_some() { - self.inner.update(reg, provider).await? - } else { - RegistrationChainInner::new(reg, provider).await? - }; - Some(Self { - inner: Arc::new(new_inner), - }) - } - /// Returns a Catalyst ID. #[must_use] pub fn catalyst_id(&self) -> &CatalystId { @@ -255,11 +317,17 @@ impl RegistrationChain { .and_then(|rdr| rdr.encryption_key_from_rotation(rotation)) } - /// Returns all stake addresses associated to this registration. + /// Returns all stake addresses associated to this chain. #[must_use] pub fn stake_addresses(&self) -> HashSet { self.inner.certificate_uris.stake_addresses() } + + /// Returns the latest know applied registration's `PointTxnIdx`. + #[must_use] + pub fn latest_applied(&self) -> PointTxnIdx { + self.inner.latest_applied() + } } /// Inner structure of registration chain. @@ -269,6 +337,9 @@ struct RegistrationChainInner { catalyst_id: CatalystId, /// The current transaction ID hash (32 bytes) current_tx_id_hash: PointData, + /// The latest `PointTxnIdx` of the stolen taken URIs by another registration chains. + latest_taken_uris_point: Option, + /// List of purpose for this registration chain purpose: Vec, @@ -305,7 +376,7 @@ impl RegistrationChainInner { /// # Arguments /// - `cip509` - The CIP509. #[must_use] - fn new_stateless(cip509: Cip509) -> Option { + fn new(cip509: &Cip509) -> Option { let context = "Registration Chain new"; // Should be chain root, return immediately if not if cip509.previous_transaction().is_some() { @@ -318,23 +389,21 @@ impl RegistrationChainInner { return None; }; - let point_tx_idx = cip509.origin().clone(); - let current_tx_id_hash = PointData::new(point_tx_idx.clone(), cip509.txn_hash()); - let validation_signature = cip509.validation_signature().cloned(); - let raw_aux_data = cip509.raw_aux_data().to_vec(); + let Some(registration) = cip509.metadata().cloned() else { + cip509.report().missing_field("metadata", context); + return None; + }; // Role data let mut role_data_history = HashMap::new(); let mut role_data_record = HashMap::new(); - - if let Some(registration) = cip509.metadata() { - update_role_data( - registration, - &mut role_data_history, - &mut role_data_record, - &point_tx_idx, - ); - } + let point_tx_idx = cip509.origin().clone(); + update_role_data( + ®istration, + &mut role_data_history, + &mut role_data_record, + &point_tx_idx, + ); // There should be role 0 since we already check that the chain root (no previous tx id) // must contain role 0 @@ -354,17 +423,26 @@ impl RegistrationChainInner { }; check_validation_signature( - validation_signature, - &raw_aux_data, + cip509.validation_signature(), + cip509.raw_aux_data(), signing_pk, cip509.report(), context, ); - let Ok((purpose, registration, payment_history)) = cip509.consume() else { + if cip509.txn_inputs_hash().is_none() { + cip509.report().missing_field("txn inputs hash", context); + } + + let Some(purpose) = cip509.purpose() else { + cip509.report().missing_field("purpose", context); return None; }; + if cip509.report().is_problematic() { + return None; + } + let purpose = vec![purpose]; let certificate_uris = registration.certificate_uris.clone(); let mut x509_certs = HashMap::new(); @@ -386,6 +464,8 @@ impl RegistrationChainInner { &point_tx_idx, ); let revocations = revocations_list(registration.revocation_list.clone(), &point_tx_idx); + let current_tx_id_hash = PointData::new(point_tx_idx, cip509.txn_hash()); + let payment_history = cip509.payment_history().clone(); Some(Self { catalyst_id, @@ -394,6 +474,7 @@ impl RegistrationChainInner { x509_certs, c509_certs, certificate_uris, + latest_taken_uris_point: None, simple_keys, revocations, role_data_history, @@ -407,15 +488,41 @@ impl RegistrationChainInner { /// # Arguments /// - `cip509` - The CIP509. #[must_use] - fn update_stateless( + fn update( &self, - cip509: Cip509, + cip509: &Cip509, signing_pk: VerifyingKey, ) -> Option { let context = "Registration Chain update"; + if self.latest_applied().point() >= cip509.origin().point() { + cip509.report().functional_validation( + &format!( + "The provided registration is earlier {} than the current one {}", + cip509.origin().point(), + self.current_tx_id_hash.point() + ), + "Provided registrations must be applied in the correct order.", + ); + return None; + } + let mut new_inner = self.clone(); let Some(prv_tx_id) = cip509.previous_transaction() else { + if let Some(cat_id) = cip509.catalyst_id() { + if cat_id == &self.catalyst_id { + cip509.report().functional_validation( + &format!( + "Trying to apply the first registration to the associated {} again", + cat_id.as_short_id() + ), + "It isn't allowed to submit first registration twice", + ); + return None; + } + + return Some(new_inner.update_cause_another_chain(cip509)); + } cip509 .report() .missing_field("previous transaction ID", context); @@ -427,7 +534,7 @@ impl RegistrationChainInner { // Perform signature validation // This should be done before updating the signing key check_validation_signature( - cip509.validation_signature().cloned(), + cip509.validation_signature(), cip509.raw_aux_data(), signing_pk, cip509.report(), @@ -447,18 +554,34 @@ impl RegistrationChainInner { return None; } - let point_tx_idx = cip509.origin().clone(); - let Ok((purpose, registration, payment_history)) = cip509.consume() else { + if cip509.txn_inputs_hash().is_none() { + cip509.report().missing_field("txn inputs hash", context); + } + + let Some(purpose) = cip509.purpose() else { + cip509.report().missing_field("purpose", context); + return None; + }; + let Some(registration) = cip509.metadata().cloned() else { + cip509.report().missing_field("metadata", context); return None; }; + if cip509.report().is_problematic() { + return None; + } + // Add purpose to the chain, if not already exist if !self.purpose.contains(&purpose) { new_inner.purpose.push(purpose); } + let point_tx_idx = cip509.origin().clone(); + new_inner.certificate_uris = new_inner.certificate_uris.update(®istration); - new_inner.payment_history.extend(payment_history); + new_inner + .payment_history + .extend(cip509.payment_history().clone()); update_x509_certs( &mut new_inner.x509_certs, registration.x509_certs.clone(), @@ -489,175 +612,23 @@ impl RegistrationChainInner { Some(new_inner) } - /// Attempts to initialize a new RBAC registration chain - /// from a given CIP-509 registration, ensuring uniqueness of Catalyst ID, stake - /// addresses, and associated public keys. - pub async fn new( - reg: Cip509, - provider: &Provider, - ) -> Option - where - Provider: RbacRegistrationProvider, - { - let report = reg.report().to_owned(); - - // Try to start a new chain. - let new_chain = Self::new_stateless(reg)?; - // Verify that a Catalyst ID of this chain is unique. - let catalyst_id = new_chain.catalyst_id.as_short_id(); - if provider.is_chain_known(catalyst_id.clone()).await.ok()? { - report.functional_validation( - &format!("{catalyst_id} is already used"), - "It isn't allowed to use same Catalyst ID (certificate subject public key) in multiple registration chains", - ); - return None; - } - - // Validate stake addresses. - let new_addresses = new_chain.certificate_uris.stake_addresses(); - let mut updated_chains: HashMap<_, HashSet> = HashMap::new(); - for address in &new_addresses { - if let Some(id) = provider - .catalyst_id_from_stake_address(address) - .await - .ok()? - { - // If an address is used in existing chain then a new chain must have different role - // 0 signing key. - let previous_chain = provider.chain(id.clone()).await.ok()??; - if previous_chain.get_latest_signing_pk_for_role(RoleId::Role0) - == new_chain.get_latest_signing_pk_for_role(RoleId::Role0) - { - report.functional_validation( - &format!("A new registration ({catalyst_id}) uses the same public key as the previous one ({})", - previous_chain.catalyst_id().as_short_id() - ), - "It is only allowed to override the existing chain by using different public key", - ); - } else { - // The new root registration "takes" an address(es) from the existing chain, so - // that chain needs to be updated. - updated_chains - .entry(id) - .and_modify(|e| { - e.insert(address.clone()); - }) - .or_insert([address.clone()].into_iter().collect()); - } - } - } - - // Check that new public keys aren't used by other chains. - new_chain - .validate_public_keys(&report, provider) - .await - .ok()?; - - if report.is_problematic() { - return None; - } - - Some(new_chain) - } - - /// Attempts to update an existing RBAC registration chain - /// with a new CIP-509 registration, validating address and key usage consistency. - pub async fn update( - &self, - reg: Cip509, - provider: &Provider, - ) -> Option - where - Provider: RbacRegistrationProvider, - { - let previous_txn = reg.previous_transaction()?; - let report = reg.report().to_owned(); - - // Find a chain this registration belongs to. - let Some(catalyst_id) = provider.catalyst_id_from_txn_id(previous_txn).await.ok()? else { - // We are unable to determine a Catalyst ID, so there is no sense to update the problem - // report because we would be unable to store this registration anyway. - return None; - }; - let chain = provider.chain(catalyst_id.clone()).await.ok()??; - - // Check that addresses from the new registration aren't used in other chains. - let previous_addresses = chain.stake_addresses(); - let reg_addresses = reg.stake_addresses(); - let new_addresses: Vec<_> = reg_addresses.difference(&previous_addresses).collect(); - for address in &new_addresses { - match provider - .catalyst_id_from_stake_address(address) - .await - .ok()? - { - None => { - // All good: the address wasn't used before. - }, - Some(_) => { - report.functional_validation( - &format!("{address} stake addresses is already used"), - "It isn't allowed to use same stake address in multiple registration chains", - ); - }, - } - } - - // Try to add a new registration to the chain. - let (signing_pk, _) = self.get_latest_signing_pk_for_role(RoleId::Role0)?; - let new_chain = chain.inner.update_stateless(reg.clone(), signing_pk)?; - - // Check that new public keys aren't used by other chains. - new_chain - .validate_public_keys(&report, provider) - .await - .ok()?; - - // Return an error if any issues were recorded in the report. - if report.is_problematic() { - return None; - } - - Some(new_chain) - } - - /// Validates that none of the signing keys in a given RBAC registration chain - /// have been used by any other existing chain, ensuring global key uniqueness - /// across all Catalyst registrations. - /// - /// # Returns - /// Returns `Ok(true)` if all signing keys are unique and validation passes - /// successfully. Returns `Ok(false)` if any key conflict is detected, with the - /// issue recorded in the provided [`ProblemReport`]. + /// Update the registration chain with the `cip509` associated to another chain. + /// This is the case when registration for different chain affecting the current one, + /// by invalidating some data for the current registration chain (stealing stake + /// addresses etc.). /// - /// # Errors - /// - Propagates any I/O or provider-level errors encountered while checking key - /// ownership (e.g., database lookup failures). - async fn validate_public_keys( - &self, - report: &ProblemReport, - provider: &Provider, - ) -> anyhow::Result<()> - where - Provider: RbacRegistrationProvider, - { - let roles: Vec<_> = self.role_data_history.keys().collect(); - let catalyst_id = self.catalyst_id.as_short_id(); - - for role in roles { - if let Some((key, _)) = self.get_latest_signing_pk_for_role(*role) { - if let Some(previous) = provider.catalyst_id_from_public_key(key).await? { - if previous != catalyst_id { - report.functional_validation( - &format!("An update to {catalyst_id} registration chain uses the same public key ({key:?}) as {previous} chain"), - "It isn't allowed to use role 0 signing (certificate subject public) key in different chains", - ); - } - } - } + /// The provided `cip509` should be fully validated by another chain before trying to + /// submit to the current one. + #[must_use] + fn update_cause_another_chain( + mut self, + cip509: &Cip509, + ) -> Self { + if let Some(reg) = cip509.metadata() { + self.certificate_uris = self.certificate_uris.update_taken_uris(reg); } - - Ok(()) + self.latest_taken_uris_point = Some(cip509.origin().clone()); + self } /// Get the latest signing public key for a role. @@ -691,12 +662,26 @@ impl RegistrationChainInner { }) }) } + + /// Returns the latest know applied registration's `PointTxnIdx`. + #[must_use] + fn latest_applied(&self) -> PointTxnIdx { + if let Some(latest_taken_uris_point) = &self.latest_taken_uris_point { + if latest_taken_uris_point.point() > self.current_tx_id_hash.point() { + return latest_taken_uris_point.clone(); + } + } + PointTxnIdx::new( + self.current_tx_id_hash.point().clone(), + self.current_tx_id_hash.txn_index(), + ) + } } /// Perform a check on the validation signature. /// The auxiliary data should be sign with the latest signing public key. fn check_validation_signature( - validation_signature: Option, + validation_signature: Option<&ValidationSignature>, raw_aux_data: &[u8], signing_pk: VerifyingKey, report: &ProblemReport, @@ -734,6 +719,32 @@ fn check_validation_signature( } } +/// Checks that a new registration doesn't contain a signing key that was used by any +/// other chain. +async fn check_signing_pk( + cat_id: &CatalystId, + cip509: &Cip509, + state: &State, +) -> anyhow::Result<()> +where + State: RBACState, +{ + for role in cip509.all_roles() { + if let Some(key) = cip509.signing_pk_for_role(role) { + if let Some(previous) = state.chain_catalyst_id_from_public_key(&key).await? { + if &previous != cat_id { + cip509.report().functional_validation( + &format!("An update to {cat_id} registration chain uses the same public key ({key:?}) as {previous} chain"), + "It isn't allowed to use role 0 signing (certificate subject public) key in different chains", + ); + } + } + } + } + + Ok(()) +} + #[cfg(test)] mod test { use catalyst_types::catalyst_id::role_index::RoleId; @@ -749,8 +760,9 @@ mod test { .unwrap(); data.assert_valid(®istration); + // Performs only stateless validations // Create a chain with the first registration. - let chain = RegistrationChain::new_stateless(registration).unwrap(); + let chain = RegistrationChain::new_stateless(®istration).unwrap(); assert_eq!(chain.purpose(), &[data.purpose]); assert_eq!(1, chain.x509_certs().len()); let origin = &chain.x509_certs().get(&0).unwrap().first().unwrap(); @@ -777,7 +789,7 @@ mod test { assert!(registration.report().is_problematic()); let report = registration.report().to_owned(); - assert!(chain.update_stateless(registration).is_none()); + assert!(chain.update_stateless(®istration).is_none()); let report = format!("{report:?}"); assert!( report.contains("kind: InvalidValue { field: \"previous transaction ID\""), @@ -791,7 +803,7 @@ mod test { .unwrap() .unwrap(); data.assert_valid(®istration); - let update = chain.update_stateless(registration).unwrap(); + let update = chain.update_stateless(®istration).unwrap(); // Current tx hash should be equal to the hash from block 4. assert_eq!(update.current_tx_id_hash(), data.txn_hash); assert!(update.role_data_record().contains_key(&data.role)); diff --git a/rust/rbac-registration/src/utils/test.rs b/rust/rbac-registration/src/utils/test.rs index 3d8f0e86111..345a019b555 100644 --- a/rust/rbac-registration/src/utils/test.rs +++ b/rust/rbac-registration/src/utils/test.rs @@ -48,8 +48,7 @@ impl BlockTestData { assert!(cip509.role_data(self.role).is_some()); assert_eq!(cip509.txn_hash(), self.txn_hash); assert_eq!(cip509.previous_transaction(), self.prv_hash); - let (purpose, ..) = cip509.clone().consume().unwrap(); - assert_eq!(purpose, self.purpose); + assert_eq!(cip509.purpose().unwrap(), self.purpose); } }