diff --git a/Cargo.lock b/Cargo.lock index 2fdfe94b..9a552ca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5514,9 +5514,12 @@ version = "1.7.1" dependencies = [ "blst", "hex", + "pluto-core", + "pluto-eth2api", "rand 0.8.5", "rand_core 0.6.4", "serde", + "test-case", "thiserror 2.0.18", ] diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 38f407af..9cc95d85 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -441,6 +441,12 @@ impl Signature { } } +impl AsRef<[u8; SIG_LEN]> for Signature { + fn as_ref(&self) -> &[u8; SIG_LEN] { + &self.0 + } +} + /// Signed data type pub trait SignedData: Clone + Serialize + StdDebug { /// The error type diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index 341f88ca..04e75b38 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -9,6 +9,8 @@ publish.workspace = true [dependencies] blst.workspace = true hex.workspace = true +pluto-core.workspace = true +pluto-eth2api.workspace = true rand.workspace = true rand_core.workspace = true serde.workspace = true @@ -29,3 +31,6 @@ cast_sign_loss = "deny" needless_return = "deny" panicking_overflow_checks = "deny" unwrap_used = "deny" + +[dev-dependencies] +test-case.workspace = true diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index dd14f7e2..d5132bfe 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -14,5 +14,8 @@ pub mod blst_impl; /// TBLS trait definition pub mod tbls; +/// Conversions between crypto (tbls), core, and eth2 BLS types. +pub mod tblsconv; + /// Error types and constants pub mod types; diff --git a/crates/crypto/src/tblsconv.rs b/crates/crypto/src/tblsconv.rs new file mode 100644 index 00000000..16e3ca49 --- /dev/null +++ b/crates/crypto/src/tblsconv.rs @@ -0,0 +1,181 @@ +//! Conversions between crypto (tbls), core, and eth2 BLS types. + +use pluto_core::types as core_types; +use pluto_eth2api::spec::phase0; + +use crate::types::{self, PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; + +/// Converts a core workflow [`core_types::Signature`] into a +/// [`types::Signature`]. +pub fn sig_from_core(sig: &core_types::Signature) -> types::Signature { + *sig.as_ref() +} + +/// Converts a [`types::Signature`] into a core workflow +/// [`core_types::Signature`]. +pub fn sig_to_core(sig: types::Signature) -> core_types::Signature { + core_types::Signature::new(sig) +} + +/// Converts a [`types::Signature`] into an eth2 phase0 +/// [`phase0::BLSSignature`]. +pub fn sig_to_eth2(sig: types::Signature) -> phase0::BLSSignature { + sig +} + +/// Converts a [`types::PublicKey`] into an eth2 phase0 [`phase0::BLSPubKey`]. +pub fn pubkey_to_eth2(pk: types::PublicKey) -> phase0::BLSPubKey { + pk +} + +/// Returns a [`types::PrivateKey`] from the given byte slice. +/// +/// Returns an error if the data isn't exactly [`PRIVATE_KEY_LENGTH`] bytes. +pub fn privkey_from_bytes(data: &[u8]) -> Result { + let key: [u8; PRIVATE_KEY_LENGTH] = data.try_into().map_err(|_| ConvError::InvalidLength { + expected: PRIVATE_KEY_LENGTH, + got: data.len(), + })?; + Ok(key) +} + +/// Returns a [`types::PublicKey`] from the given byte slice. +/// +/// Returns an error if the data isn't exactly [`PUBLIC_KEY_LENGTH`] bytes. +pub fn pubkey_from_bytes(data: &[u8]) -> Result { + let key: [u8; PUBLIC_KEY_LENGTH] = data.try_into().map_err(|_| ConvError::InvalidLength { + expected: PUBLIC_KEY_LENGTH, + got: data.len(), + })?; + Ok(key) +} + +/// Returns a [`types::PublicKey`] from a core [`core_types::PubKey`]. +pub fn pubkey_from_core(pk: &core_types::PubKey) -> types::PublicKey { + let bytes: &[u8] = pk.as_ref(); + bytes + .try_into() + .expect("PubKey must be PUBLIC_KEY_LENGTH bytes") +} + +/// Returns a [`types::Signature`] from the given byte slice. +/// +/// Returns an error if the data isn't exactly [`SIGNATURE_LENGTH`] bytes. +pub fn signature_from_bytes(data: &[u8]) -> Result { + let sig: [u8; SIGNATURE_LENGTH] = data.try_into().map_err(|_| ConvError::InvalidLength { + expected: SIGNATURE_LENGTH, + got: data.len(), + })?; + Ok(sig) +} + +/// Conversion error. +#[derive(Debug, thiserror::Error)] +pub enum ConvError { + /// Data is not of the expected length. + #[error("data is not of the correct length: expected {expected}, got {got}")] + InvalidLength { + /// Expected byte length. + expected: usize, + /// Actual byte length. + got: usize, + }, +} + +#[cfg(test)] +mod tests { + use test_case::test_case; + + use super::*; + + #[test_case(&[], PRIVATE_KEY_LENGTH, 0 ; "empty input")] + #[test_case(&[42u8; PRIVATE_KEY_LENGTH + 1], PRIVATE_KEY_LENGTH, PRIVATE_KEY_LENGTH + 1 ; "more data than expected")] + #[test_case(&[42u8; PRIVATE_KEY_LENGTH - 1], PRIVATE_KEY_LENGTH, PRIVATE_KEY_LENGTH - 1 ; "less data than expected")] + fn privkey_from_bytes_invalid(data: &[u8], expected: usize, got: usize) { + assert!(matches!( + privkey_from_bytes(data), + Err(ConvError::InvalidLength { expected: e, got: g }) if e == expected && g == got + )); + } + + #[test] + fn privkey_from_bytes_valid() { + let data = vec![42u8; PRIVATE_KEY_LENGTH]; + let key = privkey_from_bytes(&data).unwrap(); + assert_eq!(key, [42u8; PRIVATE_KEY_LENGTH]); + } + + #[test_case(&[], PUBLIC_KEY_LENGTH, 0 ; "empty input")] + #[test_case(&[42u8; PUBLIC_KEY_LENGTH + 1], PUBLIC_KEY_LENGTH, PUBLIC_KEY_LENGTH + 1 ; "more data than expected")] + #[test_case(&[42u8; PUBLIC_KEY_LENGTH - 1], PUBLIC_KEY_LENGTH, PUBLIC_KEY_LENGTH - 1 ; "less data than expected")] + fn pubkey_from_bytes_invalid(data: &[u8], expected: usize, got: usize) { + assert!(matches!( + pubkey_from_bytes(data), + Err(ConvError::InvalidLength { expected: e, got: g }) if e == expected && g == got + )); + } + + #[test] + fn pubkey_from_bytes_valid() { + let data = vec![42u8; PUBLIC_KEY_LENGTH]; + let key = pubkey_from_bytes(&data).expect("should succeed"); + assert_eq!(key, [42u8; PUBLIC_KEY_LENGTH]); + } + + #[test] + fn pubkey_to_eth2_roundtrip() { + let data = vec![42u8; PUBLIC_KEY_LENGTH]; + let pubkey = pubkey_from_bytes(&data).expect("should succeed"); + let res = pubkey_to_eth2(pubkey); + assert_eq!(pubkey[..], res[..]); + } + + #[test] + fn pubkey_from_core_roundtrip() { + let bytes = [42u8; PUBLIC_KEY_LENGTH]; + let core_pk = core_types::PubKey::new(bytes); + let res = pubkey_from_core(&core_pk); + assert_eq!(res, bytes); + } + + #[test_case(&[], SIGNATURE_LENGTH, 0 ; "empty input")] + #[test_case(&[42u8; SIGNATURE_LENGTH + 1], SIGNATURE_LENGTH, SIGNATURE_LENGTH + 1 ; "more data than expected")] + #[test_case(&[42u8; SIGNATURE_LENGTH - 1], SIGNATURE_LENGTH, SIGNATURE_LENGTH - 1 ; "less data than expected")] + fn signature_from_bytes_invalid(data: &[u8], expected: usize, got: usize) { + assert!(matches!( + signature_from_bytes(data), + Err(ConvError::InvalidLength { expected: e, got: g }) if e == expected && g == got + )); + } + + #[test] + fn signature_from_bytes_valid() { + let data = vec![42u8; SIGNATURE_LENGTH]; + let sig = signature_from_bytes(&data).expect("should succeed"); + assert_eq!(sig, [42u8; SIGNATURE_LENGTH]); + } + + #[test] + fn sig_from_core_roundtrip() { + let data = [42u8; SIGNATURE_LENGTH]; + let core_sig = core_types::Signature::new(data); + let res = sig_from_core(&core_sig); + assert_eq!(res, data); + } + + #[test] + fn sig_to_core_roundtrip() { + let data = [42u8; SIGNATURE_LENGTH]; + let core_sig = sig_to_core(data); + let bytes: &[u8] = core_sig.as_ref(); + assert_eq!(bytes, &data[..]); + } + + #[test] + fn sig_to_eth2_roundtrip() { + let data = vec![42u8; SIGNATURE_LENGTH]; + let sig = signature_from_bytes(&data).expect("should succeed"); + let eth2_sig = sig_to_eth2(sig); + assert_eq!(sig[..], eth2_sig[..]); + } +}