diff --git a/rust/cardano-blockchain-types/Cargo.toml b/rust/cardano-blockchain-types/Cargo.toml index 4fdad5567a5..01a04d4c1b2 100644 --- a/rust/cardano-blockchain-types/Cargo.toml +++ b/rust/cardano-blockchain-types/Cargo.toml @@ -21,6 +21,7 @@ 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" } ouroboros = "0.18.4" tracing = "0.1.41" diff --git a/rust/cardano-blockchain-types/src/cip36/key_registration.rs b/rust/cardano-blockchain-types/src/cip36/key_registration.rs deleted file mode 100644 index 54ebc12a2ae..00000000000 --- a/rust/cardano-blockchain-types/src/cip36/key_registration.rs +++ /dev/null @@ -1,368 +0,0 @@ -//! 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 deleted file mode 100644 index b21f9058714..00000000000 --- a/rust/cardano-blockchain-types/src/cip36/mod.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! 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/validation.rs b/rust/cardano-blockchain-types/src/cip36/validation.rs deleted file mode 100644 index ee043b5acf8..00000000000 --- a/rust/cardano-blockchain-types/src/cip36/validation.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! 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 deleted file mode 100644 index 6f3d70aa751..00000000000 --- a/rust/cardano-blockchain-types/src/cip36/voting_pk.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! 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 0a7b091e691..b2ae2a1bf96 100644 --- a/rust/cardano-blockchain-types/src/lib.rs +++ b/rust/cardano-blockchain-types/src/lib.rs @@ -1,17 +1,16 @@ //! Catalyst Enhanced `MultiEraBlock` Structures mod auxdata; -mod cip36; pub mod conversion; mod fork; pub mod hashes; +mod metadata; mod multi_era_block_data; mod network; mod point; mod slot; mod txn_index; mod txn_witness; -pub mod utils; pub use auxdata::{ aux_data::TransactionAuxData, @@ -21,11 +20,8 @@ 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 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 00000000000..b6e47ff52ed --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/key_registration.rs @@ -0,0 +1,483 @@ +//! CIP-36 Key Registration 61284. +//! +//! Catalyst registration data +//! +//! +//! + +use anyhow::Context; +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<'_, ()> 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: Vec = Vec::new(); + + // Record of errors found during decoding + let mut err_report = Vec::new(); + + for index 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.contains(&key) { + err_report.push(format!( + "Duplicate key in CIP36 Key Registration voting key at item {} in map", index + 1), + ); + continue; + } + if let Some((is_cip36, voting_keys)) = decode_voting_key(d, &mut err_report) + { + cip36_key_registration.is_cip36 = is_cip36; + cip36_key_registration.voting_pks = voting_keys; + } + }, + Cip36KeyRegistrationKeys::StakePk => { + if found_keys.contains(&key) { + err_report.push(format!( + "Duplicate key in CIP36 Key Registration stake public key at item {} in map", index + 1), + ); + continue; + } + if let Some(stake_pk) = decode_stake_pk(d, &mut err_report) { + cip36_key_registration.stake_pk = Some(stake_pk); + } + }, + Cip36KeyRegistrationKeys::PaymentAddr => { + if found_keys.contains(&key) { + err_report.push(format!( + "Duplicate key in CIP36 Key Registration payment address at item {} in map", index + 1), + ); + continue; + } + if let Some(shelley_addr) = decode_payment_addr(d, &mut err_report) { + cip36_key_registration.payment_addr = Some(shelley_addr.clone()); + cip36_key_registration.is_payable = + Some(!shelley_addr.payment().is_script()); + } + }, + Cip36KeyRegistrationKeys::Nonce => { + if found_keys.contains(&key) { + err_report.push(format!( + "Duplicate key in CIP36 Key Registration nonce at item {} in map", + index + 1 + )); + continue; + } + if let Some(nonce) = decode_nonce(d, &mut err_report) { + cip36_key_registration.raw_nonce = Some(nonce); + } + }, + Cip36KeyRegistrationKeys::Purpose => { + if found_keys.contains(&key) { + err_report.push(format!( + "Duplicate key in CIP36 Key Registration purpose at item {} in map", + index + 1 + )); + continue; + } + if let Some(purpose) = decode_purpose(d, &mut err_report) { + cip36_key_registration.purpose = purpose; + } + }, + } + // Update the founded keys. + found_keys.push(key); + } + } + + if !found_keys.contains(&Cip36KeyRegistrationKeys::VotingKey) { + err_report + .push("Missing required key in CIP36 Key Registration: Voting Key".to_string()); + } + + if !found_keys.contains(&Cip36KeyRegistrationKeys::StakePk) { + err_report.push( + "Missing required key in CIP36 Key Registration: Stake Public Key".to_string(), + ); + } + + if !found_keys.contains(&Cip36KeyRegistrationKeys::PaymentAddr) { + err_report.push( + "Missing required key in CIP36 Key Registration: Payment Address".to_string(), + ); + } + + if !found_keys.contains(&Cip36KeyRegistrationKeys::Nonce) { + err_report.push("Missing required key in CIP36 Key Registration: Nonce".to_string()); + } + + if !err_report.is_empty() { + return Err(decode::Error::message(format!("{err_report:?}"))); + } + + Ok(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, 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. +/// - Return None if there is an error with decoding. +fn decode_voting_key( + d: &mut Decoder, err_report: &mut Vec, +) -> Option<(Option, Vec)> { + let mut voting_keys = Vec::new(); + 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") + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok()?; + let vk = match voting_pk_vec_to_verifying_key(&pub_key) { + Ok(vk) => Some(vk), + Err(e) => { + err_report.push(format!( + "CIP36 Key Registration voting key, single voting key, {e}" + )); + None + }, + }; + + // 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", + ) + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok()?; + + for _ in 0..len { + let len = + decode_array_len(d, "CIP36 Key Registration voting key, delegations") + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok()?; + // This fixed array should be a length of 2 (voting key, weight). + if len != 2 { + err_report.push(format!("Invalid length for CIP36 Key Registration voting key delegations, expected 2, got {len}")); + return None; + } + // The first entry. + let pub_key = decode_bytes(d, "CIP36 Key Registration voting key, delegation array first entry (voting public key)").map_err(|e| { + err_report.push(format!("{e}")); + }).ok()?; + + // The second entry. + let weight: u32 = decode_helper(d, "CIP36 Key Registration voting key, delegation array second entry (weight)", &mut (),).map_err(|e| { + err_report.push(format!("{e}")); + }).ok()?; + + let vk = match voting_pk_vec_to_verifying_key(&pub_key) { + Ok(vk) => Some(vk), + Err(e) => { + err_report.push(format!( + "CIP36 Key Registration voting key, multiple voting keys, {e}" + )); + // Don't early return, continue with the next key. + None + }, + }; + + voting_keys.push(VotingPubKey::new(vk, weight)); + } + }, + + _ => { + err_report.push("Invalid datatype for CIP36 Key Registration voting key, should be either Array or Bytes".to_string()); + }, + } + }, + Err(e) => { + err_report.push(format!("Decoding voting key, invalid data type: {e}")); + return None; + }, + } + Some((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(format!( + "Invalid verifying key length got {}", + pub_key.len() + ))?; + VerifyingKey::from_bytes(bytes) + .map_err(|e| anyhow::anyhow!("Failed to convert to VerifyingKey: {:?}", e)) +} + +/// 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` or decoding error. +fn decode_stake_pk(d: &mut Decoder, err_report: &mut Vec) -> Option { + let pub_key = decode_bytes(d, "CIP36 Key Registration stake public key") + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok()?; + match voting_pk_vec_to_verifying_key(&pub_key) { + Ok(vk) => Some(vk), + Err(e) => { + err_report.push(format!( + "CIP36 Key Registration voting key, multiple voting keys, {e}" + )); + None + }, + } +} + +/// 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` or decoding error. +fn decode_payment_addr(d: &mut Decoder, err_report: &mut Vec) -> Option { + let raw_addr = decode_bytes(d, "CIP36 Key Registration payment address") + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok()?; + let address = Address::from_bytes(&raw_addr) + .map_err(|e| err_report.push(format!("CIP36 Key Registration payment address, {e}"))) + .ok()?; + + if let Address::Shelley(addr) = address { + Some(addr.clone()) + } else { + err_report.push(format!( + "Invalid CIP36 Key Registration payment address, expected Shelley address, got {address}" + )); + None + } +} + +/// Helper function for decoding raw nonce. +/// +/// ```cddl +/// 4 : $nonce, +/// $nonce /= uint +/// ``` +/// +/// # Returns +/// +/// Raw nonce. +/// None if decoding error. +fn decode_nonce(d: &mut Decoder, err_report: &mut Vec) -> Option { + decode_helper(d, "CIP36 Key Registration nonce", &mut ()) + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok() +} + +/// Helper function for decoding the purpose. +/// +/// ```cddl +/// 5 : $voting_purpose .default 0 +/// $voting_purpose /= uint +/// ``` +/// +/// # Returns +/// +/// The purpose. +fn decode_purpose(d: &mut Decoder, err_report: &mut Vec) -> Option { + decode_helper(d, "CIP36 Key Registration purpose", &mut ()) + .map_err(|e| { + err_report.push(format!("{e}")); + }) + .ok() +} + +#[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 mut err_report = Vec::new(); + let address = decode_payment_addr(&mut decoder, &mut err_report); + assert!(err_report.is_empty()); + 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 mut err_report = Vec::new(); + let stake_pk = decode_stake_pk(&mut decoder, &mut err_report); + assert!(err_report.is_empty()); + 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 mut err_report = Vec::new(); + let (is_cip36, voting_pk) = decode_voting_key(&mut decoder, &mut err_report).unwrap(); + assert!(err_report.is_empty()); + 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 mut err_report = Vec::new(); + let (is_cip36, voting_pk) = decode_voting_key(&mut decoder, &mut err_report).unwrap(); + assert!(err_report.is_empty()); + 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 mut err_report = Vec::new(); + let nonce = decode_nonce(&mut decoder, &mut err_report); + assert!(err_report.is_empty()); + assert_eq!(nonce.unwrap(), 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 00000000000..4a8df55afe2 --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/mod.rs @@ -0,0 +1,171 @@ +//! CIP-36 Catalyst registration module + +pub mod key_registration; +pub mod registration_witness; +mod validation; +pub mod voting_pk; + +use anyhow::bail; +use ed25519_dalek::VerifyingKey; +use key_registration::Cip36KeyRegistration; +use minicbor::{Decode, Decoder}; +use pallas::ledger::addresses::ShelleyAddress; +use registration_witness::Cip36RegistrationWitness; +use validation::validate_cip36; +use voting_pk::VotingPubKey; + +use crate::{MetadatumLabel, MultiEraBlock, TxnIndex}; + +/// 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, +} + +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? + /// + /// # 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, + ) -> anyhow::Result { + let Some(k61284) = block.txn_metadata(txn_idx, MetadatumLabel::CIP036_REGISTRATION) else { + bail!("CIP-36 key registration metadata not found") + }; + let Some(k61285) = block.txn_metadata(txn_idx, MetadatumLabel::CIP036_WITNESS) else { + bail!("CIP-36 registration witness metadata not found") + }; + + 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 ()) { + 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) => { + bail!("Failed to construct CIP-36 key registration, {e}") + }, + }; + + let registration_witness = + match Cip36RegistrationWitness::decode(&mut registration_witness, &mut ()) { + Ok(metadata) => metadata, + Err(e) => { + bail!("Failed to construct CIP-36 registration witness {e}") + }, + }; + + let mut validation_report = Vec::new(); + // If the code reach here, then the CIP36 decoding is successful. + let validation = validate_cip36( + &key_registration, + ®istration_witness, + is_catalyst_strict, + network, + k61284, + &mut validation_report, + ); + + let cip36 = Self { + key_registration, + registration_witness, + is_catalyst_strict, + }; + + if validation_report.is_empty() { + Ok(cip36) + } else { + // If there are validation errors, the CIP36 is invalid + bail!("CIP-36 validation failed: {cip36:?}, Validation: {validation:?}, Reports: {validation_report:?}") + } + } + + /// 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) -> Option<&VerifyingKey> { + self.key_registration.stake_pk.as_ref() + } + + /// 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) -> Option { + 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) -> Option { + self.key_registration.raw_nonce + } + + /// Is the payment address in the registration payable? + #[must_use] + pub fn is_payable(&self) -> Option { + self.key_registration.is_payable + } + + /// 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 + } +} diff --git a/rust/cardano-blockchain-types/src/cip36/registration_witness.rs b/rust/cardano-blockchain-types/src/metadata/cip36/registration_witness.rs similarity index 68% rename from rust/cardano-blockchain-types/src/cip36/registration_witness.rs rename to rust/cardano-blockchain-types/src/metadata/cip36/registration_witness.rs index 9d818c629bd..1d3d3ab9b7b 100644 --- a/rust/cardano-blockchain-types/src/cip36/registration_witness.rs +++ b/rust/cardano-blockchain-types/src/metadata/cip36/registration_witness.rs @@ -3,10 +3,9 @@ //! //! +use cbork_utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; use minicbor::{decode, Decode, Decoder}; -use crate::utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; - /// CIP-36 registration witness - 61285 /// /// ```cddl @@ -16,7 +15,7 @@ use crate::utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; /// ``` #[allow(clippy::module_name_repetitions)] #[derive(Clone, Default, Debug)] -pub struct Cip36RegistrationWitness { +pub(crate) struct Cip36RegistrationWitness { /// Signature of the registration data. pub signature: Option, } @@ -25,24 +24,35 @@ impl Decode<'_, ()> for Cip36RegistrationWitness { fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { let map_len = decode_map_len(d, "CIP36 Registration Witness")?; + // Record of errors found during decoding + let mut err_report = Vec::new(); + // Expected only 1 key in the map. if map_len != 1 { - return Err(decode::Error::message(format!( + err_report.push(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!( + err_report.push(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(); + let signature = ed25519_dalek::Signature::from_slice(&sig_bytes) + .map_err(|_| { + err_report.push("Invalid CIP36 Registration Witness signature".to_string()); + }) + .ok(); + + if !err_report.is_empty() { + return Err(decode::Error::message(format!("{err_report:?}"))); + } 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 00000000000..22a5d90500a --- /dev/null +++ b/rust/cardano-blockchain-types/src/metadata/cip36/validation.rs @@ -0,0 +1,287 @@ +//! Validation function for CIP-36 + +use super::{Cip36KeyRegistration, Cip36RegistrationWitness}; +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]; + +/// Validation value for CIP-36. +#[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)] +#[derive(Clone, Default, Debug)] +#[allow(dead_code)] +pub(crate) struct Cip36Validation { + /// 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, +} + +/// 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(crate) fn validate_cip36( + key_registration: &Cip36KeyRegistration, registration_witness: &Cip36RegistrationWitness, + is_strict_catalyst: bool, network: Network, metadata: &MetadatumValue, + validation_report: &mut Vec, +) -> Cip36Validation { + // Need to make sure that when return false, the validation_report is updated. + let is_valid_signature = validate_signature( + key_registration, + registration_witness, + metadata, + validation_report, + ); + let is_valid_payment_address_network = + validate_payment_address_network(key_registration, network, validation_report) + .unwrap_or_default(); + let is_valid_voting_keys = + validate_voting_keys(key_registration, is_strict_catalyst, validation_report); + let is_valid_purpose = + validate_purpose(key_registration, is_strict_catalyst, validation_report); + + Cip36Validation { + is_valid_signature, + is_valid_payment_address_network, + is_valid_voting_keys, + is_valid_purpose, + } +} + +/// Validate the signature against the public key. +#[allow(clippy::too_many_lines)] +fn validate_signature( + key_registration: &Cip36KeyRegistration, registration_witness: &Cip36RegistrationWitness, + 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) = registration_witness.signature else { + validation_report.push("Validate CIP36 Signature, signature is invalid".to_string()); + return false; + }; + + if let Some(stake_pk) = key_registration.stake_pk { + if let Ok(()) = stake_pk.verify_strict(hash.as_bytes(), &sig) { + return true; + } + validation_report.push("Validate CIP36 Signature, cannot verify signature".to_string()); + return false; + } + + validation_report.push("Validate CIP36 Signature, stake public key is missing".to_string()); + false +} + +/// Validate the payment address network against the given network. +fn validate_payment_address_network( + key_registration: &Cip36KeyRegistration, network: Network, validation_report: &mut Vec, +) -> Option { + if let Some(address) = &key_registration.payment_addr { + 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 { + validation_report.push( + "Validate CIP36 payment address network, cannot find payment address in the registration".to_string() + ); + None + } +} + +/// Validate the voting keys. +fn validate_voting_keys( + key_registration: &Cip36KeyRegistration, is_strict_catalyst: bool, + validation_report: &mut Vec, +) -> bool { + if is_strict_catalyst && key_registration.voting_pks.len() != 1 { + validation_report.push(format!( + "Validate CIP-36 Voting Keys, Catalyst supports only a single voting key per registration, found {}", + key_registration.voting_pks.len() + )); + return false; + } + true +} + +/// Validate the purpose. +fn validate_purpose( + key_registration: &Cip36KeyRegistration, is_strict_catalyst: bool, + validation_report: &mut Vec, +) -> bool { + if is_strict_catalyst && key_registration.purpose != PROJECT_CATALYST_PURPOSE { + validation_report.push(format!( + "Validate CIP-36 Purpose, registration contains unknown purpose: {}", + key_registration.purpose + )); + return false; + } + true +} + +#[cfg(test)] +mod tests { + + use ed25519_dalek::VerifyingKey; + use pallas::ledger::addresses::Address; + + use super::validate_purpose; + use crate::{ + metadata::cip36::{ + key_registration::Cip36KeyRegistration, + validation::{validate_payment_address_network, validate_voting_keys}, + voting_pk::VotingPubKey, + }, + Network, + }; + + #[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 key_registration = Cip36KeyRegistration { + payment_addr: Some(shelley_addr), + ..Default::default() + }; + let mut report = Vec::new(); + let valid = + validate_payment_address_network(&key_registration, Network::Preprod, &mut report); + + assert_eq!(report.len(), 0); + assert_eq!(valid, Some(true)); + } + + #[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 key_registration = Cip36KeyRegistration { + payment_addr: Some(shelley_addr), + ..Default::default() + }; + let mut report = Vec::new(); + let valid = + validate_payment_address_network(&key_registration, 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 key_registration = Cip36KeyRegistration::default(); + + key_registration + .voting_pks + .push(VotingPubKey::new(Some(VerifyingKey::default()), 1)); + let mut report = Vec::new(); + + let valid = validate_voting_keys(&key_registration, true, &mut report); + + assert_eq!(report.len(), 0); + assert!(valid); + } + + #[test] + fn test_validate_invalid_voting_keys() { + let mut key_registration = Cip36KeyRegistration::default(); + + key_registration + .voting_pks + .push(VotingPubKey::new(Some(VerifyingKey::default()), 1)); + + key_registration + .voting_pks + .push(VotingPubKey::new(Some(VerifyingKey::default()), 1)); + let mut report = Vec::new(); + + let valid = validate_voting_keys(&key_registration, true, &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 key_registration = Cip36KeyRegistration::default(); + let mut report = Vec::new(); + + let valid = validate_purpose(&key_registration, true, &mut report); + + assert_eq!(report.len(), 0); + assert_eq!(key_registration.purpose, 0); + assert!(valid); + } + + #[test] + fn test_validate_invalid_purpose() { + let key_registration = Cip36KeyRegistration { + purpose: 1, + ..Default::default() + }; + let mut report = Vec::new(); + + let valid = validate_purpose(&key_registration, true, &mut report); + + assert_eq!(report.len(), 1); + assert!(report + .first() + .expect("Failed to get the first index") + .contains("unknown purpose")); + assert_eq!(key_registration.purpose, 1); + assert!(!valid); + } +} 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 00000000000..a35ce68dd59 --- /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 00000000000..a4fc3152d15 --- /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/utils/decode_helper.rs b/rust/cardano-blockchain-types/src/utils/decode_helper.rs deleted file mode 100644 index 7ebf2d7afb1..00000000000 --- a/rust/cardano-blockchain-types/src/utils/decode_helper.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! 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 deleted file mode 100644 index 7c2dd72ccfa..00000000000 --- a/rust/cardano-blockchain-types/src/utils/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Utility functions. - -pub mod decode_helper;