diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml index 153c0a5392..7b7bcb3fd2 100644 --- a/rust/rbac-registration/Cargo.toml +++ b/rust/rbac-registration/Cargo.toml @@ -26,11 +26,12 @@ brotli = "7.0.0" zstd = "0.13.2" x509-cert = "0.2.5" der-parser = "9.0.0" -dashmap = "6.1.0" -blake2b_simd = "1.0.2" tracing = "0.1.40" ed25519-dalek = "2.1.1" uuid = "1.11.0" -c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git" , tag = "v0.0.3" } +c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.3" } pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } +cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250124-00" } +cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250124-00" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250124-00" } diff --git a/rust/rbac-registration/src/cardano/cip509/cip509.rs b/rust/rbac-registration/src/cardano/cip509/cip509.rs new file mode 100644 index 0000000000..cdef22577b --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/cip509.rs @@ -0,0 +1,578 @@ +//! Cardano Improvement Proposal 509 (CIP-509) metadata module. +//! Doc Reference: +//! CDDL Reference: + +use std::{borrow::Cow, collections::HashMap}; + +use anyhow::{anyhow, Context}; +use cardano_blockchain_types::{MetadatumLabel, MultiEraBlock, TxnIndex}; +use catalyst_types::{ + hashes::{Blake2b256Hash, BLAKE_2B256_SIZE}, + problem_report::ProblemReport, + uuid::UuidV4, +}; +use cbork_utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; +use minicbor::{ + decode::{self}, + Decode, Decoder, +}; +use pallas::{ + codec::utils::Nullable, + ledger::{ + addresses::{Address, ShelleyAddress}, + primitives::conway, + traverse::MultiEraTx, + }, +}; +use strum_macros::FromRepr; +use tracing::warn; +use uuid::Uuid; + +use crate::{ + cardano::cip509::{ + decode_context::DecodeContext, + rbac::Cip509RbacMetadata, + types::{PaymentHistory, RoleNumber, TxInputHash, ValidationSignature}, + utils::Cip0134UriSet, + validation::{ + validate_aux, validate_role_data, validate_stake_public_key, validate_txn_inputs_hash, + }, + x509_chunks::X509Chunks, + Payment, PointTxnIdx, RoleData, + }, + utils::decode_helper::{report_duplicated_key, report_missing_keys}, +}; + +/// A x509 metadata envelope. +/// +/// The envelope is required to prevent replayability attacks. See [this document] for +/// more details. +/// +/// [this document]: https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md +#[derive(Debug, Clone)] +#[allow(clippy::module_name_repetitions)] +pub struct Cip509 { + /// A registration purpose (`UUIDv4`). + /// + /// The purpose is defined by the consuming dApp. + purpose: Option, + /// Transaction inputs hash. + txn_inputs_hash: Option, + /// An optional hash of the previous transaction. + /// + /// The hash must always be present except for the first registration transaction. + prv_tx_id: Option, + /// Metadata. + /// + /// This field encoded in chunks. See [`X509Chunks`] for more details. + metadata: Option, + /// A validation signature. + validation_signature: Option, + /// A payment history. + /// + /// The history is only tracked for the addresses that are passed to `Cip509` + /// constructors. + payment_history: PaymentHistory, + /// A hash of the transaction from which this registration is extracted. + txn_hash: Blake2b256Hash, + /// A point (slot) and a transaction index identifying the block and the transaction + /// that this `Cip509` was extracted from. + origin: PointTxnIdx, + /// A report potentially containing all the issues occurred during `Cip509` decoding + /// and validation. + /// + /// The data located in `Cip509` is only considered valid if + /// `ProblemReport::is_problematic()` returns false. + report: ProblemReport, +} + +/// Enum of CIP509 metadatum with its associated unsigned integer value. +#[allow(clippy::module_name_repetitions)] +#[derive(FromRepr, Debug, PartialEq, Copy, Clone)] +#[repr(u8)] +enum Cip509IntIdentifier { + /// Purpose. + Purpose = 0, + /// Transaction inputs hash. + TxInputsHash = 1, + /// Previous transaction ID. + PreviousTxId = 2, + /// Validation signature. + ValidationSignature = 99, +} + +impl Cip509 { + /// Returns a `Cip509` instance if it is present in the given transaction, otherwise + /// `None` is returned. + /// + /// # Errors + /// + /// An error is only returned if the data is completely corrupted. In all other cases + /// the `Cip509` structure contains fully or partially decoded data. + pub fn new( + block: &MultiEraBlock, index: TxnIndex, track_payment_addresses: &[ShelleyAddress], + ) -> Result, anyhow::Error> { + // Find the transaction and decode the relevant data. + let txns = block.txs(); + let txn = txns.get(usize::from(index)).ok_or_else(|| { + anyhow!( + "Invalid transaction index {index:?}, transactions count = {}", + txns.len() + ) + })?; + let MultiEraTx::Conway(txn) = txn else { + return Ok(None); + }; + let raw_aux_data = match &txn.auxiliary_data { + Nullable::Some(v) => v.raw_cbor(), + _ => return Ok(None), + }; + let Some(metadata) = block.txn_metadata(index, MetadatumLabel::CIP509_RBAC) else { + return Ok(None); + }; + + let mut decoder = Decoder::new(metadata.as_ref()); + let mut report = ProblemReport::new("Decoding and validating Cip509"); + let origin = PointTxnIdx::from_block(block, index); + let payment_history = payment_history(txn, track_payment_addresses, &origin, &report); + let mut decode_context = DecodeContext { + origin, + txn, + payment_history, + report: &mut report, + }; + let cip509 = + Cip509::decode(&mut decoder, &mut decode_context).context("Failed to decode Cip509")?; + + // Perform the validation. + if let Some(txn_inputs_hash) = &cip509.txn_inputs_hash { + validate_txn_inputs_hash(txn_inputs_hash, txn, &cip509.report); + }; + validate_aux( + raw_aux_data, + txn.transaction_body.auxiliary_data_hash.as_ref(), + &cip509.report, + ); + if cip509.role_data(RoleNumber::ROLE_0).is_some() { + // The following check is only performed for the role 0. + validate_stake_public_key(txn, cip509.certificate_uris(), &cip509.report); + } + if let Some(metadata) = &cip509.metadata { + validate_role_data(metadata, &cip509.report); + } + + Ok(Some(cip509)) + } + + /// Returns a list of Cip509 instances from all the transactions of the given block. + pub fn from_block( + block: &MultiEraBlock, track_payment_addresses: &[ShelleyAddress], + ) -> Vec { + let mut result = Vec::new(); + + for index in 0..block.decode().tx_count() { + let index = TxnIndex::from(index); + match Self::new(block, index, track_payment_addresses) { + Ok(Some(v)) => result.push(v), + // Normal situation: there is no Cip509 data in this transaction. + Ok(None) => {}, + Err(e) => { + warn!( + "Unable to extract Cip509 from the {} block {index:?} transaction: {e:?}", + block.point() + ); + }, + } + } + + result + } + + /// Returns all role numbers present in this `Cip509` instance. + #[must_use] + pub fn all_roles(&self) -> Vec { + if let Some(metadata) = &self.metadata { + metadata.role_data.keys().copied().collect() + } else { + Vec::new() + } + } + + /// Returns a role data for the given role if it is present. + #[must_use] + pub fn role_data(&self, role: RoleNumber) -> Option<&RoleData> { + self.metadata.as_ref().and_then(|m| m.role_data.get(&role)) + } + + /// Returns a hash of the previous transaction. + #[must_use] + pub fn previous_transaction(&self) -> Option { + self.prv_tx_id + } + + /// Returns a problem report. + #[must_use] + pub fn report(&self) -> &ProblemReport { + &self.report + } + + /// Returns a point and a transaction index where this data is originating from. + #[must_use] + pub fn origin(&self) -> &PointTxnIdx { + &self.origin + } + + /// Returns a hash of the transaction where this data is originating from. + #[must_use] + pub fn txn_hash(&self) -> Blake2b256Hash { + self.txn_hash + } + + /// Returns URIs contained in both x509 and c509 certificates of `Cip509` metadata. + #[must_use] + pub fn certificate_uris(&self) -> Option<&Cip0134UriSet> { + self.metadata.as_ref().map(|m| &m.certificate_uris) + } + + /// Returns a transaction inputs hash. + #[must_use] + pub fn txn_inputs_hash(&self) -> Option<&TxInputHash> { + self.txn_inputs_hash.as_ref() + } + + /// 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), + } + } +} + +impl Decode<'_, DecodeContext<'_, '_>> for Cip509 { + fn decode(d: &mut Decoder, decode_context: &mut DecodeContext) -> Result { + let context = "Decoding Cip509"; + + // It is ok to return error here because we were unable to decode anything, but everywhere + // below we should try to recover as much data as possible and not to return early. + let map_len = decode_map_len(d, context)?; + + let mut purpose = None; + let mut txn_inputs_hash = None; + let mut prv_tx_id = None; + let mut validation_signature = None; + let mut metadata = None; + + let mut found_keys = Vec::new(); + let mut is_metadata_found = false; + + for index in 0..map_len { + // We don't want to consume key here because it can be a part of chunked metadata that + // is decoded below. + let Ok(key) = d.probe().u8() else { + decode_context.report.other( + &format!("Unable to decode map key ({index} index)"), + context, + ); + break; + }; + if let Some(key) = Cip509IntIdentifier::from_repr(key) { + // Consume the key. This should never fail because we used `probe` above. + let _: u8 = decode_helper(d, context, &mut ())?; + + if report_duplicated_key(&found_keys, &key, index, context, decode_context.report) { + continue; + } + found_keys.push(key); + + match key { + Cip509IntIdentifier::Purpose => { + match decode_purpose(d, context, decode_context.report) { + Ok(v) => purpose = v, + Err(()) => break, + } + }, + Cip509IntIdentifier::TxInputsHash => { + match decode_input_hash(d, context, decode_context.report) { + Ok(v) => txn_inputs_hash = v, + Err(()) => break, + } + }, + Cip509IntIdentifier::PreviousTxId => { + match decode_previous_transaction_id(d, context, decode_context.report) { + Ok(v) => prv_tx_id = v, + Err(()) => break, + } + }, + Cip509IntIdentifier::ValidationSignature => { + match decode_validation_signature(d, context, decode_context.report) { + Ok(v) => validation_signature = v, + Err(()) => break, + } + }, + } + } else { + // Handle the x509 chunks 10 11 12 + // Technically it is possible to store multiple copies (or different instances) of + // metadata, but it isn't allowed. See this link for more details: + // https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md#keys-10-11-or-12---x509-chunked-data + if is_metadata_found { + decode_context.report.duplicate_field( + "metadata", + "Only one instance of the chunked metadata should be present", + context, + ); + continue; + } + is_metadata_found = true; + + match X509Chunks::decode(d, decode_context) { + Ok(chunks) => metadata = chunks.into(), + Err(e) => { + decode_context.report.other( + &format!("Unable to decode metadata from chunks: {e:?}"), + context, + ); + break; + }, + }; + } + } + + let required_keys = [ + Cip509IntIdentifier::Purpose, + Cip509IntIdentifier::TxInputsHash, + Cip509IntIdentifier::ValidationSignature, + ]; + report_missing_keys(&found_keys, &required_keys, context, decode_context.report); + if !is_metadata_found { + decode_context + .report + .missing_field("metadata (10, 11 or 12 chunks)", context); + } + + let txn_hash = MultiEraTx::Conway(Box::new(Cow::Borrowed(decode_context.txn))) + .hash() + .into(); + Ok(Self { + purpose, + txn_inputs_hash, + prv_tx_id, + metadata, + validation_signature, + payment_history: HashMap::new(), + txn_hash, + origin: decode_context.origin.clone(), + report: decode_context.report.clone(), + }) + } +} + +/// Records the payment history for the given set of addresses. +fn payment_history( + txn: &conway::MintedTx, track_payment_addresses: &[ShelleyAddress], origin: &PointTxnIdx, + report: &ProblemReport, +) -> HashMap> { + let hash = MultiEraTx::Conway(Box::new(Cow::Borrowed(txn))).hash(); + let context = format!("Populating payment history for Cip509, transaction hash = {hash:?}"); + + let mut result: HashMap<_, _> = track_payment_addresses + .iter() + .cloned() + .map(|a| (a, Vec::new())) + .collect(); + + for (index, output) in txn.transaction_body.outputs.iter().enumerate() { + let conway::PseudoTransactionOutput::PostAlonzo(output) = output else { + continue; + }; + + let address = match Address::from_bytes(&output.address) { + Ok(Address::Shelley(a)) => a, + Ok(_) => { + continue; + }, + Err(e) => { + report.other(&format!("Invalid output address: {e:?}"), &context); + continue; + }, + }; + + let index = match u16::try_from(index) { + Ok(v) => v, + Err(e) => { + report.other(&format!("Invalid output index ({index}): {e:?}"), &context); + continue; + }, + }; + + if let Some(history) = result.get_mut(&address) { + history.push(Payment::new( + origin.clone(), + hash, + index, + output.value.clone(), + )); + } + } + + result +} + +/// Decodes purpose. +fn decode_purpose( + d: &mut Decoder, context: &str, report: &ProblemReport, +) -> Result, ()> { + let bytes = match decode_bytes(d, "Cip509 purpose") { + Ok(v) => v, + Err(e) => { + report.other(&format!("Unable to decode purpose: {e:?}"), context); + return Err(()); + }, + }; + + let len = bytes.len(); + let Ok(uuid) = Uuid::try_from(bytes) else { + report.invalid_value( + "purpose", + &format!("{len} bytes"), + "must be 16 bytes long", + context, + ); + return Ok(None); + }; + match UuidV4::try_from(uuid) { + Ok(v) => Ok(Some(v)), + Err(e) => { + report.other(&format!("Invalid purpose UUID: {e:?}"), context); + Ok(None) + }, + } +} + +/// Decodes input hash. +fn decode_input_hash( + d: &mut Decoder, context: &str, report: &ProblemReport, +) -> Result, ()> { + let bytes = match decode_bytes(d, "Cip509 txn inputs hash") { + Ok(v) => v, + Err(e) => { + report.other( + &format!("Unable to decode transaction inputs hash: {e:?}"), + context, + ); + return Err(()); + }, + }; + + let len = bytes.len(); + if let Ok(v) = TxInputHash::try_from(bytes.as_slice()) { + Ok(Some(v)) + } else { + report.invalid_value( + "transaction inputs hash", + &format!("{len} bytes"), + "must be 16 bytes long", + context, + ); + Ok(None) + } +} + +/// Decodes previous transaction id. +fn decode_previous_transaction_id( + d: &mut Decoder, context: &str, report: &ProblemReport, +) -> Result, ()> { + let bytes = match decode_bytes(d, "Cip509 previous transaction id") { + Ok(v) => v, + Err(e) => { + report.other( + &format!("Unable to decode previous transaction id: {e:?}"), + context, + ); + return Err(()); + }, + }; + + let len = bytes.len(); + if let Ok(v) = Blake2b256Hash::try_from(bytes) { + Ok(Some(v)) + } else { + report.invalid_value( + "previous transaction hash", + &format!("{len} bytes"), + &format!("must be {BLAKE_2B256_SIZE} bytes long"), + context, + ); + Ok(None) + } +} + +/// Decodes validation signature. +fn decode_validation_signature( + d: &mut Decoder, context: &str, report: &ProblemReport, +) -> Result, ()> { + let bytes = match decode_bytes(d, "Cip509 validation signature") { + Ok(v) => v, + Err(e) => { + report.other( + &format!("Unable to decode validation signature: {e:?}"), + context, + ); + return Err(()); + }, + }; + + let len = bytes.len(); + if let Ok(v) = ValidationSignature::try_from(bytes) { + Ok(Some(v)) + } else { + report.invalid_value( + "validation signature", + &format!("{len} bytes"), + "must be at least 1 byte and at most 64 bytes long", + context, + ); + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::test; + + #[test] + fn new() { + let data = test::block_1(); + let res = Cip509::new(&data.block, data.txn_index, &[]) + .expect("Failed to get Cip509") + .expect("There must be Cip509 in block"); + assert!(!res.report.is_problematic(), "{:?}", res.report); + data.assert_valid(&res); + } + + #[test] + fn from_block() { + let data = test::block_1(); + let res = Cip509::from_block(&data.block, &[]); + assert_eq!(1, res.len()); + let cip509 = res.first().unwrap(); + assert!(!cip509.report.is_problematic(), "{:?}", cip509.report); + data.assert_valid(cip509); + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/decode_context.rs b/rust/rbac-registration/src/cardano/cip509/decode_context.rs new file mode 100644 index 0000000000..f112486318 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/decode_context.rs @@ -0,0 +1,24 @@ +//! A context used to pass additional parameters during decoding. + +use std::collections::HashMap; + +use catalyst_types::problem_report::ProblemReport; +use pallas::ledger::{addresses::ShelleyAddress, primitives::conway}; + +use crate::cardano::cip509::{Payment, PointTxnIdx}; + +/// A context used to pass the problem report and additional parameters during decoding. +pub struct DecodeContext<'r, 't> { + /// A slot and a transaction index. + pub origin: PointTxnIdx, + /// A transaction. + pub txn: &'t conway::MintedTx<'t>, + /// A payment history. + pub payment_history: HashMap>, + /// A problem report. + /// + /// The reference must be mutable because the `Decode::decode` function takes a + /// mutable reference to the context and sometimes we want to pass just the report + /// without whole `DecodeContext`. + pub report: &'r mut ProblemReport, +} diff --git a/rust/rbac-registration/src/cardano/cip509/mod.rs b/rust/rbac-registration/src/cardano/cip509/mod.rs index 421cf9852d..b100233bf8 100644 --- a/rust/rbac-registration/src/cardano/cip509/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/mod.rs @@ -2,202 +2,19 @@ //! Doc Reference: //! CDDL Reference: -// cspell: words pkix - -pub mod rbac; -pub mod types; -pub mod utils; -pub(crate) mod validation; -pub mod x509_chunks; - -use minicbor::{ - decode::{self}, - Decode, Decoder, -}; -use pallas::{crypto::hash::Hash, ledger::traverse::MultiEraTx}; -use strum_macros::FromRepr; -use types::tx_input_hash::TxInputHash; -use uuid::Uuid; -use validation::{ - validate_aux, validate_payment_key, validate_role_singing_key, validate_stake_public_key, - validate_txn_inputs_hash, +pub use cip509::Cip509; +pub use rbac::{C509Cert, SimplePublicKeyType, X509DerCert}; +pub use types::{ + CertKeyHash, KeyLocalRef, LocalRefInt, Payment, PaymentHistory, PointTxnIdx, RoleData, + RoleNumber, TxInputHash, ValidationSignature, }; -use x509_chunks::X509Chunks; - -use super::transaction::witness::TxWitness; -use crate::utils::{ - decode_helper::{decode_bytes, decode_helper, decode_map_len}, - general::{decode_utf8, decremented_index}, - hashing::{blake2b_128, blake2b_256}, -}; - -/// CIP509 label. -pub const LABEL: u64 = 509; - -/// CIP509. -#[derive(Debug, PartialEq, Clone, Default)] -pub struct Cip509 { - /// `UUIDv4` Purpose . - pub purpose: Uuid, // (bytes .size 16) - /// Transaction inputs hash. - pub txn_inputs_hash: TxInputHash, // bytes .size 16 - /// Optional previous transaction ID. - pub prv_tx_id: Option>, // bytes .size 32 - /// x509 chunks. - pub x509_chunks: X509Chunks, // chunk_type => [ + x509_chunk ] - /// Validation signature. - pub validation_signature: Vec, // bytes size (1..64) -} - -/// Validation value for CIP509 metadatum. -#[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)] -#[derive(Debug, PartialEq, Clone, Default)] -pub struct Cip509Validation { - /// Boolean value for the validity of the transaction inputs hash. - pub is_valid_txn_inputs_hash: bool, - /// Boolean value for the validity of the auxiliary data. - pub is_valid_aux: bool, - /// Boolean value for the validity of the stake public key. - pub is_valid_stake_public_key: bool, - /// Boolean value for the validity of the payment key. - pub is_valid_payment_key: bool, - /// Boolean value for the validity of the signing key. - pub is_valid_signing_key: bool, - /// Additional data from the CIP509 validation.. - pub additional_data: AdditionalData, -} - -/// Additional data from the CIP509 validation. -#[derive(Debug, PartialEq, Clone, Default)] -pub struct AdditionalData { - /// Bytes of precomputed auxiliary data. - pub precomputed_aux: Vec, -} - -/// Enum of CIP509 metadatum with its associated unsigned integer value. -#[allow(clippy::module_name_repetitions)] -#[derive(FromRepr, Debug, PartialEq)] -#[repr(u8)] -pub(crate) enum Cip509IntIdentifier { - /// Purpose. - Purpose = 0, - /// Transaction inputs hash. - TxInputsHash = 1, - /// Previous transaction ID. - PreviousTxId = 2, - /// Validation signature. - ValidationSignature = 99, -} - -impl Decode<'_, ()> for Cip509 { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { - let map_len = decode_map_len(d, "CIP509")?; - let mut cip509_metadatum = Cip509::default(); - for _ in 0..map_len { - // Use probe to peak - let key = d.probe().u8()?; - if let Some(key) = Cip509IntIdentifier::from_repr(key) { - // Consuming the int - let _: u8 = decode_helper(d, "CIP509", ctx)?; - match key { - Cip509IntIdentifier::Purpose => { - cip509_metadatum.purpose = - Uuid::try_from(decode_bytes(d, "CIP509 purpose")?).map_err(|_| { - decode::Error::message("Invalid data size of Purpose") - })?; - }, - Cip509IntIdentifier::TxInputsHash => { - cip509_metadatum.txn_inputs_hash = - TxInputHash::try_from(decode_bytes(d, "CIP509 txn inputs hash")?) - .map_err(|_| { - decode::Error::message("Invalid data size of TxInputsHash") - })?; - }, - Cip509IntIdentifier::PreviousTxId => { - let prv_tx_hash: [u8; 32] = decode_bytes(d, "CIP509 previous tx ID")? - .try_into() - .map_err(|_| { - decode::Error::message("Invalid data size of PreviousTxId") - })?; - cip509_metadatum.prv_tx_id = Some(Hash::from(prv_tx_hash)); - }, - Cip509IntIdentifier::ValidationSignature => { - let validation_signature = decode_bytes(d, "CIP509 validation signature")?; - if validation_signature.is_empty() || validation_signature.len() > 64 { - return Err(decode::Error::message( - "Invalid data size of ValidationSignature", - )); - } - cip509_metadatum.validation_signature = validation_signature; - }, - } - } else { - // Handle the x509 chunks 10 11 12 - let x509_chunks = X509Chunks::decode(d, ctx)?; - cip509_metadatum.x509_chunks = x509_chunks; - } - } - Ok(cip509_metadatum) - } -} - -impl Cip509 { - /// Basic validation for CIP509 - /// The validation include the following: - /// * Hashing the transaction inputs within the transaction should match the - /// txn-inputs-hash - /// * Auxiliary data hash within the transaction should match the hash of the - /// auxiliary data itself. - /// * Public key validation for role 0 where public key extracted from x509 and c509 - /// subject alternative name should match one of the witness in witness set within - /// the transaction. - /// * Payment key reference validation for role 0 where the reference should be either - /// 1. Negative index reference - reference to transaction output in transaction: - /// should match some of the key within witness set. - /// 2. Positive index reference - reference to the transaction input in - /// transaction: only check whether the index exist within the transaction - /// inputs. - /// * Role signing key validation for role 0 where the signing keys should only be the - /// certificates - /// - /// See: - /// * - /// * - /// - /// Note: This CIP509 is still under development and is subject to change. - /// - /// # Parameters - /// * `txn` - Transaction data was attached to and to be validated/decoded against. - /// * `validation_report` - Validation report to store the validation result. - pub fn validate( - &self, txn: &MultiEraTx, validation_report: &mut Vec, - ) -> Cip509Validation { - let is_valid_txn_inputs_hash = - validate_txn_inputs_hash(self, txn, validation_report).unwrap_or(false); - let (is_valid_aux, precomputed_aux) = - validate_aux(txn, validation_report).unwrap_or_default(); - let mut is_valid_stake_public_key = true; - let mut is_valid_payment_key = true; - let mut is_valid_signing_key = true; - if let Some(role_set) = &self.x509_chunks.0.role_set { - // Validate only role 0 - for role in role_set { - if role.role_number == 0 { - is_valid_stake_public_key = - validate_stake_public_key(self, txn, validation_report).unwrap_or(false); - is_valid_payment_key = - validate_payment_key(txn, role, validation_report).unwrap_or(false); - is_valid_signing_key = validate_role_singing_key(role, validation_report); - } - } - } - Cip509Validation { - is_valid_txn_inputs_hash, - is_valid_aux, - is_valid_stake_public_key, - is_valid_payment_key, - is_valid_signing_key, - additional_data: AdditionalData { precomputed_aux }, - } - } -} +pub use utils::Cip0134UriSet; + +#[allow(clippy::module_inception)] +mod cip509; +mod decode_context; +mod rbac; +mod types; +mod utils; +mod validation; +mod x509_chunks; diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/certs.rs b/rust/rbac-registration/src/cardano/cip509/rbac/certs.rs deleted file mode 100644 index de34cd392c..0000000000 --- a/rust/rbac-registration/src/cardano/cip509/rbac/certs.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Certificates for the RBAC metadata. - -use c509_certificate::c509::C509; -use minicbor::{decode, Decode, Decoder}; -use x509_cert::{der::Decode as x509Decode, Certificate}; - -use super::tag::KeyTag; -use crate::utils::decode_helper::{decode_array_len, decode_bytes, decode_helper, decode_tag}; - -// ------------------x509------------------------ - -/// Enum of possible X.509 DER certificate. -#[derive(Debug, PartialEq, Clone, Default)] -pub enum X509DerCert { - /// Undefined indicates skipped element. - #[default] - Undefined, - /// Deleted indicates the key is deleted. - Deleted, - /// X.509 certificate. - X509Cert(Vec), -} - -impl Decode<'_, ()> for X509DerCert { - fn decode(d: &mut Decoder, _ctx: &mut ()) -> Result { - match d.datatype()? { - minicbor::data::Type::Tag => { - let tag = decode_tag(d, "X509DerCert")?; - match tag { - t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), - _ => Err(decode::Error::message("Unknown tag for X509DerCert")), - } - }, - minicbor::data::Type::Undefined => Ok(Self::Undefined), - minicbor::data::Type::Bytes => { - let data = decode_bytes(d, "X509DerCert")?; - Certificate::from_der(&data) - .map_err(|_| decode::Error::message("Invalid x509 certificate"))?; - Ok(Self::X509Cert(data.clone())) - }, - _ => Err(decode::Error::message("Invalid datatype for X509DerCert")), - } - } -} - -// ------------------c509----------------------- - -/// Enum of possible X.509 DER certificate. -#[derive(Debug, PartialEq, Clone, Default)] -pub enum C509Cert { - /// Undefined indicates skipped element. - #[default] - Undefined, - /// Deleted indicates the key is deleted. - Deleted, - /// A c509 certificate in metadatum reference. - C509CertInMetadatumReference(C509CertInMetadatumReference), - /// A c509 certificate. - C509Certificate(Box), -} - -impl Decode<'_, ()> for C509Cert { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { - match d.datatype()? { - minicbor::data::Type::Tag => { - let tag = decode_tag(d, "C509Cert")?; - match tag { - t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), - _ => Err(decode::Error::message("Unknown tag for C509Cert")), - } - }, - minicbor::data::Type::Array => { - let arr_len = decode_array_len(d, "C509Cert")?; - // C509CertInMetadatumReference must have 3 items - if arr_len == 3 { - Ok(Self::C509CertInMetadatumReference( - C509CertInMetadatumReference::decode(d, ctx)?, - )) - } else { - Err(decode::Error::message( - "Invalid length C509CertInMetadatumReference, expected 3", - )) - } - }, - minicbor::data::Type::Bytes => { - let c509 = decode_bytes(d, "C509Cert")?; - let mut c509_d = Decoder::new(&c509); - Ok(Self::C509Certificate(Box::new(C509::decode( - &mut c509_d, - ctx, - )?))) - }, - minicbor::data::Type::Undefined => Ok(Self::Undefined), - _ => Err(decode::Error::message("Invalid datatype for C509Cert")), - } - } -} - -/// C509 certificate in metadatum reference. -#[derive(Debug, PartialEq, Clone)] -pub struct C509CertInMetadatumReference { - /// Transaction output field. - pub txn_output_field: u8, - /// Transaction output index. - pub txn_output_index: u64, - /// Optional certificate reference. - pub cert_ref: Option>, -} - -impl Decode<'_, ()> for C509CertInMetadatumReference { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { - let txn_output_field: u8 = - decode_helper(d, "txn output field in C509CertInMetadatumReference", ctx)?; - let txn_output_index: u64 = - decode_helper(d, "txn output index in C509CertInMetadatumReference", ctx)?; - let cert_ref = match d.datatype()? { - minicbor::data::Type::Array => { - let len = decode_array_len(d, "cert ref in C509CertInMetadatumReference")?; - let arr: Result, _> = (0..len).map(|_| d.u64()).collect(); - arr.map(Some) - }, - minicbor::data::Type::Null => Ok(None), - _ => { - Ok(Some(vec![decode_helper( - d, - "C509CertInMetadatumReference", - ctx, - )?])) - }, - }?; - Ok(Self { - txn_output_field, - txn_output_index, - cert_ref, - }) - } -} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/certs/c509.rs b/rust/rbac-registration/src/cardano/cip509/rbac/certs/c509.rs new file mode 100644 index 0000000000..4911c7d974 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/certs/c509.rs @@ -0,0 +1,63 @@ +//! C509 certificate. + +use c509_certificate::c509::C509; +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{decode_array_len, decode_bytes, decode_tag}; +use minicbor::{decode, Decode, Decoder}; + +use crate::cardano::cip509::rbac::{certs::C509CertInMetadatumReference, tag::KeyTag}; + +/// An enum of possible C509 certificate values. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, PartialEq, Clone, Default)] +pub enum C509Cert { + /// Undefined indicates skipped element. + #[default] + Undefined, + /// Deleted indicates the key is deleted. + Deleted, + /// A c509 certificate in metadatum reference. + C509CertInMetadatumReference(C509CertInMetadatumReference), + /// A c509 certificate. + C509Certificate(Box), +} + +impl Decode<'_, ProblemReport> for C509Cert { + fn decode(d: &mut Decoder, _report: &mut ProblemReport) -> Result { + match d.datatype()? { + minicbor::data::Type::Tag => { + let tag = decode_tag(d, "C509Cert")?; + match tag { + t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), + _ => Err(decode::Error::message("Unknown tag for C509Cert")), + } + }, + minicbor::data::Type::Array => { + let arr_len = decode_array_len(d, "C509Cert")?; + // C509CertInMetadatumReference must have 3 items + if arr_len == 3 { + Ok(Self::C509CertInMetadatumReference( + C509CertInMetadatumReference::decode(d, &mut ())?, + )) + } else { + Err(decode::Error::message( + "Invalid length C509CertInMetadatumReference, expected 3", + )) + } + }, + minicbor::data::Type::Bytes => { + let c509 = decode_bytes(d, "C509Cert")?; + let mut c509_d = Decoder::new(&c509); + Ok(Self::C509Certificate(Box::new(C509::decode( + &mut c509_d, + &mut (), + )?))) + }, + minicbor::data::Type::Undefined => { + d.undefined()?; + Ok(Self::Undefined) + }, + _ => Err(decode::Error::message("Invalid datatype for C509Cert")), + } + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/certs/c509_metadatum.rs b/rust/rbac-registration/src/cardano/cip509/rbac/certs/c509_metadatum.rs new file mode 100644 index 0000000000..7720867c7a --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/certs/c509_metadatum.rs @@ -0,0 +1,44 @@ +//! C509 certificate in metadatum reference. + +use cbork_utils::decode_helper::{decode_array_len, decode_helper}; +use minicbor::{decode, Decode, Decoder}; + +/// C509 certificate in metadatum reference. +#[derive(Debug, PartialEq, Clone)] +pub struct C509CertInMetadatumReference { + /// Transaction output field. + pub txn_output_field: u8, + /// Transaction output index. + pub txn_output_index: u64, + /// Optional certificate reference. + pub cert_ref: Option>, +} + +impl Decode<'_, ()> for C509CertInMetadatumReference { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + let txn_output_field: u8 = + decode_helper(d, "txn output field in C509CertInMetadatumReference", ctx)?; + let txn_output_index: u64 = + decode_helper(d, "txn output index in C509CertInMetadatumReference", ctx)?; + let cert_ref = match d.datatype()? { + minicbor::data::Type::Array => { + let len = decode_array_len(d, "cert ref in C509CertInMetadatumReference")?; + let arr: Result, _> = (0..len).map(|_| d.u64()).collect(); + arr.map(Some) + }, + minicbor::data::Type::Null => Ok(None), + _ => { + Ok(Some(vec![decode_helper( + d, + "C509CertInMetadatumReference", + ctx, + )?])) + }, + }?; + Ok(Self { + txn_output_field, + txn_output_index, + cert_ref, + }) + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/certs/mod.rs b/rust/rbac-registration/src/cardano/cip509/rbac/certs/mod.rs new file mode 100644 index 0000000000..6c7faf91c7 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/certs/mod.rs @@ -0,0 +1,9 @@ +//! X509 and C509 certificates. + +pub use c509::C509Cert; +pub use c509_metadatum::C509CertInMetadatumReference; +pub use x509::X509DerCert; + +mod c509; +mod c509_metadatum; +mod x509; diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/certs/x509.rs b/rust/rbac-registration/src/cardano/cip509/rbac/certs/x509.rs new file mode 100644 index 0000000000..abdbfcf015 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/certs/x509.rs @@ -0,0 +1,47 @@ +//! X509 certificate. + +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{decode_bytes, decode_tag}; +use minicbor::{decode, Decode, Decoder}; +use x509_cert::{der::Decode as x509Decode, Certificate}; + +use crate::cardano::cip509::rbac::tag::KeyTag; + +/// Enum of possible X.509 DER certificate. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, PartialEq, Clone, Default)] +pub enum X509DerCert { + /// Undefined indicates skipped element. + #[default] + Undefined, + /// Deleted indicates the key is deleted. + Deleted, + /// X.509 certificate. + X509Cert(Box), +} + +impl Decode<'_, ProblemReport> for X509DerCert { + fn decode(d: &mut Decoder, _report: &mut ProblemReport) -> Result { + match d.datatype()? { + minicbor::data::Type::Tag => { + let tag = decode_tag(d, "X509DerCert")?; + match tag { + t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), + _ => Err(decode::Error::message("Unknown tag for X509DerCert")), + } + }, + minicbor::data::Type::Undefined => { + d.undefined()?; + Ok(Self::Undefined) + }, + minicbor::data::Type::Bytes => { + let data = decode_bytes(d, "X509DerCert")?; + let certificate = Certificate::from_der(&data).map_err(|e| { + decode::Error::message(format!("Invalid x509 certificate: {e:?}")) + })?; + Ok(Self::X509Cert(Box::new(certificate))) + }, + _ => Err(decode::Error::message("Invalid datatype for X509DerCert")), + } + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs b/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs new file mode 100644 index 0000000000..687cd50620 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs @@ -0,0 +1,290 @@ +//! Cip509 RBAC metadata. + +use std::collections::{HashMap, HashSet}; + +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{ + decode_any, decode_array_len, decode_bytes, decode_helper, decode_map_len, +}; +use minicbor::{decode, Decode, Decoder}; +use strum_macros::FromRepr; + +use crate::{ + cardano::cip509::{ + decode_context::DecodeContext, + rbac::{role_data::CborRoleData, C509Cert, SimplePublicKeyType, X509DerCert}, + utils::Cip0134UriSet, + CertKeyHash, RoleData, RoleNumber, + }, + utils::decode_helper::report_duplicated_key, +}; + +/// Cip509 RBAC metadata. +/// +/// See [this document] for more details. +/// +/// [this document]: https://github.com/input-output-hk/catalyst-CIPs/tree/x509-role-registration-metadata/CIP-XXXX +#[derive(Debug, PartialEq, Clone)] +#[allow(clippy::module_name_repetitions)] +pub struct Cip509RbacMetadata { + /// A potentially empty list of x509 certificates. + pub x509_certs: Vec, + /// A potentially empty list of c509 certificates. + pub c509_certs: Vec, + /// A set of URIs contained in both x509 and c509 certificates. + /// + /// URIs from different certificate types are stored separately and certificate + /// indexes are preserved too. + /// + /// This field isn't present in the encoded format and is populated by processing both + /// `x509_certs` and `c509_certs` fields. + pub certificate_uris: Cip0134UriSet, + /// A list of public keys that can be used instead of storing full certificates. + /// + /// Check [this section] to understand how certificates and the public keys list are + /// related. + /// + /// [this section]: https://github.com/input-output-hk/catalyst-CIPs/tree/x509-role-registration-metadata/CIP-XXXX#storing-certificates-and-public-key + pub pub_keys: Vec, + /// A potentially empty list of revoked certificates. + pub revocation_list: Vec, + /// A potentially empty role data. + pub role_data: HashMap, + /// Optional map of purpose key data. + /// Empty map if no purpose key data is present. + pub purpose_key_data: HashMap>, +} + +/// The first valid purpose key. +const FIRST_PURPOSE_KEY: u16 = 200; +/// The last valid purpose key. +const LAST_PURPOSE_KEY: u16 = 299; + +/// Enum of CIP509 RBAC metadata with its associated unsigned integer value. +#[derive(FromRepr, Debug, PartialEq, Copy, Clone)] +#[repr(u16)] +pub enum Cip509RbacMetadataInt { + /// x509 certificates. + X509Certs = 10, + /// c509 certificates. + C509Certs = 20, + /// Public keys. + PubKeys = 30, + /// Revocation list. + RevocationList = 40, + /// Role data set. + RoleSet = 100, +} + +impl Decode<'_, DecodeContext<'_, '_>> for Cip509RbacMetadata { + fn decode(d: &mut Decoder, decode_context: &mut DecodeContext) -> Result { + let context = "Decoding Cip509RbacMetadata"; + + let map_len = decode_map_len(d, context)?; + + let mut found_keys = Vec::new(); + + let mut x509_certs = Vec::new(); + let mut c509_certs = Vec::new(); + let mut pub_keys = Vec::new(); + let mut revocation_list = Vec::new(); + let mut role_data = HashMap::new(); + let mut purpose_key_data = HashMap::new(); + + for index in 0..map_len { + let key: u16 = decode_helper(d, "key in Cip509RbacMetadata", &mut ())?; + if let Some(key) = Cip509RbacMetadataInt::from_repr(key) { + if report_duplicated_key(&found_keys, &key, index, context, decode_context.report) { + continue; + } + found_keys.push(key); + + match key { + Cip509RbacMetadataInt::X509Certs => { + match decode_array( + d, + "Cip509RbacMetadata x509 certificates", + decode_context.report, + ) { + Some(v) => x509_certs = v, + None => break, + } + }, + Cip509RbacMetadataInt::C509Certs => { + match decode_array( + d, + "Cip509RbacMetadata c509 certificates", + decode_context.report, + ) { + Some(v) => c509_certs = v, + None => break, + } + }, + Cip509RbacMetadataInt::PubKeys => { + match decode_array( + d, + "Cip509RbacMetadata public keys", + decode_context.report, + ) { + Some(v) => pub_keys = v, + None => break, + } + }, + Cip509RbacMetadataInt::RevocationList => { + match decode_revocation_list(d, decode_context.report) { + Some(v) => revocation_list = v, + None => break, + } + }, + Cip509RbacMetadataInt::RoleSet => { + if let Some(data) = decode_role_data(d, context, decode_context) { + role_data = data; + } else { + break; + } + }, + } + } else { + if !(FIRST_PURPOSE_KEY..=LAST_PURPOSE_KEY).contains(&key) { + decode_context.report.other(&format!("Invalid purpose key set ({key}), should be with the range {FIRST_PURPOSE_KEY} - {LAST_PURPOSE_KEY}"), context); + continue; + } + + match decode_any(d, "purpose key") { + Ok(v) => { + purpose_key_data.insert(key, v.to_vec()); + }, + Err(e) => { + decode_context + .report + .other(&format!("Unable to decode purpose value: {e:?}"), context); + break; + }, + } + } + } + + let certificate_uris = Cip0134UriSet::new(&x509_certs, &c509_certs, decode_context.report); + + Ok(Self { + x509_certs, + c509_certs, + certificate_uris, + pub_keys, + revocation_list, + role_data, + purpose_key_data, + }) + } +} + +/// Decodes an array of type T. +fn decode_array<'b, T>( + d: &mut Decoder<'b>, context: &str, report: &mut ProblemReport, +) -> Option> +where T: Decode<'b, ProblemReport> { + let len = match decode_array_len(d, context) { + Ok(v) => v, + Err(e) => { + report.other(&format!("Unable to decode array length: {e:?}"), context); + return None; + }, + }; + let len = match usize::try_from(len) { + Ok(v) => v, + Err(e) => { + report.other(&format!("Invalid array length: {e:?}"), context); + return Some(Vec::new()); + }, + }; + + let mut result = Vec::with_capacity(len); + for _ in 0..len { + match T::decode(d, report) { + Ok(v) => result.push(v), + Err(e) => { + report.other(&format!("Unable to decode array value: {e:?}"), context); + return None; + }, + } + } + Some(result) +} + +/// Decode an array of revocation list. +fn decode_revocation_list(d: &mut Decoder, report: &ProblemReport) -> Option> { + let context = "Cip509RbacMetadata revocation list"; + let len = match decode_array_len(d, context) { + Ok(v) => v, + Err(e) => { + report.other(&format!("Unable to decode array length: {e:?}"), context); + return None; + }, + }; + let len = match usize::try_from(len) { + Ok(v) => v, + Err(e) => { + report.other(&format!("Invalid array length: {e:?}"), context); + return Some(Vec::new()); + }, + }; + + let mut result = Vec::with_capacity(len); + for _ in 0..len { + let bytes = match decode_bytes(d, context) { + Ok(v) => v, + Err(e) => { + report.other( + &format!("Unable to decode certificate hash bytes: {e:?}"), + context, + ); + return None; + }, + }; + match CertKeyHash::try_from(bytes) { + Ok(v) => result.push(v), + Err(e) => { + report.other( + &format!("Invalid revocation list certificate hash: {e:?}"), + context, + ); + }, + } + } + Some(result) +} + +/// Adds report entries if duplicated roles are found. +fn report_duplicated_roles(data: &[CborRoleData], context: &str, report: &ProblemReport) { + let mut roles = HashSet::new(); + for role in data { + let Some(number) = role.number else { + continue; + }; + if !roles.insert(number) { + report.other(&format!("Duplicated role number {number:?} found"), context); + } + } +} + +/// Decodes and converts a role data. +fn decode_role_data( + d: &mut Decoder, context: &str, decode_context: &mut DecodeContext, +) -> Option> { + let roles = decode_array(d, "Cip509RbacMetadata role set", decode_context.report)?; + report_duplicated_roles(&roles, context, decode_context.report); + let roles = roles + .into_iter() + .filter_map(|data| { + if let Some(number) = data.number { + Some(( + number, + RoleData::new(data, decode_context.txn, decode_context.report), + )) + } else { + None + } + }) + .collect(); + Some(roles) +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs b/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs index 34675d80ce..9577415a34 100644 --- a/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs @@ -2,168 +2,13 @@ //! Doc Reference: //! CDDL Reference: -pub mod certs; -pub mod pub_key; pub mod role_data; -pub(crate) mod tag; -use std::collections::HashMap; +pub use certs::{C509Cert, X509DerCert}; +pub use metadata::{Cip509RbacMetadata, Cip509RbacMetadataInt}; +pub use pub_key::SimplePublicKeyType; -use certs::{C509Cert, X509DerCert}; -use minicbor::{decode, Decode, Decoder}; -use pub_key::SimplePublicKeyType; -use role_data::RoleData; -use strum_macros::FromRepr; - -use super::types::cert_key_hash::CertKeyHash; -use crate::utils::decode_helper::{ - decode_any, decode_array_len, decode_bytes, decode_helper, decode_map_len, -}; - -/// Cip509 RBAC metadata. -#[derive(Debug, PartialEq, Clone, Default)] -pub struct Cip509RbacMetadata { - /// Optional list of x509 certificates. - pub x509_certs: Option>, - /// Optional list of c509 certificates. - /// The value can be either the c509 certificate or c509 metadatum reference. - pub c509_certs: Option>, - /// Optional list of Public keys. - pub pub_keys: Option>, - /// Optional list of revocation list. - pub revocation_list: Option>, - /// Optional list of role data. - pub role_set: Option>, - /// Optional map of purpose key data. - /// Empty map if no purpose key data is present. - pub purpose_key_data: HashMap>, -} - -/// The first valid purpose key. -const FIRST_PURPOSE_KEY: u16 = 200; -/// The last valid purpose key. -const LAST_PURPOSE_KEY: u16 = 299; - -/// Enum of CIP509 RBAC metadata with its associated unsigned integer value. -#[derive(FromRepr, Debug, PartialEq)] -#[repr(u16)] -pub enum Cip509RbacMetadataInt { - /// x509 certificates. - X509Certs = 10, - /// c509 certificates. - C509Certs = 20, - /// Public keys. - PubKeys = 30, - /// Revocation list. - RevocationList = 40, - /// Role data set. - RoleSet = 100, -} - -impl Cip509RbacMetadata { - /// Create a new instance of `Cip509RbacMetadata`. - pub(crate) fn new() -> Self { - Self { - x509_certs: None, - c509_certs: None, - pub_keys: None, - revocation_list: None, - role_set: None, - purpose_key_data: HashMap::new(), - } - } - - /// Set the x509 certificates. - fn set_x509_certs(&mut self, x509_certs: Vec) { - self.x509_certs = Some(x509_certs); - } - - /// Set the c509 certificates. - fn set_c509_certs(&mut self, c509_certs: Vec) { - self.c509_certs = Some(c509_certs); - } - - /// Set the public keys. - fn set_pub_keys(&mut self, pub_keys: Vec) { - self.pub_keys = Some(pub_keys); - } - - /// Set the revocation list. - fn set_revocation_list(&mut self, revocation_list: Vec) { - self.revocation_list = Some(revocation_list); - } - - /// Set the role data set. - fn set_role_set(&mut self, role_set: Vec) { - self.role_set = Some(role_set); - } -} - -impl Decode<'_, ()> for Cip509RbacMetadata { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { - let map_len = decode_map_len(d, "Cip509RbacMetadata")?; - - let mut x509_rbac_metadata = Cip509RbacMetadata::new(); - - for _ in 0..map_len { - let key: u16 = decode_helper(d, "key in Cip509RbacMetadata", ctx)?; - if let Some(key) = Cip509RbacMetadataInt::from_repr(key) { - match key { - Cip509RbacMetadataInt::X509Certs => { - let x509_certs = decode_array_rbac(d, "x509 certificate")?; - x509_rbac_metadata.set_x509_certs(x509_certs); - }, - Cip509RbacMetadataInt::C509Certs => { - let c509_certs = decode_array_rbac(d, "c509 certificate")?; - x509_rbac_metadata.set_c509_certs(c509_certs); - }, - Cip509RbacMetadataInt::PubKeys => { - let pub_keys = decode_array_rbac(d, "public keys")?; - x509_rbac_metadata.set_pub_keys(pub_keys); - }, - Cip509RbacMetadataInt::RevocationList => { - let revocation_list = decode_revocation_list(d)?; - x509_rbac_metadata.set_revocation_list(revocation_list); - }, - Cip509RbacMetadataInt::RoleSet => { - let role_set = decode_array_rbac(d, "role set")?; - x509_rbac_metadata.set_role_set(role_set); - }, - } - } else { - if !(FIRST_PURPOSE_KEY..=LAST_PURPOSE_KEY).contains(&key) { - return Err(decode::Error::message(format!("Invalid purpose key set, should be with the range {FIRST_PURPOSE_KEY} - {LAST_PURPOSE_KEY}"))); - } - x509_rbac_metadata - .purpose_key_data - .insert(key, decode_any(d, "purpose key")?); - } - } - Ok(x509_rbac_metadata) - } -} - -/// Decode an array of type T. -fn decode_array_rbac<'b, T>(d: &mut Decoder<'b>, from: &str) -> Result, decode::Error> -where T: Decode<'b, ()> { - let len = decode_array_len(d, &format!("{from} Cip509RbacMetadata"))?; - let mut vec = Vec::with_capacity(usize::try_from(len).map_err(decode::Error::message)?); - for _ in 0..len { - vec.push(T::decode(d, &mut ())?); - } - Ok(vec) -} - -/// Decode an array of revocation list. -fn decode_revocation_list(d: &mut Decoder) -> Result, decode::Error> { - let len = decode_array_len(d, "revocation list Cip509RbacMetadata")?; - let mut revocation_list = - Vec::with_capacity(usize::try_from(len).map_err(decode::Error::message)?); - for _ in 0..len { - let arr: [u8; 16] = decode_bytes(d, "revocation list Cip509RbacMetadata")? - .try_into() - .map_err(|_| decode::Error::message("Invalid revocation list size"))?; - revocation_list.push(CertKeyHash::from(arr)); - } - Ok(revocation_list) -} +mod certs; +mod metadata; +mod pub_key; +mod tag; diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs b/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs index 80320e2871..e3abfe0aa1 100644 --- a/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs +++ b/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs @@ -1,10 +1,11 @@ //! Public key type for RBAC metadata +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{decode_bytes, decode_tag}; use ed25519_dalek::VerifyingKey; use minicbor::{decode, Decode, Decoder}; use super::tag::KeyTag; -use crate::utils::decode_helper::{decode_bytes, decode_tag}; /// Enum of possible public key type. #[derive(Debug, PartialEq, Clone, Default)] @@ -18,8 +19,8 @@ pub enum SimplePublicKeyType { Ed25519(VerifyingKey), } -impl Decode<'_, ()> for SimplePublicKeyType { - fn decode(d: &mut Decoder, _ctx: &mut ()) -> Result { +impl Decode<'_, ProblemReport> for SimplePublicKeyType { + fn decode(d: &mut Decoder, _report: &mut ProblemReport) -> Result { match d.datatype()? { minicbor::data::Type::Tag => { let tag = decode_tag(d, "SimplePublicKeyType")?; @@ -44,7 +45,10 @@ impl Decode<'_, ()> for SimplePublicKeyType { _ => Err(decode::Error::message("Unknown tag for Self")), } }, - minicbor::data::Type::Undefined => Ok(Self::Undefined), + minicbor::data::Type::Undefined => { + d.undefined()?; + Ok(Self::Undefined) + }, _ => { Err(decode::Error::message( "Invalid datatype for SimplePublicKeyType", diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs b/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs index 486c2c8ded..6d25680448 100644 --- a/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs +++ b/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs @@ -2,26 +2,31 @@ use std::collections::HashMap; +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{decode_any, decode_array_len, decode_helper, decode_map_len}; use minicbor::{decode, Decode, Decoder}; use strum_macros::FromRepr; -use super::{decode_any, decode_map_len, Cip509RbacMetadataInt}; -use crate::utils::decode_helper::{decode_array_len, decode_helper}; +use crate::{ + cardano::cip509::{KeyLocalRef, RoleNumber}, + utils::decode_helper::{report_duplicated_key, report_missing_keys}, +}; -/// Role data. +/// Role data as encoded in CBOR. +#[allow(clippy::module_name_repetitions)] #[derive(Debug, PartialEq, Clone, Default)] -pub struct RoleData { - /// Role number. - pub role_number: u8, +pub struct CborRoleData { + /// A role number. + pub number: Option, /// Optional role signing key. - pub role_signing_key: Option, + pub signing_key: Option, /// Optional role encryption key. - pub role_encryption_key: Option, + pub encryption_key: Option, /// Optional payment key. - pub payment_key: Option, + pub payment_key: Option, /// Optional role extended data keys. /// Empty map if no role extended data keys. - pub role_extended_data_keys: HashMap>, + pub extended_data: HashMap>, } /// The first valid role extended data key. @@ -31,7 +36,7 @@ const LAST_ROLE_EXT_KEY: u8 = 99; /// Enum of role data with its associated unsigned integer value. #[allow(clippy::module_name_repetitions)] -#[derive(FromRepr, Debug, PartialEq)] +#[derive(FromRepr, Debug, PartialEq, Copy, Clone)] #[repr(u8)] pub enum RoleDataInt { /// Role number. @@ -44,71 +49,130 @@ pub enum RoleDataInt { PaymentKey = 3, } -impl Decode<'_, ()> for RoleData { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { +impl Decode<'_, ProblemReport> for CborRoleData { + fn decode(d: &mut Decoder, report: &mut ProblemReport) -> Result { + let context = "Decoding role data"; let map_len = decode_map_len(d, "RoleData")?; - let mut role_data = RoleData::default(); - for _ in 0..map_len { - let key: u8 = decode_helper(d, "key in RoleData", ctx)?; + + let mut found_keys = Vec::new(); + + let mut data = CborRoleData::default(); + + for index in 0..map_len { + let key: u8 = decode_helper(d, "key in RoleData", &mut ())?; if let Some(key) = RoleDataInt::from_repr(key) { + if report_duplicated_key(&found_keys, &key, index, context, report) { + continue; + } + found_keys.push(key); + match key { RoleDataInt::RoleNumber => { - role_data.role_number = decode_helper(d, "RoleNumber in RoleData", ctx)?; + match decode_helper::(d, "RoleNumber in RoleData", &mut ()) { + Ok(v) => data.number = Some(v.into()), + Err(e) => { + report.other( + &format!("Unable to decode role number: {e:?}"), + context, + ); + break; + }, + } }, RoleDataInt::RoleSigningKey => { - decode_array_len(d, "RoleSigningKey")?; - role_data.role_signing_key = Some(KeyLocalRef::decode(d, ctx)?); + match decode_signing_key(d, context, report) { + Ok(v) => data.signing_key = v, + Err(()) => break, + } }, RoleDataInt::RoleEncryptionKey => { - decode_array_len(d, "RoleEncryptionKey")?; - role_data.role_encryption_key = Some(KeyLocalRef::decode(d, ctx)?); + match decode_encryption_key(d, context, report) { + Ok(v) => data.encryption_key = v, + Err(()) => break, + } }, RoleDataInt::PaymentKey => { - role_data.payment_key = - Some(decode_helper(d, "PaymentKey in RoleData", ctx)?); + match decode_helper(d, "PaymentKey in RoleData", &mut ()) { + Ok(v) => data.payment_key = Some(v), + Err(e) => { + report.other( + &format!("Unable to decode role payment key: {e:?}"), + context, + ); + break; + }, + } }, } } else { if !(FIRST_ROLE_EXT_KEY..=LAST_ROLE_EXT_KEY).contains(&key) { - return Err(decode::Error::message(format!("Invalid role extended data key, should be with the range {FIRST_ROLE_EXT_KEY} - {LAST_ROLE_EXT_KEY}"))); + report.other(&format!("Invalid role extended data key ({key}), should be with the range {FIRST_ROLE_EXT_KEY} - {LAST_ROLE_EXT_KEY}"), context); + continue; + } + let value = match decode_any(d, "Role extended data keys") { + Ok(v) => v, + Err(e) => { + report.other( + &format!("Unable to decode role extended data for {key} key: {e:?}"), + context, + ); + continue; + }, + }; + if data.extended_data.insert(key, value.to_vec()).is_some() { + report.other( + &format!("Duplicated {key} key in the role extended data"), + context, + ); } - role_data - .role_extended_data_keys - .insert(key, decode_any(d, "Role extended data keys")?); } } - Ok(role_data) + + let required_keys = [RoleDataInt::RoleNumber]; + report_missing_keys(&found_keys, &required_keys, context, report); + + Ok(data) } } -/// Local key reference. -#[derive(Debug, PartialEq, Clone)] -pub struct KeyLocalRef { - /// Local reference. - pub local_ref: LocalRefInt, - /// Key offset. - pub key_offset: u64, -} -/// Enum of local reference with its associated unsigned integer value. -#[derive(FromRepr, Debug, PartialEq, Clone, Eq, Hash)] -#[repr(u8)] -pub enum LocalRefInt { - /// x509 certificates. - X509Certs = Cip509RbacMetadataInt::X509Certs as u8, // 10 - /// c509 certificates. - C509Certs = Cip509RbacMetadataInt::C509Certs as u8, // 20 - /// Public keys. - PubKeys = Cip509RbacMetadataInt::PubKeys as u8, // 30 +/// Decodes a signing key. +fn decode_signing_key( + d: &mut Decoder, context: &str, report: &ProblemReport, +) -> Result, ()> { + if let Err(e) = decode_array_len(d, "RoleSigningKey") { + report.other(&format!("{e:?}"), context); + return Err(()); + } + + match KeyLocalRef::decode(d, &mut ()) { + Ok(v) => Ok(Some(v)), + Err(e) => { + report.other( + &format!("Unable to decode role signing key: {e:?}"), + context, + ); + Ok(None) + }, + } } -impl Decode<'_, ()> for KeyLocalRef { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { - let local_ref = LocalRefInt::from_repr(decode_helper(d, "LocalRef in KeyLocalRef", ctx)?) - .ok_or(decode::Error::message("Invalid local reference"))?; - let key_offset: u64 = decode_helper(d, "KeyOffset in KeyLocalRef", ctx)?; - Ok(Self { - local_ref, - key_offset, - }) +/// Decodes an encryption key. +fn decode_encryption_key( + d: &mut Decoder, context: &str, report: &ProblemReport, +) -> Result, ()> { + if let Err(e) = decode_array_len(d, "RoleEncryptionKey") { + report.other(&format!("{e:?}"), context); + return Err(()); + } + + match KeyLocalRef::decode(d, &mut ()) { + Ok(v) => Ok(Some(v)), + Err(e) => { + report.other( + &format!("Unable to decode role encryption key: {e:?}"), + context, + ); + Ok(None) + }, } } diff --git a/rust/rbac-registration/src/cardano/cip509/types/key_local_ref.rs b/rust/rbac-registration/src/cardano/cip509/types/key_local_ref.rs new file mode 100644 index 0000000000..f8cc933e7f --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/types/key_local_ref.rs @@ -0,0 +1,40 @@ +//! A local key reference. + +use cbork_utils::decode_helper::decode_helper; +use minicbor::{decode, Decode, Decoder}; +use strum_macros::FromRepr; + +use crate::cardano::cip509::rbac::Cip509RbacMetadataInt; + +/// Local key reference. +#[derive(Debug, PartialEq, Clone)] +pub struct KeyLocalRef { + /// Local reference. + pub local_ref: LocalRefInt, + /// Key offset. + pub key_offset: u64, +} + +/// Enum of local reference with its associated unsigned integer value. +#[derive(FromRepr, Debug, PartialEq, Clone, Eq, Hash)] +#[repr(u8)] +pub enum LocalRefInt { + /// x509 certificates. + X509Certs = Cip509RbacMetadataInt::X509Certs as u8, // 10 + /// c509 certificates. + C509Certs = Cip509RbacMetadataInt::C509Certs as u8, // 20 + /// Public keys. + PubKeys = Cip509RbacMetadataInt::PubKeys as u8, // 30 +} + +impl Decode<'_, ()> for KeyLocalRef { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + let local_ref = LocalRefInt::from_repr(decode_helper(d, "LocalRef in KeyLocalRef", ctx)?) + .ok_or(decode::Error::message("Invalid local reference"))?; + let key_offset: u64 = decode_helper(d, "KeyOffset in KeyLocalRef", ctx)?; + Ok(Self { + local_ref, + key_offset, + }) + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/types/mod.rs b/rust/rbac-registration/src/cardano/cip509/types/mod.rs index 2b0a3811e5..0f1f601754 100644 --- a/rust/rbac-registration/src/cardano/cip509/types/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/types/mod.rs @@ -1,4 +1,19 @@ //! Types use in CIP-509 -pub mod cert_key_hash; -pub mod tx_input_hash; +pub use cert_key_hash::CertKeyHash; +pub use key_local_ref::{KeyLocalRef, LocalRefInt}; +pub use payment_history::{Payment, PaymentHistory}; +pub use point_tx_idx::PointTxnIdx; +pub use role_data::RoleData; +pub use role_number::RoleNumber; +pub use tx_input_hash::TxInputHash; +pub use validation_signature::ValidationSignature; + +mod cert_key_hash; +mod key_local_ref; +mod payment_history; +mod point_tx_idx; +mod role_data; +mod role_number; +mod tx_input_hash; +mod validation_signature; diff --git a/rust/rbac-registration/src/registration/cardano/payment_history.rs b/rust/rbac-registration/src/cardano/cip509/types/payment_history.rs similarity index 66% rename from rust/rbac-registration/src/registration/cardano/payment_history.rs rename to rust/rbac-registration/src/cardano/cip509/types/payment_history.rs index 13e4aabc5d..8371bcc424 100644 --- a/rust/rbac-registration/src/registration/cardano/payment_history.rs +++ b/rust/rbac-registration/src/cardano/cip509/types/payment_history.rs @@ -1,14 +1,22 @@ //! Payment history of the public key in tracking payment keys. -use pallas::{crypto::hash::Hash, ledger::primitives::conway::Value}; +use std::collections::HashMap; -use super::point_tx_idx::PointTxIdx; +use pallas::{ + crypto::hash::Hash, + ledger::{addresses::ShelleyAddress, primitives::conway::Value}, +}; + +use super::point_tx_idx::PointTxnIdx; + +/// A map from address to a list of payments. +pub type PaymentHistory = HashMap>; /// Payment history of the public key in tracking payment keys. -#[derive(Clone)] -pub struct PaymentHistory { +#[derive(Debug, Clone)] +pub struct Payment { /// The point and transaction index. - point_tx_idx: PointTxIdx, + point_tx_idx: PointTxnIdx, /// Transaction hash that this payment come from. tx_hash: Hash<32>, /// The transaction output index that this payment come from. @@ -17,12 +25,12 @@ pub struct PaymentHistory { value: Value, } -impl PaymentHistory { +impl Payment { /// Create an instance of payment history. pub(crate) fn new( - point_tx_idx: PointTxIdx, tx_hash: Hash<32>, output_index: u16, value: Value, + point_tx_idx: PointTxnIdx, tx_hash: Hash<32>, output_index: u16, value: Value, ) -> Self { - PaymentHistory { + Payment { point_tx_idx, tx_hash, output_index, @@ -32,7 +40,7 @@ impl PaymentHistory { /// Get the point and transaction index. #[must_use] - pub fn point_tx_idx(&self) -> &PointTxIdx { + pub fn point_tx_idx(&self) -> &PointTxnIdx { &self.point_tx_idx } diff --git a/rust/rbac-registration/src/cardano/cip509/types/point_tx_idx.rs b/rust/rbac-registration/src/cardano/cip509/types/point_tx_idx.rs new file mode 100644 index 0000000000..86a1de59b6 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/types/point_tx_idx.rs @@ -0,0 +1,38 @@ +//! Point or absolute slot and transaction index. + +use cardano_blockchain_types::{MultiEraBlock, Point, TxnIndex}; + +/// Point (slot) and transaction index. +#[derive(Debug, Clone, PartialEq)] +pub struct PointTxnIdx { + /// A point. + point: Point, + /// A transaction index. + txn_index: TxnIndex, +} + +impl PointTxnIdx { + /// Creates an instance of point and transaction index. + #[must_use] + pub fn new(point: Point, txn_index: TxnIndex) -> Self { + Self { point, txn_index } + } + + /// Creates an instance of `PointTxnIdx` from the given block and index. + #[must_use] + pub fn from_block(block: &MultiEraBlock, txn_index: TxnIndex) -> Self { + Self::new(block.point(), txn_index) + } + + /// Get the point. + #[must_use] + pub fn point(&self) -> &Point { + &self.point + } + + /// Get the transaction index. + #[must_use] + pub fn txn_index(&self) -> TxnIndex { + self.txn_index + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/types/role_data.rs b/rust/rbac-registration/src/cardano/cip509/types/role_data.rs new file mode 100644 index 0000000000..7938f8a4d8 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/types/role_data.rs @@ -0,0 +1,171 @@ +//! RBAC role data + +use std::{borrow::Cow, collections::HashMap}; + +use cardano_blockchain_types::TxnWitness; +use catalyst_types::problem_report::ProblemReport; +use pallas::ledger::{ + addresses::{Address, ShelleyAddress}, + primitives::conway, + traverse::MultiEraTx, +}; + +use crate::cardano::cip509::{ + rbac::role_data::CborRoleData, + utils::cip19::{compare_key_hash, extract_key_hash}, + KeyLocalRef, RoleNumber, +}; + +/// A role data. +#[derive(Debug, Clone, PartialEq)] +pub struct RoleData { + /// A signing key. + signing_key: Option, + /// An encryption key. + encryption_key: Option, + /// A payment key where reward will be distributed to. + payment_key: Option, + /// A map of the extended data. + extended_data: HashMap>, +} + +impl RoleData { + /// Create an instance of role data. + #[must_use] + pub fn new(data: CborRoleData, txn: &conway::MintedTx, report: &ProblemReport) -> Self { + let payment_key = if data.number == Some(RoleNumber::ROLE_0) && data.payment_key.is_none() { + report.other( + "Missing payment key in role0", + "Role data payment key validation", + ); + None + } else { + let context = format!( + "Validating the role data payment key for {:?} role", + data.number + ); + convert_payment_key(data.payment_key, txn, &context, report) + }; + + Self { + signing_key: data.signing_key, + encryption_key: data.encryption_key, + payment_key, + extended_data: data.extended_data, + } + } + + /// Returns a reference to the signing key. + #[must_use] + pub fn signing_key(&self) -> Option<&KeyLocalRef> { + self.signing_key.as_ref() + } + + /// Returns a reference to the encryption key. + #[must_use] + pub fn encryption_key(&self) -> Option<&KeyLocalRef> { + self.encryption_key.as_ref() + } + + /// Returns a reference to the payment key. + #[must_use] + pub fn payment_key(&self) -> Option<&ShelleyAddress> { + self.payment_key.as_ref() + } + + /// Returns a reference to the extended data. + #[must_use] + pub fn extended_data(&self) -> &HashMap> { + &self.extended_data + } + + /// Sets a new value for the signing key. + pub fn set_signing_key(&mut self, key: Option) { + self.signing_key = key; + } + + /// Sets a new value for the encryption key. + pub fn set_encryption_key(&mut self, key: Option) { + self.encryption_key = key; + } +} + +/// Converts the payment key from the form encoded in CBOR role data to `ShelleyAddress`. +fn convert_payment_key( + index: Option, txn: &conway::MintedTx, context: &str, report: &ProblemReport, +) -> Option { + let index: usize = index?.into(); + + let outputs = &txn.transaction_body.outputs; + let txn = MultiEraTx::Conway(Box::new(Cow::Borrowed(txn))); + let witness = match TxnWitness::new(&[txn]) { + Ok(witnesses) => witnesses, + Err(e) => { + report.other(&format!("Failed to create TxWitness: {e:?}"), context); + return None; + }, + }; + + let address = match outputs.get(index) { + Some(conway::PseudoTransactionOutput::PostAlonzo(o)) => &o.address, + Some(conway::PseudoTransactionOutput::Legacy(_)) => { + report.other( + &format!( + "Unsupported legacy transaction output type in payment key index ({index})" + ), + context, + ); + return None; + }, + None => { + report.other( + &format!( + "Role payment key reference index ({index}) is not found in transaction outputs" + ), + context, + ); + return None; + }, + }; + validate_payment_output(address, &witness, context, report); + + match Address::from_bytes(address) { + Ok(Address::Shelley(a)) => Some(a), + Ok(a) => { + report.other( + &format!("Unsupported address type ({a:?}) in payment key index ({index})"), + context, + ); + None + }, + Err(e) => { + report.other( + &format!("Invalid address in payment key index ({index}): {e:?}"), + context, + ); + None + }, + } +} + +/// Helper function for validating payment output key. +fn validate_payment_output( + output_address: &[u8], witness: &TxnWitness, context: &str, report: &ProblemReport, +) { + let Some(key) = extract_key_hash(output_address) else { + report.other("Failed to extract payment key hash from address", context); + return; + }; + + // Set transaction index to 0 because the list of transaction is manually constructed + // for TxWitness -> &[txn.clone()], so we can assume that the witness contains only + // the witness within this transaction. + if let Err(e) = compare_key_hash(&[key], witness, 0.into()) { + report.other( + &format!( + "Unable to find payment output key ({key:?}) in the transaction witness set: {e:?}" + ), + context, + ); + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/types/role_number.rs b/rust/rbac-registration/src/cardano/cip509/types/role_number.rs new file mode 100644 index 0000000000..b808c2ce9b --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/types/role_number.rs @@ -0,0 +1,16 @@ +//! A role number for `RoleData` in RBAC metadata. + +/// A role number for `RoleData` in `Cip509RbacMetadata`. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub struct RoleNumber(u8); + +impl RoleNumber { + /// A number of the `Role0` role. + pub const ROLE_0: Self = RoleNumber(0); +} + +impl From for RoleNumber { + fn from(value: u8) -> Self { + Self(value) + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs b/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs index bcc0c0bc49..45a2f7543f 100644 --- a/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs +++ b/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs @@ -1,37 +1,33 @@ //! Transaction input hash type -/// Transaction input hash representing in 16 bytes. -#[derive(Debug, PartialEq, Clone, Default)] -pub struct TxInputHash([u8; 16]); +use anyhow::Context; +use catalyst_types::hashes::Blake2b128Hash; + +/// A 16-byte hash of the transaction inputs field. +/// +/// This type is described [here]. +/// +/// [here]: https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md#key-1-txn-inputs-hash +#[derive(Debug, PartialEq, Clone)] +pub struct TxInputHash(Blake2b128Hash); impl From<[u8; 16]> for TxInputHash { fn from(bytes: [u8; 16]) -> Self { - TxInputHash(bytes) + Self(Blake2b128Hash::from(bytes)) } } -impl TryFrom> for TxInputHash { - type Error = &'static str; - - fn try_from(vec: Vec) -> Result { - if vec.len() == 16 { - let mut array = [0u8; 16]; - array.copy_from_slice(&vec); - Ok(TxInputHash(array)) - } else { - Err("Input Vec must be exactly 16 bytes") - } - } -} +impl TryFrom<&[u8]> for TxInputHash { + type Error = anyhow::Error; -impl From for Vec { - fn from(val: TxInputHash) -> Self { - val.0.to_vec() + fn try_from(value: &[u8]) -> Result { + let hash = Blake2b128Hash::try_from(value).context("Invalid transaction input hash")?; + Ok(Self(hash)) } } -impl From for [u8; 16] { - fn from(val: TxInputHash) -> Self { - val.0 +impl From for TxInputHash { + fn from(value: Blake2b128Hash) -> Self { + Self(value) } } diff --git a/rust/rbac-registration/src/cardano/cip509/types/validation_signature.rs b/rust/rbac-registration/src/cardano/cip509/types/validation_signature.rs new file mode 100644 index 0000000000..e5120fa567 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/types/validation_signature.rs @@ -0,0 +1,35 @@ +//! A validation signature wrapper. + +use anyhow::{anyhow, Error}; + +/// A validation signature. +/// +/// The signature must be at least 1 byte and at most 64 bytes long. +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct ValidationSignature(Vec); + +impl TryFrom> for ValidationSignature { + type Error = Error; + + fn try_from(value: Vec) -> Result { + if value.is_empty() || value.len() > 64 { + return Err(anyhow!("Invalid length ({}), 1..=64 expected", value.len())); + } + + Ok(Self(value)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_length() { + let error = ValidationSignature::try_from(Vec::new()).unwrap_err(); + assert!(format!("{error}").starts_with("Invalid length")); + + let error = ValidationSignature::try_from(vec![0; 65]).unwrap_err(); + assert!(format!("{error}").starts_with("Invalid length")); + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs deleted file mode 100644 index 6aa42f57c3..0000000000 --- a/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Utility functions for CIP-0134 address. - -// Ignore URIs that are used in tests and doc-examples. -// cSpell:ignoreRegExp web\+cardano:.+ - -use std::fmt::{Display, Formatter}; - -use anyhow::{anyhow, Context, Result}; -use pallas::ledger::addresses::Address; - -/// An URI in the CIP-0134 format. -/// -/// See the [proposal] for more details. -/// -/// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888 -#[derive(Debug)] -pub struct Cip0134Uri { - /// A URI string. - uri: String, - /// An address parsed from the URI. - address: Address, -} - -impl Cip0134Uri { - /// Creates a new `Cip0134Uri` instance by parsing the given URI. - /// - /// # Errors - /// - Invalid URI. - /// - /// # Examples - /// - /// ``` - /// use rbac_registration::cardano::cip509::utils::Cip0134Uri; - /// - /// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; - /// let cip0134_uri = Cip0134Uri::parse(uri).unwrap(); - /// ``` - pub fn parse(uri: &str) -> Result { - let bech32 = uri - .strip_prefix("web+cardano://addr/") - .ok_or_else(|| anyhow!("Missing schema part of URI"))?; - let address = Address::from_bech32(bech32).context("Unable to parse bech32 part of URI")?; - - Ok(Self { - uri: uri.to_owned(), - address, - }) - } - - /// Returns a URI string. - /// - /// # Examples - /// - /// ``` - /// use rbac_registration::cardano::cip509::utils::Cip0134Uri; - /// - /// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; - /// let cip0134_uri = Cip0134Uri::parse(uri).unwrap(); - /// assert_eq!(cip0134_uri.uri(), uri); - /// ``` - #[must_use] - pub fn uri(&self) -> &str { - &self.uri - } - - /// Returns a URI string. - /// - /// # Examples - /// - /// ``` - /// use pallas::ledger::addresses::{Address, Network}; - /// use rbac_registration::cardano::cip509::utils::Cip0134Uri; - /// - /// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; - /// let cip0134_uri = Cip0134Uri::parse(uri).unwrap(); - /// let Address::Stake(address) = cip0134_uri.address() else { - /// panic!("Unexpected address type"); - /// }; - /// assert_eq!(address.network(), Network::Mainnet); - /// ``` - #[must_use] - pub fn address(&self) -> &Address { - &self.address - } -} - -impl Display for Cip0134Uri { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.uri()) - } -} - -#[cfg(test)] -mod tests { - use pallas::ledger::addresses::{Address, Network}; - - use super::*; - - #[test] - fn invalid_prefix() { - // cSpell:disable - let test_uris = [ - "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", - "//addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", - "web+cardano:/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", - "somthing+unexpected://addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", - ]; - // cSpell:enable - - for uri in test_uris { - let err = format!("{:?}", Cip0134Uri::parse(uri).expect_err(uri)); - assert!(err.starts_with("Missing schema part of URI")); - } - } - - #[test] - fn invalid_bech32() { - let uri = "web+cardano://addr/adr1qx2fxv2umyh"; - let err = format!("{:?}", Cip0134Uri::parse(uri).unwrap_err()); - assert!(err.starts_with("Unable to parse bech32 part of URI")); - } - - #[test] - fn stake_address() { - let test_data = [ - ( - "web+cardano://addr/stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn", - Network::Testnet, - "337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251", - ), - ( - "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw", - Network::Mainnet, - "337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251", - ), - ( - "web+cardano://addr/drep_vk17axh4sc9zwkpsft3tlgpjemfwc0u5mnld80r85zw7zdqcst6w54sdv4a4e", - Network::Other(7), - "4d7ac30513ac1825715fd0196769761fca6e7f69de33d04ef09a0c41", - ) - ]; - - for (uri, network, payload) in test_data { - let cip0134_uri = Cip0134Uri::parse(uri).expect(uri); - let Address::Stake(address) = cip0134_uri.address() else { - panic!("Unexpected address type ({uri})"); - }; - assert_eq!(network, address.network()); - assert_eq!(payload, address.payload().as_hash().to_string()); - } - } - - #[test] - fn shelley_address() { - let test_data = [ - ( - "web+cardano://addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x", - Network::Mainnet, - ), - ( - "web+cardano://addr/addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer5pnz75xxcrdw5vky", - Network::Testnet, - ), - ( - "web+cardano://addr/cc_hot_vk10y48lq72hypxraew74lwjjn9e2dscuwphckglh2nrrpkgweqk5hschnzv5", - Network::Other(9), - ) - ]; - - for (uri, network) in test_data { - let cip0134_uri = Cip0134Uri::parse(uri).expect(uri); - let Address::Shelley(address) = cip0134_uri.address() else { - panic!("Unexpected address type ({uri})"); - }; - assert_eq!(network, address.network()); - } - } - - // The Display should return the original URI. - #[test] - fn display() { - let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"; - let cip0134_uri = Cip0134Uri::parse(uri).expect(uri); - assert_eq!(uri, cip0134_uri.to_string()); - } -} 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 new file mode 100644 index 0000000000..7b0ae26dc5 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs @@ -0,0 +1,304 @@ +//! A set of [`Cip0134Uri`]. + +use std::{collections::HashMap, sync::Arc}; + +use c509_certificate::{ + extensions::{alt_name::GeneralNamesOrText, extension::ExtensionValue}, + general_names::general_name::{GeneralNameTypeRegistry, GeneralNameValue}, + C509ExtensionType, +}; +use cardano_blockchain_types::Cip0134Uri; +use catalyst_types::problem_report::ProblemReport; +use der_parser::der::parse_der_sequence; +use tracing::debug; +use x509_cert::der::oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME; + +use crate::cardano::cip509::{ + rbac::{C509Cert, Cip509RbacMetadata, X509DerCert}, + validation::URI, +}; + +/// A mapping from a certificate index to URIs contained within. +type UrisMap = HashMap>; + +/// A set of [`Cip0134Uri`] contained in both x509 and c509 certificates stored in the +/// metadata part of [`Cip509`](crate::cardano::cip509::Cip509). +/// +/// This structure uses [`Arc`] internally, so it is cheap to clone. +#[derive(Debug, Clone, Eq, PartialEq)] +#[allow(clippy::module_name_repetitions)] +pub struct Cip0134UriSet(Arc); + +/// Internal `Cip0134UriSet` data. +#[derive(Debug, Clone, Eq, PartialEq)] +struct Cip0134UriSetInner { + /// URIs from x509 certificates. + x_uris: UrisMap, + /// URIs from c509 certificates. + c_uris: UrisMap, +} + +impl Cip0134UriSet { + /// Creates a new `Cip0134UriSet` instance from the given certificates. + #[must_use] + pub fn new( + x509_certs: &[X509DerCert], c509_certs: &[C509Cert], report: &ProblemReport, + ) -> 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 })) + } + + /// Returns a mapping from the x509 certificate index to URIs contained within. + #[must_use] + pub fn x_uris(&self) -> &UrisMap { + &self.0.x_uris + } + + /// Returns a mapping from the c509 certificate index to URIs contained within. + #[must_use] + pub fn c_uris(&self) -> &UrisMap { + &self.0.c_uris + } + + /// Returns `true` if both x509 and c509 certificate maps are empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.x_uris().is_empty() && self.c_uris().is_empty() + } + + /// Return the updated URIs set. + /// + /// The resulting set includes all the data from both the original and a new one. In + /// the following example for brevity we only consider ony type of uris: + /// ```text + /// // Original data: + /// 0: [uri_1] + /// 1: [uri_2, uri_3] + /// + /// // New data: + /// 0: undefined + /// 1: deleted + /// 2: [uri_4] + /// + /// // Resulting data: + /// 0: [uri_1] + /// 2: [uri_4] + /// ``` + #[must_use] + pub fn update(self, metadata: &Cip509RbacMetadata) -> Self { + if self == metadata.certificate_uris { + // Nothing to update. + return self; + } + + let Cip0134UriSetInner { + mut x_uris, + mut c_uris, + } = Arc::unwrap_or_clone(self.0); + + for (index, cert) in metadata.x509_certs.iter().enumerate() { + match cert { + X509DerCert::Undefined => { + // The certificate wasn't changed - there is nothing to do. + }, + X509DerCert::Deleted => { + x_uris.remove(&index); + }, + X509DerCert::X509Cert(_) => { + if let Some(uris) = metadata.certificate_uris.x_uris().get(&index) { + x_uris.insert(index, uris.clone()); + } + }, + } + } + + for (index, cert) in metadata.c509_certs.iter().enumerate() { + match cert { + C509Cert::Undefined => { + // The certificate wasn't changed - there is nothing to do. + }, + C509Cert::Deleted => { + c_uris.remove(&index); + }, + C509Cert::C509CertInMetadatumReference(_) => { + debug!("Ignoring unsupported metadatum reference"); + }, + C509Cert::C509Certificate(_) => { + if let Some(uris) = metadata.certificate_uris.c_uris().get(&index) { + c_uris.insert(index, uris.clone()); + } + }, + } + } + + Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris })) + } +} + +/// Iterates over X509 certificates and extracts CIP-0134 URIs. +fn extract_x509_uris(certificates: &[X509DerCert], report: &ProblemReport) -> UrisMap { + let mut result = UrisMap::new(); + let context = "Extracting URIs from X509 certificates in Cip509 metadata"; + + for (index, cert) in certificates.iter().enumerate() { + let X509DerCert::X509Cert(cert) = cert else { + continue; + }; + // Find the "subject alternative name" extension. + let Some(extension) = cert + .tbs_certificate + .extensions + .iter() + .flatten() + .find(|e| e.extn_id == ID_CE_SUBJECT_ALT_NAME) + else { + continue; + }; + let Ok((_, der)) = parse_der_sequence(extension.extn_value.as_bytes()) else { + report.other( + &format!( + "Failed to parse DER sequence for Subject Alternative Name ({extension:?})" + ), + context, + ); + continue; + }; + + let mut uris = Vec::new(); + for data in der.ref_iter() { + if data.header.raw_tag() != Some(&[URI]) { + continue; + } + let Ok(bytes) = data.content.as_slice() else { + report.other(&format!("Unable to process content for {data:?}"), context); + continue; + }; + match Cip0134Uri::try_from(bytes) { + Ok(u) => uris.push(u), + Err(e) => { + // X.509 doesn't restrict the "alternative name" extension to be utf8 only, so + // we cannot treat this as error. + debug!("Ignoring invalid CIP-0134 address: {e:?}"); + continue; + }, + }; + } + + if !uris.is_empty() { + result.insert(index, uris.into_boxed_slice()); + } + } + + result +} + +/// Iterates over C509 certificates and extracts CIP-0134 URIs. +fn extract_c509_uris(certificates: &[C509Cert], report: &ProblemReport) -> UrisMap { + let mut result = UrisMap::new(); + let context = "Extracting URIs from C509 certificates in Cip509 metadata"; + + for (index, cert) in certificates.iter().enumerate() { + let cert = match cert { + C509Cert::C509Certificate(c) => c, + C509Cert::C509CertInMetadatumReference(_) => { + debug!("Ignoring unsupported metadatum reference"); + continue; + }, + _ => continue, + }; + + for extension in cert.tbs_cert().extensions().extensions() { + if extension.registered_oid().c509_oid().oid() + != &C509ExtensionType::SubjectAlternativeName.oid() + { + continue; + } + let ExtensionValue::AlternativeName(alt_name) = extension.value() else { + report.other( + &format!("Unexpected extension value type for {extension:?}"), + context, + ); + continue; + }; + let GeneralNamesOrText::GeneralNames(gen_names) = alt_name.general_name() else { + report.other( + &format!("Unexpected general name type: {extension:?}"), + context, + ); + continue; + }; + + let mut uris = Vec::new(); + for name in gen_names.general_names() { + if *name.gn_type() != GeneralNameTypeRegistry::UniformResourceIdentifier { + continue; + } + let GeneralNameValue::Text(address) = name.gn_value() else { + report.other( + &format!("Unexpected general name value format: {name:?}"), + context, + ); + continue; + }; + match Cip0134Uri::parse(address) { + Ok(u) => uris.push(u), + Err(e) => { + debug!("Ignoring invalid CIP-0134 address: {e:?}"); + continue; + }, + }; + } + + if !uris.is_empty() { + result.insert(index, uris.into_boxed_slice()); + } + } + } + + result +} + +#[cfg(test)] +mod tests { + use pallas::ledger::addresses::{Address, Network}; + + use crate::{cardano::cip509::Cip509, utils::test}; + + #[test] + fn set_new() { + let data = test::block_1(); + let cip509 = Cip509::new(&data.block, data.txn_index, &[]) + .unwrap() + .unwrap(); + assert!( + !cip509.report().is_problematic(), + "Failed to decode Cip509: {:?}", + cip509.report() + ); + + let set = cip509.certificate_uris().unwrap(); + assert!(!set.is_empty()); + assert!(set.c_uris().is_empty()); + + let x_uris = set.x_uris(); + assert_eq!(x_uris.len(), 1); + + let uris = x_uris.get(&0).unwrap(); + assert_eq!(uris.len(), 1); + + let uri = uris.first().unwrap(); + assert_eq!( + uri.uri(), + format!("web+cardano://addr/{}", data.stake_addr.unwrap()) + ); + let Address::Stake(address) = uri.address() else { + panic!("Unexpected address type"); + }; + assert_eq!(Network::Testnet, address.network()); + assert_eq!( + "e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903", + address.payload().as_hash().to_string() + ); + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs index e7fe994c7d..b238f5d094 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs @@ -1,33 +1,25 @@ //! Utility functions for CIP-19 address. use anyhow::bail; - -use crate::cardano::transaction::witness::TxWitness; +use cardano_blockchain_types::{TxnIndex, TxnWitness, VKeyHash}; /// Extract the first 28 bytes from the given key /// Refer to for more information. -pub(crate) fn extract_key_hash(key: &[u8]) -> Option> { - key.get(1..29).map(<[u8]>::to_vec) +pub(crate) fn extract_key_hash(key: &[u8]) -> Option { + key.get(1..29).and_then(|v| v.try_into().ok()) } /// Compare the given public key bytes with the transaction witness set. pub(crate) fn compare_key_hash( - pk_addrs: &[Vec], witness: &TxWitness, txn_idx: u16, + pk_addrs: &[VKeyHash], witness: &TxnWitness, txn_idx: TxnIndex, ) -> anyhow::Result<()> { if pk_addrs.is_empty() { bail!("No public key addresses provided"); } pk_addrs.iter().try_for_each(|pk_addr| { - let pk_addr: [u8; 28] = pk_addr.as_slice().try_into().map_err(|_| { - anyhow::anyhow!( - "Invalid length for vkey, expected 28 bytes but got {}", - pk_addr.len() - ) - })?; - // Key hash not found in the transaction witness set - if !witness.check_witness_in_tx(&pk_addr, txn_idx) { + if !witness.check_witness_in_tx(pk_addr, txn_idx) { bail!( "Public key hash not found in transaction witness set given {:?}", pk_addr diff --git a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs index ab7c3954d6..aa39ed338b 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs @@ -1,6 +1,7 @@ //! Utility functions for CIP-509 pub mod cip19; -pub use cip134::Cip0134Uri; -mod cip134; +pub use cip134_uri_set::Cip0134UriSet; + +mod cip134_uri_set; diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index f30839aa03..aec646aba2 100644 --- a/rust/rbac-registration/src/cardano/cip509/validation.rs +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -1,679 +1,409 @@ -//! Basic validation for CIP-0509 -//! The validation include the following: -//! * Hashing the transaction inputs within the transaction should match the -//! txn-inputs-hash in CIP-0509 data. -//! * Auxiliary data hash within the transaction should match the hash of the auxiliary -//! data itself. -//! * Public key validation for role 0 where public key extracted from x509 and c509 -//! subject alternative name should match one of the witness in witness set within the -//! transaction. -//! * Payment key reference validation for role 0 where the reference should be either -//! 1. Negative index reference - reference to transaction output in transaction: -//! should match some of the key within witness set. -//! 2. Positive index reference - reference to the transaction input in transaction: -//! only check whether the index exist within the transaction inputs. -//! * Role signing key validation for role 0 where the signing keys should only be the -//! certificates +//! Utilities for validation CIP-0509. //! -//! See: -//! * -//! * +//! See [this document] for all the details. //! -//! Note: This CIP509 is still under development and is subject to change. +//! [this document]: https://github.com/input-output-hk/catalyst-CIPs/tree/x509-role-registration-metadata/CIP-XXXX -use c509_certificate::{general_names::general_name::GeneralNameValue, C509ExtensionType}; -use der_parser::der::parse_der_sequence; +use std::borrow::Cow; + +use cardano_blockchain_types::{TxnWitness, VKeyHash}; +use catalyst_types::{ + hashes::{Blake2b128Hash, Blake2b256Hash}, + problem_report::ProblemReport, +}; use pallas::{ codec::{ minicbor::{Encode, Encoder}, utils::Bytes, }, - ledger::{addresses::Address, traverse::MultiEraTx}, + ledger::{addresses::Address, primitives::conway, traverse::MultiEraTx}, }; -use x509_cert::der::{oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME, Decode}; -use super::{ - blake2b_128, blake2b_256, decode_utf8, decremented_index, - rbac::{ - certs::{C509Cert, X509DerCert}, - role_data::{LocalRefInt, RoleData}, - }, - utils::{ - cip19::{compare_key_hash, extract_key_hash}, - Cip0134Uri, - }, - Cip509, TxInputHash, TxWitness, +use super::utils::cip19::compare_key_hash; +use crate::cardano::cip509::{ + rbac::Cip509RbacMetadata, types::TxInputHash, C509Cert, Cip0134UriSet, LocalRefInt, RoleData, + RoleNumber, SimplePublicKeyType, X509DerCert, }; -use crate::utils::general::zero_out_last_n_bytes; /// Context-specific primitive type with tag number 6 (`raw_tag` 134) for /// uniform resource identifier (URI) in the subject alternative name extension. /// Following the ASN.1 /// -/// the tag is derive from +/// the tag is derived from /// | Class (2 bit) | P/C (1 bit) | Tag Number (5 bit) | /// |`CONTEXT_SPECIFIC` | `PRIMITIVE` `| 6` | /// |`10` | `0` `| 00110` | /// Result in 0x86 or 134 in decimal. pub(crate) const URI: u8 = 134; -// ------------------------ Validate Txn Inputs Hash ------------------------ +/// Checks that hashing transactions inputs produces the value equal to the given one. +pub fn validate_txn_inputs_hash( + hash: &TxInputHash, transaction: &conway::MintedTx, report: &ProblemReport, +) { + let context = "Cip509 transaction input hash validation"; -/// Transaction inputs hash validation. -/// CIP509 `txn_inputs_hash` must match the hash of the transaction inputs within the -/// body. -pub(crate) fn validate_txn_inputs_hash( - cip509: &Cip509, txn: &MultiEraTx, validation_report: &mut Vec, -) -> Option { - let function_name = "Validate Transaction Inputs Hash"; let mut buffer = Vec::new(); let mut e = Encoder::new(&mut buffer); - // CIP-0509 should only be in conway era - if let MultiEraTx::Conway(tx) = txn { - let inputs = tx.transaction_body.inputs.clone(); - if let Err(e) = e.array(inputs.len() as u64) { - validation_report.push(format!( - "{function_name}, Failed to encode array of transaction input: {e}" - )); - return None; - } - for input in &inputs { - match input.encode(&mut e, &mut ()) { - Ok(()) => {}, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to encode transaction input {e}" - )); - return None; - }, - } + + let inputs = &transaction.transaction_body.inputs; + if let Err(e) = e.array(inputs.len() as u64) { + report.other( + &format!("Failed to encode array of transaction inputs: {e:?}"), + context, + ); + return; + }; + for input in inputs { + if let Err(e) = input.encode(&mut e, &mut ()) { + report.other( + &format!("Failed to encode transaction input ({input:?}): {e:?}"), + context, + ); + return; } - // Hash the transaction inputs - let inputs_hash = match blake2b_128(&buffer) { - Ok(hash) => hash, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to hash transaction inputs {e}" - )); - return None; - }, - }; - Some(TxInputHash::from(inputs_hash) == cip509.txn_inputs_hash) - } else { - validation_report.push(format!("{function_name}, Unsupported transaction era for")); - None } -} -// ------------------------ Validate Stake Public Key ------------------------ - -/// Validate the stake public key in the certificate with witness set in transaction. -#[allow(clippy::too_many_lines)] -pub(crate) fn validate_stake_public_key( - cip509: &Cip509, txn: &MultiEraTx, validation_report: &mut Vec, -) -> Option { - let function_name = "Validate Stake Public Key"; - let mut pk_addrs = Vec::new(); - - // CIP-0509 should only be in conway era - if let MultiEraTx::Conway(_) = txn { - // X509 certificate - if let Some(x509_certs) = &cip509.x509_chunks.0.x509_certs { - for x509_cert in x509_certs { - match x509_cert { - X509DerCert::X509Cert(cert) => { - // Attempt to decode the DER certificate - let der_cert = match x509_cert::Certificate::from_der(cert) { - Ok(cert) => cert, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to decode x509 certificate DER: {e}" - )); - return None; - }, - }; - - // Find the Subject Alternative Name extension - let san_ext = - der_cert - .tbs_certificate - .extensions - .as_ref() - .and_then(|exts| { - exts.iter() - .find(|ext| ext.extn_id == ID_CE_SUBJECT_ALT_NAME) - }); - - // Subject Alternative Name extension if it exists - if let Some(san_ext) = san_ext { - match parse_der_sequence(san_ext.extn_value.as_bytes()) { - Ok((_, parsed_seq)) => { - for data in parsed_seq.ref_iter() { - // Check for context-specific primitive type with tag - // number - // 6 (raw_tag 134) - if data.header.raw_tag() == Some(&[URI]) { - match data.content.as_slice() { - Ok(content) => { - // Decode the UTF-8 string - let addr: String = match decode_utf8(content) { - Ok(addr) => addr, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to decode UTF-8 string for context-specific primitive type with raw tag 134: {e}", - ), - ); - return None; - }, - }; - - // Extract the CIP19 hash and push into - // array - if let Ok(uri) = Cip0134Uri::parse(&addr) { - if let Address::Stake(a) = uri.address() { - pk_addrs.push( - a.payload().as_hash().to_vec(), - ); - } - } - }, - Err(e) => { - validation_report.push( - format!("{function_name}, Failed to process content for context-specific primitive type with raw tag 134: {e}")); - return None; - }, - } - } - } - }, - Err(e) => { - validation_report.push( - format!( - "{function_name}, Failed to parse DER sequence for Subject Alternative Name extension: {e}" - ) - ); - return None; - }, - } - } - }, - _ => continue, - } - } - } - // C509 Certificate - if let Some(c509_certs) = &cip509.x509_chunks.0.c509_certs { - for c509_cert in c509_certs { - match c509_cert { - C509Cert::C509CertInMetadatumReference(_) => { - validation_report.push(format!( - "{function_name}, C509 metadatum reference is currently not supported" - )); - }, - C509Cert::C509Certificate(c509) => { - for exts in c509.tbs_cert().extensions().extensions() { - if *exts.registered_oid().c509_oid().oid() - == C509ExtensionType::SubjectAlternativeName.oid() - { - match exts.value() { - c509_certificate::extensions::extension::ExtensionValue::AlternativeName(alt_name) => { - match alt_name.general_name() { - c509_certificate::extensions::alt_name::GeneralNamesOrText::GeneralNames(gn) => { - for name in gn.general_names() { - if name.gn_type() == &c509_certificate::general_names::general_name::GeneralNameTypeRegistry::UniformResourceIdentifier { - match name.gn_value() { - GeneralNameValue::Text(s) => { - if let Ok(uri) = Cip0134Uri::parse(s) { - if let Address::Stake(a) = uri.address() { - pk_addrs.push(a.payload().as_hash().to_vec()); - } - } - }, - _ => { - validation_report.push( - format!("{function_name}, Failed to get the value of subject alternative name"), - ); - } - } - } - } - }, - c509_certificate::extensions::alt_name::GeneralNamesOrText::Text(_) => { - validation_report.push( - format!("{function_name}, Failed to find C509 general names in subject alternative name"), - ); - } - } - }, - _ => { - validation_report.push( - format!("{function_name}, Failed to get C509 subject alternative name") - ); - } - } - } - } - }, - _ => continue, - } - } - } - } else { - validation_report.push(format!("{function_name}, Unsupported transaction era")); - return None; + let calculated_hash = TxInputHash::from(Blake2b128Hash::new(&buffer)); + if &calculated_hash != hash { + report.invalid_value( + "txn_inputs_hash", + &format!("{hash:?}"), + &format!("Must be equal to the value in Cip509 ({hash:?})"), + context, + ); } +} + +/// Checks that the given transaction auxiliary data hash is correct. +pub fn validate_aux( + raw_aux_data: &[u8], auxiliary_data_hash: Option<&Bytes>, report: &ProblemReport, +) { + let context = "Cip509 auxiliary data validation"; - // Create TxWitness - // Note that TxWitness designs to work with multiple transactions - let witnesses = match TxWitness::new(&[txn.clone()]) { - Ok(witnesses) => witnesses, + let Some(auxiliary_data_hash) = auxiliary_data_hash else { + report.other("Auxiliary data hash not found in transaction", context); + return; + }; + let auxiliary_data_hash = match Blake2b256Hash::try_from(auxiliary_data_hash.as_slice()) { + Ok(v) => v, Err(e) => { - validation_report.push(format!("{function_name}, Failed to create TxWitness: {e}")); - return None; + report.other( + &format!("Invalid transaction auxiliary data hash: {e:?}"), + context, + ); + return; }, }; - Some( - // Set transaction index to 0 because the list of transaction is manually constructed - // for TxWitness -> &[txn.clone()], so we can assume that the witness contains only - // the witness within this transaction. - compare_key_hash(&pk_addrs, &witnesses, 0) - .map_err(|e| { - validation_report.push(format!( - "{function_name}, Failed to compare public keys with witnesses: {e}" - )); - }) - .is_ok(), - ) -} - -// ------------------------ Validate Aux ------------------------ - -/// Validate the auxiliary data with the auxiliary data hash in the transaction body. -/// Also return the pre-computed hash where the validation signature (99) set to -pub(crate) fn validate_aux( - txn: &MultiEraTx, validation_report: &mut Vec, -) -> Option<(bool, Vec)> { - let function_name = "Validate Aux"; - - // CIP-0509 should only be in conway era - if let MultiEraTx::Conway(tx) = txn { - if let pallas::codec::utils::Nullable::Some(a) = &tx.auxiliary_data { - let original_aux = a.raw_cbor(); - let aux_data_hash = tx - .transaction_body - .auxiliary_data_hash - .as_ref() - .or_else(|| { - validation_report.push(format!( - "{function_name}, Auxiliary data hash not found in transaction" - )); - None - })?; - validate_aux_helper(original_aux, aux_data_hash, validation_report) - } else { - validation_report.push(format!( - "{function_name}, Auxiliary data not found in transaction" - )); - None - } - } else { - validation_report.push(format!("{function_name}, Unsupported transaction era")); - None + let hash = Blake2b256Hash::new(raw_aux_data); + if hash != auxiliary_data_hash { + report.other( + &format!("Incorrect transaction auxiliary data hash = '{hash:?}', expected = '{auxiliary_data_hash:?}'"), + context, + ); } } -/// Helper function for auxiliary data validation. -/// Also compute The pre-computed hash. -fn validate_aux_helper( - original_aux: &[u8], aux_data_hash: &Bytes, validation_report: &mut Vec, -) -> Option<(bool, Vec)> { - let mut vec_aux = original_aux.to_vec(); - - // Pre-computed aux with the last 64 bytes set to zero - zero_out_last_n_bytes(&mut vec_aux, 64); +/// Checks that all public keys extracted from x509 and c509 certificates are present in +/// the witness set of the transaction. +pub fn validate_stake_public_key( + transaction: &conway::MintedTx, uris: Option<&Cip0134UriSet>, report: &ProblemReport, +) { + let context = "Cip509 stake public key validation"; - // Compare the hash - match blake2b_256(original_aux) { - Ok(original_hash) => Some((aux_data_hash.as_ref() == original_hash, vec_aux)), + let transaction = MultiEraTx::Conway(Box::new(Cow::Borrowed(transaction))); + let witness = match TxnWitness::new(&[transaction.clone()]) { + Ok(w) => w, Err(e) => { - validation_report.push(format!("Cannot hash auxiliary data {e}")); - None + report.other(&format!("Failed to create TxWitness: {e:?}"), context); + return; }, + }; + + let pk_addrs = extract_stake_addresses(uris); + if pk_addrs.is_empty() { + report.other( + "Unable to find stake addresses in Cip509 certificates", + context, + ); + return; + } + + if let Err(e) = compare_key_hash(&pk_addrs, &witness, 0.into()) { + report.other( + &format!("Failed to compare public keys with witnesses: {e:?}"), + context, + ); } } -// ------------------------ Validate Payment Key ------------------------ - -/// Validate the payment key reference. -/// Negative ref is for transaction output. -/// Positive ref is for transaction input. -pub(crate) fn validate_payment_key( - txn: &MultiEraTx, role_data: &RoleData, validation_report: &mut Vec, -) -> Option { - let function_name = "Validate Payment Key"; - - if let Some(payment_key) = role_data.payment_key { - if payment_key == 0 { - validation_report.push(format!( - "{function_name}, Invalid payment reference key, 0 is not allowed" - )); - return None; +/// Extracts all stake addresses from both X509 and C509 certificates containing in the +/// given `Cip509` and converts their hashes to bytes. +fn extract_stake_addresses(uris: Option<&Cip0134UriSet>) -> Vec { + let Some(uris) = uris else { + return Vec::new(); + }; + + uris.x_uris() + .iter() + .chain(uris.c_uris()) + .flat_map(|(_index, uris)| uris.iter()) + .filter_map(|uri| { + if let Address::Stake(a) = uri.address() { + a.payload().as_hash().as_slice().try_into().ok() + } else { + None + } + }) + .collect() +} + +/// Checks that only role 0 uses certificates with zero index. +#[allow(clippy::similar_names)] +pub fn validate_role_data(metadata: &Cip509RbacMetadata, report: &ProblemReport) { + let context = "Role data validation"; + + if metadata.role_data.contains_key(&RoleNumber::ROLE_0) { + // For the role 0 there must be exactly once certificate and it must not have `deleted`, + // `undefined` or `C509CertInMetadatumReference` values. + if matches!(metadata.x509_certs.first(), Some(X509DerCert::X509Cert(_))) + && matches!( + metadata.c509_certs.first(), + Some(C509Cert::C509Certificate(_)) + ) + { + report.other( + "Only one certificate can be defined at index 0 for the role 0", + context, + ); } - // CIP-0509 should only be in conway era - if let MultiEraTx::Conway(tx) = txn { - // Negative indicates reference to tx output - if payment_key < 0 { - let index = match decremented_index(payment_key.abs()) { - Ok(value) => value, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to get index of payment key: {e}" - )); - return None; - }, - }; - let outputs = tx.transaction_body.outputs.clone(); - let witness = match TxWitness::new(&[txn.clone()]) { - Ok(witnesses) => witnesses, - Err(e) => { - validation_report - .push(format!("{function_name}, Failed to create TxWitness: {e}")); - return None; - }, - }; - - if let Some(output) = outputs.get(index) { - match output { - pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(o) => { - return validate_payment_output_key_helper( - &o.address.to_vec(), - validation_report, - &witness, - ); - }, - pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo( - o, - ) => { - return validate_payment_output_key_helper( - &o.address.to_vec(), - validation_report, - &witness, - ); - }, - }; + if !matches!(metadata.x509_certs.first(), Some(X509DerCert::X509Cert(_))) + && !matches!( + metadata.c509_certs.first(), + Some(C509Cert::C509Certificate(_)) + ) + { + report.other("The role 0 certificate must be present", context); + } + } else { + // For other roles there still must be exactly one certificate at 0 index, but it must + // have the `undefined` value. + if matches!(metadata.x509_certs.first(), Some(X509DerCert::X509Cert(_))) + || matches!( + metadata.c509_certs.first(), + Some(C509Cert::C509Certificate(_)) + ) + { + report.other("Only role 0 can contain a certificate at 0 index", context); + } + if matches!(metadata.x509_certs.first(), Some(X509DerCert::Deleted)) + || matches!(metadata.c509_certs.first(), Some(C509Cert::Deleted)) + { + report.other("Only role 0 can delete a certificate at 0 index", context); + } + } + + // It isn't allowed for any role to use a public key at 0 index. + if !matches!( + metadata.pub_keys.first(), + None | Some(SimplePublicKeyType::Undefined) + ) { + report.other( + "The public key cannot be used at 0 index. Role 0 requires a certificate and other roles must set 0 public key to undefined if needed.", + context, + ); + } + // It isn't allowed for the role 0 to have a certificate in the + // `C509CertInMetadatumReference` form and other roles must not contain certificate at 0 + // index. + if matches!( + metadata.c509_certs.first(), + Some(C509Cert::C509CertInMetadatumReference(_)) + ) { + report.other( + "C509 certificate at 0 index cannot be in metadatum reference", + context, + ); + } + + for (number, data) in &metadata.role_data { + if number == &RoleNumber::ROLE_0 { + validate_role_0(data, metadata, context, report); + } else { + if let Some(signing_key) = data.signing_key() { + if signing_key.key_offset == 0 { + report.other( + &format!( + "Invalid signing key: only role 0 can reference a certificate with 0 index ({number:?} {data:?})" + ), + context, + ); } - validation_report.push( - format!("{function_name}, Role payment key reference index is not found in transaction outputs") - ); - return None; } - // Positive indicates reference to tx input - let inputs = &tx.transaction_body.inputs; - let index = match decremented_index(payment_key) { - Ok(value) => value, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to get index of payment key: {e}" - )); - return None; - }, - }; - // Check whether the index exists in transaction inputs - if inputs.get(index).is_none() { - validation_report.push( - format!("{function_name}, Role payment key reference index is not found in transaction inputs") - ); - return None; + if let Some(encryption_key) = data.encryption_key() { + if encryption_key.key_offset == 0 { + report.other( + &format!( + "Invalid encryption key: only role 0 can reference a certificate with 0 index ({number:?} {data:?})" + ), + context, + ); + } } - Some(true) - } else { - validation_report.push(format!( - "{function_name}, Unsupported transaction era for stake payment key validation" - )); - None } - } else { - Some(false) } } -/// Helper function for validating payment output key. -fn validate_payment_output_key_helper( - output_address: &[u8], validation_report: &mut Vec, witness: &TxWitness, -) -> Option { - // Extract the key hash from the output address - if let Some(key) = extract_key_hash(output_address) { - // Compare the key hash and return the result - // Set transaction index to 0 because the list of transaction is manually constructed - // for TxWitness -> &[txn.clone()], so we can assume that the witness contains only - // the witness within this transaction. - return Some(compare_key_hash(&[key], witness, 0).is_ok()); +/// Checks that the role 0 data is correct. +fn validate_role_0( + role: &RoleData, metadata: &Cip509RbacMetadata, context: &str, report: &ProblemReport, +) { + if let Some(key) = role.encryption_key() { + report.invalid_value( + "Role 0 encryption key", + &format!("{key:?}"), + "The role 0 shouldn't have the encryption key", + context, + ); } - validation_report.push("Failed to extract payment key hash from address".to_string()); - None -} -// ------------------------ Validate role signing key ------------------------ - -/// Validate role singing key for role 0. -/// Must reference certificate not the public key -pub(crate) fn validate_role_singing_key( - role_data: &RoleData, validation_report: &mut Vec, -) -> bool { - let function_name = "Validate Role Signing Key"; - - // If signing key exist, it should not contain public key - if let Some(local_ref) = &role_data.role_signing_key { - if local_ref.local_ref == LocalRefInt::PubKeys { - validation_report.push(format!( - "{function_name}, Role signing key should reference certificate, not public key", - )); - println!("ja"); - return false; - } + let Some(signing_key) = role.signing_key() else { + report.missing_field("(Role 0) RoleData::signing_key", context); + return; + }; + + if signing_key.key_offset != 0 { + report.other( + &format!("The role 0 must reference a certificate with 0 index ({role:?})"), + context, + ); + return; } - true + match signing_key.local_ref { + LocalRefInt::X509Certs => { + match metadata.x509_certs.first() { + Some(X509DerCert::X509Cert(_)) => { + // All good: role 0 references a valid X509 certificate. + } + Some(c) => report.other(&format!("Invalid X509 certificate value ({c:?}) for role 0 ({role:?})"), context), + None => report.other("Role 0 reference X509 certificate at index 0, but there is no such certificate", context), + } + }, + LocalRefInt::C509Certs => { + match metadata.c509_certs.first() { + Some(C509Cert::C509Certificate(_)) => { + // All good: role 0 references a valid C509 certificate. + } + Some(c) => report.other(&format!("Invalid C509 certificate value ({c:?}) for role 0 ({role:?})"), context), + None => report.other("Role 0 reference C509 certificate at index 0, but there is no such certificate", context), + } + }, + LocalRefInt::PubKeys => { + report.invalid_value( + "(Role 0) RoleData::signing_key", + &format!("{signing_key:?}"), + "Role signing key should reference certificate, not public key", + context, + ); + }, + } } -// ------------------------ Tests ------------------------ - #[cfg(test)] mod tests { - - use minicbor::{Decode, Decoder}; - use super::*; - use crate::cardano::transaction::raw_aux_data::RawAuxData; + use crate::{cardano::cip509::Cip509, utils::test}; - fn cip_509_aux_data(tx: &MultiEraTx<'_>) -> Vec { - let raw_auxiliary_data = tx - .as_conway() - .unwrap() - .clone() - .auxiliary_data - .map(|aux| aux.raw_cbor()); + #[test] + fn block_1() { + let data = test::block_1(); - let raw_cbor_data = match raw_auxiliary_data { - pallas::codec::utils::Nullable::Some(data) => Ok(data), - _ => Err("Auxiliary data not found"), - }; + let mut registrations = Cip509::from_block(&data.block, &[]); + assert_eq!(1, registrations.len()); - let auxiliary_data = RawAuxData::new(raw_cbor_data.expect("Failed to get raw cbor data")); - auxiliary_data - .get_metadata(509) - .expect("Failed to get metadata") - .to_vec() + let registration = registrations.pop().unwrap(); + data.assert_valid(®istration); } - fn conway_1() -> Vec { - hex::decode(include_str!("../../test_data/cardano/conway_1.block")) - .expect("Failed to decode hex block.") - } + #[test] + fn block_2() { + let data = test::block_2(); - fn conway_2() -> Vec { - hex::decode(include_str!("../../test_data/cardano/conway_2.block")) - .expect("Failed to decode hex block.") - } + let mut registrations = Cip509::from_block(&data.block, &[]); + assert_eq!(1, registrations.len()); - fn conway_3() -> Vec { - hex::decode(include_str!("../../test_data/cardano/conway_3.block")) - .expect("Failed to decode hex block.") - } + let registration = registrations.pop().unwrap(); + assert!(registration.report().is_problematic()); - #[test] - fn test_validate_txn_inputs_hash() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_1(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // Forth transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .get(3) - .expect("Failed to get transaction index"); - let aux_data = cip_509_aux_data(tx); - let mut decoder = Decoder::new(aux_data.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - assert!(validate_txn_inputs_hash(&cip509, tx, &mut validation_report).unwrap()); - } + let origin = registration.origin(); + assert_eq!(origin.txn_index(), data.txn_index); + assert_eq!(origin.point().slot_or_default(), data.slot); - #[test] - fn test_validate_aux() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_1(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // Forth transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .get(3) - .expect("Failed to get transaction index"); - - validate_aux(tx, &mut validation_report); - assert!(validate_aux(tx, &mut validation_report).unwrap().0); + // The consume function must return the problem report contained within the registration. + let report = registration.consume().unwrap_err(); + assert!(report.is_problematic()); + let report = format!("{report:?}"); + assert!(report.contains("Public key hash not found in transaction witness set")); } #[test] - fn test_validate_public_key_success() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_1(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // Forth transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .get(3) - .expect("Failed to get transaction index"); - - let aux_data = cip_509_aux_data(tx); - - let mut decoder = Decoder::new(aux_data.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - assert!(validate_stake_public_key(&cip509, tx, &mut validation_report).unwrap()); - } + fn block_3() { + let data = test::block_3(); - #[test] - fn test_validate_payment_key_success_negative_ref() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_1(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // Forth transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .get(3) - .expect("Failed to get transaction index"); - - let aux_data = cip_509_aux_data(tx); - - let mut decoder = Decoder::new(aux_data.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - - if let Some(role_set) = &cip509.x509_chunks.0.role_set { - for role in role_set { - if role.role_number == 0 { - assert!(validate_payment_key(tx, role, &mut validation_report,).unwrap()); - } - } - } - } + let mut registrations = Cip509::from_block(&data.block, &[]); + assert_eq!(1, registrations.len()); - #[test] - fn test_role_0_signing_key() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_1(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // Forth transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .get(3) - .expect("Failed to get transaction index"); - - let aux_data = cip_509_aux_data(tx); - - let mut decoder = Decoder::new(aux_data.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - if let Some(role_set) = &cip509.x509_chunks.0.role_set { - for role in role_set { - if role.role_number == 0 { - assert!(validate_role_singing_key(role, &mut validation_report,)); - } - } - } + let registration = registrations.pop().unwrap(); + assert!(registration.report().is_problematic()); + + assert_eq!(registration.previous_transaction(), data.prv_hash); + + let origin = registration.origin(); + assert_eq!(origin.txn_index(), data.txn_index); + assert_eq!(origin.point().slot_or_default(), data.slot); + + let report = registration.consume().unwrap_err(); + assert!(report.is_problematic()); + let report = format!("{report:?}"); + assert!(report + .contains("Role payment key reference index (1) is not found in transaction outputs")); } #[test] - fn test_validate_payment_key_success_positive_ref() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_3(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // First transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .first() - .expect("Failed to get transaction index"); - - let aux_data = cip_509_aux_data(tx); - - let mut decoder = Decoder::new(aux_data.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - - if let Some(role_set) = &cip509.x509_chunks.0.role_set { - for role in role_set { - if role.role_number == 0 { - assert!(validate_payment_key(tx, role, &mut validation_report,).unwrap()); - } - } - } + fn block_4() { + let data = test::block_4(); + + let mut registrations = Cip509::from_block(&data.block, &[]); + assert_eq!(1, registrations.len()); + + let registration = registrations.pop().unwrap(); + data.assert_valid(®istration); } #[test] - fn test_validate_public_key_fail() { - let mut validation_report = Vec::new(); - let conway_block_data = conway_2(); - let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) - .expect("Failed to decode MultiEraBlock"); - - let transactions = multi_era_block.txs(); - // First transaction of this test data contains the CIP509 auxiliary data - let tx = transactions - .first() - .expect("Failed to get transaction index"); - - let aux_data = cip_509_aux_data(tx); - - let mut decoder = Decoder::new(aux_data.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - assert!(!validate_stake_public_key(&cip509, tx, &mut validation_report).unwrap()); + fn extract_stake_addresses_from_metadata() { + let data = test::block_1(); + let cip509 = Cip509::new(&data.block, data.txn_index, &[]) + .unwrap() + .unwrap(); + assert!( + !cip509.report().is_problematic(), + "Failed to decode Cip509: {:?}", + cip509.report() + ); + + let uris = cip509.certificate_uris().unwrap(); + assert!(uris.c_uris().is_empty()); + assert_eq!(1, uris.x_uris().len()); + let Address::Stake(address) = uris.x_uris().get(&0).unwrap().first().unwrap().address() + else { + panic!("Unexpected address type"); + }; + let hash = address.payload().as_hash().as_ref().try_into().unwrap(); + + let addresses = extract_stake_addresses(cip509.certificate_uris()); + assert_eq!(1, addresses.len()); + assert_eq!(addresses.first().unwrap(), &hash); } } diff --git a/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs b/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs index 457faef6a2..92a8d56d94 100644 --- a/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs +++ b/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs @@ -2,11 +2,12 @@ use std::io::Read; +use cbork_utils::decode_helper::{decode_array_len, decode_bytes, decode_helper}; use minicbor::{decode, Decode, Decoder}; use strum_macros::FromRepr; use super::rbac::Cip509RbacMetadata; -use crate::utils::decode_helper::{decode_array_len, decode_bytes, decode_helper}; +use crate::cardano::cip509::decode_context::DecodeContext; /// Enum of compression algorithms used to compress chunks. #[derive(FromRepr, Debug, PartialEq, Clone, Default)] @@ -21,35 +22,64 @@ pub enum CompressionAlgorithm { Zstd = 12, } -/// x509 chunks. -#[derive(Debug, PartialEq, Clone, Default)] -pub struct X509Chunks(pub Cip509RbacMetadata); - -#[allow(dead_code)] -impl X509Chunks { - /// Create new instance of `X509Chunks`. - fn new(chunk_data: Cip509RbacMetadata) -> Self { - Self(chunk_data) +/// A helper for decoding [`Cip509RbacMetadata`]. +/// +/// Due to encoding restrictions the [`Cip509`](crate::cardano::cip509::Cip509) metadata +/// is encoded in chunks: +/// ```text +/// chunk_type => [ + x509_chunk ] +/// ``` +/// This helper is used to decode them into the actual structure. +#[derive(Debug, PartialEq, Clone)] +pub struct X509Chunks(Option); + +impl From for Option { + fn from(value: X509Chunks) -> Self { + value.0 } } -impl Decode<'_, ()> for X509Chunks { - fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { +impl Decode<'_, DecodeContext<'_, '_>> for X509Chunks { + fn decode(d: &mut Decoder, decode_context: &mut DecodeContext) -> Result { // Determine the algorithm - let algo: u8 = decode_helper(d, "algorithm in X509Chunks", ctx)?; - let algorithm = CompressionAlgorithm::from_repr(algo) - .ok_or(decode::Error::message("Invalid chunk data type"))?; - - // Decompress the data - let decompressed = decompress(d, &algorithm) - .map_err(|e| decode::Error::message(format!("Failed to decompress {e}")))?; + let algorithm: u8 = decode_helper(d, "algorithm in X509Chunks", &mut ())?; + let Some(algorithm) = CompressionAlgorithm::from_repr(algorithm) else { + decode_context.report.invalid_value( + "compression algorithm", + &format!("{algorithm}"), + "Allowed values: 10, 11, 12", + "Cip509 chunked metadata", + ); + return Ok(Self(None)); + }; + + let decompressed = match decompress(d, &algorithm) { + Ok(v) => v, + Err(e) => { + decode_context.report.invalid_value( + "Chunked metadata", + &format!("{algorithm:?}"), + "Must contain properly compressed or raw metadata", + &format!("Cip509 chunks decompression error: {e:?}"), + ); + return Ok(Self(None)); + }, + }; // Decode the decompressed data. let mut decoder = Decoder::new(&decompressed); - let chunk_data = Cip509RbacMetadata::decode(&mut decoder, &mut ()) - .map_err(|e| decode::Error::message(format!("Failed to decode {e}")))?; - - Ok(X509Chunks(chunk_data)) + let chunk_data = match Cip509RbacMetadata::decode(&mut decoder, decode_context) { + Ok(d) => d, + Err(e) => { + decode_context.report.other( + &format!("Failed to decode: {e:?}"), + "Cip509 chunked metadata", + ); + return Ok(Self(None)); + }, + }; + + Ok(X509Chunks(Some(chunk_data))) } } @@ -84,7 +114,14 @@ fn decompress(d: &mut Decoder, algorithm: &CompressionAlgorithm) -> anyhow::Resu #[cfg(test)] mod tests { + use std::collections::HashMap; + + use cardano_blockchain_types::Point; + use catalyst_types::problem_report::ProblemReport; + use pallas::ledger::traverse::MultiEraTx; + use super::*; + use crate::{cardano::cip509::PointTxnIdx, utils::test}; // RAW data: 10 const RAW: &str = "0a8c5840a30a815902ae308202aa3082025ca00302010202147735a70599e68b49554b1cb3a6cf5e34583b3c2f300506032b6570307c310b300906035504061302555331584013301106035504080c0a43616c69666f726e69613116301406035504070c0d53616e204672616e636973636f31123010060355040a0c094d79436f6d70616e79584031153013060355040b0c0c4d794465706172746d656e743115301306035504030c0c6d79646f6d61696e2e636f6d301e170d3234313132393034333134305a1758400d3235313132393034333134305a307c310b30090603550406130255533113301106035504080c0a43616c69666f726e69613116301406035504070c0d53616e5840204672616e636973636f31123010060355040a0c094d79436f6d70616e7931153013060355040b0c0c4d794465706172746d656e743115301306035504030c0c58406d79646f6d61696e2e636f6d302a300506032b65700321007e082c662a8d4d3271d797067f36caf25d6472b83901620a2eac193331a7f871a381ef3081ec308158409e0603551d11048196308193820c6d79646f6d61696e2e636f6d82107777772e6d79646f6d61696e2e636f6d820b6578616d706c652e636f6d820f7777772e65584078616d706c652e636f6d86537765622b63617264616e6f3a2f2f616464722f7374616b655f7465737431757165686b636b306c616a713867723238743975786e5840757667637172633630373078336b3972383034387a3879356773737274766e300b0603551d0f0404030205e0301d0603551d250416301406082b06010505070358400106082b06010505070302301d0603551d0e04160414251ddd56123655faa9348ff93c1e92ce3bc15a29300506032b6570034100b11c80d36fdcba650b950f06584087e448b3bcbeb2caa5249b24aff83d16ebbb71249e44bd0ecfab8b40fb772b6f977f98ac9122e13954439d0120980b347e3f9707181e81d9800558206e42f8e5582e89a76ebb13ef279df7841efce978f106bee196f0e3cfd347bb31a2e8186481a4000001820a0003010a6454657374"; @@ -97,26 +134,98 @@ mod tests { fn test_decode_x509_chunks_raw() { let raw_bytes = hex::decode(RAW).unwrap(); let mut decoder = Decoder::new(raw_bytes.as_slice()); - let x509_chunks = X509Chunks::decode(&mut decoder, &mut ()); - // Decode the decompressed data should success. - assert!(x509_chunks.is_ok()); + let mut report = ProblemReport::new("X509Chunks"); + // We don't care about actual values in the context, all we want is to check the decoding + // of differently compressed data. + let data = test::block_3(); + let transactions = data.block.txs(); + let MultiEraTx::Conway(txn) = transactions.first().unwrap() else { + panic!("Unexpected transaction type"); + }; + let origin = PointTxnIdx::new(Point::fuzzy(0.into()), 0.into()); + let mut context = DecodeContext { + origin, + txn, + payment_history: HashMap::new(), + report: &mut report, + }; + let x509_chunks = X509Chunks::decode(&mut decoder, &mut context).unwrap(); + // We don't want to check `report.is_problematic()` because there will be errors because + // of the context. Instead we check that the fields are decoded. + let metadata = x509_chunks.0.unwrap(); + assert_eq!(1, metadata.x509_certs.len()); + assert!(metadata.c509_certs.is_empty()); + assert_eq!(1, metadata.certificate_uris.x_uris().len()); + assert!(metadata.certificate_uris.c_uris().is_empty()); + assert_eq!(1, metadata.pub_keys.len()); + assert!(metadata.revocation_list.is_empty()); + assert_eq!(1, metadata.role_data.len()); + assert!(metadata.purpose_key_data.is_empty()); } #[test] - fn test_decode_x509_chunks_brotli() { + fn decode_x509_chunks_brotli() { let brotli_bytes = hex::decode(BROTLI).unwrap(); let mut decoder = Decoder::new(brotli_bytes.as_slice()); - let x509_chunks = X509Chunks::decode(&mut decoder, &mut ()); - // Decode the decompressed data should success. - assert!(x509_chunks.is_ok()); + let mut report = ProblemReport::new("X509Chunks"); + // We don't care about actual values in the context, all we want is to check the decoding + // of differently compressed data. + let data = test::block_3(); + let transactions = data.block.txs(); + let MultiEraTx::Conway(txn) = transactions.first().unwrap() else { + panic!("Unexpected transaction type"); + }; + let origin = PointTxnIdx::new(Point::fuzzy(0.into()), 0.into()); + let mut context = DecodeContext { + origin, + txn, + payment_history: HashMap::new(), + report: &mut report, + }; + let x509_chunks = X509Chunks::decode(&mut decoder, &mut context).unwrap(); + // We don't want to check `report.is_problematic()` because there will be errors because + // of the context. Instead we check that the fields are decoded. + let metadata = x509_chunks.0.unwrap(); + assert_eq!(1, metadata.x509_certs.len()); + assert!(metadata.c509_certs.is_empty()); + assert_eq!(1, metadata.certificate_uris.x_uris().len()); + assert!(metadata.certificate_uris.c_uris().is_empty()); + assert_eq!(1, metadata.pub_keys.len()); + assert!(metadata.revocation_list.is_empty()); + assert_eq!(1, metadata.role_data.len()); + assert!(metadata.purpose_key_data.is_empty()); } #[test] - fn test_decode_x509_chunks_zstd() { + fn decode_x509_chunks_zstd() { let zstd_bytes = hex::decode(ZSTD).unwrap(); let mut decoder = Decoder::new(zstd_bytes.as_slice()); - let x509_chunks = X509Chunks::decode(&mut decoder, &mut ()); - // Decode the decompressed data should success. - assert!(x509_chunks.is_ok()); + let mut report = ProblemReport::new("X509Chunks"); + // We don't care about actual values in the context, all we want is to check the decoding + // of differently compressed data. + let data = test::block_3(); + let transactions = data.block.txs(); + let MultiEraTx::Conway(txn) = transactions.first().unwrap() else { + panic!("Unexpected transaction type"); + }; + let origin = PointTxnIdx::new(Point::fuzzy(0.into()), 0.into()); + let mut context = DecodeContext { + origin, + txn, + payment_history: HashMap::new(), + report: &mut report, + }; + let x509_chunks = X509Chunks::decode(&mut decoder, &mut context).unwrap(); + // We don't want to check `report.is_problematic()` because there will be errors because + // of the context. Instead we check that the fields are decoded. + let metadata = x509_chunks.0.unwrap(); + assert_eq!(1, metadata.x509_certs.len()); + assert!(metadata.c509_certs.is_empty()); + assert_eq!(1, metadata.certificate_uris.x_uris().len()); + assert!(metadata.certificate_uris.c_uris().is_empty()); + assert_eq!(1, metadata.pub_keys.len()); + assert!(metadata.revocation_list.is_empty()); + assert_eq!(1, metadata.role_data.len()); + assert!(metadata.purpose_key_data.is_empty()); } } diff --git a/rust/rbac-registration/src/cardano/mod.rs b/rust/rbac-registration/src/cardano/mod.rs index f8c6b996d8..8af2182b83 100644 --- a/rust/rbac-registration/src/cardano/mod.rs +++ b/rust/rbac-registration/src/cardano/mod.rs @@ -1,4 +1,3 @@ //! Cardano module pub mod cip509; -pub mod transaction; diff --git a/rust/rbac-registration/src/cardano/transaction/mod.rs b/rust/rbac-registration/src/cardano/transaction/mod.rs deleted file mode 100644 index b5faf4ab48..0000000000 --- a/rust/rbac-registration/src/cardano/transaction/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Cardano transaction module - -pub(crate) mod raw_aux_data; -pub(crate) mod witness; diff --git a/rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs b/rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs deleted file mode 100644 index 6117c03975..0000000000 --- a/rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! Raw Auxiliary Data Decoding - -use std::sync::Arc; - -use anyhow::bail; -use dashmap::DashMap; -use minicbor::{data::Type, Decoder}; -use tracing::{error, warn}; - -/// What type of smart contract is this list. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, strum_macros::Display, Hash)] -#[allow(dead_code)] -pub enum SmartContractType { - /// Native smart contracts - Native, - /// Plutus smart contracts (with version number 1-x) - Plutus(u64), -} - -// We CAN NOT use the Pallas library metadata decoding because it does not preserve raw -// metadata values which are critical for performing operations like signature checks on -// data. So we have a bespoke metadata decoder here. -#[derive(Debug)] -#[allow(dead_code)] -pub(crate) struct RawAuxData { - /// Metadata: key = label, value = raw metadata bytes - metadata: DashMap>>, - /// Scripts: 1 = Native, 2 = Plutus V1, 3 = Plutus V2, 4 = Plutus V3 - scripts: DashMap>>>, -} - -impl RawAuxData { - /// Create a new `RawDecodedMetadata`. - #[allow(dead_code)] - pub(crate) fn new(aux_data: &[u8]) -> Self { - let mut raw_decoded_data = Self { - metadata: DashMap::new(), - scripts: DashMap::new(), - }; - - let mut decoder = Decoder::new(aux_data); - - match decoder.datatype() { - Ok(minicbor::data::Type::Map) => { - if let Err(error) = Self::decode_shelley_map(&mut raw_decoded_data, &mut decoder) { - error!("Failed to Deserialize Shelley Metadata: {error}: {aux_data:02x?}"); - } - }, - Ok(minicbor::data::Type::Array) => { - if let Err(error) = - Self::decode_shelley_ma_array(&mut raw_decoded_data, &mut decoder) - { - error!("Failed to Deserialize Shelley-MA Metadata: {error}: {aux_data:02x?}"); - } - }, - Ok(minicbor::data::Type::Tag) => { - if let Err(error) = - Self::decode_alonzo_plus_map(&mut raw_decoded_data, &mut decoder) - { - error!("Failed to Deserialize Alonzo+ Metadata: {error}: {aux_data:02x?}"); - } - }, - Ok(unexpected) => { - error!("Unexpected datatype for Aux data: {unexpected}: {aux_data:02x?}"); - }, - Err(error) => { - error!("Error decoding metadata: {error}: {aux_data:02x?}"); - }, - } - - raw_decoded_data - } - - /// Decode the Shelley map of metadata. - fn decode_shelley_map( - raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, - ) -> anyhow::Result<()> { - let entries = match decoder.map() { - Ok(Some(entries)) => entries, - Ok(None) => { - // Sadly... Indefinite Maps are allowed in Cardano CBOR Encoding. - u64::MAX - }, - Err(error) => { - bail!("Error decoding metadata: {error}"); - }, - }; - - // debug!("Decoding shelley metadata map with {} entries", entries); - - let raw_metadata = decoder.input(); - - for _ in 0..entries { - let key = match decoder.u64() { - Ok(key) => key, - Err(error) => { - bail!("Error decoding metadata key: {error}"); - }, - }; - let value_start = decoder.position(); - if let Err(error) = decoder.skip() { - bail!("Error decoding metadata value: {error}"); - } - let value_end = decoder.position(); - let Some(value_slice) = raw_metadata.get(value_start..value_end) else { - bail!("Invalid metadata value found. Unable to extract raw value slice."); - }; - let value = value_slice.to_vec(); - - // debug!("Decoded metadata key: {key}, value: {value:?}"); - - let _unused = raw_decoded_data.metadata.insert(key, Arc::new(value)); - - // Look for End Sentinel IF its an indefinite MAP (which we know because entries is - // u64::MAX). - if entries == u64::MAX { - match decoder.datatype() { - Ok(Type::Break) => { - // Skip over the break token. - let _unused = decoder.skip(); - break; - }, - Ok(_) => (), // Not break, so do next loop, should be the next key. - Err(error) => { - bail!("Error checking indefinite metadata map end sentinel: {error}"); - }, - } - } - } - - Ok(()) - } - - /// Decode a Shelley-MA Auxiliary Data Array - fn decode_shelley_ma_array( - raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, - ) -> anyhow::Result<()> { - match decoder.array() { - Ok(Some(entries)) => { - if entries != 2 { - bail!( - "Invalid number of entries in Metadata Array. Expected 2, found {entries}." - ); - } - }, - Ok(None) => { - bail!("Indefinite Array found decoding Metadata. Invalid."); - }, - Err(error) => { - bail!("Error decoding metadata: {error}"); - }, - }; - - // First entry is the metadata map, so just decode that now. - Self::decode_shelley_map(raw_decoded_data, decoder)?; - // Second entry is an array of native scripts. - Self::decode_script_array(raw_decoded_data, decoder, SmartContractType::Native)?; - - Ok(()) - } - - /// Decode a Shelley-MA Auxiliary Data Array - fn decode_alonzo_plus_map( - raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, - ) -> anyhow::Result<()> { - match decoder.tag() { - Ok(tag) => { - if tag.as_u64() != 259 { - bail!("Invalid tag for alonzo+ aux data. Expected 259, found {tag}."); - } - }, - Err(error) => { - bail!("Error decoding tag for alonzo+ aux data: {error}"); - }, - } - - let entries = match decoder.map() { - Ok(Some(entries)) => entries, - Ok(None) => bail!("Indefinite Map found decoding Alonzo+ Metadata. Invalid."), - Err(error) => bail!("Error decoding Alonzo+ Metadata: {error}"), - }; - - // iterate the map - for _ in 0..entries { - let aux_type_key = match decoder.u64() { - Ok(key) => key, - Err(error) => { - bail!("Error decoding Alonzo+ Metadata Aux Data Type Key: {error}"); - }, - }; - - let contract_type = match aux_type_key { - 0 => { - if raw_decoded_data.metadata.is_empty() { - Self::decode_shelley_map(raw_decoded_data, decoder)?; - continue; - } - bail!("Multiple Alonzo+ Metadata entries found. Invalid."); - }, - 1 => SmartContractType::Native, - _ => { - if aux_type_key > 4 { - warn!( - "Auxiliary Type Key > 4 detected, assuming its a plutus script > V3." - ); - } - SmartContractType::Plutus(aux_type_key - 1) - }, - }; - - if raw_decoded_data.scripts.contains_key(&contract_type) { - bail!("Multiple Alonzo+ Scripts of type {contract_type} found. Invalid."); - } - - Self::decode_script_array(raw_decoded_data, decoder, contract_type)?; - } - Ok(()) - } - - /// Decode an array of smart contract scripts - fn decode_script_array( - raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, - contract_type: SmartContractType, - ) -> anyhow::Result<()> { - let mut scripts: Vec> = Vec::new(); - - let entries = match decoder.array() { - Ok(Some(entries)) => entries, - Ok(None) => { - bail!("Indefinite Script Array found decoding Metadata. Invalid."); - }, - Err(error) => { - bail!("Error decoding metadata: {error}"); - }, - }; - - let raw_metadata = decoder.input(); - - for _entry in 0..entries { - if contract_type == SmartContractType::Native { - // Native Scripts are actually CBOR arrays, so capture their data as bytes for - // later processing. - let value_start = decoder.position(); - if let Err(error) = decoder.skip() { - bail!("Error decoding native script value: {error}"); - } - let value_end = decoder.position(); - let Some(value_slice) = raw_metadata.get(value_start..value_end) else { - bail!("Invalid metadata value found. Unable to extract native script slice."); - }; - scripts.push(value_slice.to_vec()); - } else { - let script = match decoder.bytes() { - Ok(script) => script, - Err(error) => bail!("Error decoding script data from metadata: {error}"), - }; - scripts.push(script.to_vec()); - } - } - - let _unused = raw_decoded_data - .scripts - .insert(contract_type, Arc::new(scripts)); - - Ok(()) - } - - /// Get Raw metadata for a given metadata label, if it exists. - #[allow(dead_code)] - pub(crate) fn get_metadata(&self, label: u64) -> Option>> { - self.metadata.get(&label).map(|v| v.value().clone()) - } -} diff --git a/rust/rbac-registration/src/cardano/transaction/witness.rs b/rust/rbac-registration/src/cardano/transaction/witness.rs deleted file mode 100644 index 736f670135..0000000000 --- a/rust/rbac-registration/src/cardano/transaction/witness.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Transaction Witness -use std::fmt::{Display, Formatter}; - -use anyhow::bail; -use dashmap::DashMap; -use pallas::{codec::utils::Bytes, ledger::traverse::MultiEraTx}; - -use crate::utils::hashing::blake2b_244; - -/// `WitnessMap` type of `DashMap` with -/// key as [u8; 28] = (`blake2b_244` hash of the public key) -/// value as `(Bytes, Vec) = (public key, tx index within the block)` -pub(crate) type WitnessMap = DashMap<[u8; 28], (Bytes, Vec)>; - -#[derive(Debug)] -/// `TxWitness` struct to store the witness data. -pub(crate) struct TxWitness(WitnessMap); - -impl TxWitness { - /// Create a new `TxWitness` from a list of `MultiEraTx`. - pub(crate) fn new(txs: &[MultiEraTx]) -> anyhow::Result { - let map: WitnessMap = DashMap::new(); - for (i, tx) in txs.iter().enumerate() { - match tx { - MultiEraTx::AlonzoCompatible(tx, _) => { - let witness_set = &tx.transaction_witness_set; - if let Some(vkey_witness_set) = witness_set.vkeywitness.clone() { - for vkey_witness in vkey_witness_set { - let vkey_hash = blake2b_244(&vkey_witness.vkey)?; - let tx_num = u16::try_from(i)?; - map.entry(vkey_hash) - .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) - .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); - } - }; - }, - MultiEraTx::Babbage(tx) => { - let witness_set = &tx.transaction_witness_set; - if let Some(vkey_witness_set) = witness_set.vkeywitness.clone() { - for vkey_witness in vkey_witness_set { - let vkey_hash = blake2b_244(&vkey_witness.vkey)?; - let tx_num = u16::try_from(i)?; - map.entry(vkey_hash) - .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) - .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); - } - } - }, - MultiEraTx::Conway(tx) => { - let witness_set = &tx.transaction_witness_set; - if let Some(vkey_witness_set) = &witness_set.vkeywitness.clone() { - for vkey_witness in vkey_witness_set { - let vkey_hash = blake2b_244(&vkey_witness.vkey)?; - let tx_num = u16::try_from(i)?; - map.entry(vkey_hash) - .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) - .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); - } - } - }, - _ => { - bail!("Unsupported transaction type"); - }, - }; - } - Ok(Self(map)) - } - - /// Check whether the public key hash is in the given transaction number. - pub(crate) fn check_witness_in_tx(&self, vkey_hash: &[u8; 28], tx_num: u16) -> bool { - self.0 - .get(vkey_hash) - .map_or(false, |entry| entry.1.contains(&tx_num)) - } - - /// Get the actual address from the given public key hash. - #[allow(dead_code)] - pub(crate) fn get_witness_pk_addr(&self, vkey_hash: &[u8; 28]) -> Option { - self.0.get(vkey_hash).map(|entry| entry.0.clone()) - } -} - -impl Display for TxWitness { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for data in &self.0 { - let vkey_hash = hex::encode(data.key()); - let vkey: Vec = data.0.clone().into(); - let vkey_encoded = hex::encode(&vkey); - writeln!( - f, - "Key Hash: {}, PublicKey: {}, Tx: {:?}", - vkey_hash, vkey_encoded, data.1 - )?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - fn conway() -> Vec { - hex::decode(include_str!("../../test_data/cardano/conway_1.block")) - .expect("Failed to decode hex block.") - } - - #[test] - fn tx_witness() { - let conway = conway(); - let conway_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway) - .expect("Failed to decode MultiEraBlock"); - let txs_conway = conway_block.txs(); - let tx_witness_conway = TxWitness::new(&txs_conway).expect("Failed to create TxWitness"); - let vkey1_hash: [u8; 28] = - hex::decode("c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d2") - .expect("Failed to decode vkey1_hash") - .try_into() - .expect("Invalid length of vkey1_hash"); - assert!(tx_witness_conway.get_witness_pk_addr(&vkey1_hash).is_some()); - assert!(tx_witness_conway.check_witness_in_tx(&vkey1_hash, 0)); - } -} diff --git a/rust/rbac-registration/src/lib.rs b/rust/rbac-registration/src/lib.rs index ab9cfd1e49..6e35f51656 100644 --- a/rust/rbac-registration/src/lib.rs +++ b/rust/rbac-registration/src/lib.rs @@ -2,4 +2,5 @@ pub mod cardano; pub mod registration; -pub(crate) mod utils; + +mod utils; diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index eeac8dd427..8d562944a4 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -1,42 +1,21 @@ //! Chain of Cardano registration data -pub mod payment_history; -pub mod point_tx_idx; -pub mod role_data; - use std::{collections::HashMap, sync::Arc}; use anyhow::bail; use c509_certificate::c509::C509; +use catalyst_types::{hashes::Blake2b256Hash, uuid::UuidV4}; use ed25519_dalek::VerifyingKey; -use pallas::{ - crypto::hash::Hash, - ledger::{ - addresses::{Address, ShelleyAddress}, - traverse::MultiEraTx, - }, - network::miniprotocols::Point, -}; -use payment_history::PaymentHistory; -use point_tx_idx::PointTxIdx; -use role_data::RoleData; -use tracing::error; -use uuid::Uuid; - -use crate::{ - cardano::cip509::{ - self, - rbac::{ - certs::{C509Cert, X509DerCert}, - pub_key::SimplePublicKeyType, - }, - types::cert_key_hash::CertKeyHash, - Cip509, Cip509Validation, - }, - utils::general::decremented_index, +use tracing::{error, warn}; +use x509_cert::certificate::Certificate as X509Certificate; + +use crate::cardano::cip509::{ + C509Cert, CertKeyHash, Cip0134UriSet, Cip509, PaymentHistory, PointTxnIdx, RoleData, + RoleNumber, SimplePublicKeyType, X509DerCert, }; /// Registration chains. +#[derive(Debug)] pub struct RegistrationChain { /// Inner part of the registration chain. inner: Arc, @@ -48,19 +27,12 @@ impl RegistrationChain { /// /// # Arguments /// - `cip509` - The CIP509. - /// - `tracking_payment_keys` - The list of payment keys to track. - /// - `point` - The point (slot) of the transaction. - /// - `tx_idx` - The transaction index. - /// - `txn` - The transaction. /// /// # Errors /// /// Returns an error if data is invalid - pub fn new( - point: Point, tracking_payment_keys: &[ShelleyAddress], tx_idx: usize, txn: &MultiEraTx, - cip509: Cip509, - ) -> anyhow::Result { - let inner = RegistrationChainInner::new(cip509, tracking_payment_keys, point, tx_idx, txn)?; + pub fn new(cip509: Cip509) -> anyhow::Result { + let inner = RegistrationChainInner::new(cip509)?; Ok(Self { inner: Arc::new(inner), @@ -70,18 +42,13 @@ impl RegistrationChain { /// Update the registration chain. /// /// # Arguments - /// - `point` - The point (slot) of the transaction. - /// - `tx_idx` - The transaction index. - /// - `txn` - The transaction. /// - `cip509` - The CIP509. /// /// # Errors /// /// Returns an error if data is invalid - pub fn update( - &self, point: Point, tx_idx: usize, txn: &MultiEraTx, cip509: Cip509, - ) -> anyhow::Result { - let new_inner = self.inner.update(point, tx_idx, txn, cip509)?; + pub fn update(&self, cip509: Cip509) -> anyhow::Result { + let new_inner = self.inner.update(cip509)?; Ok(Self { inner: Arc::new(new_inner), @@ -90,76 +57,78 @@ impl RegistrationChain { /// Get the current transaction ID hash. #[must_use] - pub fn current_tx_id_hash(&self) -> Hash<32> { + pub fn current_tx_id_hash(&self) -> Blake2b256Hash { self.inner.current_tx_id_hash } /// Get a list of purpose for this registration chain. #[must_use] - pub fn purpose(&self) -> &[Uuid] { + pub fn purpose(&self) -> &[UuidV4] { &self.inner.purpose } /// Get the map of index in array to point, transaction index, and x509 certificate. #[must_use] - pub fn x509_certs(&self) -> &HashMap)> { + pub fn x509_certs(&self) -> &HashMap { &self.inner.x509_certs } /// Get the map of index in array to point, transaction index, and c509 certificate. #[must_use] - pub fn c509_certs(&self) -> &HashMap { + pub fn c509_certs(&self) -> &HashMap { &self.inner.c509_certs } /// Get the map of index in array to point, transaction index, and public key. #[must_use] - pub fn simple_keys(&self) -> &HashMap { + pub fn simple_keys(&self) -> &HashMap { &self.inner.simple_keys } /// Get a list of revocations. #[must_use] - pub fn revocations(&self) -> &[(PointTxIdx, CertKeyHash)] { + pub fn revocations(&self) -> &[(PointTxnIdx, CertKeyHash)] { &self.inner.revocations } /// Get the map of role number to point, transaction index, and role data. #[must_use] - pub fn role_data(&self) -> &HashMap { + pub fn role_data(&self) -> &HashMap { &self.inner.role_data } /// Get the map of tracked payment keys to its history. #[must_use] - pub fn tracking_payment_history(&self) -> &HashMap> { - &self.inner.tracking_payment_history + pub fn tracking_payment_history(&self) -> &PaymentHistory { + &self.inner.payment_history } } /// Inner structure of registration chain. -#[derive(Clone)] +#[derive(Debug, Clone)] struct RegistrationChainInner { /// The current transaction ID hash (32 bytes) - current_tx_id_hash: Hash<32>, + current_tx_id_hash: Blake2b256Hash, /// List of purpose for this registration chain - purpose: Vec, + purpose: Vec, // RBAC /// Map of index in array to point, transaction index, and x509 certificate. - x509_certs: HashMap)>, + x509_certs: HashMap, /// Map of index in array to point, transaction index, and c509 certificate. - c509_certs: HashMap, + c509_certs: HashMap, + /// A set of URIs contained in both x509 and c509 certificates. + certificate_uris: Cip0134UriSet, /// Map of index in array to point, transaction index, and public key. - simple_keys: HashMap, + simple_keys: HashMap, /// List of point, transaction index, and certificate key hash. - revocations: Vec<(PointTxIdx, CertKeyHash)>, + revocations: Vec<(PointTxnIdx, CertKeyHash)>, // Role /// Map of role number to point, transaction index, and role data. - role_data: HashMap, + role_data: HashMap, /// Map of tracked payment key to its history. - tracking_payment_history: HashMap>, + payment_history: PaymentHistory, } impl RegistrationChainInner { @@ -168,191 +137,151 @@ impl RegistrationChainInner { /// /// # Arguments /// - `cip509` - The CIP509. - /// - `tracking_payment_keys` - The list of payment keys to track. - /// - `point` - The point (slot) of the transaction. - /// - `tx_idx` - The transaction index. - /// - `txn` - The transaction. /// /// # Errors /// /// Returns an error if data is invalid - fn new( - cip509: Cip509, tracking_payment_keys: &[ShelleyAddress], point: Point, tx_idx: usize, - txn: &MultiEraTx, - ) -> anyhow::Result { + fn new(cip509: Cip509) -> anyhow::Result { // Should be chain root, return immediately if not - if cip509.prv_tx_id.is_some() { + if cip509.previous_transaction().is_some() { bail!("Invalid chain root, previous transaction ID should be None."); } - let mut validation_report = Vec::new(); - let validation_data = cip509.validate(txn, &mut validation_report); - - // Do the CIP509 validation, ensuring the basic validation pass. - if !is_valid_cip509(&validation_data) { - // Log out the error if any - error!("CIP509 validation failed: {:?}", validation_report); - bail!("CIP509 validation failed, {:?}", validation_report); - } - - // Add purpose to the list - let purpose = vec![cip509.purpose]; - - let registration = cip509.x509_chunks.0; - let point_tx_idx = PointTxIdx::new(point, tx_idx); + let point_tx_idx = cip509.origin().clone(); + let current_tx_id_hash = cip509.txn_hash(); + let (purpose, registration, payment_history) = match cip509.consume() { + Ok(v) => v, + Err(e) => { + let error = format!("Invalid Cip509: {e:?}"); + error!(error); + bail!(error); + }, + }; - let x509_cert_map = chain_root_x509_certs(registration.x509_certs, &point_tx_idx); - let c509_cert_map = chain_root_c509_certs(registration.c509_certs, &point_tx_idx); - let public_key_map = chain_root_public_keys(registration.pub_keys, &point_tx_idx); + let purpose = vec![purpose]; + let certificate_uris = registration.certificate_uris; + let x509_certs = chain_root_x509_certs(registration.x509_certs, &point_tx_idx); + let c509_certs = chain_root_c509_certs(registration.c509_certs, &point_tx_idx); + let simple_keys = chain_root_public_keys(registration.pub_keys, &point_tx_idx); let revocations = revocations_list(registration.revocation_list, &point_tx_idx); - let role_data_map = chain_root_role_data(registration.role_set, txn, &point_tx_idx)?; - - let mut tracking_payment_history = HashMap::new(); - // Create a payment history for each tracking payment key - for tracking_key in tracking_payment_keys { - tracking_payment_history.insert(tracking_key.clone(), Vec::new()); - } - // Keep record of payment history, the payment key that we want to track - update_tracking_payment_history(&mut tracking_payment_history, txn, &point_tx_idx)?; + let role_data = chain_root_role_data(registration.role_data, &point_tx_idx); Ok(Self { + current_tx_id_hash, purpose, - current_tx_id_hash: txn.hash(), - x509_certs: x509_cert_map, - c509_certs: c509_cert_map, - simple_keys: public_key_map, + x509_certs, + c509_certs, + certificate_uris, + simple_keys, revocations, - role_data: role_data_map, - tracking_payment_history, + role_data, + payment_history, }) } /// Update the registration chain. /// /// # Arguments - /// - `point` - The point (slot) of the transaction. - /// - `tx_idx` - The transaction index. - /// - `txn` - The transaction. /// - `cip509` - The CIP509. /// /// # Errors /// /// Returns an error if data is invalid - fn update( - &self, point: Point, tx_idx: usize, txn: &MultiEraTx, cip509: Cip509, - ) -> anyhow::Result { + fn update(&self, cip509: Cip509) -> anyhow::Result { let mut new_inner = self.clone(); - let mut validation_report = Vec::new(); - let validation_data = cip509.validate(txn, &mut validation_report); - - // Do the CIP509 validation, ensuring the basic validation pass. - if !is_valid_cip509(&validation_data) { - error!("CIP509 validation failed: {:?}", validation_report); - bail!("CIP509 validation failed, {:?}", validation_report); + let Some(prv_tx_id) = cip509.previous_transaction() else { + bail!("Empty previous transaction ID"); + }; + // Previous transaction ID in the CIP509 should equal to the current transaction ID + // or else it is not a part of the chain + if prv_tx_id == self.current_tx_id_hash { + // Update the current transaction ID hash + new_inner.current_tx_id_hash = cip509.txn_hash(); + } else { + bail!("Invalid previous transaction ID, not a part of this registration chain"); } - // Check and update the current transaction ID hash - if let Some(prv_tx_id) = cip509.prv_tx_id { - // Previous transaction ID in the CIP509 should equal to the current transaction ID - // or else it is not a part of the chain - if prv_tx_id == self.current_tx_id_hash { - new_inner.current_tx_id_hash = prv_tx_id; - } else { - bail!("Invalid previous transaction ID, not a part of this registration chain"); - } - } + let point_tx_idx = cip509.origin().clone(); + let (purpose, registration, payment_history) = match cip509.consume() { + Ok(v) => v, + Err(e) => { + let error = format!("Invalid Cip509: {e:?}"); + error!(error); + bail!(error); + }, + }; // Add purpose to the chain, if not already exist - let purpose = cip509.purpose; if !self.purpose.contains(&purpose) { new_inner.purpose.push(purpose); } - let registration = cip509.x509_chunks.0; - let point_tx_idx = PointTxIdx::new(point, tx_idx); - + new_inner.certificate_uris = new_inner.certificate_uris.update(®istration); + new_inner.payment_history.extend(payment_history); update_x509_certs(&mut new_inner, registration.x509_certs, &point_tx_idx); - update_c509_certs(&mut new_inner, registration.c509_certs, &point_tx_idx)?; + update_c509_certs(&mut new_inner, registration.c509_certs, &point_tx_idx); update_public_keys(&mut new_inner, registration.pub_keys, &point_tx_idx); let revocations = revocations_list(registration.revocation_list, &point_tx_idx); // Revocation list should be appended new_inner.revocations.extend(revocations); - update_role_data(&mut new_inner, registration.role_set, txn, &point_tx_idx)?; - - update_tracking_payment_history( - &mut new_inner.tracking_payment_history, - txn, - &point_tx_idx, - )?; + update_role_data(&mut new_inner, registration.role_data, &point_tx_idx); Ok(new_inner) } } -/// Check if the CIP509 is valid. -fn is_valid_cip509(validation_data: &Cip509Validation) -> bool { - validation_data.is_valid_aux - && validation_data.is_valid_txn_inputs_hash - && validation_data.is_valid_stake_public_key - && validation_data.is_valid_payment_key - && validation_data.is_valid_signing_key -} - /// Process x509 certificate for chain root. fn chain_root_x509_certs( - x509_certs: Option>, point_tx_idx: &PointTxIdx, -) -> HashMap)> { - let mut map = HashMap::new(); - if let Some(cert_list) = x509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - // Chain root, expect only the certificate not undefined or delete - if let cip509::rbac::certs::X509DerCert::X509Cert(cert) = cert { - map.insert(idx, (point_tx_idx.clone(), cert.clone())); + x509_certs: Vec, point_tx_idx: &PointTxnIdx, +) -> HashMap { + x509_certs + .into_iter() + .enumerate() + .filter_map(|(index, cert)| { + if let X509DerCert::X509Cert(cert) = cert { + Some((index, (point_tx_idx.clone(), *cert))) + } else { + None } - } - } - map + }) + .collect() } /// Update x509 certificates in the registration chain. fn update_x509_certs( - new_inner: &mut RegistrationChainInner, x509_certs: Option>, - point_tx_idx: &PointTxIdx, + new_inner: &mut RegistrationChainInner, x509_certs: Vec, + point_tx_idx: &PointTxnIdx, ) { - if let Some(cert_list) = x509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - match cert { - // Unchanged to that index, so continue - cip509::rbac::certs::X509DerCert::Undefined => continue, - // Delete the certificate - cip509::rbac::certs::X509DerCert::Deleted => { - new_inner.x509_certs.remove(&idx); - }, - // Add the new certificate - cip509::rbac::certs::X509DerCert::X509Cert(cert) => { - new_inner - .x509_certs - .insert(idx, (point_tx_idx.clone(), cert.clone())); - }, - } + for (idx, cert) in x509_certs.into_iter().enumerate() { + match cert { + // Unchanged to that index, so continue + X509DerCert::Undefined => continue, + // Delete the certificate + X509DerCert::Deleted => { + new_inner.x509_certs.remove(&idx); + }, + // Add the new certificate + X509DerCert::X509Cert(cert) => { + new_inner + .x509_certs + .insert(idx, (point_tx_idx.clone(), *cert)); + }, } } } /// Process c509 certificates for chain root. fn chain_root_c509_certs( - c509_certs: Option>, point_tx_idx: &PointTxIdx, -) -> HashMap { + c509_certs: Vec, point_tx_idx: &PointTxnIdx, +) -> HashMap { let mut map = HashMap::new(); - if let Some(cert_list) = c509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - if let cip509::rbac::certs::C509Cert::C509Certificate(cert) = cert { - // Chain root, expect only the certificate not undefined or delete - map.insert(idx, (point_tx_idx.clone(), *cert.clone())); - } + for (idx, cert) in c509_certs.into_iter().enumerate() { + if let C509Cert::C509Certificate(cert) = cert { + // Chain root, expect only the certificate not undefined or delete + map.insert(idx, (point_tx_idx.clone(), *cert)); } } map @@ -360,45 +289,39 @@ fn chain_root_c509_certs( /// Update c509 certificates in the registration chain. fn update_c509_certs( - new_inner: &mut RegistrationChainInner, c509_certs: Option>, - point_tx_idx: &PointTxIdx, -) -> anyhow::Result<()> { - if let Some(cert_list) = c509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - match cert { - // Unchanged to that index, so continue - cip509::rbac::certs::C509Cert::Undefined => continue, - // Delete the certificate - cip509::rbac::certs::C509Cert::Deleted => { - new_inner.c509_certs.remove(&idx); - }, - // Certificate reference - cip509::rbac::certs::C509Cert::C509CertInMetadatumReference(_) => { - bail!("Unsupported c509 certificate in metadatum reference") - }, - // Add the new certificate - cip509::rbac::certs::C509Cert::C509Certificate(c509) => { - new_inner - .c509_certs - .insert(idx, (point_tx_idx.clone(), *c509.clone())); - }, - } + new_inner: &mut RegistrationChainInner, c509_certs: Vec, point_tx_idx: &PointTxnIdx, +) { + for (idx, cert) in c509_certs.into_iter().enumerate() { + match cert { + // Unchanged to that index, so continue + C509Cert::Undefined => continue, + // Delete the certificate + C509Cert::Deleted => { + new_inner.c509_certs.remove(&idx); + }, + // Certificate reference + C509Cert::C509CertInMetadatumReference(_) => { + warn!("Unsupported C509CertInMetadatumReference"); + }, + // Add the new certificate + C509Cert::C509Certificate(c509) => { + new_inner + .c509_certs + .insert(idx, (point_tx_idx.clone(), *c509)); + }, } } - Ok(()) } /// Process public keys for chain root. fn chain_root_public_keys( - pub_keys: Option>, point_tx_idx: &PointTxIdx, -) -> HashMap { + pub_keys: Vec, point_tx_idx: &PointTxnIdx, +) -> HashMap { let mut map = HashMap::new(); - if let Some(key_list) = pub_keys { - for (idx, key) in key_list.iter().enumerate() { - // Chain root, expect only the public key not undefined or delete - if let cip509::rbac::pub_key::SimplePublicKeyType::Ed25519(key) = key { - map.insert(idx, (point_tx_idx.clone(), *key)); - } + for (idx, key) in pub_keys.into_iter().enumerate() { + // Chain root, expect only the public key not undefined or delete + if let SimplePublicKeyType::Ed25519(key) = key { + map.insert(idx, (point_tx_idx.clone(), key)); } } map @@ -406,295 +329,126 @@ fn chain_root_public_keys( /// Update public keys in the registration chain. fn update_public_keys( - new_inner: &mut RegistrationChainInner, pub_keys: Option>, - point_tx_idx: &PointTxIdx, + new_inner: &mut RegistrationChainInner, pub_keys: Vec, + point_tx_idx: &PointTxnIdx, ) { - if let Some(key_list) = pub_keys { - for (idx, cert) in key_list.iter().enumerate() { - match cert { - // Unchanged to that index, so continue - cip509::rbac::pub_key::SimplePublicKeyType::Undefined => continue, - // Delete the public key - cip509::rbac::pub_key::SimplePublicKeyType::Deleted => { - new_inner.simple_keys.remove(&idx); - }, - // Add the new public key - cip509::rbac::pub_key::SimplePublicKeyType::Ed25519(key) => { - new_inner - .simple_keys - .insert(idx, (point_tx_idx.clone(), *key)); - }, - } + for (idx, cert) in pub_keys.into_iter().enumerate() { + match cert { + // Unchanged to that index, so continue + SimplePublicKeyType::Undefined => continue, + // Delete the public key + SimplePublicKeyType::Deleted => { + new_inner.simple_keys.remove(&idx); + }, + // Add the new public key + SimplePublicKeyType::Ed25519(key) => { + new_inner + .simple_keys + .insert(idx, (point_tx_idx.clone(), key)); + }, } } } /// Process the revocation list. fn revocations_list( - revocation_list: Option>, point_tx_idx: &PointTxIdx, -) -> Vec<(PointTxIdx, CertKeyHash)> { + revocation_list: Vec, point_tx_idx: &PointTxnIdx, +) -> Vec<(PointTxnIdx, CertKeyHash)> { let mut revocations = Vec::new(); - if let Some(revocations_data) = revocation_list { - for item in revocations_data { - revocations.push((point_tx_idx.clone(), item.clone())); - } + for item in revocation_list { + revocations.push((point_tx_idx.clone(), item.clone())); } revocations } /// Process the role data for chain root. fn chain_root_role_data( - role_set: Option>, txn: &MultiEraTx, - point_tx_idx: &PointTxIdx, -) -> anyhow::Result> { - let mut role_data_map = HashMap::new(); - if let Some(role_set_data) = role_set { - for role_data in role_set_data { - let signing_key = role_data.role_signing_key.clone(); - let encryption_key = role_data.role_encryption_key.clone(); - - // Get the payment key - let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; - - // Map of role number to point and role data - role_data_map.insert( - role_data.role_number, - ( - point_tx_idx.clone(), - RoleData::new( - signing_key, - encryption_key, - payment_key, - role_data.role_extended_data_keys.clone(), - ), - ), - ); - } - } - Ok(role_data_map) + role_data: HashMap, point_tx_idx: &PointTxnIdx, +) -> HashMap { + role_data + .into_iter() + .map(|(number, data)| (number, (point_tx_idx.clone(), data))) + .collect() } /// Update the role data in the registration chain. fn update_role_data( - inner: &mut RegistrationChainInner, role_set: Option>, - txn: &MultiEraTx, point_tx_idx: &PointTxIdx, -) -> anyhow::Result<()> { - if let Some(role_set_data) = role_set { - for role_data in role_set_data { - // If there is new role singing key, use it, else use the old one - let signing_key = match role_data.role_signing_key { - Some(key) => Some(key), - None => { - match inner.role_data.get(&role_data.role_number) { - Some((_, role_data)) => role_data.signing_key_ref().clone(), - None => None, - } - }, - }; - - // If there is new role encryption key, use it, else use the old one - let encryption_key = match role_data.role_encryption_key { - Some(key) => Some(key), - None => { - match inner.role_data.get(&role_data.role_number) { - Some((_, role_data)) => role_data.encryption_ref().clone(), - None => None, - } - }, - }; - let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; - - // Map of role number to point and role data - // Note that new role data will overwrite the old one - inner.role_data.insert( - role_data.role_number, - ( - point_tx_idx.clone(), - RoleData::new( - signing_key, - encryption_key, - payment_key, - role_data.role_extended_data_keys.clone(), - ), - ), - ); + inner: &mut RegistrationChainInner, role_set: HashMap, + point_tx_idx: &PointTxnIdx, +) { + for (number, mut data) in role_set { + // If there is new role singing key, use it, else use the old one + if data.signing_key().is_none() { + let signing_key = inner + .role_data + .get(&number) + .and_then(|(_, d)| d.signing_key()) + .cloned(); + data.set_signing_key(signing_key); } - } - Ok(()) -} -/// Helper function for retrieving the Shelley address from the transaction. -fn get_payment_addr_from_tx( - txn: &MultiEraTx, payment_key_ref: Option, -) -> anyhow::Result> { - // The index should exist since it pass the basic validation - if let Some(key_ref) = payment_key_ref { - if let MultiEraTx::Conway(tx) = txn { - // Transaction output - if key_ref < 0 { - let index = decremented_index(key_ref.abs())?; - if let Some(output) = tx.transaction_body.outputs.get(index) { - // Conway era -> Post alonzo tx output - match output { - pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo( - o, - ) => { - let address = - Address::from_bytes(&o.address).map_err(|e| anyhow::anyhow!(e))?; - - if let Address::Shelley(addr) = address { - return Ok(Some(addr.clone())); - } - bail!("Unsupported address type in payment key reference"); - }, - // Not support legacy form of transaction output - pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(_) => { - bail!("Unsupported transaction output type in payment key reference"); - }, - } - } - // Index doesn't exist - bail!("Payment key not found in transaction output"); - } - // Transaction input, currently unsupported because of the reference to transaction hash - bail!("Unsupported payment key reference to transaction input"); + // If there is new role encryption key, use it, else use the old one + if data.encryption_key().is_none() { + let signing_key = inner + .role_data + .get(&number) + .and_then(|(_, d)| d.encryption_key()) + .cloned(); + data.set_encryption_key(signing_key); } - } - Ok(None) -} -/// Update the payment history given the tracking payment keys. -fn update_tracking_payment_history( - tracking_payment_history: &mut HashMap>, txn: &MultiEraTx, - point_tx_idx: &PointTxIdx, -) -> anyhow::Result<()> { - if let MultiEraTx::Conway(tx) = txn { - // Conway era -> Post alonzo tx output - for (index, output) in tx.transaction_body.outputs.iter().enumerate() { - match output { - pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo(o) => { - let address = - Address::from_bytes(&o.address).map_err(|e| anyhow::anyhow!(e))?; - let shelley_payment = if let Address::Shelley(addr) = address { - addr.clone() - } else { - bail!("Unsupported address type in update payment history"); - }; - // If the payment key from the output exist in the payment history, add the - // history - if let Some(vec) = tracking_payment_history.get_mut(&shelley_payment) { - let output_index: u16 = index.try_into().map_err(|_| { - anyhow::anyhow!("Cannot convert usize to u16 in update payment history") - })?; - - vec.push(PaymentHistory::new( - point_tx_idx.clone(), - txn.hash(), - output_index, - o.value.clone(), - )); - } - }, - pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(_) => { - bail!("Unsupported transaction output type in update payment history"); - }, - } - } + // Map of role number to point and role data + // Note that new role data will overwrite the old one + inner.role_data.insert(number, (point_tx_idx.clone(), data)); } - Ok(()) } #[cfg(test)] mod test { - use minicbor::{Decode, Decoder}; - use pallas::{ledger::traverse::MultiEraTx, network::miniprotocols::Point}; - - use super::RegistrationChain; - use crate::cardano::{cip509::Cip509, transaction::raw_aux_data::RawAuxData}; - - fn cip_509_aux_data(tx: &MultiEraTx<'_>) -> Vec { - let raw_auxiliary_data = tx - .as_conway() - .unwrap() - .clone() - .auxiliary_data - .map(|aux| aux.raw_cbor()); - - let raw_cbor_data = match raw_auxiliary_data { - pallas::codec::utils::Nullable::Some(data) => Ok(data), - _ => Err("Auxiliary data not found"), - }; - - let auxiliary_data = RawAuxData::new(raw_cbor_data.expect("Failed to get raw cbor data")); - auxiliary_data - .get_metadata(509) - .expect("Failed to get metadata") - .to_vec() - } - - fn conway_1() -> Vec { - hex::decode(include_str!("../../test_data/cardano/conway_1.block")) - .expect("Failed to decode hex block.") - } - - fn conway_4() -> Vec { - hex::decode(include_str!("../../test_data/cardano/conway_4.block")) - .expect("Failed to decode hex block.") - } + use super::*; + use crate::utils::test; #[test] - fn test_new_and_update_registration() { - let conway_block_data_1 = conway_1(); - let point_1 = Point::new( - 77_429_134, - hex::decode("62483f96613b4c48acd28de482eb735522ac180df61766bdb476a7bf83e7bb98") - .unwrap(), - ); - let multi_era_block_1 = - pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data_1) - .expect("Failed to decode MultiEraBlock"); - - let transactions_1 = multi_era_block_1.txs(); - // Forth transaction of this test data contains the CIP509 auxiliary data - let tx_1 = transactions_1 - .get(3) - .expect("Failed to get transaction index"); - - let aux_data_1 = cip_509_aux_data(tx_1); - let mut decoder = Decoder::new(aux_data_1.as_slice()); - let cip509_1 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - let tracking_payment_keys = vec![]; - - let registration_chain = - RegistrationChain::new(point_1.clone(), &tracking_payment_keys, 3, tx_1, cip509_1); - // Able to add chain root to the registration chain - assert!(registration_chain.is_ok()); - - let conway_block_data_4 = conway_4(); - let point_4 = Point::new( - 77_436_369, - hex::decode("b174fc697126f05046b847d47e60d66cbedaf25240027f9c07f27150889aac24") - .unwrap(), + fn multiple_registrations() { + let data = test::block_1(); + let registration = Cip509::new(&data.block, data.txn_index, &[]) + .unwrap() + .unwrap(); + data.assert_valid(®istration); + + // Create a chain with the first registration. + let chain = RegistrationChain::new(registration).unwrap(); + assert_eq!(chain.purpose(), &[data.purpose]); + assert_eq!(1, chain.x509_certs().len()); + let origin = &chain.x509_certs().get(&0).unwrap().0; + assert_eq!(origin.point().slot_or_default(), data.slot); + assert_eq!(origin.txn_index(), data.txn_index); + + // Try to add an invalid registration. + let data = test::block_2(); + let registration = Cip509::new(&data.block, data.txn_index, &[]) + .unwrap() + .unwrap(); + assert!(registration.report().is_problematic()); + + let error = chain.update(registration).unwrap_err(); + let error = format!("{error:?}"); + assert!( + error.contains("Invalid previous transaction ID"), + "{}", + error ); - let multi_era_block_4 = - pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data_4) - .expect("Failed to decode MultiEraBlock"); - - let transactions_4 = multi_era_block_4.txs(); - // Second transaction of this test data contains the CIP509 auxiliary data - let tx = transactions_4 - .get(1) - .expect("Failed to get transaction index"); - - let aux_data_4 = cip_509_aux_data(tx); - let mut decoder = Decoder::new(aux_data_4.as_slice()); - let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - - // Update the registration chain - assert!(registration_chain + // Add the second registration. + let data = test::block_4(); + let registration = Cip509::new(&data.block, data.txn_index, &[]) .unwrap() - .update(point_4.clone(), 1, tx, cip509) - .is_ok()); + .unwrap(); + data.assert_valid(®istration); + let update = chain.update(registration).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().contains_key(&data.role)); } } diff --git a/rust/rbac-registration/src/registration/cardano/point_tx_idx.rs b/rust/rbac-registration/src/registration/cardano/point_tx_idx.rs deleted file mode 100644 index 27247dd5c2..0000000000 --- a/rust/rbac-registration/src/registration/cardano/point_tx_idx.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Point or absolute slot and transaction index. - -use pallas::network::miniprotocols::Point; - -/// Point (slot) and transaction index. -#[derive(Clone)] -pub struct PointTxIdx((Point, usize)); - -impl PointTxIdx { - /// Create an instance of point and transaction index. - pub(crate) fn new(point: Point, tx_idx: usize) -> Self { - PointTxIdx((point, tx_idx)) - } - - /// Get the point. - #[must_use] - pub fn point(&self) -> &Point { - &self.0 .0 - } - - /// Get the transaction index. - #[must_use] - pub fn tx_idx(&self) -> usize { - self.0 .1 - } -} diff --git a/rust/rbac-registration/src/registration/cardano/role_data.rs b/rust/rbac-registration/src/registration/cardano/role_data.rs deleted file mode 100644 index 3df6a05dfa..0000000000 --- a/rust/rbac-registration/src/registration/cardano/role_data.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! RBAC role data - -use std::collections::HashMap; - -use pallas::ledger::addresses::ShelleyAddress; - -use crate::cardano::cip509::rbac::role_data::KeyLocalRef; - -/// Role data -#[derive(Clone)] -pub struct RoleData { - /// A signing keys to the data within registration. - signing_key_ref: Option, - /// An encryption keys to the data within registration. - encryption_ref: Option, - /// A payment key where reward will be distributed to. - payment_key: Option, - /// Map of role extended data (10-99) to its data - role_extended_data: HashMap>, -} - -impl RoleData { - /// Create an instance of role data. - pub(crate) fn new( - signing_key_ref: Option, encryption_ref: Option, - payment_key: Option, role_extended_data: HashMap>, - ) -> Self { - RoleData { - signing_key_ref, - encryption_ref, - payment_key, - role_extended_data, - } - } - - /// Get the reference of signing keys. - #[must_use] - pub fn signing_key_ref(&self) -> &Option { - &self.signing_key_ref - } - - /// Get the reference of encryption keys. - #[must_use] - pub fn encryption_ref(&self) -> &Option { - &self.encryption_ref - } - - /// Get the payment key. - #[must_use] - pub fn payment_key(&self) -> &Option { - &self.payment_key - } - - /// Get the role extended data. - #[must_use] - pub fn role_extended_data(&self) -> &HashMap> { - &self.role_extended_data - } -} diff --git a/rust/rbac-registration/src/test_data/cardano/conway_1.block b/rust/rbac-registration/src/test_data/cardano/conway_1.block index 5361450212..5b245761d6 100644 --- a/rust/rbac-registration/src/test_data/cardano/conway_1.block +++ b/rust/rbac-registration/src/test_data/cardano/conway_1.block @@ -1 +1 @@ -820785828a1a002cf1361a049d798e5820f8105b5f1ff5805a1ad73a4bed95297c11a530957fd124fe116d847b9a8a2d18582054ca56226c90d5544bf6f558026b814027b634e60de0bc215237944110569c6c582037d6147adbf2907948d66f9e4751774bbe9d6c4ad562ce04ef66f0fc1e7ac3018258408efeee2071a87217e4e5d45c8afcefbca69a9f7521be99cf007cadd5657436f05e801b96f75465cc631dbca05f58c0f44406f3bf80f055ac47285dfe4e72f5185850dc503d811e615d063298b2688dfcdeb4c82b82a6509672ad7eaeafa87ec8af4144530d7ad542daf0f007b3ba2133ea5abebb253ec95180f3805eefd1a1cfebafd497bf0215506912fd50497db4ba150e19173458200cb42de7dcf6a9dd27dac89dd10e98af017f352c5d4c9d995fea811ed22221d484582074deff4c062c74d426154a221193a5fb9472c401d42217b07d617c03cc2e9bb00819023f584049885039bea644a49bce5493fd4dffb0c835074820bf1912e3b372bfbba36904c7a47ae3ad59da8148876d83868488b94fa81df458fd3bf3d3706f4ec11a5d00820a005901c0509399cc5ca4713330d1370f986def5b3096f61c96f3de54015334c5abccad85c0b4c99a898399ccdf5979a0b810be83479c2b4430c665d350878f08c1bdc80b7c4cdd132322a7ccf28796aedaf1767c7ba8d9029430f8e3ecc88963f671a07163f7d2677e3c31db8af39c51732fd158d4784bd48381006d4a892d186266279241a9e786e2b4fe28d83a9890694670950c6d981e96a1d182d5e02241cf36f6278ff8e7cf6206735d54e8bd026f696d3b46d31268357318c4e2a91b084b7a49975bf06ec920ca357a17f85d6fca8ac7b388c599e542ec32f0361e2fda5c8d43e3e3b52e31761953c114ca9f952eff1195245df3aeef714de2bc5dab7e971b325b99ed37346741bf75ef68b201a9a9f85978d83ad602e9c8e31987395473eb87b2f5a34c30882a1812dc5081f04e731071cb8a58c5c7fc4ba168028154b07827faa64a6fd86e0594804c8e74213860a0d0a602306b8468a2f7ee112b380f143437227bdc37e8efee149b5bbe436f1b1d23a1afecd4c2d17a2484827f655b85228d587e54d26530645bb4de4aa4a706d0676974ac64fa4c2bc158adecd78efbed7f7522e0627056391093fcf37967f35f3479878ef3e731e21eac3d787f7a2af2c784ab00828258206f135b9e227de387653f19561fdc0a014eacdb9c30c5d8da5fb8adb302f24c6d01825820dee38df4b12e03388390d6452765086e4348da004f13f4b3dff24cc2fdd99055000182a300581d70bdc22da682cd9aceed5fd48914789fc98c94abc79fed8b40cb8c431401821a001e8480a1581cc13ddf298a5d25aff2933695987912b4f1748bdf0df8e4b5d85f2360a14e50524550524f445f4f5241434c4501028201d81856d8799fd8799f1a0bf5b443ff1b0000019386d2d7a8ff82581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011722b776021a0006a164031a049d7c7d081a049d75760b582078c96a0e0f77a9be3baedbc5e24fe7c5f4de3f13dc79ada0676d7a0fd12fa6d90d818258206f135b9e227de387653f19561fdc0a014eacdb9c30c5d8da5fb8adb302f24c6d010e81581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d20f001082581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b0000000116dd0d9a111a004c4b40a900d9010282825820b7141e4950cd9311eb083caffda6dad64ca0956e44e2855316d4c1553da230e800825820b7141e4950cd9311eb083caffda6dad64ca0956e44e2855316d4c1553da230e8010dd9010281825820688a09a2a039214f244e8f8eed49bf4f190104ebe32f6d9680277d70901c20220112d9010281825820c47f5b7661fd8295674076b8b76db11d08d4ddafb2529a6159e059ed6128af73010182a300581d701db5ea0b1cb006f32a3c3f6438f6c4b3df7b2018b965a63d29b54d0301821a001b9f18a1581caa7f3d7240a9d06e947fcf387957e7f01fd4484a432a4b879cdbf24aa14d446a65644f7261636c654e465401028201d81858a6d8799f5840fe28ac9c796a0157a6da71b9738beae6a085d1debdd682838584e03592e6581a50bc341e879b8b197a88a6d175a01e8d51c72c4acbf3fbaac3fb7158e3c2500fd8799fd8799f1a00030d401a000381f7ffd8799fd8799fd87a9f1b00000193858940a8ffd87a80ffd8799fd87a9f1b000001938596fc48ffd87a80ffff43555344ff581caa7f3d7240a9d06e947fcf387957e7f01fd4484a432a4b879cdbf24aff82583900f8e2356e21af3563556c167c31bcc00f9fc061a8a852a16a42ffcc7927ce10b87147e3e19a5dc622b6a535ab9ce01cb2d586a3a690a74c0e1a00f81c6a021a000426b4031a049d7c7d081a049d78f90ed9010281581cf8e2356e21af3563556c167c31bcc00f9fc061a8a852a16a42ffcc790b5820cc99e113594d6781a6f5a4a16205a0ae3072c1f4414121e08df388fdeb656fd9a700d9010282825820d59aa48a2208563e6fac203419186430c87b5c07ed72c5c1947f5c2b8cdd810600825820ffc0aaef9f13145bee27320c87410bd607c9eeb7f9d624ad4d18266227ac9596010183a300581d709dcb619aefe878561bc3067b7a0e923f1ce961c38b7bfc1a283a873e01821a001bafeea1581cec64872c1965bbbaa8868aef2bd9a343b821a9f2c7787ea096f62262a154000643b043495036382047656e657261746f727301028201d8185884d8799fa6446e616d6549496d616765204e46544b6465736372697074696f6e514173736574204465736372697074696f6e45696d6167654a697066733a2f2f2e2e2e496d656469615479706549696d6167652f706e6744726f6c654475736572435f706b581c8e3b54aca70c6778daca5f9ec4e99a7817052e64f23979080f97beca01ff82583900f7a232fecd012dc3c556587593c5a5cbed826a49ce06c19ece9f350cb74a4ce22b780d2c9139c32806dd402df8947de0303ed3f976136a231a000f4240825839008e3b54aca70c6778daca5f9ec4e99a7817052e64f23979080f97beca718188b968725132a2cf6bb0925bad1f4b30e000990e526b2e9443611a24ad8844021a0003bf5c0b58201cf1170ee1369bd082ae8d39b0d99db7caacc10a79b41cde1122bd0f0c7e5bf00dd90102818258206a3f0a51813098b342c8fa71b5766922ca2cef531cdcf925da83c990c82dcb75050ed9010281581c8e3b54aca70c6778daca5f9ec4e99a7817052e64f23979080f97beca12d90102818258208a4ad7d31a93bc9c25430056bc7910b4cfe706bcd099e59e2332e041ade7132c00a600818258205bb5b41eae680aca501bf92d2830fef5dbe9ef7d74d605c018899031a7f74463000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f96ecf5021a0002fea1075820b1553a57d1cbb2ac233e98a4700d1dfd7972210cfd4e248ab4ade30afefd00d30e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0084a30081825820ef603099b2579d5ca273dce1f1257c11f054664ba972fd61c008b58a1b2325c158405faba20a8a371470fac259901656b63bb81201e1ef6768809b581ff0bab12cc6f236e6a52923e0ce4772f8e6d4c88db03da5db3d8b8a1770b7b0eb5e4a7e9b0f0681590bd7590bd401000033232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323233027222232325335333006533357346074607e0022646605a60586ae84004dd69aba135744002607c00206e64a666ae68c0ecc1000044c8c8c848cc00400c008c0b8d5d09aba2002375a6ae84004c0fc0040e0dd50022999ab9a303a303f002132321233001003002357426ae88c0fc00cd5d0981f00101b8890008a99ab9c490103505435001637540086ea8004cc09c8888c8d4014888d40188c88c94cd54cd4ccd54c0d40e0c0a80a4c0ac01ccc0b08004d40048888888888880100e44cd5ce24812d546865207472616e73616374696f6e206973206e6f74207369676e6564206279206f7261636c65206f776e657200030132533553355335333553036039302b02a253335302135001202103e215333573466ebcc118cd5d0182319aba0375000a6ec4080cd5d01ba835500b03937620400022a66a666036446a0044466603e446a00444a666ae68c12c004400c4cc11848ccc00401c00c00800c00c0040c0cd54c0e80ec8d400488cc0e0008cd54c0f40f88d400488cc0ec008ccd4004dc0a4000e048cdc0800a400000266aa607407646a0024466070004666a002466aa607c07e46a0024466078004604600200244666038066004002466aa607c07e46a0024466078004604000200266602e05c666050400204e6a004404e0060784426a004444a66a0082a666ae68cdd78018238a999ab9a3375e00408e2a666ae68cdc4000a400006e2666ae68cdc4800a410125e80206a07006e06e44072064407e6044014074266ae712401024c310003103a1335738921156f757470757420646f6573206e6f74206d6174636800031153355333573466e2400d20001335738921165072696365206d75737420626520706f7369746976650003103a15335533535353301275a6a00407e06e44a666a00442a666ae68cdc419b8000100935500c03a0331333573466e24004d540300e80c40d00c80c84d4cc049d69a80101f911a801112999a99980980a980f19b8100900b0021533500113330120150030081333012015003008034133301201500300803003a13357389212345787069726174696f6e2074696d65206973206e6f742070726f7065726c792073657400031030030333025200102435302635533530270092100116036202402f302d350080393302f3016337000020060542a666a602e6aa66a60320022602602a442a66a0022004442602e032402e02842a66a6666666ae900048c94ccd5cd181d98200008991999aab9f001202d23233335573e002405e46666aae7cd5d1001119299a9999999aba400123253335734608660900022646666aae7c00480d48cccd55cf9aba20022533530203574200642605e00206a406c07a078608e0020646ea800880c880c880c880c80e4854cd4c070d5d08029098159981a8010008188181aba1005203003703635744004068607e0020546ea800880a880a880a880a80c48400405480548c94ccd5cd181b181d800899191919091998008028018011bad357426ae88008dd69aba10013574460760046ae84c0e80040ccdd50009111a801111a801912999a9998040038020010a99a8018800819814819911192999a80190a999a80190a999a80290980200b0980180a8a999a80210980200b0980180a80c0060a999a80210980180a8980100a0a999a80190980180a8980100a00b8a999a801100700b0068a999a80110a999a80210980180a8980100a0a999a80190980180a8980100a00b8058a999a80190980100a098008098a999a80110980100a0980080980b12999a80110a999a80210a999a802109998040038010008b0b0b0060a999a80190a999a801909998038030010008b0b0b00580691a80091111111003891999999980091199ab9a3370e00400204004644a666ae68cdc38010008098a999ab9a3371200400201401044666ae68cdc400100081001191199ab9a3371200400204004644666ae68cdc480100081181011199ab9a3371000400204604044a666ae68cdc480100088008801112999ab9a33712004002200420024464a666ae68c0c8c0dc0044c8c8cc0994ccd5cd181a181c801099813198038029aba130380023006357426ae88c0e00080c54ccd5cd181a181c800899813198038029aba130380013006357426ae88c0e00040c4dd51aba135744606e0046ea8d5d0981b0008179baa0012325333573460620020522a666ae68c0c000407c0b4c0d0dd50009119192999ab9a303300100815333573460640022601460086ae84c0d400854ccd5cd1818800803017181a8009baa00122233355302302702a335530260272350012233024002300b001333553023027223500222533533355302802b301d01c235001223300a002005006100313302e00400301c001335530260272350012233024002330382253350011300a003221350022253353300c002008112223300200a0041300600300400211009212223001004112220012230302253350011003221330060023004001212223003004233333335748002403040304030460206eb4008806007c94cd5ce2481286578706563746564206f6e6c7920612073696e676c6520636f6e74696e75696e67206f7574707574001615335738920117496e76616c696420646174756d20696e206f757470757400164988880088c8c94ccd5cd1813000899091118010021aba13028002153335734604a002264244460020086ae84c0a000854ccd5cd181200080201098140009baa00111003253353007001213335530150192235300535300935003019222200422353007350042222004225333573466ebc01000854ccd5cd19baf0030011330220060051005100500e3300d00735300f3500201b22222222222200a15335738921024c660016232533357346040604a0022646424660020060046ae84d5d118128011aba1302400101d3754002444006660024002eb4888cc09088cccd55cf80090071191980e9980a180398138009803181300098021aba2003357420040306eac0048c94ccd5cd180e181080089919191919190919998008038028018011aba1357440046ae84004d5d10011aba10013574460420046ae84c080004064dd5000919191999aa999ab9a3370e90030008990911118020029aba13020002153335734603c00226424444600400a6ae84c08000854ccd5cd180e8008990911118008029aba13020002153335734603800226424444600600a6ae84c0800080648034803480348ccd54c048054cc03c894cd40088400c400403494ccd5cd19baf002350010181300600100d3300923253335734603e60480022646424660020060046ae84d5d118120011aba1302300101c37540026a60166a00802e44444444444401860400026ea8d40040408488c00800c48cc004894cd400804c40040208cc02488ccd400c04c008004d400403488ccd5cd19baf002001004007223301c2233335573e002400c466028600a6ae84008c00cd5d10010081bac001100d23253335734602860320022646464646464646464646464646464646464646464642466666666666600202e02a02602201e01a01601200e00a0060046ae84d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d1180c8011aba1301800101137540022002200c464a666ae68c044c0580044dd69aba1301500100e37540024424660020060044446006600400260244422444a66a00220044426600a004666aa600e01600a00800260224422444a66a00226a00600c442666a00a0186008004666aa600e01400a00800244002601e442244a66a00200c44266014600800466aa600c00e00800224002220024400444244660020080062a66ae712411f496e76616c696420646174756d20746f20636865636b4f776e4f757470757400161533573892010350543100162222222222220054c1014000370e90001b8748008dc3a40086e9520005573caae748c8c00400488cc00cc00800800530012cd8799f581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21a000dbba01a01499700ff00010581840001d8799f1b00000193858940a8d8799f1a0bf5b443ffff821a001862581a18087eb7a200d9010281825820c58b6e67ad72d3e4de04c0c143b15e0e60573ea9b740cf0a8fbfafb0475d50fd5840e8a05b2cc57a1952d1c3179b2b5019c1511fd49590dd2967b1fc5b3469251592698aca04e5b7a190b94f1d4a2545dce999aaa0921b50705e022bf02eb4b8720a05a182000082d87980821a000810e71a0b99de0ea20081825820f78c0ef2db02673d535e54b3e534281bce897181a3725593eb73f9d13cc7344a58407ca5fef9ff8a97006259f3308ea0320b2f361aaf38992c2316caea9bd62a8de30c99b9be9316fa36aca04a58eeaedc5f39335d714c05e09f3e282071466e4c0405a182000082d87980821a000619741a0893bcdfa100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c0160205840bf6cb0ebd00118d87bd8ea9a0e70f9f7954a3450dbbb2ba76a1558b1f7664d8c3dff9da85419f1f6a50e1a041d86de98ebcb511b22d885e0e092ffa7dc78910182582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b58401f42ca3f529ee9765330674a56c3d70a885a8c62912a18145f51afaafb2da78b6319c70aa432f5f8d976a4d9dc309b1f17bcf89af1d08e939aef43f7bbea4004a103a11901fda40050ca7a1457ef9f4c7f9c747f8c4a4cfa6c0150d127265b6c2024bf7e254571bce41fa70b8758401b3f020064404f2b8fc22b3919605fb86cb9d3c7573ce58ffaedb716517ba0020ab9162510786a81643723fe5297dac6f96dd080032c60107110fa2fe7ddb570584018140643422a31e1cde248145cc8c55f5c53022b8e46c14d10282a880206d080033cc1bd382b1d51525a62210b69716949294b6305852895928c82f7b73cc70c58402f7d155c03e2abf9fd94d6e23b41f5ce57f595c8faff16fe899abe2bfe78d839d09b90a87fcd96f55ffbcfc99674fc5a392896703ceb8ffacb6b193beb1bd9a858405c94f4e2de6d001ac5813a71a03a962c1408a0581c2800093e112d939167909cae2d6af8fd08809295d4a04b360a737114e2c8d4c1d74f514cccc1d939502c6858409e6ac7cd4724420283e483c583821ca4c214c2dc15c4bd9d6582fd03fda5dc4202c39da5e5e50283dd2503bc03bc645d027d3d039c5c5ca543dc021ffd8d347b58408b716dad363d2b3776d27fc3acb2a63d1f99d49dfffebca526c518ba311787c30c97732f77b04e2844c7c3c5aa8bfdcd526d3e822da4d57aba35b15c1445cf0a579cc523cdd99bb7a177061a2110288883c0d971cebfea00186358400cd4f3828590fb603b04ce534efbce76320f0828b3773a3d5b80f0f8da8a3b134b33ab6e621c9a4e749f6fd61fede6c1ee6210c2264c176cdd8271e39a116d0680 \ No newline at end of file +820785828a1a002f95331a04e349455820ee3f3e5f7856727c22138899c0ddcac344bb61e5b02a79238008ab61fdb3037758202f5ad4772c00767b6d75c8c1fae31e3479d91921abf41b1989346d2896b61de258201003b81af09c4acb552e0fbb33ffe12415360f2842c0e020b11b09cb82f7c5e48258402e4863095829f03441b4774da6076924840d148a91e0d926d2775844ea89afc492ad2cffe309d2c138b674f80ed473c69ea9c4e20ed835d8274695781fac383958506b4be509c35f300800ff947caf47b35662fb307704ae172244facf1d39add6074ac242126c2f1034a562d546ece4388d71b6f5826a83a052426d09c3690495d2166a91b3a1410c76f5cb68de36d44f0819039b5820d666c5f0ef9f0207bae82f4d32d6f22529413357830bc723a8a7c701df9bf44d845820bf1574f0126d9a0388fed2014a3440844410c9ce035e9c983da5112841625197071902485840c642b28a06fc6d4828e0b43bc9761962862d7f12d6bb18e19dcd8d574256158d3c4a586741086032d3e648310919abfe30b3a8c8544505ad20a257109e95b004820a025901c03131f34e4b5671bbb85f435e2933363502b4d1170d44f0e73ee388e5a3492f3902249ff92a8d09b4fd88fb5ea1f5bc82a4c230c33fa631d2fbe99eb592f8eb0100fe684bb10cbb4e458590c86715f843f0547d1aa0a9bc39a6e0ca91547f460d93139769af2aa4bf06930ad0be971e4eb25e59757be59a73b18b1c4c71b514a98493ff84a3cc828d2aa4b761494d302c14065951bbafe36317fd78436127b74036862f4c0153a33e3637082a76fd496dab575a992a51e995a15f8ad0b795bfd8a5625b9bc87b787539decddb4ac792412aae37ede219e331707d8006055fcdb4f27f159281bd66ad98ab0af76eb0941d144a7d6adc4ff27e204d521e24e5a46c18e0990bdba0fbdd9bfa08a036861afcadcc69136626c9cd8ff93fad6c6f8851aa4598558d767d2841ec678402367f4027e3434f379ad1786f34c5b6dd8d0c58267e40294e41a51e89d397163979de3079cf00a33b029d18c840c3345b3e35d995ddc444d98eb43d8671aae99e4b7bfe4c74c79ff8043cec445a0967b47cbc0b764114587d3861f8b749c442e6114391b94c443aa0c588b11eeb8e4e6399772e8a8c57864a2f2630a9d22ce0cf37a0cdf7065f0c78a3ad50ae2eb5b26579ee6581a6008182582098cf12dc6187626de50b5562ebf1753f1b39e1e568e99d83bfdcfb1713c8f4a4000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f78c2a3021a0002fdc50758203f3ad695d89cf9dda56c4f4acac517c56d86455a219192bbf980cecd6daf78d90e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0081a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c016020584057537416ae383ad98eca35711bdc54a71395b25c9b37709edd41b92d01c2fdba9b45a320dc63a6dac6c04fc36cfcbd0f49a3c078afb8d89d7b1b70994652f00c82582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b5840c821237247702f96abeb9fd0eaa02a54eadd9dd2b9fed30ea29de22964c9776d0c4614746bbca41d737d6b40ca58ac7dafd944d310cad972faae8aaec715c60fa100a11901fda40050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01500d7be296ac5cd935589c2d4ace495ec20b8758401b1d020864282f03f1922629e5a070b6368e7e11663b9dd1845f79b3dded6e97581a5001855c1704986829a5db54f4d743b4af88a84677832e06c29d820c90c85840ffd1c15807250539050535d99b51ef75404d4329131c070c207a40cbec4c45c3490674400b0cc048947b20c2cb0c2b03085602b0b2aa12ec21595d1d82157d285840ab6b7f8f5e6e31f85594fa643106bfde14f6c72c737e9468fd9923f2ef43d2679ab517759f5e9d5f9b2e435defcb54e20eff9597c1013ff7afeba0800753a97f58402bfb171f4cce1fb7ecc091a217dd440f203a01d1664b61762aa20e10d524a6e8e4b5c9de1389ed66dbc2195f4443636dab9322cf313138400693a97f4cac86825840827f5010a2809ed47cf1fc04c211540d0328eaaf98a89e18a60ea28294b138244e311447928294d454112c0c8e8f8a8f5409466222e203834394f05064f2c7cc5840c73ac7c43ffdad7ef11bd1f6faf5ecc4665252e6700a6f7ea87befbfca96e88fd8600a66be02ff9502ca9e13914c3f95edf9f7ae2ff916b8b5d5460e4e2de66952f982881e323272120319251943c4c74c91021863584060385f9a16e3be2cc34051edbea145e91106350ac0d15eeed00f60ce0c791c5e8b07bdeb8b6c4d0cddd9d5855f8a2281a5ac089019690b66ddf2ff7557cc240480 \ No newline at end of file diff --git a/rust/rbac-registration/src/test_data/cardano/conway_4.block b/rust/rbac-registration/src/test_data/cardano/conway_4.block index b16edd16df..6844495335 100644 --- a/rust/rbac-registration/src/test_data/cardano/conway_4.block +++ b/rust/rbac-registration/src/test_data/cardano/conway_4.block @@ -1 +1 @@ -820785828a1a002cf24d1a049d95d15820a82d38572884fc69c5b1105dd190651e4fc5c821878a1a166b6bca897983b83d58206f281ba212f118ac018aaeb805dda9f6fd54fe28cecafe7bec1347509510797f5820870dca606397b85d2e4ee1e46bdb9b784a092a1ec25cb8146cf4046456ff861c825840c085ebef69c078f6fcfd53a2eb2e209b6bd1a517390c8971f079fe55a4b30c82641385a618acf269bceb2d3c83a4ad6addc8901cc3bb359da7875c672ed5a6a55850758500c16a229759d672f0c5aea206fe924238915de34dfdf54413d438a27c4bdb453a080d1fe6ce0f3427452d20aeb1d1907d034899d19021e951a1badeef442de3f4f1bd4e9a4ce5924870f068a4021911c65820c8fc1a8e30b35038644284be081128c4a7ad8b3c39edbfeea871084b9b3566cb845820b7e792177f506de2a34d1c4099f7a1687ff7af54caa093d85c5755e709f757fd0d190241584095a171db3d0e3808bca5e84970ead6b0a736e1d5a2b5fc8c169c8fab7bc1a5d3d60b0b582fbbc8c23d43b3301c4b3eede3ac5438bddfebe1cc8e5a8d71a94802820a005901c05ae6633913fc08585a1ddf905f10b2cc1134d914f9f656b58e72cad542e53a90ce0cc9c6f0e819f7243431a5416cbc1a3fa67d8ab8c547a09f78c86c20073208dc3b328c9d70a24a912aa4e170b9d6fbe8e55c03b52dc296435c86a358bc3f20e89f4c92748dff2ed24a71b8377b4683851a52cfd023dccaa92239d9e989fe8d1d360529c15417937f60530a085ee82dd4ed5f1bc46d0bcf2ab68d4729063a7a5c5074cdb289655f23954f7eebfd418e3a133bb53cfa0bd5ff544fc9d0599640b46619d3bfc5d9782c98ee86fcaa1efe8503980fd9219db77c3003ed2cee13d85f510f9a3381d442f5314a91c52ccf01cc796b9fb7ea14d046a00c3f96e6ee2ec28896f72808952cda9aec2feb690ecb59c1a729816f602a58b06fbab79977a9e96ff7eed2dd262eb86cee8f35d4bad55be2919413ff5cd8fbab3acc25f608630fa7796debcdae0239aafb45e949053af2523bb112b5f6be5fc4ece0ac55f59783466565dad12d42b76fb3ad45112c25d0f4c0880b88ad0ff87f207538c5c7cb96690633a72de47fd36980846cfba4865e324b7ccf1ca80382a4eedc72caf1a0d828049fbd7b3906877b89980bd2f0ca5ffc229e667d56fd11c1dd6e3c0ba7ed82ab0082825820276df9bb9d245ea87e6def1a995ad919edacc907c6cbf028e0e552b1c53c10df0182582062e4dfcb06b8401ea1c405d6290c6aede934c7a6cd31ae000c2a5dd34961af5b000182a300581d70bdc22da682cd9aceed5fd48914789fc98c94abc79fed8b40cb8c431401821a001e8480a1581cc13ddf298a5d25aff2933695987912b4f1748bdf0df8e4b5d85f2360a14e50524550524f445f4f5241434c4501028201d81856d8799fd8799f1a0bf85ea8ff1b000001938742fe98ff82581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011587887e021a0006a164031a049d9933081a049d922c0b5820e33c25b2f65204adbf1a8f29ba4c7157c35f9479346e2d6717e23d6786e8b6340d81825820276df9bb9d245ea87e6def1a995ad919edacc907c6cbf028e0e552b1c53c10df010e81581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d20f001082581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011541dea2111a004c4b40a600818258206695b9cac9230af5c8ee50747b1ca3c78a854d181c7e5c6c371de01b80274d31000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f93e7f8021a000304fd07582091e3a1050ba7f37e9d94938ec94a9d187f24b363374c109e3fcc44fedb5de8b70e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0082a30081825820ef603099b2579d5ca273dce1f1257c11f054664ba972fd61c008b58a1b2325c1584049e2d6c6a948a7d1398f200213a65cd15ac2dd491e903892aaca6ad487717f3cad6c66e201dd8fd74f69898f77aa3de953e83df61f2822c01a30eb992f58f0060681590bd7590bd401000033232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323233027222232325335333006533357346074607e0022646605a60586ae84004dd69aba135744002607c00206e64a666ae68c0ecc1000044c8c8c848cc00400c008c0b8d5d09aba2002375a6ae84004c0fc0040e0dd50022999ab9a303a303f002132321233001003002357426ae88c0fc00cd5d0981f00101b8890008a99ab9c490103505435001637540086ea8004cc09c8888c8d4014888d40188c88c94cd54cd4ccd54c0d40e0c0a80a4c0ac01ccc0b08004d40048888888888880100e44cd5ce24812d546865207472616e73616374696f6e206973206e6f74207369676e6564206279206f7261636c65206f776e657200030132533553355335333553036039302b02a253335302135001202103e215333573466ebcc118cd5d0182319aba0375000a6ec4080cd5d01ba835500b03937620400022a66a666036446a0044466603e446a00444a666ae68c12c004400c4cc11848ccc00401c00c00800c00c0040c0cd54c0e80ec8d400488cc0e0008cd54c0f40f88d400488cc0ec008ccd4004dc0a4000e048cdc0800a400000266aa607407646a0024466070004666a002466aa607c07e46a0024466078004604600200244666038066004002466aa607c07e46a0024466078004604000200266602e05c666050400204e6a004404e0060784426a004444a66a0082a666ae68cdd78018238a999ab9a3375e00408e2a666ae68cdc4000a400006e2666ae68cdc4800a410125e80206a07006e06e44072064407e6044014074266ae712401024c310003103a1335738921156f757470757420646f6573206e6f74206d6174636800031153355333573466e2400d20001335738921165072696365206d75737420626520706f7369746976650003103a15335533535353301275a6a00407e06e44a666a00442a666ae68cdc419b8000100935500c03a0331333573466e24004d540300e80c40d00c80c84d4cc049d69a80101f911a801112999a99980980a980f19b8100900b0021533500113330120150030081333012015003008034133301201500300803003a13357389212345787069726174696f6e2074696d65206973206e6f742070726f7065726c792073657400031030030333025200102435302635533530270092100116036202402f302d350080393302f3016337000020060542a666a602e6aa66a60320022602602a442a66a0022004442602e032402e02842a66a6666666ae900048c94ccd5cd181d98200008991999aab9f001202d23233335573e002405e46666aae7cd5d1001119299a9999999aba400123253335734608660900022646666aae7c00480d48cccd55cf9aba20022533530203574200642605e00206a406c07a078608e0020646ea800880c880c880c880c80e4854cd4c070d5d08029098159981a8010008188181aba1005203003703635744004068607e0020546ea800880a880a880a880a80c48400405480548c94ccd5cd181b181d800899191919091998008028018011bad357426ae88008dd69aba10013574460760046ae84c0e80040ccdd50009111a801111a801912999a9998040038020010a99a8018800819814819911192999a80190a999a80190a999a80290980200b0980180a8a999a80210980200b0980180a80c0060a999a80210980180a8980100a0a999a80190980180a8980100a00b8a999a801100700b0068a999a80110a999a80210980180a8980100a0a999a80190980180a8980100a00b8058a999a80190980100a098008098a999a80110980100a0980080980b12999a80110a999a80210a999a802109998040038010008b0b0b0060a999a80190a999a801909998038030010008b0b0b00580691a80091111111003891999999980091199ab9a3370e00400204004644a666ae68cdc38010008098a999ab9a3371200400201401044666ae68cdc400100081001191199ab9a3371200400204004644666ae68cdc480100081181011199ab9a3371000400204604044a666ae68cdc480100088008801112999ab9a33712004002200420024464a666ae68c0c8c0dc0044c8c8cc0994ccd5cd181a181c801099813198038029aba130380023006357426ae88c0e00080c54ccd5cd181a181c800899813198038029aba130380013006357426ae88c0e00040c4dd51aba135744606e0046ea8d5d0981b0008179baa0012325333573460620020522a666ae68c0c000407c0b4c0d0dd50009119192999ab9a303300100815333573460640022601460086ae84c0d400854ccd5cd1818800803017181a8009baa00122233355302302702a335530260272350012233024002300b001333553023027223500222533533355302802b301d01c235001223300a002005006100313302e00400301c001335530260272350012233024002330382253350011300a003221350022253353300c002008112223300200a0041300600300400211009212223001004112220012230302253350011003221330060023004001212223003004233333335748002403040304030460206eb4008806007c94cd5ce2481286578706563746564206f6e6c7920612073696e676c6520636f6e74696e75696e67206f7574707574001615335738920117496e76616c696420646174756d20696e206f757470757400164988880088c8c94ccd5cd1813000899091118010021aba13028002153335734604a002264244460020086ae84c0a000854ccd5cd181200080201098140009baa00111003253353007001213335530150192235300535300935003019222200422353007350042222004225333573466ebc01000854ccd5cd19baf0030011330220060051005100500e3300d00735300f3500201b22222222222200a15335738921024c660016232533357346040604a0022646424660020060046ae84d5d118128011aba1302400101d3754002444006660024002eb4888cc09088cccd55cf80090071191980e9980a180398138009803181300098021aba2003357420040306eac0048c94ccd5cd180e181080089919191919190919998008038028018011aba1357440046ae84004d5d10011aba10013574460420046ae84c080004064dd5000919191999aa999ab9a3370e90030008990911118020029aba13020002153335734603c00226424444600400a6ae84c08000854ccd5cd180e8008990911118008029aba13020002153335734603800226424444600600a6ae84c0800080648034803480348ccd54c048054cc03c894cd40088400c400403494ccd5cd19baf002350010181300600100d3300923253335734603e60480022646424660020060046ae84d5d118120011aba1302300101c37540026a60166a00802e44444444444401860400026ea8d40040408488c00800c48cc004894cd400804c40040208cc02488ccd400c04c008004d400403488ccd5cd19baf002001004007223301c2233335573e002400c466028600a6ae84008c00cd5d10010081bac001100d23253335734602860320022646464646464646464646464646464646464646464642466666666666600202e02a02602201e01a01601200e00a0060046ae84d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d1180c8011aba1301800101137540022002200c464a666ae68c044c0580044dd69aba1301500100e37540024424660020060044446006600400260244422444a66a00220044426600a004666aa600e01600a00800260224422444a66a00226a00600c442666a00a0186008004666aa600e01400a00800244002601e442244a66a00200c44266014600800466aa600c00e00800224002220024400444244660020080062a66ae712411f496e76616c696420646174756d20746f20636865636b4f776e4f757470757400161533573892010350543100162222222222220054c1014000370e90001b8748008dc3a40086e9520005573caae748c8c00400488cc00cc00800800530012cd8799f581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21a000dbba01a01499700ff00010581840001d8799f1b0000019385f96798d8799f1a0bf85ea8ffff821a001862581a18087eb7a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c01602058404ac843657462bd1b0f2db7e1abfa2fa3ccd3105ad2278649e155ceb3cf4e44b3cc2532815570cee26a55910a04db4de50cd3c07784ecfacf4c74706f54cd470382582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b5840c9fe85cdcef2b32d76958d564be1b7bc0124a99594cc9e69e1a4d0cbe8f96eed4ca94fe6f2471382d19d74f610a6a5e01e56d8f6dc2a5519a090ec2e00dc3e05a101a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01505be7b97fa11335b727bbf095a3f0fb7c0258206695b9cac9230af5c8ee50747b1ca3c78a854d181c7e5c6c371de01b80274d310b8758401b3e020864c84e53c9f7a9c54b8d09c6047bc2049aed7438c5575ed4ee265182dd808a52ae8b12083cb540fa9ccc7e0ec3a7b52ad5f134682286f658ff0083f558400dc73df538d858d8d8b89e5247f2000f1f47c43b0c68204480001fc71a179f0685400804400c2484e3013b03195a0a79c94206484bc93b10cbcb474999a5e5a55840b17f9619347e158e3a2a44e3e33eb72d648ae645a0e63d83fde739ee0d7ff6baeaf5767357770a697c2a92095bfe292e427bbc2fee56211ec78389dfa56d13c758400363abd573e840dead26d80c600380b5bc64a3c285550096634883e3a7265b4f18ca836d1d23b991a02a45ac365c9665acb787484da6ee21a10a1212ee5e5e11584012912b55d77a3e82444744ca4581c84877c958f9583f7910e4251d15161126e91b1d11e72525271b11e5870e0f0a0f94f18e080908f7f4f6918af68de8fc1bdd5840ec35fc967e64936c930f279eb8d043d31112e6a71aaaa76fc3455e9d30b90ea671971e5c9ecd4be5b4d87ab3e11f7de3b8984ea353e9a2d8d65c6ad6f9102c46581876789c8667c739c00da3176c46a1b030c4281c4e62afcf3a186358401824797b7e12054dabb6fef573ceba9c3bbed87b522a4141c2357d05d63ffabee59add33aac71f4573e43520a1c000d0c4e7fa2adf7f8e92718320527d23e60580 \ No newline at end of file +820785828a1a002f953b1a04e34a595820ea86bf19868ec65c0dec890452c6bf7cede2e541bbdad20aa51e2f3e0ffc6edc58200339649fecc4224156494435a46d783d72c8b47fe54e411e6b8fdadda3a7585058203997f8cde4ebadc8993fb0e73c88a7058ec089a6311cc1f930388b9114fc3c998258407e69974619a6210863a9d880c14fec85cb2656bfa97ba5221a9e19c6287d14b43a415f8a167123e0ef72a2c9dd2e73a163161707fa7c970a90b47640d760ab57585088f506c5d7b88141006a636afa1b9d7a7eb447cb2b8a4c4e95679946691eaa03c5a1d6dbf33bba8bf2ea635bbe719259a9cfad47758d85774f17d1a7769a38d3ac5c443216db8358bc566c38fa870b0f1905ec5820bfa619a5a31a115fb33022a88f9fab9e3ea1dd7554ff4d0a3867ce48501635d68458208d0b11786fc561ee669729ebabb4fab867d20c3c3917443ce1dfb9381a6463130919026e584078c02da3358757a5255fbdf2e21ac9988e7e34aa96cb0ca12f54ea90072af4233748b146431c15d6594db3d93307db4ac36782d15d26decf2f048d7d7daf4208820a025901c08a4ee7c80899db809179d04e45d85542135ccb512855e45237c3ea8f080740f06a12818c5a6bc183d9f30d6ab4a68edb23b114bb1a77d2538483dc113b1bcb02af1d4f07e60b5b032dca82429a57204c597b1e729967393bc8c6f0955c3d80f3ea37517b913a4e94441b2e26638e9f04ce59d478f1df9dc01130b23867f7f7432bc7df10323b59695d33af7031dba1c08c2015b19b231f74a6d1f15e71f12769a6b603e72fa2fb60a27c7eedb0fe24303301e1d90bea765c7414d92f856dba25c4003647381f7621133ee06839e9c013933196b6ad21ccde7906644e4fece34f86b3b674442b3ef2e1b48a150b5dbd53f343f6bfc185eb92f5c348d7f0b960b6e8f8bbb531e1df000a4cfbc71f9ca04842647c83ca23a98cf02d86446729ff6a34376377c33a18a5f8ac7e487b7cb7e0131afa6ed024c4e40ef2fccce9ea61e7d34a69ad7c29f75a1fb23f4412027f32b55155bc89d2c1165dbbef4623b9d00ef58365bc0b39972de9ccd4e4fe648732cae8af26fbae69a6466a21562d37187b10c11f54f4938f803b913182393933253d2973bcb86452d03d52f06b833b124624c1fa4fe6c9d33bcceb03e96e6b76c6838b22656275a6d2efc4aaedd26107ad82a40081825820774efa94d5f3de90fd5487d06bc793bcae70444143ca3d1834a0fd6d729bd4e901018282583900bb8b91d9ab29b87cc8f6a96660fe0476e1484084e46be251ab8e1a312af5ec28707ac68efff4e4ad676532676380e2c861154da9da07c7b11a000ecc1682583900bb8b91d9ab29b87cc8f6a96660fe0476e1484084e46be251ab8e1a312af5ec28707ac68efff4e4ad676532676380e2c861154da9da07c7b11b000000022418dfb3021a0002bc75075820e203370a7728a01e5359cf26da08921861957bb3490382435ec320cf71845050a600818258201bf8eb4da8fe5910cc890025deb9740ba5fa4fd2ac418ccbebfd6a09ed10e88b000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f75bb96021a0003070d075820ea51ece8336ca85298fcf3bbb65b2e0b1bdda9840c7e0a528a5d0a7a749d32b30e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0082a1008182582046ffb2d8c474e83a5acfee4bd36fc49e6358320700d6b29d51a2b1d97a4552b95840a9929118faca232e870c2a8392019223aadf7a225f017575ae0164c003c55a30f2d809c58391e7ca4a6f602bcdbf9f9f7ca9d40bc925f26f1ef21b5942cec805a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c01602058405b9794478f4111564ab37ea191727a668f416d0a67833da6c475f1ad84e7e9dc79d3eb147f7b89233b52b2f273134f4c62d4c96ecf59fea24f7bcfa783348d0e82582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b584054e5bb8488d4ec41b1d9a9e3351059147754caf88df96ed6aedbd8c0544da3cd33842f228d6fa9817e7174d21b631e1eee7292ef122932e52e75698d9d55db0fa200a11902a2a1636d73678573436174616c79737420417564697420446174617146756e64206e616d653a2066756e643131782941727469666163743a204172636869766520666f722065706f6368203337372c20736c6f74203138357840436865636b73756d3a20333762306164623730643663356330396163666339343836323333393566333434663536666634656538643561356564353433373830783b4c696e6b3a20697066733a2f2f516d594d66706a51454c575a43677270414a74754e69373861435a5674396169735344415550586642465a34545a01a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01505f8be23104a28bab0caab1da9780aad00258201bf8eb4da8fe5910cc890025deb9740ba5fa4fd2ac418ccbebfd6a09ed10e88b0b8758401b4b020064f876f8a25e8d72185c837503f5619c66cb9dbed10b9f6a65bffdd6a2b6c4022aa0906b410b28d1d40249b7a9e8294830824ad753bd2126f2dc8f8a5840029094377230de4f4d454945454bf1704c7100d0d2512b47a502738811d02b7bd1d071530003d003133013f51a88f1b3c25a0082358106803534fd2deae94161584006afa5e7edfdd751fb4d5f456d46916c7ef1b9622c7985fbaf7ccf79b1d8cdf7ccff74ebef5bff7dbcfbd066051afc56ab9d7a7453570b879f1f3c6c85c29f4f5840e75c378c2d3d9f5a38e9de851364ee0d11c3801800446f4e45396988564034912c495935b33d0f24075f6b54992f8803847d4a0966a51e1951e1ca24198625a75840e8abab87454622ea684325849c8f403882ea620045c33432f43262f54062a416968aa46ac4e04866a4a6ae0e82c5c269896909da5148727c5a4454b4261e831c5840fa1f196950b0bcdb89fb7efa4431bfe17c5bf7552383a4eacaf596f3bd07aada671bf14c6ce3ac1ee7ed2fcef7e45c6ef5c20ce8981737297ec82d528ab4ff2d58246aa862121023cf9f17d2fa4a546f2310490c53d050924c94d4144cc9ef6a09898af9c60318635840c3b381d5cb0362b2737998dcb14c98434ae4df2aceb2c16f8a390b7d4057d3dd0d2fa578cc0269b32db9720bce6e9d15f7174e55039f98f90e34044502b3490c80 \ No newline at end of file diff --git a/rust/rbac-registration/src/utils/decode_helper.rs b/rust/rbac-registration/src/utils/decode_helper.rs index b6f12c7a24..9f0632ae74 100644 --- a/rust/rbac-registration/src/utils/decode_helper.rs +++ b/rust/rbac-registration/src/utils/decode_helper.rs @@ -1,214 +1,33 @@ //! CBOR decoding helper functions. -use minicbor::{data::Tag, decode, Decoder}; - -/// Generic helper function for decoding different types. -pub(crate) fn decode_helper<'a, T, C>( - d: &mut Decoder<'a>, from: &str, context: &mut C, -) -> Result -where T: minicbor::Decode<'a, C> { - T::decode(d, context).map_err(|e| { - decode::Error::message(format!( - "Failed to decode {:?} in {from}: {e}", - std::any::type_name::() - )) - }) -} - -/// Helper function for decoding bytes. -pub(crate) fn decode_bytes(d: &mut Decoder, from: &str) -> Result, decode::Error> { - d.bytes().map(<[u8]>::to_vec).map_err(|e| { - decode::Error::message(format!( - "Failed to decode bytes in {from}: - {e}" - )) - }) -} - -/// Helper function for decoding array. -pub(crate) fn decode_array_len(d: &mut Decoder, from: &str) -> Result { - d.array() - .map_err(|e| { - decode::Error::message(format!( - "Failed to decode array in {from}: - {e}" - )) - })? - .ok_or(decode::Error::message(format!( - "Failed to decode array in {from}, unexpected indefinite length", - ))) -} - -/// Helper function for decoding map. -pub(crate) fn decode_map_len(d: &mut Decoder, from: &str) -> Result { - d.map() - .map_err(|e| decode::Error::message(format!("Failed to decode map in {from}: {e}")))? - .ok_or(decode::Error::message(format!( - "Failed to decode map in {from}, unexpected indefinite length", - ))) -} - -/// Helper function for decoding tag. -pub(crate) fn decode_tag(d: &mut Decoder, from: &str) -> Result { - d.tag() - .map_err(|e| decode::Error::message(format!("Failed to decode tag in {from}: {e}"))) -} - -/// Decode any in CDDL, only support basic datatype -pub(crate) fn decode_any(d: &mut Decoder, from: &str) -> Result, decode::Error> { - match d.datatype()? { - minicbor::data::Type::String => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.as_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::U8 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::U16 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::U32 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::U64 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::I8 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::I16 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::I32 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::I64 => { - match decode_helper::(d, &format!("{from} Any"), &mut ()) { - Ok(i) => Ok(i.to_be_bytes().to_vec()), - Err(e) => Err(e), - } - }, - minicbor::data::Type::Bytes => Ok(decode_bytes(d, &format!("{from} Any"))?), - minicbor::data::Type::Array => { - Ok(decode_array_len(d, &format!("{from} Any"))? - .to_be_bytes() - .to_vec()) - }, - _ => { - Err(decode::Error::message(format!( - "{from} Any, Data type not supported" - ))) - }, - } -} - -#[cfg(test)] -mod tests { - - use minicbor::Encoder; - - use super::*; - - #[test] - fn test_decode_any_bytes() { - let mut buf = Vec::new(); - let mut e = Encoder::new(&mut buf); - e.bytes(&[1, 2, 3, 4]).expect("Error encoding bytes"); - - let mut d = Decoder::new(&buf); - let result = decode_any(&mut d, "test").expect("Error decoding bytes"); - assert_eq!(result, vec![1, 2, 3, 4]); - } - - #[test] - fn test_decode_any_string() { - let mut buf = Vec::new(); - let mut e = Encoder::new(&mut buf); - e.str("hello").expect("Error encoding string"); - - let mut d = Decoder::new(&buf); - let result = decode_any(&mut d, "test").expect("Error decoding string"); - assert_eq!(result, b"hello".to_vec()); - } - - #[test] - fn test_decode_any_array() { - // The array should contain a supported type - let mut buf = Vec::new(); - let mut e = Encoder::new(&mut buf); - e.array(2).expect("Error encoding array"); - e.u8(1).expect("Error encoding u8"); - e.u8(2).expect("Error encoding u8"); - let mut d = Decoder::new(&buf); - let result = decode_any(&mut d, "test").expect("Error decoding array"); - // The decode of array is just a length of the array - assert_eq!( - u64::from_be_bytes(result.try_into().expect("Error converting bytes to u64")), - 2 - ); - } - - #[test] - fn test_decode_any_u32() { - let mut buf = Vec::new(); - let mut e = Encoder::new(&mut buf); - let num: u32 = 123_456_789; - e.u32(num).expect("Error encoding u32"); - - let mut d = Decoder::new(&buf); - let result = decode_any(&mut d, "test").expect("Error decoding u32"); - assert_eq!( - u32::from_be_bytes(result.try_into().expect("Error converting bytes to u32")), - num - ); - } - - #[test] - fn test_decode_any_i32() { - let mut buf = Vec::new(); - let mut e = Encoder::new(&mut buf); - let num: i32 = -123_456_789; - e.i32(num).expect("Error encoding i32"); - let mut d = Decoder::new(&buf); - let result = decode_any(&mut d, "test").expect("Error decoding i32"); - assert_eq!( - i32::from_be_bytes(result.try_into().expect("Error converting bytes to i32")), - num +use std::fmt::Debug; + +use catalyst_types::problem_report::ProblemReport; + +/// Adds a "duplicated field" entry to the report and returns true if the field is already +/// present in the given found keys list. +pub fn report_duplicated_key( + found_keys: &[T], key: &T, index: u64, context: &str, report: &ProblemReport, +) -> bool { + if found_keys.contains(key) { + report.duplicate_field( + format!("{key:?}").as_str(), + format!("Redundant key found in item {} in RBAC map", index + 1).as_str(), + context, ); + return true; } + false +} - #[test] - fn test_decode_any_unsupported_type() { - let mut buf = Vec::new(); - let mut e = Encoder::new(&mut buf); - e.null().expect("Error encoding null"); // Encode a null type which is unsupported - - let mut d = Decoder::new(&buf); - let result = decode_any(&mut d, "test"); - // Should print out the error message with the location of the error - assert!(result.is_err()); +/// Adds a "missing field" entry to the report for every required key that isn't present +/// in the found keys list. +pub fn report_missing_keys( + found_keys: &[T], required_keys: &[T], context: &str, report: &ProblemReport, +) { + for key in required_keys { + if !found_keys.contains(key) { + report.missing_field(&format!("{key:?}"), context); + } } } diff --git a/rust/rbac-registration/src/utils/general.rs b/rust/rbac-registration/src/utils/general.rs deleted file mode 100644 index 0af72c7d92..0000000000 --- a/rust/rbac-registration/src/utils/general.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! General utility functions - -/// Getting the index by decrementing by 1. -/// e.g. 1 should refers to index 0 -pub(crate) fn decremented_index(int: i16) -> anyhow::Result { - match usize::try_from(int) { - Ok(value) => Ok(value - 1), - Err(e) => { - Err(anyhow::Error::msg(format!( - "Failed to convert to usize: {e}" - ))) - }, - } -} - -/// Decode the given UTF-8 content. -pub(crate) fn decode_utf8(content: &[u8]) -> anyhow::Result { - // Decode the UTF-8 string - std::str::from_utf8(content) - .map(std::string::ToString::to_string) - .map_err(|_| { - anyhow::anyhow!( - "Invalid UTF-8 string, expected valid UTF-8 string but got {:?}", - content - ) - }) -} - -/// Zero out the last n bytes -pub(crate) fn zero_out_last_n_bytes(vec: &mut [u8], n: usize) { - if let Some(slice) = vec.get_mut(vec.len().saturating_sub(n)..) { - slice.fill(0); - } -} diff --git a/rust/rbac-registration/src/utils/hashing.rs b/rust/rbac-registration/src/utils/hashing.rs deleted file mode 100644 index 2ccf2e636d..0000000000 --- a/rust/rbac-registration/src/utils/hashing.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Hashing utility function. - -use blake2b_simd::{self, Params}; - -/// Convert the given value to `blake2b_244` array. -pub(crate) fn blake2b_244(value: &[u8]) -> anyhow::Result<[u8; 28]> { - let h = Params::new().hash_length(28).hash(value); - let b = h.as_bytes(); - b.try_into() - .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_244, expected 28 got {}", b.len())) -} - -/// Convert the given value to `blake2b_256` array. -pub(crate) fn blake2b_256(value: &[u8]) -> anyhow::Result<[u8; 32]> { - let h = Params::new().hash_length(32).hash(value); - let b = h.as_bytes(); - b.try_into() - .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_256, expected 32 got {}", b.len())) -} - -/// Convert the given value to `blake2b_128` array. -pub(crate) fn blake2b_128(value: &[u8]) -> anyhow::Result<[u8; 16]> { - let h = Params::new().hash_length(16).hash(value); - let b = h.as_bytes(); - b.try_into() - .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_128, expected 16 got {}", b.len())) -} diff --git a/rust/rbac-registration/src/utils/mod.rs b/rust/rbac-registration/src/utils/mod.rs index 9baae37940..9a5e61bfa5 100644 --- a/rust/rbac-registration/src/utils/mod.rs +++ b/rust/rbac-registration/src/utils/mod.rs @@ -1,5 +1,6 @@ //! Utility functions for the RBAC registration module. -pub(crate) mod decode_helper; -pub(crate) mod general; -pub(crate) mod hashing; +pub mod decode_helper; + +#[cfg(test)] +pub mod test; diff --git a/rust/rbac-registration/src/utils/test.rs b/rust/rbac-registration/src/utils/test.rs new file mode 100644 index 0000000000..4d8b141da5 --- /dev/null +++ b/rust/rbac-registration/src/utils/test.rs @@ -0,0 +1,193 @@ +//! Utilities for testing. + +// cspell: words stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr + +use cardano_blockchain_types::{MultiEraBlock, Network, Point, Slot, TxnIndex}; +use catalyst_types::{hashes::Blake2b256Hash, uuid::UuidV4}; +use uuid::Uuid; + +use crate::cardano::cip509::{Cip509, RoleNumber}; + +/// Test data expected from block. +#[allow(dead_code)] +pub struct BlockTestData { + /// Block data. + pub block: MultiEraBlock, + /// Slot number. + pub slot: Slot, + /// Role. + pub role: RoleNumber, + /// Transaction index. + pub txn_index: TxnIndex, + /// Transaction hash. + pub txn_hash: Blake2b256Hash, + /// Previous hash. + pub prv_hash: Option, + /// Purpose. + pub purpose: UuidV4, + /// Stake address. + pub stake_addr: Option, +} + +impl BlockTestData { + /// Asserts that the problem report doesn't contain errors and all fields have + /// expected values. + #[track_caller] + pub fn assert_valid(&self, cip509: &Cip509) { + assert!(!cip509.report().is_problematic(), "{:?}", cip509.report()); + + let origin = cip509.origin(); + assert_eq!(origin.txn_index(), self.txn_index); + assert_eq!(origin.point().as_fuzzy(), Point::fuzzy(self.slot)); + 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); + } +} + +/// Returns the decoded `conway_1.block` block that contains 1 transaction +/// Slot number: `82_004_293`, Block number: `3_118_387` +/// Tx hash: 0x1bf8eb4da8fe5910cc890025deb9740ba5fa4fd2ac418ccbebfd6a09ed10e88b +/// +/// CIP509 details (valid data): +/// Role: 0 +/// Tx index: 0 +/// prv hash: None +/// purpose: ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c +/// stake addr: `stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr` +pub fn block_1() -> BlockTestData { + let data = hex::decode(include_str!("../test_data/cardano/conway_1.block")).unwrap(); + BlockTestData { + block: block(data), + slot: 82_004_293.into(), + role: 0.into(), + txn_index: 0.into(), + txn_hash: "1bf8eb4da8fe5910cc890025deb9740ba5fa4fd2ac418ccbebfd6a09ed10e88b" + .parse() + .unwrap(), + prv_hash: None, + purpose: "ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c" + .parse::() + .unwrap() + .try_into() + .unwrap(), + stake_addr: Some( + "stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr".to_string(), + ), + } +} + +/// Returns the decoded `conway_2.block` block that contains one transaction. +/// This registration contains an invalid public key that isn't present in the transaction +/// witness set. +/// Slot number: `77_171_632`, Block number: `2_935_642` +/// tx hash: 0x337d35026efaa48b5ee092d38419e102add1b535364799eb8adec8ac6d573b79 +/// +/// CIP509 details (invalid data): +/// Role: 0 +/// Tx index: 0 +/// prv hash: 0x4d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c2 +/// purpose: ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c +pub fn block_2() -> BlockTestData { + let data = hex::decode(include_str!("../test_data/cardano/conway_2.block")).unwrap(); + BlockTestData { + block: block(data), + slot: 77_171_632.into(), + role: 0.into(), + txn_index: 0.into(), + txn_hash: "337d35026efaa48b5ee092d38419e102add1b535364799eb8adec8ac6d573b79" + .parse() + .unwrap(), + prv_hash: Some( + "4d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c2" + .parse() + .unwrap(), + ), + purpose: "ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c" + .parse::() + .unwrap() + .try_into() + .unwrap(), + stake_addr: None, + } +} + +/// Returns the decoded `conway_3.block` block that contains one transaction +/// The registration contains invalid payment key reference. +/// Slot number: `77_170_639`, Block number: `2_935_613` +/// Tx hash: 0x0fda4c9f86e763fecd33f57d8f93540b1598c0a0e539dd996c48052ce94bab80 +/// +/// CIP509 details (invalid data): +/// Role: 0 +/// Tx index: 0 +/// prv hash: 0x4d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c2 +/// purpose: ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c +/// stake addr: `stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr` +pub fn block_3() -> BlockTestData { + let data = hex::decode(include_str!("../test_data/cardano/conway_3.block")).unwrap(); + BlockTestData { + block: block(data), + slot: 77_170_639.into(), + role: 0.into(), + txn_index: 0.into(), + txn_hash: "0fda4c9f86e763fecd33f57d8f93540b1598c0a0e539dd996c48052ce94bab80" + .parse() + .unwrap(), + prv_hash: Some( + "4d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c2" + .parse() + .unwrap(), + ), + purpose: "ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c" + .parse::() + .unwrap() + .try_into() + .unwrap(), + stake_addr: Some( + "stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr" + .parse() + .unwrap(), + ), + } +} + +/// Returns the decoded `conway_4.block` block that contains 2 transactions. +/// Slot number: `82_004_569`, Block number: `3_118_395` +/// Tx hash: 0xeef40a97a4ed1e40c3febd05a84b3ffaa191141b60806c2bba85d9c6879fb378 +/// +/// CIP509 details (valid data, signing key ref to x509 cert index 1): +/// Role: 4 +/// Tx index: 1 +/// prv hash: Link to `block_1` +/// purpose: ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c +/// stake addr: `stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr` +pub fn block_4() -> BlockTestData { + let data = hex::decode(include_str!("../test_data/cardano/conway_4.block")).unwrap(); + BlockTestData { + block: block(data), + slot: 82_004_569.into(), + role: 4.into(), + txn_index: 1.into(), + txn_hash: "eef40a97a4ed1e40c3febd05a84b3ffaa191141b60806c2bba85d9c6879fb378" + .parse() + .unwrap(), + prv_hash: Some(block_1().txn_hash), + purpose: "ca7a1457-ef9f-4c7f-9c74-7f8c4a4cfa6c" + .parse::() + .unwrap() + .try_into() + .unwrap(), + stake_addr: Some( + "stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr".to_string(), + ), + } +} + +/// Converts the given raw data to a block. +fn block(data: Vec) -> MultiEraBlock { + // This point is used to bypass validation in the block constructor. + let previous = Point::fuzzy(0.into()); + MultiEraBlock::new(Network::Preprod, data, &previous, 0.into()).unwrap() +}