diff --git a/Cargo.toml b/Cargo.toml index f611632b..53c1ad55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "pallas-chainsync", "pallas-txsubmission", "pallas-localstate", + "pallas-crypto", "pallas-alonzo", "pallas", ] diff --git a/pallas-alonzo/Cargo.toml b/pallas-alonzo/Cargo.toml index af60fcbd..5d4ef139 100644 --- a/pallas-alonzo/Cargo.toml +++ b/pallas-alonzo/Cargo.toml @@ -12,13 +12,9 @@ authors = [ "Santiago Carmuega " ] -[features] -crypto = ["cryptoxide"] - [dependencies] minicbor = { version = "0.12", features = ["std"] } minicbor-derive = "0.8.0" hex = "0.4.3" log = "0.4.14" -cryptoxide = { version = "0.3.6", optional = true } - +pallas-crypto = { version = "0.3", path = "../pallas-crypto" } \ No newline at end of file diff --git a/pallas-alonzo/src/crypto.rs b/pallas-alonzo/src/crypto.rs index 4951a39c..0ee66b03 100644 --- a/pallas-alonzo/src/crypto.rs +++ b/pallas-alonzo/src/crypto.rs @@ -1,66 +1,20 @@ use crate::{AuxiliaryData, Header, PlutusData, TransactionBody}; -use cryptoxide::blake2b::Blake2b; -use minicbor::Encode; +use pallas_crypto::hash::{Hash, Hasher}; -pub type Hash32 = [u8; 32]; - -pub type Error = Box; - -struct Hasher { - inner: Blake2b, -} - -impl Hasher<256> { - #[inline] - fn new() -> Self { - Self { - inner: Blake2b::new(32), - } - } - - #[inline] - fn result(mut self) -> Hash32 { - use cryptoxide::digest::Digest as _; - - let mut hash = [0; 32]; - self.inner.result(&mut hash); - hash - } -} - -impl<'a, const N: usize> minicbor::encode::write::Write for &'a mut Hasher { - type Error = std::convert::Infallible; - - fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { - use cryptoxide::digest::Digest as _; - self.inner.input(buf); - Ok(()) - } -} - -// TODO: think if we should turn this into a blanket implementation of a new -// trait -fn hash_cbor_encodable(data: &impl Encode) -> Result { - let mut hasher = Hasher::<256>::new(); - let () = minicbor::encode(data, &mut hasher)?; - - Ok(hasher.result()) -} - -pub fn hash_block_header(data: &Header) -> Result { - hash_cbor_encodable(data) +pub fn hash_block_header(data: &Header) -> Hash<32> { + Hasher::<256>::hash_cbor(data) } -pub fn hash_auxiliary_data(data: &AuxiliaryData) -> Result { - hash_cbor_encodable(data) +pub fn hash_auxiliary_data(data: &AuxiliaryData) -> Hash<32> { + Hasher::<256>::hash_cbor(data) } -pub fn hash_transaction(data: &TransactionBody) -> Result { - hash_cbor_encodable(data) +pub fn hash_transaction(data: &TransactionBody) -> Hash<32> { + Hasher::<256>::hash_cbor(data) } -pub fn hash_plutus_data(data: &PlutusData) -> Result { - hash_cbor_encodable(data) +pub fn hash_plutus_data(data: &PlutusData) -> Hash<32> { + Hasher::<256>::hash_cbor(data) } #[cfg(test)] @@ -88,10 +42,7 @@ mod tests { ]; for (tx_idx, tx) in block_model.1.transaction_bodies.iter().enumerate() { - let computed_hash = hash_transaction(tx).expect(&format!( - "error hashing tx {} from block {}", - tx_idx, block_idx - )); + let computed_hash = hash_transaction(tx); let known_hash = valid_hashes[tx_idx]; assert_eq!(hex::encode(computed_hash), known_hash) } diff --git a/pallas-alonzo/src/lib.rs b/pallas-alonzo/src/lib.rs index 7db969ec..e26af565 100644 --- a/pallas-alonzo/src/lib.rs +++ b/pallas-alonzo/src/lib.rs @@ -7,5 +7,4 @@ mod utils; pub use framework::*; pub use model::*; -#[cfg(feature = "crypto")] pub mod crypto; diff --git a/pallas-chainsync/Cargo.toml b/pallas-chainsync/Cargo.toml index ad195992..0e15da4a 100644 --- a/pallas-chainsync/Cargo.toml +++ b/pallas-chainsync/Cargo.toml @@ -25,4 +25,4 @@ cryptoxide = "0.3.6" env_logger = "0.9.0" pallas-handshake = { version = "0.3.0", path = "../pallas-handshake/" } pallas-txsubmission = { version = "0.3.0", path = "../pallas-txsubmission/" } -pallas-alonzo = { version = "0.3.0", path = "../pallas-alonzo/", features = ["crypto"] } +pallas-alonzo = { version = "0.3.0", path = "../pallas-alonzo/" } diff --git a/pallas-chainsync/examples/blocks.rs b/pallas-chainsync/examples/blocks.rs index 7e9fc746..84138cc9 100644 --- a/pallas-chainsync/examples/blocks.rs +++ b/pallas-chainsync/examples/blocks.rs @@ -29,8 +29,8 @@ impl DecodePayload for Content { impl BlockLike for Content { fn block_point(&self) -> Result> { - let hash = crypto::hash_block_header(&self.0.header)?; - Ok(Point(self.0.header.header_body.slot, Vec::from(hash))) + let hash = crypto::hash_block_header(&self.0.header); + Ok(Point(self.0.header.header_body.slot, hash.to_vec())) } } diff --git a/pallas-chainsync/examples/headers.rs b/pallas-chainsync/examples/headers.rs index 572b0758..8d1c0f86 100644 --- a/pallas-chainsync/examples/headers.rs +++ b/pallas-chainsync/examples/headers.rs @@ -37,8 +37,8 @@ impl DecodePayload for Content { impl BlockLike for Content { fn block_point(&self) -> Result> { - let hash = crypto::hash_block_header(&self.1)?; - Ok(Point(self.1.header_body.slot, Vec::from(hash))) + let hash = crypto::hash_block_header(&self.1); + Ok(Point(self.1.header_body.slot, hash.to_vec())) } } diff --git a/pallas-crypto/Cargo.toml b/pallas-crypto/Cargo.toml new file mode 100644 index 00000000..81101a54 --- /dev/null +++ b/pallas-crypto/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pallas-crypto" +description = "Cryptographic primitives for Cardano" +version = "0.3.0" +edition = "2021" +repository = "https://github.com/txpipe/pallas" +homepage = "https://github.com/txpipe/pallas" +documentation = "https://docs.rs/pallas-crypto" +license = "Apache-2.0" +readme = "README.md" +authors = [ + "Nicolas Di Prima " +] + +[dependencies] +minicbor = { version = "0.12" } +hex = "0.4" +cryptoxide = { version = "0.3.6" } + diff --git a/pallas-crypto/README.md b/pallas-crypto/README.md new file mode 100644 index 00000000..849a0534 --- /dev/null +++ b/pallas-crypto/README.md @@ -0,0 +1,14 @@ +# Pallas Crypto + +Crate with all the cryptographic material to support Cardano protocol: + +- [x] Blake2b 256 +- [x] Blake2b 224 +- [ ] Ed25519 asymmetric key pair and ECDSA +- [ ] Ed25519 Extended asymmetric key pair +- [ ] Bip32-Ed25519 key derivation +- [ ] BIP39 mnemonics +- [ ] VRF +- [ ] KES +- [ ] SECP256k1 + diff --git a/pallas-crypto/src/hash/hash.rs b/pallas-crypto/src/hash/hash.rs new file mode 100644 index 00000000..2efb710e --- /dev/null +++ b/pallas-crypto/src/hash/hash.rs @@ -0,0 +1,96 @@ +use std::{fmt, ops::Deref, str::FromStr}; + +/// data that is a cryptographic [`struct@Hash`] of `BYTES` long. +/// +/// Possible values with Cardano are 32 bytes long (block hash or transaction +/// hash). Or 28 bytes long (as used in addresses) +/// +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Hash([u8; BYTES]); + +impl Hash { + #[inline] + pub const fn new(bytes: [u8; BYTES]) -> Self { + Self(bytes) + } +} + +impl From<[u8; BYTES]> for Hash { + #[inline] + fn from(bytes: [u8; BYTES]) -> Self { + Self::new(bytes) + } +} + +impl AsRef<[u8]> for Hash { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for Hash { + type Target = [u8; BYTES]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq<[u8]> for Hash { + fn eq(&self, other: &[u8]) -> bool { + self.0.eq(other) + } +} + +impl fmt::Debug for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple(&format!("Hash<{size}>", size = BYTES)) + .field(&hex::encode(self)) + .finish() + } +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&hex::encode(self)) + } +} + +impl FromStr for Hash { + type Err = hex::FromHexError; + fn from_str(s: &str) -> Result { + let mut bytes = [0; BYTES]; + hex::decode_to_slice(s, &mut bytes)?; + Ok(Self::new(bytes)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str() { + let _digest: Hash<28> = "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3" + .parse() + .unwrap(); + + let _digest: Hash<32> = "0d8d00cdd4657ac84d82f0a56067634a7adfdf43da41cb534bcaa45060973d21" + .parse() + .unwrap(); + } + + #[test] + #[should_panic] + fn from_str_fail_1() { + let _digest: Hash<28> = "27".parse().unwrap(); + } + + #[test] + #[should_panic] + fn from_str_fail_2() { + let _digest: Hash<32> = "0d8d00cdd465".parse().unwrap(); + } +} diff --git a/pallas-crypto/src/hash/hasher.rs b/pallas-crypto/src/hash/hasher.rs new file mode 100644 index 00000000..eaf55907 --- /dev/null +++ b/pallas-crypto/src/hash/hasher.rs @@ -0,0 +1,125 @@ +use crate::hash::Hash; +use cryptoxide::blake2b::Blake2b; +use minicbor::encode::Write; + +/// handy method to create a hash of given `SIZE` bit size. +/// +/// The hash algorithm is `Blake2b` and the constant parameter is +/// the number of bits to generate. Good values are `256` or `224` for +/// Cardano. +/// +/// # Generate a cryptographic hash with Blake2b 256 +/// +/// The following will generate a 32 bytes digest output +/// +/// ``` +/// # use pallas_crypto::hash::Hasher; +/// +/// let mut hasher = Hasher::<256>::new(); +/// hasher.input(b"My transaction"); +/// +/// let digest = hasher.finalize(); +/// # assert_eq!( +/// # "0d8d00cdd4657ac84d82f0a56067634a7adfdf43da41cb534bcaa45060973d21", +/// # hex::encode(digest) +/// # ); +/// ``` +/// +/// # Generate a cryptographic hash with Blake2b 224 +/// +/// The following will generate a 28 bytes digest output. This is used +/// to generate the hash of public keys for addresses. +/// +/// ``` +/// # use pallas_crypto::hash::Hasher; +/// +/// let digest = Hasher::<224>::hash(b"My Public Key"); +/// # assert_eq!( +/// # "c123c9bc0e9e31a20a4aa23518836ec5fb54bdc85735c56b38eb79a5", +/// # hex::encode(digest) +/// # ); +/// ``` +pub struct Hasher(Blake2b); + +impl Hasher { + /// update the [`Hasher`] with the given inputs + #[inline] + pub fn input(&mut self, bytes: &[u8]) { + use cryptoxide::digest::Digest as _; + self.0.input(bytes); + } +} + +macro_rules! common_hasher { + ($size:literal) => { + impl Hasher<$size> { + /// create a new [`Hasher`] + #[inline] + pub fn new() -> Self { + Self(Blake2b::new($size / 8)) + } + + /// convenient function to directly generate the hash + /// of the given bytes without creating the intermediary + /// types [`Hasher`] and calling [`Hasher::input`]. + #[inline] + pub fn hash(bytes: &[u8]) -> Hash<{ $size / 8 }> { + let mut hasher = Self::new(); + hasher.input(bytes); + hasher.finalize() + } + + /// convenient function to directly generate the hash + /// of the given [minicbor::Encode] data object + #[inline] + pub fn hash_cbor(data: &impl minicbor::Encode) -> Hash<{ $size / 8 }> { + let mut hasher = Self::new(); + let () = minicbor::encode(data, &mut hasher).expect("Infallible"); + hasher.finalize() + } + + /// consume the [`Hasher`] and returns the computed digest + pub fn finalize(mut self) -> Hash<{ $size / 8 }> { + use cryptoxide::digest::Digest as _; + let mut hash = [0; $size / 8]; + self.0.result(&mut hash); + Hash::new(hash) + } + } + + impl Default for Hasher<$size> { + fn default() -> Self { + Self::new() + } + } + }; +} + +common_hasher!(224); +common_hasher!(256); + +/* +TODO: somehow the `minicbor::Write` does not allow to implement this + version of the trait and to automatically have the impl of the + other one automatically derived by default. + +impl Write for Hasher { + type Error = std::convert::Infallible; + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + self.input(buf); + Ok(()) + } +} +*/ + +impl<'a, const BITS: usize> Write for &'a mut Hasher { + type Error = std::convert::Infallible; + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + self.input(buf); + Ok(()) + } +} diff --git a/pallas-crypto/src/hash/mod.rs b/pallas-crypto/src/hash/mod.rs new file mode 100644 index 00000000..ca3b15e8 --- /dev/null +++ b/pallas-crypto/src/hash/mod.rs @@ -0,0 +1,34 @@ +//! Cryptographic Hash for Cardano +//! +//! we expose two helper objects: +//! +//! * [`Hasher`] to help streaming objects or bytes into a hasher +//! and computing a hash without allocating extra memory due to +//! the required **CBOR** encoding for everything by the cardano +//! protocol +//! * [`struct@Hash`] a conveniently strongly typed byte array +//! +//! The algorithm exposed here is `Blake2b`. We currently support two +//! digest size for the algorithm: 224 bits and 256 bits. They are the +//! only two required to use with the Cardano protocol +//! +//! # Example +//! +//! ``` +//! use pallas_crypto::hash::Hasher; +//! +//! let mut hasher = Hasher::<224>::new(); +//! hasher.input(b"my key"); +//! +//! let digest = hasher.finalize(); +//! # assert_eq!( +//! # "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3", +//! # hex::encode(digest) +//! # ); +//! ``` + +#[allow(clippy::module_inception)] +mod hash; +mod hasher; + +pub use self::{hash::Hash, hasher::Hasher}; diff --git a/pallas-crypto/src/lib.rs b/pallas-crypto/src/lib.rs new file mode 100644 index 00000000..ec5d33c1 --- /dev/null +++ b/pallas-crypto/src/lib.rs @@ -0,0 +1 @@ +pub mod hash; diff --git a/pallas/Cargo.toml b/pallas/Cargo.toml index fb86b8f3..66712b0d 100644 --- a/pallas/Cargo.toml +++ b/pallas/Cargo.toml @@ -20,4 +20,5 @@ pallas-chainsync = { version = "0.3.5", path = "../pallas-chainsync/" } pallas-blockfetch = { version = "0.3.4", path = "../pallas-blockfetch/" } pallas-localstate = { version = "0.3.5", path = "../pallas-localstate/" } pallas-txsubmission = { version = "0.3.5", path = "../pallas-txsubmission/" } -pallas-alonzo = { version = "0.3.7", path = "../pallas-alonzo/", features=["crypto"] } +pallas-alonzo = { version = "0.3.7", path = "../pallas-alonzo/" } +pallas-crypto = { version = "0.3.0", path = "../pallas-crypto/" } diff --git a/pallas/src/lib.rs b/pallas/src/lib.rs index 2919145c..52a27934 100644 --- a/pallas/src/lib.rs +++ b/pallas/src/lib.rs @@ -12,3 +12,4 @@ pub mod ouroboros; pub mod ledger; +pub use pallas_crypto as crypto;