diff --git a/rust/cardano-blockchain-types/Cargo.toml b/rust/cardano-blockchain-types/Cargo.toml index 248bf24ecf8..e4037bd59fe 100644 --- a/rust/cardano-blockchain-types/Cargo.toml +++ b/rust/cardano-blockchain-types/Cargo.toml @@ -2,7 +2,7 @@ name = "cardano-blockchain-types" description = "Common Cardano Blockchain data types for use in both applications and crates" keywords = ["cardano", "catalyst", ] -version = "0.0.2" +version = "0.0.3" authors = [ "Steven Johnson " ] @@ -20,8 +20,8 @@ workspace = true [dependencies] pallas = { 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 = "r20250218-00" } -catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } +cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } +catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } ouroboros = "0.18.4" tracing = "0.1.41" diff --git a/rust/cardano-blockchain-types/src/hashes.rs b/rust/cardano-blockchain-types/src/hashes.rs index 89cd55aefd4..f10902ab579 100644 --- a/rust/cardano-blockchain-types/src/hashes.rs +++ b/rust/cardano-blockchain-types/src/hashes.rs @@ -6,8 +6,8 @@ use catalyst_types::{ }; define_hashes!( - /// A transaction hash - Blake2b-256 hash of a transaction. - (TransactionHash, Blake2b256Hash), + /// A transaction ID - Blake2b-256 hash of a transaction. + (TransactionId, Blake2b256Hash), /// A public key hash - raw Blake2b-224 hash of an Ed25519 public key (has no discriminator, just the hash). (PubKeyHash, Blake2b224Hash), ); diff --git a/rust/cardano-blockchain-types/src/lib.rs b/rust/cardano-blockchain-types/src/lib.rs index 6e7bf47bd42..5ae5f442253 100644 --- a/rust/cardano-blockchain-types/src/lib.rs +++ b/rust/cardano-blockchain-types/src/lib.rs @@ -9,6 +9,7 @@ mod multi_era_block_data; mod network; mod point; mod slot; +mod stake_address; mod txn_index; mod txn_output_offset; mod txn_witness; @@ -23,12 +24,13 @@ pub use auxdata::{ }; pub use cip134_uri::Cip0134Uri; pub use fork::Fork; -pub use hashes::{PubKeyHash, TransactionHash}; +pub use hashes::{PubKeyHash, TransactionId}; pub use metadata::cip36::{voting_pk::VotingPubKey, Cip36}; pub use multi_era_block_data::MultiEraBlock; pub use network::Network; pub use point::Point; pub use slot::Slot; +pub use stake_address::StakeAddress; pub use txn_index::TxnIndex; pub use txn_output_offset::TxnOutputOffset; pub use txn_witness::{TxnWitness, VKeyHash}; diff --git a/rust/cardano-blockchain-types/src/network.rs b/rust/cardano-blockchain-types/src/network.rs index 8bc12a9ec7c..d0d93b49b8f 100644 --- a/rust/cardano-blockchain-types/src/network.rs +++ b/rust/cardano-blockchain-types/src/network.rs @@ -2,14 +2,13 @@ use std::{ffi::OsStr, path::PathBuf}; +use anyhow::anyhow; use catalyst_types::conversion::from_saturating; use chrono::{DateTime, Utc}; use pallas::{ - ledger::traverse::wellknown::GenesisValues, + ledger::{addresses::Network as PallasNetwork, traverse::wellknown::GenesisValues}, network::miniprotocols::{MAINNET_MAGIC, PREVIEW_MAGIC, PRE_PRODUCTION_MAGIC}, }; -// use strum::IntoEnumIterator; -// use strum_macros; use tracing::debug; use crate::Slot; @@ -220,6 +219,27 @@ impl From for u64 { } } +impl From for PallasNetwork { + fn from(value: Network) -> Self { + match value { + Network::Mainnet => PallasNetwork::Mainnet, + Network::Preprod | Network::Preview => PallasNetwork::Testnet, + } + } +} + +impl TryFrom for Network { + type Error = anyhow::Error; + + fn try_from(value: PallasNetwork) -> Result { + match value { + PallasNetwork::Mainnet => Ok(Network::Mainnet), + PallasNetwork::Testnet => Ok(Network::Preprod), + n @ PallasNetwork::Other(_) => Err(anyhow!("Unsupported network: {n:?}")), + } + } +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/rust/cardano-blockchain-types/src/stake_address.rs b/rust/cardano-blockchain-types/src/stake_address.rs new file mode 100644 index 00000000000..b179363a7bd --- /dev/null +++ b/rust/cardano-blockchain-types/src/stake_address.rs @@ -0,0 +1,234 @@ +//! A stake address. + +// cspell: words Scripthash, Keyhash + +use std::fmt::{Display, Formatter}; + +use anyhow::{anyhow, Context}; +use pallas::{ + crypto::hash::Hash, + ledger::{ + addresses::{ + ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart, + StakeAddress as PallasStakeAddress, + }, + primitives::conway, + }, +}; + +use crate::Network; + +/// A stake address. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)] +pub struct StakeAddress(PallasStakeAddress); + +impl StakeAddress { + /// Creates a new instance from the given parameters. + #[allow(clippy::expect_used, clippy::missing_panics_doc)] + #[must_use] + pub fn new(network: Network, is_script: bool, hash: Hash<28>) -> Self { + let network = network.into(); + // `pallas::StakeAddress` can only be constructed from `ShelleyAddress`, so we are forced + // to create a dummy shelley address. The input hash parameter is used to construct both + // payment and delegation parts, but the payment part isn't used in the stake address + // construction, so it doesn't matter. + let payment = ShelleyPaymentPart::Key(hash); + let delegation = if is_script { + ShelleyDelegationPart::Script(hash) + } else { + ShelleyDelegationPart::Key(hash) + }; + let address = ShelleyAddress::new(network, payment, delegation); + // This conversion can only fail if the delegation part isn't key or script, but we know + // it is valid because we construct it just above. + let address = address.try_into().expect("Unexpected delegation part"); + Self(address) + } + + /// Creates `StakeAddress` from `StakeCredential`. + #[must_use] + pub fn from_stake_cred(network: Network, cred: &conway::StakeCredential) -> Self { + match cred { + conway::StakeCredential::Scripthash(h) => Self::new(network, true, *h), + conway::StakeCredential::AddrKeyhash(h) => Self::new(network, false, *h), + } + } + + /// Returns true if it is a script address. + #[must_use] + pub fn is_script(&self) -> bool { + self.0.is_script() + } +} + +impl From for StakeAddress { + fn from(value: PallasStakeAddress) -> Self { + Self(value) + } +} + +impl TryFrom for StakeAddress { + type Error = anyhow::Error; + + fn try_from(value: ShelleyAddress) -> Result { + let address = PallasStakeAddress::try_from(value.clone()) + .with_context(|| format!("Unable to get stake address from {value:?}"))?; + Ok(Self(address)) + } +} + +impl TryFrom<&[u8]> for StakeAddress { + type Error = anyhow::Error; + + fn try_from(bytes: &[u8]) -> Result { + /// A stake address length in bytes. + const ADDRESS_LENGTH: usize = 29; + /// A hash length in bytes. + const HASH_LENGTH: usize = 28; + + let (header, hash) = match bytes { + [header, hash @ ..] if hash.len() == HASH_LENGTH => (header, Hash::<28>::from(hash)), + _ => { + return Err(anyhow!( + "Invalid bytes length: {}, expected {ADDRESS_LENGTH}", + bytes.len() + )); + }, + }; + + // The network part stored in the last four bits of the header. + let network = match header & 0b0000_1111 { + 0 => Network::Preprod, + 1 => Network::Mainnet, + v => return Err(anyhow!("Unexpected network value: {v}, header = {header}")), + }; + + // The 'type' (stake or script) is stored in the first four bits of the header. + let type_ = header >> 4; + let is_script = match type_ { + 0b1110 => false, + 0b1111 => true, + v => return Err(anyhow!("Unexpected type value: {v}, header = {header}")), + }; + + Ok(Self::new(network, is_script, hash)) + } +} + +/// This conversion returns a 29 bytes value that includes both header and hash. +impl From for Vec { + fn from(value: StakeAddress) -> Self { + value.0.to_vec() + } +} + +impl Display for StakeAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // The `to_bech32` implementation returns an error if the network isn't equal to testnet + // or mainnet. We don't allow other networks, so it is safe to unwrap, but just in case + // return a debug representation. + let bech32 = self + .0 + .to_bech32() + .unwrap_or_else(|_| format!("{:?}", self.0)); + write!(f, "{bech32}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::indexing_slicing)] + #[test] + fn roundtrip() { + let hash: Hash<28> = "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3" + .parse() + .unwrap(); + let test_data = [ + (Network::Mainnet, true, hash, 0b1111_0001), + (Network::Mainnet, false, hash, 0b1110_0001), + (Network::Preprod, true, hash, 0b1111_0000), + (Network::Preprod, false, hash, 0b1110_0000), + (Network::Preview, true, hash, 0b1111_0000), + (Network::Preview, false, hash, 0b1110_0000), + ]; + + for (network, is_script, hash, expected_header) in test_data { + let stake_address = StakeAddress::new(network, is_script, hash); + assert_eq!(stake_address.is_script(), is_script); + + // Check that conversion to bytes includes the expected header value. + let bytes: Vec<_> = stake_address.clone().into(); + assert_eq!(29, bytes.len(), "Invalid length for {network} {is_script}"); + assert_eq!( + &bytes[1..], + hash.as_ref(), + "Invalid hash for {network} {is_script}" + ); + assert_eq!( + expected_header, + *bytes.first().unwrap(), + "Invalid header for {network} {is_script}" + ); + + // Check that it is possible to create an address from the bytes. + let from_bytes = StakeAddress::try_from(bytes.as_slice()).unwrap(); + assert_eq!(from_bytes.is_script(), is_script); + assert_eq!(from_bytes, stake_address); + } + } + + #[test] + fn display() { + let hash: Hash<28> = "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3" + .parse() + .unwrap(); + + // cSpell:disable + let test_data = [ + ( + Network::Mainnet, + true, + hash, + "stake17ynkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcpxcgqv", + ), + ( + Network::Mainnet, + false, + hash, + "stake1uynkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcgwyghv", + ), + ( + Network::Preprod, + true, + hash, + "stake_test17qnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcxvj2y3", + ), + ( + Network::Preprod, + false, + hash, + "stake_test1uqnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvc0yw2n3", + ), + ( + Network::Preview, + true, + hash, + "stake_test17qnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcxvj2y3", + ), + ( + Network::Preview, + false, + hash, + "stake_test1uqnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvc0yw2n3", + ), + ]; + // cSpell:enable + + for (network, is_script, hash, expected) in test_data { + let address = StakeAddress::new(network, is_script, hash); + assert_eq!(expected, format!("{address}")); + } + } +} diff --git a/rust/cardano-chain-follower/Cargo.toml b/rust/cardano-chain-follower/Cargo.toml index 80aa2227f56..2630e215d3d 100644 --- a/rust/cardano-chain-follower/Cargo.toml +++ b/rust/cardano-chain-follower/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardano-chain-follower" -version = "0.0.7" +version = "0.0.8" edition.workspace = true authors.workspace = true homepage.workspace = true @@ -19,8 +19,8 @@ mithril-client = { version = "0.10.4", default-features = false, features = [ "full", "num-integer-backend", ] } -cardano-blockchain-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } -catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } +cardano-blockchain-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } +catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } thiserror = "1.0.69" tokio = { version = "1.42.0", features = [ @@ -63,7 +63,7 @@ test-log = { version = "0.2.16", default-features = false, features = [ "trace", ] } clap = "4.5.23" -rbac-registration = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } +rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } # Note, these features are for support of features exposed by dependencies. [features] diff --git a/rust/catalyst-types/Cargo.toml b/rust/catalyst-types/Cargo.toml index a5a0021aec3..0da484d2092 100644 --- a/rust/catalyst-types/Cargo.toml +++ b/rust/catalyst-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catalyst-types" -version = "0.0.2" +version = "0.0.3" edition.workspace = true license.workspace = true authors.workspace = true diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml index 39f612e3dcb..f95bd321a70 100644 --- a/rust/rbac-registration/Cargo.toml +++ b/rust/rbac-registration/Cargo.toml @@ -2,7 +2,7 @@ name = "rbac-registration" description = "Role Based Access Control Registration" keywords = ["cardano", "catalyst", "rbac registration"] -version = "0.0.3" +version = "0.0.4" authors = [ "Arissara Chotivichit " ] @@ -30,8 +30,8 @@ tracing = "0.1.40" ed25519-dalek = "2.1.1" uuid = "1.11.0" -c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } +c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } -cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } -cardano-blockchain-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } -catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } +cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } +cardano-blockchain-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } +catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } diff --git a/rust/rbac-registration/src/cardano/cip509/cip509.rs b/rust/rbac-registration/src/cardano/cip509/cip509.rs index b04ec7925e1..8de68fd7049 100644 --- a/rust/rbac-registration/src/cardano/cip509/cip509.rs +++ b/rust/rbac-registration/src/cardano/cip509/cip509.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, collections::HashMap}; use anyhow::{anyhow, Context}; -use cardano_blockchain_types::{MetadatumLabel, MultiEraBlock, TransactionHash, TxnIndex}; +use cardano_blockchain_types::{MetadatumLabel, MultiEraBlock, TransactionId, TxnIndex}; use catalyst_types::{ cbor_utils::{report_duplicated_key, report_missing_keys}, hashes::{Blake2b256Hash, BLAKE_2B256_SIZE}, @@ -60,7 +60,7 @@ pub struct Cip509 { /// An optional hash of the previous transaction. /// /// The hash must always be present except for the first registration transaction. - prv_tx_id: Option, + prv_tx_id: Option, /// Metadata. /// /// This field encoded in chunks. See [`X509Chunks`] for more details. @@ -73,7 +73,7 @@ pub struct Cip509 { /// constructors. payment_history: PaymentHistory, /// A hash of the transaction from which this registration is extracted. - txn_hash: TransactionHash, + txn_hash: TransactionId, /// A point (slot) and a transaction index identifying the block and the transaction /// that this `Cip509` was extracted from. origin: PointTxnIdx, @@ -215,7 +215,7 @@ impl Cip509 { /// Returns a hash of the previous transaction. #[must_use] - pub fn previous_transaction(&self) -> Option { + pub fn previous_transaction(&self) -> Option { self.prv_tx_id } @@ -233,7 +233,7 @@ impl Cip509 { /// Returns a hash of the transaction where this data is originating from. #[must_use] - pub fn txn_hash(&self) -> TransactionHash { + pub fn txn_hash(&self) -> TransactionId { self.txn_hash } @@ -514,7 +514,7 @@ fn decode_input_hash( /// Decodes previous transaction id. fn decode_previous_transaction_id( d: &mut Decoder, context: &str, report: &ProblemReport, -) -> Result, ()> { +) -> Result, ()> { let bytes = match decode_bytes(d, "Cip509 previous transaction id") { Ok(v) => v, Err(e) => { diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index 0ac95fb4a15..900a5154a48 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::bail; use c509_certificate::c509::C509; -use cardano_blockchain_types::TransactionHash; +use cardano_blockchain_types::TransactionId; use catalyst_types::uuid::UuidV4; use ed25519_dalek::VerifyingKey; use tracing::{error, warn}; @@ -58,7 +58,7 @@ impl RegistrationChain { /// Get the current transaction ID hash. #[must_use] - pub fn current_tx_id_hash(&self) -> TransactionHash { + pub fn current_tx_id_hash(&self) -> TransactionId { self.inner.current_tx_id_hash } @@ -109,7 +109,7 @@ impl RegistrationChain { #[derive(Debug, Clone)] struct RegistrationChainInner { /// The current transaction ID hash (32 bytes) - current_tx_id_hash: TransactionHash, + current_tx_id_hash: TransactionId, /// List of purpose for this registration chain purpose: Vec, diff --git a/rust/rbac-registration/src/utils/test.rs b/rust/rbac-registration/src/utils/test.rs index 4a89c9ce948..12c2330e7e2 100644 --- a/rust/rbac-registration/src/utils/test.rs +++ b/rust/rbac-registration/src/utils/test.rs @@ -2,7 +2,7 @@ // cspell: words stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr -use cardano_blockchain_types::{MultiEraBlock, Network, Point, Slot, TransactionHash, TxnIndex}; +use cardano_blockchain_types::{MultiEraBlock, Network, Point, Slot, TransactionId, TxnIndex}; use catalyst_types::uuid::UuidV4; use uuid::Uuid; @@ -20,9 +20,9 @@ pub struct BlockTestData { /// Transaction index. pub txn_index: TxnIndex, /// Transaction hash. - pub txn_hash: TransactionHash, + pub txn_hash: TransactionId, /// Previous hash. - pub prv_hash: Option, + pub prv_hash: Option, /// Purpose. pub purpose: UuidV4, /// Stake address. diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 0d78324fda3..facde473f90 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catalyst-signed-doc" -version = "0.0.2" +version = "0.0.3" edition.workspace = true authors.workspace = true homepage.workspace = true @@ -11,8 +11,8 @@ license.workspace = true workspace = true [dependencies] -rbac-registration = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } -catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" } +rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } +catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134"