diff --git a/rust/cardano-blockchain-types/Cargo.toml b/rust/cardano-blockchain-types/Cargo.toml index 6c95c7b02c..a7fb5f6266 100644 --- a/rust/cardano-blockchain-types/Cargo.toml +++ b/rust/cardano-blockchain-types/Cargo.toml @@ -21,6 +21,8 @@ workspace = true pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } pallas-crypto = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } # pallas-hardano = { 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 = "v0.0.11" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250108-00" } ouroboros = "0.18.4" tracing = "0.1.41" @@ -33,4 +35,7 @@ dashmap = "6.1.0" blake2b_simd = "1.0.2" minicbor = { version = "0.25.1", features = ["alloc"] } num-traits = "0.2.19" -ed25519-dalek = "2.1.1" \ No newline at end of file +ed25519-dalek = "2.1.1" +serde = "1.0.210" +num-bigint = "0.4.6" +serde_json = "1.0.134" diff --git a/rust/cardano-blockchain-types/src/auxdata/block.rs b/rust/cardano-blockchain-types/src/auxdata/block.rs index 554ede16bb..2bb98df3d9 100644 --- a/rust/cardano-blockchain-types/src/auxdata/block.rs +++ b/rust/cardano-blockchain-types/src/auxdata/block.rs @@ -44,19 +44,19 @@ impl TryFrom<&MultiEraBlock<'_>> for BlockAuxData { for (txn_idx, metadata) in alonzo_block.auxiliary_data_set.iter() { let mut d = minicbor::Decoder::new(metadata.raw_cbor()); let txn_aux_data = d.decode::()?; - aux_data.insert(TxnIndex::from_saturating(*txn_idx), txn_aux_data); + aux_data.insert((*txn_idx).into(), txn_aux_data); } } else if let Some(babbage_block) = block.as_babbage() { for (txn_idx, metadata) in babbage_block.auxiliary_data_set.iter() { let mut d = minicbor::Decoder::new(metadata.raw_cbor()); let txn_aux_data = d.decode::()?; - aux_data.insert(TxnIndex::from_saturating(*txn_idx), txn_aux_data); + aux_data.insert((*txn_idx).into(), txn_aux_data); } } else if let Some(conway_block) = block.as_conway() { for (txn_idx, metadata) in conway_block.auxiliary_data_set.iter() { let mut d = minicbor::Decoder::new(metadata.raw_cbor()); let txn_aux_data = d.decode::()?; - aux_data.insert(TxnIndex::from_saturating(*txn_idx), txn_aux_data); + aux_data.insert((*txn_idx).into(), txn_aux_data); } } else { bail!("Undecodable metadata, unknown Era"); diff --git a/rust/cardano-blockchain-types/src/lib.rs b/rust/cardano-blockchain-types/src/lib.rs index bfc4bff9fe..b2ae2a1bf9 100644 --- a/rust/cardano-blockchain-types/src/lib.rs +++ b/rust/cardano-blockchain-types/src/lib.rs @@ -4,6 +4,7 @@ mod auxdata; pub mod conversion; mod fork; pub mod hashes; +mod metadata; mod multi_era_block_data; mod network; mod point; @@ -20,6 +21,7 @@ pub use auxdata::{ scripts::{Script, ScriptArray, ScriptType, TransactionScripts}, }; pub use fork::Fork; +pub use metadata::cip36::{voting_pk::VotingPubKey, Cip36}; pub use multi_era_block_data::MultiEraBlock; pub use network::Network; pub use point::Point; diff --git a/rust/cardano-blockchain-types/src/metadata/cip36/key_registration.rs b/rust/cardano-blockchain-types/src/metadata/cip36/key_registration.rs new file mode 100644 index 0000000000..31dd52fc06 --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/key_registration.rs @@ -0,0 +1,446 @@ +//! CIP-36 Key Registration 61284. +//! +//! Catalyst registration data +//! +//! +//! + +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{decode_array_len, decode_bytes, decode_helper, decode_map_len}; +use ed25519_dalek::VerifyingKey; +use minicbor::{decode, Decode, Decoder}; +use pallas::ledger::addresses::{Address, ShelleyAddress}; +use strum::FromRepr; + +use super::voting_pk::VotingPubKey; + +/// CIP-36 key registration - 61284 +/// +/// +/// ```cddl +/// key_registration = { +/// 1 : [+delegation] / legacy_key_registration, +/// 2 : $stake_credential, +/// 3 : $payment_address, +/// 4 : $nonce, +/// ? 5 : $voting_purpose .default 0 +// } +/// ``` +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Default, Debug)] +pub(crate) struct Cip36KeyRegistration { + /// Is this CIP36 or CIP15 format. + /// None if not either CIP36 or CIP15. + pub is_cip36: Option, + /// Voting public keys (called Delegations in the CIP-36 Spec). + /// Field 1 in the CIP-36 61284 Spec. + pub voting_pks: Vec, + /// Stake public key to associate with the voting keys. + /// Field 2 in the CIP-36 61284 Spec. + /// None if it is not set. + pub stake_pk: Option, + /// Payment Address to associate with the voting keys. + /// Field 3 in the CIP-36 61284 Spec. + /// None if it is not set. + pub payment_addr: Option, + /// Nonce (nonce that has been slot corrected). + /// Field 4 in the CIP-36 61284 Spec. + /// None if it is not set. + pub nonce: Option, + /// Registration Purpose (Always 0 for Catalyst). + /// Field 5 in the CIP-36 61284 Spec. + /// Default to 0. + pub purpose: u64, + /// Raw nonce (nonce that has not had slot correction applied). + /// None if it is not set. + pub raw_nonce: Option, + /// Is payment address payable? (not a script) + /// None if it is not set. + pub is_payable: Option, +} + +/// Enum of CIP36 registration (61284) with its associated unsigned integer key. +#[derive(FromRepr, Debug, PartialEq)] +#[repr(u16)] +enum Cip36KeyRegistrationKeys { + /// Voting key. + VotingKey = 1, + /// Stake public key. + StakePk = 2, + /// Payment address. + PaymentAddr = 3, + /// Nonce. + Nonce = 4, + /// Purpose. + Purpose = 5, +} + +impl Decode<'_, ProblemReport> for Cip36KeyRegistration { + fn decode(d: &mut Decoder, err_report: &mut ProblemReport) -> Result { + let map_len = decode_map_len(d, "CIP36 Key Registration")?; + + let mut cip36_key_registration = Cip36KeyRegistration::default(); + + // Record of founded keys. Check for duplicate keys in the map + let mut found_keys: Vec = Vec::new(); + + for index in 0..map_len { + let key: u16 = decode_helper(d, "key in CIP36 Key Registration", err_report)?; + + if let Some(key) = Cip36KeyRegistrationKeys::from_repr(key) { + if check_is_key_exist(&found_keys, &key, index, err_report) { + continue; + } + match key { + Cip36KeyRegistrationKeys::VotingKey => { + let (is_cip36, voting_keys) = decode_voting_key(d, err_report)?; + cip36_key_registration.is_cip36 = is_cip36; + cip36_key_registration.voting_pks = voting_keys; + }, + Cip36KeyRegistrationKeys::StakePk => { + let stake_pk = decode_stake_pk(d, err_report)?; + cip36_key_registration.stake_pk = stake_pk; + }, + Cip36KeyRegistrationKeys::PaymentAddr => { + let shelley_addr = decode_payment_addr(d, err_report)?; + cip36_key_registration.is_payable = shelley_addr + .as_ref() + .map(|addr| !addr.payment().is_script()) + .or(None); + }, + Cip36KeyRegistrationKeys::Nonce => { + cip36_key_registration.nonce = Some(decode_nonce(d)?); + }, + Cip36KeyRegistrationKeys::Purpose => { + cip36_key_registration.purpose = decode_purpose(d)?; + }, + } + // Update the founded keys. + found_keys.push(key); + } + } + + // Check whether all the required keys are found. + let required_keys = [ + Cip36KeyRegistrationKeys::VotingKey, + Cip36KeyRegistrationKeys::StakePk, + Cip36KeyRegistrationKeys::PaymentAddr, + Cip36KeyRegistrationKeys::Nonce, + ]; + + for key in &required_keys { + if !found_keys.contains(key) { + err_report.missing_field( + &format!("{key:?}"), + "Missing required key in CIP36 Key Registration", + ); + } + } + + Ok(cip36_key_registration) + } +} + +/// Helper function for checking whether the key is already in the `found_keys` or not. +/// True if exist, false if not. +fn check_is_key_exist( + found_keys: &[Cip36KeyRegistrationKeys], key: &Cip36KeyRegistrationKeys, index: u64, + err_report: &ProblemReport, +) -> bool { + if found_keys.contains(key) { + err_report.duplicate_field( + format!("{key:?}").as_str(), + format!("Redundant key found in item {} in RBAC map", index + 1).as_str(), + format!("CIP36 Key Registration {key:?}").as_str(), + ); + return true; + } + false +} + +/// Helper function for decoding the voting key. +/// +/// # Returns +/// +/// - A tuple containing +/// - A boolean value, true if it is CIP36 format, false if it is CIP15, None if not +/// either CIP36 or CIP15. +/// - A vector of `VotingPubKey`, if the `voting_pk` vector cannot be converted to +/// verifying key, assign `voting_pk` to None. +/// - Error if decoding failed. +fn decode_voting_key( + d: &mut Decoder, err_report: &ProblemReport, +) -> Result<(Option, Vec), decode::Error> { + let mut voting_keys = Vec::new(); + #[allow(unused_assignments)] + let mut is_cip36 = None; + + match d.datatype() { + Ok(dt) => { + match dt { + // CIP15 type registration (single voting key). + // ```cddl + // legacy_key_registration = $cip36_vote_pub_key + // $cip36_vote_pub_key /= bytes .size 32 + // ``` + minicbor::data::Type::Bytes => { + is_cip36 = Some(false); + let pub_key = + decode_bytes(d, "CIP36 Key Registration voting key, single voting key")?; + let vk = voting_pk_vec_to_verifying_key( + &pub_key, + err_report, + "CIP36 Key Registration voting key, single voting key", + ); + // Since there is 1 voting key, all the weight goes to this key = 1. + voting_keys.push(VotingPubKey::new(vk, 1)); + }, + // CIP36 type registration (multiple voting keys). + // ```cddl + // [+delegation] + // delegation = [$cip36_vote_pub_key, $weight] + // $cip36_vote_pub_key /= bytes .size 32 + // ``` + minicbor::data::Type::Array => { + is_cip36 = Some(true); + let len = decode_array_len( + d, + "CIP36 Key Registration voting key, multiple voting keys", + )?; + + for _ in 0..len { + let len = + decode_array_len(d, "CIP36 Key Registration voting key, delegations")?; + // This fixed array should be a length of 2 (voting key, weight). + if len != 2 { + return Err(decode::Error::message(format!("Invalid length for CIP36 Key Registration voting key delegations, expected 2, got {len}"))); + } + + // The first entry. + let pub_key = decode_bytes(d, "CIP36 Key Registration voting key, delegation array first entry (voting public key)")?; + // The second entry. + let weight: u32 = decode_helper(d, "CIP36 Key Registration voting key, delegation array second entry (weight)", &mut (),)?; + + let vk = voting_pk_vec_to_verifying_key( + &pub_key, + err_report, + "CIP36 Key Registration voting key, multiple voting keys", + ); + voting_keys.push(VotingPubKey::new(vk, weight)); + } + }, + + _ => { + return Err(decode::Error::message("Invalid datatype for CIP36 Key Registration voting key, should be either Array or Bytes")); + }, + } + }, + Err(e) => { + return Err(decode::Error::message(format!( + "Decoding voting key, invalid data type: {e}" + ))); + }, + } + Ok((is_cip36, voting_keys)) +} + +/// Helper function for converting `&[u8]` to `VerifyingKey`. +fn voting_pk_vec_to_verifying_key( + pub_key: &[u8], err_report: &ProblemReport, context: &str, +) -> Option { + let bytes = pub_key + .try_into() + .map_err(|_| { + err_report.invalid_value( + "Verifying key length", + format!("{}", pub_key.len()).as_str(), + "Invalid length, must be length 32", + context, + ); + }) + .ok()?; + VerifyingKey::from_bytes(bytes) + .map_err(|e| { + err_report.conversion_error( + "Verifying key ", + format!("{bytes:?}").as_str(), + format!("EdDSA VerifyingKey, {e}").as_str(), + "Failed to bytes convert to VerifyingKey", + ); + }) + .ok() +} + +/// Helper function for decoding the stake public key. +/// +/// ```cddl +/// 2 : $stake_credential, +/// $stake_credential /= $staking_pub_key +/// $staking_pub_key /= bytes .size 32 +/// ``` +/// +/// # Returns +/// +/// - The stake public key as a `VerifyingKey`. +/// - None if cannot converted `Vec` to `VerifyingKey`. +/// - Error if decoding failed. +fn decode_stake_pk( + d: &mut Decoder, err_report: &ProblemReport, +) -> Result, decode::Error> { + let pub_key = decode_bytes(d, "CIP36 Key Registration stake public key")?; + Ok(voting_pk_vec_to_verifying_key( + &pub_key, + err_report, + "CIP36 Key Registration stake public key", + )) +} + +/// Helper function for decoding the payment address. +/// +/// ```cddl +/// 3 : $payment_address, +/// $payment_address /= bytes +/// ``` +/// +/// # Returns +/// +/// - The payment address as a `ShelleyAddress`. +/// - None if cannot converted `Vec` to `ShelleyAddress`. +/// - Error if decoding failed. +fn decode_payment_addr( + d: &mut Decoder, err_report: &ProblemReport, +) -> Result, decode::Error> { + let raw_addr = decode_bytes(d, "CIP36 Key Registration payment address")?; + // Cannot convert raw address to Address type + let address = match Address::from_bytes(&raw_addr) { + Ok(addr) => addr, + Err(e) => { + err_report.conversion_error( + "Cardano address", + format!("{raw_addr:?}").as_str(), + format!("Cannot convert to type Address: {e}").as_str(), + "CIP36 Key Registration payment address", + ); + // Can't process any further + return Ok(None); + }, + }; + + if let Address::Shelley(addr) = address { + Ok(Some(addr.clone())) + } else { + err_report.invalid_value( + "Shelley Address", + format!("{address}").as_str(), + "Expected Shelley address", + "CIP36 Key Registration payment address", + ); + Ok(None) + } +} + +/// Helper function for decoding raw nonce. +/// +/// ```cddl +/// 4 : $nonce, +/// $nonce /= uint +/// ``` +/// +/// # Returns +/// +/// - Raw nonce. +/// - Error if decoding failed. +fn decode_nonce(d: &mut Decoder) -> Result { + decode_helper(d, "CIP36 Key Registration nonce", &mut ()) +} + +/// Helper function for decoding the purpose. +/// +/// ```cddl +/// 5 : $voting_purpose .default 0 +/// $voting_purpose /= uint +/// ``` +/// +/// # Returns +/// +/// - The purpose. +/// - Error if decoding failed. +fn decode_purpose(d: &mut Decoder) -> Result { + decode_helper(d, "CIP36 Key Registration purpose", &mut ()) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_decode_payment_address() { + let hex_data = hex::decode( + // 0x004777561e7d9ec112ec307572faec1aff61ff0cfed68df4cd5c847f1872b617657881e30ad17c46e4010c9cb3ebb2440653a34d32219c83e9 + "5839004777561E7D9EC112EC307572FAEC1AFF61FF0CFED68DF4CD5C847F1872B617657881E30AD17C46E4010C9CB3EBB2440653A34D32219C83E9" + ).expect("cannot decode hex"); + let mut decoder = Decoder::new(&hex_data); + let err_report = ProblemReport::new("CIP36 Key Registration Decoding"); + let address = + decode_payment_addr(&mut decoder, &err_report).expect("cannot decode payment address"); + assert!(!err_report.is_problematic()); + assert_eq!(address.unwrap().to_vec().len(), 57); + } + + #[test] + fn test_decode_stake_pk() { + let hex_data = hex::decode( + // 0xe3cd2404c84de65f96918f18d5b445bcb933a7cda18eeded7945dd191e432369 + "5820E3CD2404C84DE65F96918F18D5B445BCB933A7CDA18EEDED7945DD191E432369", + ) + .expect("cannot decode hex"); + let mut decoder = Decoder::new(&hex_data); + let err_report = ProblemReport::new("CIP36 Key Registration Decoding"); + let stake_pk = decode_stake_pk(&mut decoder, &err_report).expect("cannot decode stake pk"); + assert!(!err_report.is_problematic()); + assert!(stake_pk.is_some()); + } + + #[test] + // cip-36 version + fn test_decode_voting_key_cip36() { + let hex_data = hex::decode( + // [["0x0036ef3e1f0d3f5989e2d155ea54bdb2a72c4c456ccb959af4c94868f473f5a0", 1]] + "818258200036EF3E1F0D3F5989E2D155EA54BDB2A72C4C456CCB959AF4C94868F473F5A001", + ) + .expect("cannot decode hex"); + let mut decoder = Decoder::new(&hex_data); + let err_report = ProblemReport::new("CIP36 Key Registration Decoding"); + let (is_cip36, voting_pk) = + decode_voting_key(&mut decoder, &err_report).expect("cannot decode voting key"); + assert!(!err_report.is_problematic()); + assert!(is_cip36.unwrap()); + assert_eq!(voting_pk.len(), 1); + } + + #[test] + // cip-15 version + fn test_decode_voting_key_2() { + let hex_data = hex::decode( + // 0x0036ef3e1f0d3f5989e2d155ea54bdb2a72c4c456ccb959af4c94868f473f5a0 + "58200036EF3E1F0D3F5989E2D155EA54BDB2A72C4C456CCB959AF4C94868F473F5A0", + ) + .expect("cannot decode hex"); + let mut decoder = Decoder::new(&hex_data); + let err_report = ProblemReport::new("CIP36 Key Registration Decoding"); + let (is_cip36, voting_pk) = + decode_voting_key(&mut decoder, &err_report).expect("cannot decode voting key"); + assert!(!err_report.is_problematic()); + assert!(!is_cip36.unwrap()); + assert_eq!(voting_pk.len(), 1); + } + + #[test] + fn test_decode_nonce() { + let hex_data = hex::decode("1A014905D1").expect("cannot decode hex"); + let mut decoder = Decoder::new(&hex_data); + let nonce = decode_nonce(&mut decoder).expect("cannot decode nonce"); + assert_eq!(nonce, 21_562_833); + } +} diff --git a/rust/cardano-blockchain-types/src/metadata/cip36/mod.rs b/rust/cardano-blockchain-types/src/metadata/cip36/mod.rs new file mode 100644 index 0000000000..068cd5b0ff --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/mod.rs @@ -0,0 +1,328 @@ +//! CIP-36 Catalyst registration module + +pub mod key_registration; +pub mod registration_witness; +mod validation; +pub mod voting_pk; +use std::{collections::HashMap, fmt}; + +use catalyst_types::problem_report::ProblemReport; +use ed25519_dalek::VerifyingKey; +use key_registration::Cip36KeyRegistration; +use minicbor::{Decode, Decoder}; +use pallas::ledger::addresses::ShelleyAddress; +use registration_witness::Cip36RegistrationWitness; +use voting_pk::VotingPubKey; + +use crate::{MetadatumLabel, MultiEraBlock, Network, Slot, TxnIndex}; + +/// CIP-36 Catalyst registration +#[derive(Clone)] +#[allow(clippy::struct_excessive_bools)] +pub struct Cip36 { + /// Key registration - 61284 + key_registration: Cip36KeyRegistration, + /// Registration witness - 61285 + registration_witness: Cip36RegistrationWitness, + /// Network that this CIP-36 registration is on. + network: Network, + /// Slot that this CIP-36 registration is on. + slot: Slot, + /// Transaction index that this CIP-36 registration is on. + txn_idx: TxnIndex, + /// Is this a Catalyst strict registration? + is_catalyst_strict: bool, + /// Is the signature valid? (signature in 61285) + is_valid_signature: bool, + /// Is the payment address on the correct network? + is_valid_payment_address_network: bool, + /// Is the voting keys valid? + is_valid_voting_keys: bool, + /// Is the purpose valid? (Always 0 for Catalyst) + is_valid_purpose: bool, + /// Error report. + err_report: ProblemReport, +} + +impl fmt::Display for Cip36 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Cip36 {{ network: {network}, slot: {slot:?}, txn_idx: {txn_idx:?}, is_catalyst_strict: {is_catalyst_strict}, key_registration: {key_registration:?}, registration_witness: {registration_witness:?}, validation: {{ signature: {is_valid_signature}, payment_address_network: {is_valid_payment_address_network}, voting_keys: {is_valid_voting_keys}, purpose: {is_valid_purpose} }}, err_report: {err_report} }}", + key_registration = self.key_registration, + registration_witness = self.registration_witness, + network = self.network, + slot = self.slot, + txn_idx = self.txn_idx, + is_catalyst_strict = self.is_catalyst_strict, + is_valid_signature = self.is_valid_signature, + is_valid_payment_address_network = self.is_valid_payment_address_network, + is_valid_voting_keys = self.is_valid_voting_keys, + is_valid_purpose = self.is_valid_purpose, + err_report = serde_json::to_string(&self.err_report) + .unwrap_or_else(|_| String::from("Failed to serialize ProblemReport")) + ) + } +} + +/// CIP-36 Catalyst registration error +#[allow(dead_code, clippy::module_name_repetitions)] +pub struct Cip36Error { + /// The decoding error that make the code not able to process. + error: anyhow::Error, + /// The problem report that contains the errors found during decoding and validation. + report: ProblemReport, +} + +impl fmt::Display for Cip36Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let report_json = serde_json::to_string(&self.report) + .unwrap_or_else(|_| String::from("Failed to serialize ProblemReport")); + + write!( + fmt, + "Cip36Error {{ error: {}, report: {} }}", + self.error, report_json + ) + } +} + +impl Cip36 { + /// Create an instance of CIP-36. + /// The CIP-36 registration contains the key registration (61284) + /// and registration witness (61285) metadata. + /// + /// # Parameters + /// + /// * `block` - The block containing the auxiliary data. + /// * `txn_idx` - The transaction index that contain the auxiliary data. + /// * `is_catalyst_strict` - Is this a Catalyst strict registration? + /// + /// # Returns + /// + /// None if the metadata is not in the block at given index. + /// + /// # Errors + /// + /// If the CIP-36 key registration or registration witness metadata is not found. + /// or if the CIP-36 key registration or registration witness metadata cannot be + /// decoded. + pub fn new( + block: &MultiEraBlock, txn_idx: TxnIndex, is_catalyst_strict: bool, + ) -> Result, Cip36Error> { + // Record of errors found during decoding and validation + let mut err_report = ProblemReport::new("CIP36 Registration Decoding and Validation"); + + let Some(k61284) = block.txn_metadata(txn_idx, MetadatumLabel::CIP036_REGISTRATION) else { + return Ok(None); + }; + let Some(k61285) = block.txn_metadata(txn_idx, MetadatumLabel::CIP036_WITNESS) else { + return Ok(None); + }; + + let slot = block.decode().slot(); + let network = block.network(); + + let mut key_registration = Decoder::new(k61284.as_ref()); + let mut registration_witness = Decoder::new(k61285.as_ref()); + + let key_registration = + match Cip36KeyRegistration::decode(&mut key_registration, &mut err_report) { + Ok(mut metadata) => { + let nonce = if is_catalyst_strict && metadata.raw_nonce > Some(slot) { + Some(slot) + } else { + metadata.raw_nonce + }; + + metadata.nonce = nonce; + metadata + }, + Err(e) => { + return Err(Cip36Error { + error: anyhow::anyhow!(format!( + "Failed to construct CIP-36 key registration, {e}" + )), + report: err_report, + }); + }, + }; + + let registration_witness = + match Cip36RegistrationWitness::decode(&mut registration_witness, &mut err_report) { + Ok(metadata) => metadata, + Err(e) => { + return Err(Cip36Error { + error: anyhow::anyhow!(format!( + "Failed to construct CIP-36 registration witness {e}" + )), + report: err_report, + }); + }, + }; + + // If the code reach here, then the CIP36 decoding is successful. + // Construct the CIP-36 registration + let mut cip36 = Cip36 { + key_registration, + registration_witness, + network, + slot: slot.into(), + txn_idx, + is_catalyst_strict, + is_valid_signature: false, + is_valid_payment_address_network: false, + is_valid_voting_keys: false, + is_valid_purpose: false, + err_report, + }; + + // Now check whether everything is valid. + cip36.validate_signature(k61284); + cip36.validate_payment_address_network(); + cip36.validate_voting_keys(); + cip36.validate_purpose(); + + Ok(Some(cip36)) + } + + /// Collect all CIP-36 registrations from a block. + /// + /// # Parameters + /// + /// * `block` - The block that wanted to be processed. + /// * `is_catalyst_strict` - Is this a Catalyst strict registration? + /// + /// # Returns + /// + /// A map of transaction index to the Result of CIP-36 and its errors. + /// None if there is no CIP-36 registration found in the block. + #[must_use] + pub fn cip36_from_block( + block: &MultiEraBlock, is_catalyst_strict: bool, + ) -> Option>> { + let mut cip36_map = HashMap::new(); + + for (txn_idx, _tx) in block.decode().txs().iter().enumerate() { + let txn_idx: TxnIndex = txn_idx.into(); + let cip36 = Cip36::new(block, txn_idx, is_catalyst_strict); + match cip36 { + Ok(Some(cip36)) => { + cip36_map.insert(txn_idx, Ok(cip36)); + }, + // None - no CIP-36 metadata found in the block + Ok(None) => {}, + // Error - found CIP-36 but there is some error + Err(e) => { + cip36_map.insert(txn_idx, Err(e)); + }, + } + } + + if cip36_map.is_empty() { + return None; + } + Some(cip36_map) + } + + /// Get the `is_cip36` flag from the registration. + /// True if it is CIP-36 format, false if CIP-15 format. + pub fn is_cip36(&self) -> Option { + self.key_registration.is_cip36 + } + + /// Get the voting public keys from the registration. + pub fn voting_pks(&self) -> &Vec { + &self.key_registration.voting_pks + } + + /// Get the stake public key from the registration. + pub fn stake_pk(&self) -> Option<&VerifyingKey> { + self.key_registration.stake_pk.as_ref() + } + + /// Get the payment address from the registration. + pub fn payment_address(&self) -> Option<&ShelleyAddress> { + self.key_registration.payment_addr.as_ref() + } + + /// Get the nonce from the registration. + pub fn nonce(&self) -> Option { + self.key_registration.nonce + } + + /// Get the purpose from the registration. + pub fn purpose(&self) -> u64 { + self.key_registration.purpose + } + + /// Get the raw nonce from the registration. + pub fn raw_nonce(&self) -> Option { + self.key_registration.raw_nonce + } + + /// Is the payment address in the registration payable? + pub fn is_payable(&self) -> Option { + self.key_registration.is_payable + } + + /// Get the signature from the registration witness. + pub fn signature(&self) -> Option { + self.registration_witness.signature + } + + /// Get the slot number of this CIP-36 registration. + pub fn slot(&self) -> Slot { + self.slot + } + + /// Get the network of this CIP-36 registration. + pub fn network(&self) -> Network { + self.network + } + + /// Get the transaction index of this CIP-36 registration. + pub fn txn_idx(&self) -> TxnIndex { + self.txn_idx + } + + /// Get the Catalyst strict flag. + pub fn is_strict_catalyst(&self) -> bool { + self.is_catalyst_strict + } + + /// Is the CIP-36 registration valid? + pub fn is_valid(&self) -> bool { + // Check everything + self.is_valid_signature + && self.is_valid_payment_address_network + && self.is_valid_voting_keys + && self.is_valid_purpose + && !self.err_report.is_problematic() + } + + /// Is the signature valid? + pub fn is_valid_signature(&self) -> bool { + self.is_valid_signature + } + + /// Is the payment address network tag match the provided network? + pub fn is_valid_payment_address_network(&self) -> bool { + self.is_valid_payment_address_network + } + + /// Is the voting keys valid? + pub fn is_valid_voting_keys(&self) -> bool { + self.is_valid_voting_keys + } + + /// Is the purpose valid? + pub fn is_valid_purpose(&self) -> bool { + self.is_valid_purpose + } + + /// Get the error report. + pub fn err_report(&self) -> &ProblemReport { + &self.err_report + } +} diff --git a/rust/cardano-blockchain-types/src/metadata/cip36/registration_witness.rs b/rust/cardano-blockchain-types/src/metadata/cip36/registration_witness.rs new file mode 100644 index 0000000000..ee2ffbb7e6 --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/registration_witness.rs @@ -0,0 +1,61 @@ +//! CIP36 registration witness 61285 +//! +//! +//! + +use catalyst_types::problem_report::ProblemReport; +use cbork_utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; +use minicbor::{decode, Decode, Decoder}; + +/// CIP-36 registration witness - 61285 +/// +/// ```cddl +/// registration_witness = { +/// 1 : $stake_witness +/// } +/// ``` +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Default, Debug)] +pub(crate) struct Cip36RegistrationWitness { + /// Signature of the registration data. + pub signature: Option, +} + +impl Decode<'_, ProblemReport> for Cip36RegistrationWitness { + fn decode(d: &mut Decoder, err_report: &mut ProblemReport) -> Result { + let map_len = decode_map_len(d, "CIP36 Registration Witness")?; + + // Expected only 1 key in the map. + if map_len != 1 { + return Err(decode::Error::message(format!( + "Invalid CIP36 Registration Witness map length, expected 1, got {map_len}" + ))); + } + + let key: u16 = decode_helper(d, "key in CIP36 Registration Witness", err_report)?; + + // The key needs to be 1. + if key != 1 { + err_report.invalid_value( + "map key", + format!("{key}").as_str(), + "expected key 1", + "CIP36 Registration Witness", + ); + } + + let sig_bytes = decode_bytes(d, "CIP36 Registration Witness signature")?; + let signature = ed25519_dalek::Signature::from_slice(&sig_bytes) + .map_err(|_| { + err_report.conversion_error( + "Signature", + format!("{sig_bytes:?}").as_str(), + "Cannot convert bytes to Ed25519 signature", + "CIP36 Registration Witness signature", + ); + }) + .ok(); + + Ok(Cip36RegistrationWitness { signature }) + } +} diff --git a/rust/cardano-blockchain-types/src/metadata/cip36/validation.rs b/rust/cardano-blockchain-types/src/metadata/cip36/validation.rs new file mode 100644 index 0000000000..b25f52be95 --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/validation.rs @@ -0,0 +1,253 @@ +//! Validation function for CIP-36 +//! +//! The validation include the following: +//! * Signature validation of the registration witness 61285 against the stake public key +//! in key registration 61284. +//! * Payment address network validation against the network. The given network should +//! match the network tag within the payment address. +//! * Purpose validation, the purpose should be 0 for Catalyst (when `is_strict_catalyst` +//! is true). +//! * Voting keys validation, Catalyst supports only a single voting key per registration +//! when `is_strict_catalyst` is true. + +use super::Cip36; +use crate::{MetadatumValue, Network}; + +/// Project Catalyst Purpose +pub const PROJECT_CATALYST_PURPOSE: u64 = 0; + +/// Signdata Preamble = `{ 61284: ?? }` +/// CBOR Decoded = +/// A1 # map(1) +/// 19 EF64 # unsigned(61284) +pub const SIGNDATA_PREAMBLE: [u8; 4] = [0xA1, 0x19, 0xEF, 0x64]; + +impl Cip36 { + /// Validate the signature against the public key. + pub(crate) fn validate_signature(&mut self, metadata: &MetadatumValue) { + let hash = blake2b_simd::Params::new() + .hash_length(32) + .to_state() + .update(&SIGNDATA_PREAMBLE) + .update(metadata.as_ref()) + .finalize(); + + // Ensure the signature exists + let Some(sig) = self.registration_witness.signature else { + self.err_report + .missing_field("Signature", "Validate CIP36 Signature, signature not found"); + self.is_valid_signature = false; + return; + }; + + // Ensure the stake public key exists + let Some(stake_pk) = self.key_registration.stake_pk else { + self.err_report.missing_field( + "Stake public key", + "Validate CIP36 Signature, stake public key not found", + ); + self.is_valid_signature = false; + return; + }; + + // Verify the signature + if let Ok(()) = stake_pk.verify_strict(hash.as_bytes(), &sig) { + self.is_valid_signature = true; + } else { + self.err_report.other( + "Cannot verify the signature using this stake public key", + "Validate CIP36 Signature", + ); + self.is_valid_signature = false; + }; + } + + /// Validate the payment address network against the given network. + pub(crate) fn validate_payment_address_network(&mut self) { + // Ensure the payment address exists + let Some(address) = &self.key_registration.payment_addr else { + self.err_report.missing_field( + "Payment address", + "Validate CIP36 payment address network, payment address not found", + ); + self.is_valid_payment_address_network = false; + return; + }; + // Extract the network tag and validate + let network_tag = address.network(); + let valid = match self.network { + Network::Mainnet => network_tag.value() == 1, + Network::Preprod | Network::Preview => network_tag.value() == 0, + }; + + // Report invalid network tag if necessary + if !valid { + self.err_report.invalid_value( + "Network tag of payment address", + &format!("{network_tag:?}"), + &format!("Expected {}", self.network), + "Validate CIP36 payment address network, CIP36 payment address network does not match the network used", + ); + } + + self.is_valid_payment_address_network = valid; + } + + /// Validate the voting keys. + pub(crate) fn validate_voting_keys(&mut self) { + if self.is_catalyst_strict && self.key_registration.voting_pks.len() != 1 { + self.err_report.invalid_value( + "Voting keys", + &self.key_registration.voting_pks.len().to_string(), + "Catalyst supports only a single voting key per registration", + "Validate CIP-36 Voting Keys", + ); + self.is_valid_voting_keys = false; + return; + } + + self.is_valid_voting_keys = true; + } + + /// Validate the purpose. + pub(crate) fn validate_purpose(&mut self) { + if self.is_catalyst_strict && self.key_registration.purpose != PROJECT_CATALYST_PURPOSE { + self.err_report.invalid_value( + "Purpose", + &self.key_registration.purpose.to_string(), + &format!( + "Registration contains unknown purpose, expected {PROJECT_CATALYST_PURPOSE}" + ), + "Validate CIP-36 Purpose", + ); + self.is_valid_purpose = false; + return; + } + + self.is_valid_purpose = true; + } +} + +#[cfg(test)] +mod tests { + + use catalyst_types::problem_report::ProblemReport; + use ed25519_dalek::VerifyingKey; + use pallas::ledger::addresses::Address; + + use crate::{ + metadata::cip36::{ + key_registration::Cip36KeyRegistration, voting_pk::VotingPubKey, + Cip36RegistrationWitness, + }, + Cip36, Network, + }; + + fn create_cip36() -> Cip36 { + Cip36 { + key_registration: Cip36KeyRegistration::default(), + registration_witness: Cip36RegistrationWitness::default(), + network: Network::Preprod, + slot: 0.into(), + txn_idx: 0.into(), + is_catalyst_strict: true, + is_valid_signature: false, + is_valid_payment_address_network: false, + is_valid_voting_keys: false, + is_valid_purpose: false, + err_report: ProblemReport::new("CIP36 Registration Validation"), + } + } + + #[test] + fn test_validate_payment_address_network() { + // cSpell:disable + let addr = Address::from_bech32("addr_test1qprhw4s70k0vzyhvxp6h97hvrtlkrlcvlmtgmaxdtjz87xrjkctk27ypuv9dzlzxusqse89naweygpjn5dxnygvus05sdq9h07").expect("Failed to create address"); + // cSpell:enable + let Address::Shelley(shelley_addr) = addr else { + panic!("Invalid address type") + }; + let mut cip36 = create_cip36(); + cip36.key_registration = Cip36KeyRegistration { + payment_addr: Some(shelley_addr), + ..Default::default() + }; + cip36.validate_payment_address_network(); + + assert!(!cip36.err_report.is_problematic()); + assert!(cip36.is_valid_payment_address_network); + } + + #[test] + fn test_validate_invalid_payment_address_network() { + // cSpell:disable + let addr = Address::from_bech32("addr_test1qprhw4s70k0vzyhvxp6h97hvrtlkrlcvlmtgmaxdtjz87xrjkctk27ypuv9dzlzxusqse89naweygpjn5dxnygvus05sdq9h07").expect("Failed to create address"); + // cSpell:enable + let Address::Shelley(shelley_addr) = addr else { + panic!("Invalid address type") + }; + let mut cip36 = create_cip36(); + cip36.network = Network::Mainnet; + cip36.key_registration = Cip36KeyRegistration { + payment_addr: Some(shelley_addr), + ..Default::default() + }; + cip36.validate_payment_address_network(); + + assert!(cip36.err_report.is_problematic()); + assert!(!cip36.is_valid_payment_address_network); + } + + #[test] + fn test_validate_voting_keys() { + let mut cip36 = create_cip36(); + cip36 + .key_registration + .voting_pks + .push(VotingPubKey::new(Some(VerifyingKey::default()), 1)); + + cip36.validate_voting_keys(); + assert!(!cip36.err_report.is_problematic()); + assert!(cip36.is_valid_voting_keys); + } + + #[test] + fn test_validate_invalid_voting_keys() { + let mut cip36 = create_cip36(); + cip36 + .key_registration + .voting_pks + .push(VotingPubKey::new(Some(VerifyingKey::default()), 1)); + cip36 + .key_registration + .voting_pks + .push(VotingPubKey::new(Some(VerifyingKey::default()), 1)); + + cip36.validate_voting_keys(); + assert!(cip36.err_report.is_problematic()); + assert!(!cip36.is_valid_voting_keys); + } + + #[test] + fn test_validate_purpose() { + let mut cip36 = create_cip36(); + cip36.validate_purpose(); + assert!(!cip36.err_report.is_problematic()); + assert_eq!(cip36.key_registration.purpose, 0); + assert!(cip36.is_valid_purpose); + } + + #[test] + fn test_validate_invalid_purpose() { + let mut cip36 = create_cip36(); + cip36.key_registration = Cip36KeyRegistration { + purpose: 1, + ..Default::default() + }; + cip36.validate_purpose(); + + assert!(cip36.err_report.is_problematic()); + assert_eq!(cip36.key_registration.purpose, 1); + assert!(!cip36.is_valid_purpose); + } +} diff --git a/rust/cardano-blockchain-types/src/metadata/cip36/voting_pk.rs b/rust/cardano-blockchain-types/src/metadata/cip36/voting_pk.rs new file mode 100644 index 0000000000..a35ce68dd5 --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/voting_pk.rs @@ -0,0 +1,32 @@ +//! Voting public key containing the public key and weight. + +use ed25519_dalek::VerifyingKey; + +/// Voting public key containing the public key and weight. +#[derive(Clone, Debug)] +pub struct VotingPubKey { + /// Voting public key. + voting_pk: Option, + /// Voting key associated weight. + weight: u32, +} + +impl VotingPubKey { + /// Create a new voting public key. + #[must_use] + pub fn new(voting_pk: Option, weight: u32) -> Self { + Self { voting_pk, weight } + } + + /// Get the voting public key. + #[must_use] + pub fn voting_pk(&self) -> Option<&VerifyingKey> { + self.voting_pk.as_ref() + } + + /// Get the voting key weight. + #[must_use] + pub fn weight(&self) -> u32 { + self.weight + } +} diff --git a/rust/cardano-blockchain-types/src/metadata/mod.rs b/rust/cardano-blockchain-types/src/metadata/mod.rs new file mode 100644 index 0000000000..a4fc3152d1 --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/mod.rs @@ -0,0 +1,3 @@ +//! Metadata module + +pub mod cip36; diff --git a/rust/cardano-blockchain-types/src/slot.rs b/rust/cardano-blockchain-types/src/slot.rs index 04225cf94e..80d0512f2a 100644 --- a/rust/cardano-blockchain-types/src/slot.rs +++ b/rust/cardano-blockchain-types/src/slot.rs @@ -1,7 +1,17 @@ //! Block Slot + +use std::{ + cmp::Ordering, + ops::{MulAssign, Sub}, +}; + +use num_bigint::{BigInt, Sign}; +use serde::Serialize; + use crate::conversion::from_saturating; -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize)] + /// Slot on the blockchain, typically one slot equals one second. However chain /// parameters can alter how long a slot is. pub struct Slot(u64); @@ -33,3 +43,92 @@ impl From for u64 { val.0 } } + +impl MulAssign for Slot { + fn mul_assign(&mut self, rhs: u64) { + self.0 = self.0.saturating_mul(rhs); + } +} + +impl PartialOrd for Slot { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(&other.0) + } +} + +impl Sub for Slot { + type Output = Slot; + + fn sub(self, rhs: Slot) -> Self::Output { + Slot(self.0.saturating_sub(rhs.0)) + } +} + +impl From for Slot { + fn from(value: BigInt) -> Self { + if value.sign() == Sign::Minus { + Slot(0) + } else { + let v: u64 = value.try_into().unwrap_or(u64::MAX); + Slot::from_saturating(v) + } + } +} + +impl From for BigInt { + fn from(val: Slot) -> Self { + BigInt::from(val.0) + } +} + +#[cfg(test)] +mod tests { + use num_bigint::BigInt; + + use super::*; + + #[test] + fn test_from_bigint_to_slot_positive() { + const N: u64 = 12345; + let big_int = BigInt::from(N); // positive BigInt + let slot: Slot = big_int.into(); + assert_eq!(slot.0, N); + } + + #[test] + fn test_from_bigint_to_slot_negative() { + let big_int = BigInt::from(-12345); // negative BigInt + let slot: Slot = big_int.into(); + assert_eq!(slot.0, 0); + } + + #[test] + fn test_from_bigint_to_slot_large_value() { + let big_int = BigInt::from(u128::MAX); // large BigInt that exceeds u64 + let slot: Slot = big_int.into(); // should saturate to u64::MAX + assert_eq!(slot.0, u64::MAX); + } + + #[test] + fn test_from_slot_to_bigint_positive() { + const N: u64 = 12345; + let slot = Slot(N); + let big_int: BigInt = slot.into(); // should convert back to BigInt + assert_eq!(big_int, BigInt::from(N)); + } + + #[test] + fn test_from_slot_to_bigint_zero() { + const N: u64 = 0; + let slot = Slot(N); + let big_int: BigInt = slot.into(); // should convert to BigInt::from(0) + assert_eq!(big_int, BigInt::from(N)); + } + + #[test] + fn test_from_slot_to_bigint_large_value() { + let slot = Slot(u64::MAX); + let big_int: BigInt = slot.into(); // should convert to BigInt::from(u64::MAX) + assert_eq!(big_int, BigInt::from(u64::MAX)); + } +} diff --git a/rust/cardano-blockchain-types/src/txn_index.rs b/rust/cardano-blockchain-types/src/txn_index.rs index 036bbf7e99..716117c4b7 100644 --- a/rust/cardano-blockchain-types/src/txn_index.rs +++ b/rust/cardano-blockchain-types/src/txn_index.rs @@ -6,18 +6,79 @@ use crate::conversion::from_saturating; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct TxnIndex(u16); -impl TxnIndex { - /// Convert an `` to transaction index (saturate if out of range). - pub(crate) fn from_saturating< +impl< T: Copy + TryInto + std::ops::Sub + std::cmp::PartialOrd + num_traits::identities::Zero, - >( - value: T, - ) -> Self { - let value: u16 = from_saturating(value); - Self(value) + > From for TxnIndex +{ + fn from(value: T) -> Self { + Self(from_saturating(value)) + } +} + +impl From for i16 { + fn from(val: TxnIndex) -> Self { + i16::try_from(val.0).unwrap_or(i16::MAX) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Test for `From` conversion to `TxnIndex` + #[test] + fn test_from_u8_to_txn_index() { + let txn_index: TxnIndex = 100u8.into(); // u8 is a valid type for conversion + assert_eq!(txn_index.0, 100); + } + + #[test] + fn test_from_u16_to_txn_index() { + let txn_index: TxnIndex = 500u16.into(); // u16 is valid and within range for `TxnIndex` + assert_eq!(txn_index.0, 500); + } + + #[test] + fn test_from_i32_to_txn_index() { + let txn_index: TxnIndex = 1234i32.into(); // i32 can be converted into `TxnIndex` + assert_eq!(txn_index.0, 1234); + } + + #[test] + fn test_from_u32_to_txn_index() { + let txn_index: TxnIndex = 500_000u32.into(); // u32 is larger but should be saturated to `u16::MAX` + assert_eq!(txn_index.0, u16::MAX); + } + + #[test] + fn test_from_large_i32_to_txn_index() { + let txn_index: TxnIndex = 70000i32.into(); // i32 too large for u16, should saturate to `u16::MAX` + assert_eq!(txn_index.0, u16::MAX); + } + + // Test for `From` conversion to `i16` + #[test] + fn test_txn_index_to_i16_within_range() { + let txn_index = TxnIndex(100); + let result: i16 = txn_index.into(); // Should successfully convert to i16 + assert_eq!(result, 100); + } + + #[test] + fn test_txn_index_to_i16_with_saturation() { + let txn_index = TxnIndex(u16::MAX); // u16::MAX = 65535, which is too large for i16 + let result: i16 = txn_index.into(); // Should saturate to i16::MAX + assert_eq!(result, i16::MAX); + } + + #[test] + fn test_txn_index_to_i16_with_zero() { + let txn_index = TxnIndex(0); // Should be able to convert to i16 without issue + let result: i16 = txn_index.into(); + assert_eq!(result, 0); } } diff --git a/rust/cardano-blockchain-types/src/txn_witness.rs b/rust/cardano-blockchain-types/src/txn_witness.rs index 42f86e26b0..5d7a7977f9 100644 --- a/rust/cardano-blockchain-types/src/txn_witness.rs +++ b/rust/cardano-blockchain-types/src/txn_witness.rs @@ -37,7 +37,7 @@ impl TxnWitness { for vkey_witness in vkey_witness_set { let vkey = vkey_from_bytes(&vkey_witness.vkey)?; let vkey_hash = VKeyHash::new(vkey.as_ref()); - let tx_num = TxnIndex::from_saturating(i); + let tx_num = i.into(); if let Some(entry) = map.get(&vkey_hash) { entry.1.insert(tx_num); } else { @@ -125,7 +125,7 @@ mod tests { .expect("Failed to decode vkey1_hash"); println!("{tx_witness_alonzo}"); assert!(tx_witness_alonzo.get_witness_vkey(&vkey1_hash).is_some()); - assert!(tx_witness_alonzo.check_witness_in_tx(&vkey1_hash, TxnIndex::from_saturating(0))); + assert!(tx_witness_alonzo.check_witness_in_tx(&vkey1_hash, 0.into())); let babbage = babbage_block(); let babbage_block = pallas::ledger::traverse::MultiEraBlock::decode(&babbage) @@ -139,6 +139,6 @@ mod tests { println!("{tx_witness_babbage}"); assert!(tx_witness_babbage.get_witness_vkey(&vkey2_hash).is_some()); - assert!(tx_witness_babbage.check_witness_in_tx(&vkey2_hash, TxnIndex::from_saturating(0))); + assert!(tx_witness_babbage.check_witness_in_tx(&vkey2_hash, 0.into())); } }