diff --git a/rust/cardano-blockchain-types/src/cip36/key_registration.rs b/rust/cardano-blockchain-types/src/cip36/key_registration.rs new file mode 100644 index 0000000000..54ebc12a2a --- /dev/null +++ b/rust/cardano-blockchain-types/src/cip36/key_registration.rs @@ -0,0 +1,368 @@ +//! CIP-36 Key Registration 61284. +//! +//! Catalyst registration data +//! +//! +//! + +use std::collections::HashSet; + +use anyhow::Context; +use ed25519_dalek::VerifyingKey; +use minicbor::{decode, Decode, Decoder}; +use pallas::ledger::addresses::{Address, ShelleyAddress}; +use strum::FromRepr; + +use super::voting_pk::VotingPubKey; +use crate::utils::decode_helper::{decode_array_len, decode_bytes, decode_helper, decode_map_len}; + +/// 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 struct Cip36KeyRegistration { + /// Is this CIP36 or CIP15 format. + 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. + pub stake_pk: VerifyingKey, + /// Payment Address to associate with the voting keys. + /// Field 3 in the CIP-36 61284 Spec. + pub payment_addr: Option, + /// Nonce (nonce that has been slot corrected). + /// Field 4 in the CIP-36 61284 Spec. + pub nonce: u64, + /// Registration Purpose (Always 0 for Catalyst). + /// Field 5 in the CIP-36 61284 Spec. + pub purpose: u64, + /// Raw nonce (nonce that has not had slot correction applied). + pub raw_nonce: u64, + /// Is payment address payable? (not a script) + pub is_payable: bool, +} + +/// Enum of CIP36 registration (61284) with its associated unsigned integer key. +#[derive(FromRepr, Debug, PartialEq)] +#[repr(u16)] +pub enum Cip36KeyRegistrationKeys { + /// Voting key. + VotingKey = 1, + /// Stake public key. + StakePk = 2, + /// Payment address. + PaymentAddr = 3, + /// Nonce. + Nonce = 4, + /// Purpose. + Purpose = 5, +} + +impl Decode<'_, ()> for Cip36KeyRegistration { + fn decode(d: &mut Decoder, ctx: &mut ()) -> 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: HashSet = HashSet::new(); + + for _ in 0..map_len { + let key: u16 = decode_helper(d, "key in CIP36 Key Registration", ctx)?; + + if let Some(key) = Cip36KeyRegistrationKeys::from_repr(key) { + match key { + Cip36KeyRegistrationKeys::VotingKey => { + if !found_keys.insert(key as u16) { + return Err(decode::Error::message( + "Duplicate key in CIP36 Key Registration voting key", + )); + } + let (is_cip36, voting_keys) = decode_voting_key(d)?; + cip36_key_registration.is_cip36 = Some(is_cip36); + cip36_key_registration.voting_pks = voting_keys; + }, + Cip36KeyRegistrationKeys::StakePk => { + if !found_keys.insert(key as u16) { + return Err(decode::Error::message( + "Duplicate key in CIP36 Key Registration stake public key", + )); + } + let stake_pk = decode_stake_pk(d)?; + cip36_key_registration.stake_pk = stake_pk; + }, + Cip36KeyRegistrationKeys::PaymentAddr => { + if !found_keys.insert(key as u16) { + return Err(decode::Error::message( + "Duplicate key in CIP36 Key Registration payment address", + )); + } + let shelley_addr = decode_payment_addr(d)?; + cip36_key_registration.payment_addr = Some(shelley_addr.clone()); + cip36_key_registration.is_payable = !shelley_addr.payment().is_script(); + }, + Cip36KeyRegistrationKeys::Nonce => { + if !found_keys.insert(key as u16) { + return Err(decode::Error::message( + "Duplicate key in CIP36 Key Registration nonce", + )); + } + let raw_nonce = decode_nonce(d)?; + cip36_key_registration.raw_nonce = raw_nonce; + }, + Cip36KeyRegistrationKeys::Purpose => { + if !found_keys.insert(key as u16) { + return Err(decode::Error::message( + "Duplicate key in CIP36 Key Registration purpose", + )); + } + let purpose = decode_purpose(d)?; + cip36_key_registration.purpose = purpose; + }, + } + } + } + + // Check if all the required keys are present. + if found_keys.contains(&(Cip36KeyRegistrationKeys::VotingKey as u16)) + && found_keys.contains(&(Cip36KeyRegistrationKeys::StakePk as u16)) + && found_keys.contains(&(Cip36KeyRegistrationKeys::PaymentAddr as u16)) + && found_keys.contains(&(Cip36KeyRegistrationKeys::Nonce as u16)) + { + Ok(cip36_key_registration) + } else { + Err(decode::Error::message( + "Missing required key in CIP36 Key Registration", + )) + } + } +} + +/// 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 +/// format and a vector of voting public keys. +fn decode_voting_key(d: &mut Decoder) -> Result<(bool, Vec), decode::Error> { + let mut voting_keys = Vec::new(); + let mut is_cip36 = false; + + match d.datatype()? { + // 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 => { + let pub_key = decode_bytes(d, "CIP36 Key Registration voting key, single voting key")?; + let vk = voting_pk_vec_to_verifying_key(&pub_key).map_err(|e| { + decode::Error::message(format!( + "CIP36 Key Registration voting key, singe voting key, {e}" + )) + })?; + // Since there is 1 voting key, all the weight goes to this key = 1. + voting_keys.push(VotingPubKey { + voting_pk: vk, + weight: 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 = 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).map_err(|e| { + decode::Error::message(format!( + "CIP36 Key Registration voting key, multiple voting keys, {e}" + )) + })?; + + voting_keys.push(VotingPubKey { + voting_pk: vk, + weight, + }); + } + }, + _ => { + return Err(decode::Error::message( + "Invalid datatype for CIP36 Key Registration voting key", + )) + }, + } + Ok((is_cip36, voting_keys)) +} + +/// Helper function for converting `&[u8]` to `VerifyingKey`. +fn voting_pk_vec_to_verifying_key(pub_key: &[u8]) -> anyhow::Result { + let bytes = pub_key.try_into().context("Invalid verifying key length")?; + VerifyingKey::from_bytes(bytes).context("Failed to convert to VerifyingKey") +} + +/// 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`. +fn decode_stake_pk(d: &mut Decoder) -> Result { + let pub_key = decode_bytes(d, "CIP36 Key Registration stake public key")?; + voting_pk_vec_to_verifying_key(&pub_key).map_err(|e| { + decode::Error::message(format!("CIP36 Key Registration stake public key, {e}")) + }) +} + +/// Helper function for decoding the payment address. +/// +/// ```cddl +/// 3 : $payment_address, +/// $payment_address /= bytes +/// ``` +/// +/// # Returns +/// +/// The payment address as a `ShelleyAddress`. +fn decode_payment_addr(d: &mut Decoder) -> Result { + let raw_addr = decode_bytes(d, "CIP36 Key Registration payment address")?; + let address = Address::from_bytes(&raw_addr).map_err(|e| { + decode::Error::message(format!("CIP36 Key Registration payment address, {e}")) + })?; + if let Address::Shelley(addr) = address { + Ok(addr.clone()) + } else { + Err(decode::Error::message(format!( + "Invalid CIP36 Key Registration payment address, expected Shelley address, got {address}" + ))) + } +} + +/// Helper function for decoding raw nonce. +/// +/// ```cddl +/// 4 : $nonce, +/// $nonce /= uint +/// ``` +/// +/// # Returns +/// +/// Raw nonce. +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. +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 address = decode_payment_addr(&mut decoder); + 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 stake_pk = decode_stake_pk(&mut decoder); + assert!(stake_pk.is_ok()); + } + + #[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 (is_cip36, voting_pk) = decode_voting_key(&mut decoder).expect("Failed to decode"); + + assert!(is_cip36); + 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 (is_cip36, voting_pk) = decode_voting_key(&mut decoder).expect("Failed to decode"); + + assert!(!is_cip36); + assert_eq!(voting_pk.len(), 1); + } +} diff --git a/rust/cardano-blockchain-types/src/cip36/mod.rs b/rust/cardano-blockchain-types/src/cip36/mod.rs new file mode 100644 index 0000000000..b21f905871 --- /dev/null +++ b/rust/cardano-blockchain-types/src/cip36/mod.rs @@ -0,0 +1,143 @@ +//! CIP-36 Catalyst registration module + +pub mod key_registration; +pub mod registration_witness; +mod validation; +pub mod voting_pk; + +use ed25519_dalek::VerifyingKey; +use key_registration::Cip36KeyRegistration; +use pallas::ledger::addresses::ShelleyAddress; +use registration_witness::Cip36RegistrationWitness; +use validation::{validate_payment_address_network, validate_signature, validate_voting_keys}; +use voting_pk::VotingPubKey; + +use crate::{MetadatumValue, Network}; + +/// CIP-36 Catalyst registration +#[derive(Clone, Default, Debug)] +pub struct Cip36 { + /// Key registration - 61284 + key_registration: Cip36KeyRegistration, + /// Registration witness - 61285 + registration_witness: Cip36RegistrationWitness, + /// Is this a Catalyst strict registration? + is_catalyst_strict: bool, +} + +/// Validation value for CIP-36. +#[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)] +#[derive(Clone, Default, Debug)] +pub struct Cip36Validation { + /// Is the signature valid? (signature in 61285) + pub is_valid_signature: bool, + /// Is the payment address on the correct network? + pub is_valid_payment_address_network: bool, + /// Is the voting keys valid? + pub is_valid_voting_keys: bool, + /// Is the purpose valid? (Always 0 for Catalyst) + pub is_valid_purpose: bool, +} + +impl Cip36 { + /// Create an instance of CIP-36. + #[must_use] + pub fn new( + key_registration: Cip36KeyRegistration, registration_witness: Cip36RegistrationWitness, + is_catalyst_strict: bool, + ) -> Self { + Self { + key_registration, + registration_witness, + is_catalyst_strict, + } + } + + /// Get the `is_cip36` flag from the registration. + /// True if it is CIP-36 format, false if CIP-15 format. + #[must_use] + pub fn is_cip36(&self) -> Option { + self.key_registration.is_cip36 + } + + /// Get the voting public keys from the registration. + #[must_use] + pub fn voting_pks(&self) -> &Vec { + &self.key_registration.voting_pks + } + + /// Get the stake public key from the registration. + #[must_use] + pub fn stake_pk(&self) -> VerifyingKey { + self.key_registration.stake_pk + } + + /// Get the payment address from the registration. + #[must_use] + pub fn payment_address(&self) -> Option<&ShelleyAddress> { + self.key_registration.payment_addr.as_ref() + } + + /// Get the nonce from the registration. + #[must_use] + pub fn nonce(&self) -> u64 { + self.key_registration.nonce + } + + /// Get the purpose from the registration. + #[must_use] + pub fn purpose(&self) -> u64 { + self.key_registration.purpose + } + + /// Get the raw nonce from the registration. + #[must_use] + pub fn raw_nonce(&self) -> u64 { + self.key_registration.raw_nonce + } + + /// Get the signature from the registration witness. + #[must_use] + pub fn signature(&self) -> Option { + self.registration_witness.signature + } + + /// Get the Catalyst strict flag. + #[must_use] + pub fn is_strict_catalyst(&self) -> bool { + self.is_catalyst_strict + } + + /// Validation 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. + /// + /// # Parameters + /// + /// * `network` - The blockchain network. + /// * `metadata` - The metadata value to be validated. + /// * `validation_report` - Validation report to store the validation result. + pub fn validate( + &self, network: Network, metadata: &MetadatumValue, validation_report: &mut Vec, + ) -> Cip36Validation { + let is_valid_signature = validate_signature(self, metadata, validation_report); + let is_valid_payment_address_network = + validate_payment_address_network(self, network, validation_report).unwrap_or_default(); + let is_valid_voting_keys = validate_voting_keys(self, validation_report); + let is_valid_purpose = validation::validate_purpose(self, validation_report); + + Cip36Validation { + is_valid_signature, + is_valid_payment_address_network, + is_valid_voting_keys, + is_valid_purpose, + } + } +} diff --git a/rust/cardano-blockchain-types/src/cip36/registration_witness.rs b/rust/cardano-blockchain-types/src/cip36/registration_witness.rs new file mode 100644 index 0000000000..9d818c629b --- /dev/null +++ b/rust/cardano-blockchain-types/src/cip36/registration_witness.rs @@ -0,0 +1,49 @@ +//! CIP36 registration witness 61285 +//! +//! +//! + +use minicbor::{decode, Decode, Decoder}; + +use crate::utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; + +/// CIP-36 registration witness - 61285 +/// +/// ```cddl +/// registration_witness = { +/// 1 : $stake_witness +/// } +/// ``` +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Default, Debug)] +pub struct Cip36RegistrationWitness { + /// Signature of the registration data. + pub signature: Option, +} + +impl Decode<'_, ()> for Cip36RegistrationWitness { + fn decode(d: &mut Decoder, ctx: &mut ()) -> 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", ctx)?; + + // The key needs to be 1. + if key != 1 { + return Err(decode::Error::message(format!( + "Invalid CIP36 Registration Witness key, expected key 1, got {key}" + ))); + } + + let sig_bytes = decode_bytes(d, "CIP36 Registration Witness signature")?; + let signature = ed25519_dalek::Signature::from_slice(&sig_bytes).ok(); + + Ok(Cip36RegistrationWitness { signature }) + } +} diff --git a/rust/cardano-blockchain-types/src/cip36/validation.rs b/rust/cardano-blockchain-types/src/cip36/validation.rs new file mode 100644 index 0000000000..ee043b5acf --- /dev/null +++ b/rust/cardano-blockchain-types/src/cip36/validation.rs @@ -0,0 +1,215 @@ +//! Validation function for CIP-36 + +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]; + +/// Validate the signature against the public key. +#[allow(clippy::too_many_lines)] +pub(crate) fn validate_signature( + cip36: &Cip36, metadata: &MetadatumValue, validation_report: &mut Vec, +) -> bool { + let hash = blake2b_simd::Params::new() + .hash_length(32) + .to_state() + .update(&SIGNDATA_PREAMBLE) + .update(metadata.as_ref()) + .finalize(); + + let Some(sig) = cip36.signature() else { + validation_report.push("Validate CIP36 Signature, signature is invalid".to_string()); + return false; + }; + + if let Ok(()) = cip36.stake_pk().verify_strict(hash.as_bytes(), &sig) { + true + } else { + validation_report.push("Validate CIP36 Signature, cannot verify signature".to_string()); + false + } +} + +/// Validate the payment address network against the given network. +pub(crate) fn validate_payment_address_network( + cip36: &Cip36, network: Network, validation_report: &mut Vec, +) -> Option { + if let Some(address) = cip36.payment_address() { + let network_tag = address.network(); + let valid = match network { + Network::Mainnet => network_tag.value() == 1, + Network::Preprod | Network::Preview => network_tag.value() == 0, + }; + if !valid { + validation_report.push(format!( + "Validate CIP36 payment address network, network Tag of payment address {network_tag:?} does not match the network used", + )); + } + + Some(valid) + } else { + None + } +} + +/// Validate the voting keys. +pub(crate) fn validate_voting_keys(cip36: &Cip36, validation_report: &mut Vec) -> bool { + if cip36.is_strict_catalyst() && cip36.voting_pks().len() != 1 { + validation_report.push(format!( + "Validate CIP-36 Voting Keys, Catalyst supports only a single voting key per registration, found {}", + cip36.voting_pks().len() + )); + return false; + } + true +} + +/// Validate the purpose. +pub(crate) fn validate_purpose(cip36: &Cip36, validation_report: &mut Vec) -> bool { + if cip36.is_strict_catalyst() && cip36.purpose() != PROJECT_CATALYST_PURPOSE { + validation_report.push(format!( + "Validate CIP-36 Purpose, registration contains unknown purpose: {}", + cip36.purpose() + )); + return false; + } + true +} + +#[cfg(test)] +mod tests { + + use ed25519_dalek::VerifyingKey; + use pallas::ledger::addresses::Address; + + use super::validate_purpose; + use crate::{ + cip36::{ + key_registration::Cip36KeyRegistration, registration_witness::Cip36RegistrationWitness, + validate_payment_address_network, validate_voting_keys, voting_pk::VotingPubKey, + }, + Cip36, Network, + }; + + fn create_empty_cip36(strict: bool) -> Cip36 { + Cip36 { + key_registration: Cip36KeyRegistration::default(), + registration_witness: Cip36RegistrationWitness::default(), + is_catalyst_strict: strict, + } + } + + #[test] + fn test_validate_payment_address_network() { + let mut cip36 = create_empty_cip36(true); + // 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") + }; + cip36.key_registration.payment_addr = Some(shelley_addr); + let mut report = Vec::new(); + + let valid = validate_payment_address_network(&cip36, Network::Preprod, &mut report); + + assert_eq!(report.len(), 0); + assert_eq!(valid, Some(true)); + } + + #[test] + fn test_validate_invalid_payment_address_network() { + let mut cip36 = create_empty_cip36(true); + // 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") + }; + cip36.key_registration.payment_addr = Some(shelley_addr); + let mut report = Vec::new(); + + let valid = validate_payment_address_network(&cip36, Network::Mainnet, &mut report); + + assert_eq!(report.len(), 1); + assert!(report + .first() + .expect("Failed to get the first index") + .contains("does not match the network used")); + assert_eq!(valid, Some(false)); + } + + #[test] + fn test_validate_voting_keys() { + let mut cip36 = create_empty_cip36(true); + cip36.key_registration.voting_pks.push(VotingPubKey { + voting_pk: VerifyingKey::default(), + weight: 1, + }); + let mut report = Vec::new(); + + let valid = validate_voting_keys(&cip36, &mut report); + + assert_eq!(report.len(), 0); + assert!(valid); + } + + #[test] + fn test_validate_invalid_voting_keys() { + let mut cip36 = create_empty_cip36(true); + cip36.key_registration.voting_pks.push(VotingPubKey { + voting_pk: VerifyingKey::default(), + weight: 1, + }); + cip36.key_registration.voting_pks.push(VotingPubKey { + voting_pk: VerifyingKey::default(), + weight: 1, + }); + let mut report = Vec::new(); + + let valid = validate_voting_keys(&cip36, &mut report); + + assert_eq!(report.len(), 1); + assert!(report + .first() + .expect("Failed to get the first index") + .contains("Catalyst supports only a single voting key")); + assert!(!valid); + } + + #[test] + fn test_validate_purpose() { + let cip36 = create_empty_cip36(true); + let mut report = Vec::new(); + + let valid = validate_purpose(&cip36, &mut report); + + assert_eq!(report.len(), 0); + assert_eq!(cip36.purpose(), 0); + assert!(valid); + } + + #[test] + fn test_validate_invalid_purpose() { + let mut cip36 = create_empty_cip36(true); + cip36.key_registration.purpose = 1; + let mut report = Vec::new(); + + let valid = validate_purpose(&cip36, &mut report); + + assert_eq!(report.len(), 1); + assert!(report + .first() + .expect("Failed to get the first index") + .contains("unknown purpose")); + assert_eq!(cip36.purpose(), 1); + assert!(!valid); + } +} diff --git a/rust/cardano-blockchain-types/src/cip36/voting_pk.rs b/rust/cardano-blockchain-types/src/cip36/voting_pk.rs new file mode 100644 index 0000000000..6f3d70aa75 --- /dev/null +++ b/rust/cardano-blockchain-types/src/cip36/voting_pk.rs @@ -0,0 +1,13 @@ +//! Voting public key containing the public key and weight. + +use ed25519_dalek::VerifyingKey; + +/// Voting public key containing the public key and weight. +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct VotingPubKey { + /// Voting public key. + pub voting_pk: VerifyingKey, + /// Voting key associated weight. + pub weight: u32, +} diff --git a/rust/cardano-blockchain-types/src/lib.rs b/rust/cardano-blockchain-types/src/lib.rs index bfc4bff9fe..0a7b091e69 100644 --- a/rust/cardano-blockchain-types/src/lib.rs +++ b/rust/cardano-blockchain-types/src/lib.rs @@ -1,6 +1,7 @@ //! Catalyst Enhanced `MultiEraBlock` Structures mod auxdata; +mod cip36; pub mod conversion; mod fork; pub mod hashes; @@ -10,6 +11,7 @@ mod point; mod slot; mod txn_index; mod txn_witness; +pub mod utils; pub use auxdata::{ aux_data::TransactionAuxData, @@ -19,6 +21,10 @@ pub use auxdata::{ metadatum_value::MetadatumValue, scripts::{Script, ScriptArray, ScriptType, TransactionScripts}, }; +pub use cip36::{ + key_registration::Cip36KeyRegistration, registration_witness::Cip36RegistrationWitness, + voting_pk::VotingPubKey, Cip36, Cip36Validation, +}; pub use fork::Fork; pub use multi_era_block_data::MultiEraBlock; pub use network::Network; diff --git a/rust/cardano-blockchain-types/src/utils/decode_helper.rs b/rust/cardano-blockchain-types/src/utils/decode_helper.rs new file mode 100644 index 0000000000..7ebf2d7afb --- /dev/null +++ b/rust/cardano-blockchain-types/src/utils/decode_helper.rs @@ -0,0 +1,238 @@ +//! CBOR decoding helper functions. + +use minicbor::{data::Tag, decode, Decoder}; + +/// Generic helper function for decoding different types. +/// +/// # Errors +/// +/// Error if the decoding fails. +pub 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. +/// +/// # Errors +/// +/// Error if the decoding fails. +pub 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. +/// +/// # Errors +/// +/// Error if the decoding fails. +pub 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. +/// +/// # Errors +/// +/// Error if the decoding fails. +pub 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. +/// +/// # Errors +/// +/// Error if the decoding fails. +pub 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 +/// +/// # Errors +/// +/// Error if the decoding fails. +pub 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 + ); + } + + #[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()); + } +} diff --git a/rust/cardano-blockchain-types/src/utils/mod.rs b/rust/cardano-blockchain-types/src/utils/mod.rs new file mode 100644 index 0000000000..7c2dd72ccf --- /dev/null +++ b/rust/cardano-blockchain-types/src/utils/mod.rs @@ -0,0 +1,3 @@ +//! Utility functions. + +pub mod decode_helper;