diff --git a/crates/app/src/obolapi/exit.rs b/crates/app/src/obolapi/exit.rs index cecadd92..8269d371 100644 --- a/crates/app/src/obolapi/exit.rs +++ b/crates/app/src/obolapi/exit.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::Signature}; +use pluto_crypto::{tbls, types::Signature}; use serde::{Deserialize, Serialize}; use pluto_cluster::{ @@ -353,7 +353,7 @@ impl Client { } // Perform threshold aggregation - let full_sig = BlstImpl.threshold_aggregate(&raw_signatures)?; + let full_sig = tbls::threshold_aggregate(&raw_signatures)?; let epoch_u64: u64 = exit_response.epoch.parse()?; diff --git a/crates/app/tests/wiring.rs b/crates/app/tests/wiring.rs index 49eac261..6d8aa63c 100644 --- a/crates/app/tests/wiring.rs +++ b/crates/app/tests/wiring.rs @@ -44,7 +44,7 @@ use pluto_core::{ ProposerDutyDefinition, PubKey, SignedData, SignedDataSet, Slot, SlotNumber, }, }; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; +use pluto_crypto::tbls; use pluto_eth2api::{ BeaconNodeClient, EthBeaconNodeApiClient, GetStateValidatorsResponseResponse, GetStateValidatorsResponseResponseDatum, @@ -176,7 +176,7 @@ async fn count_posts(server: &MockServer, submit_path: &str) -> usize { /// signed message is arbitrary — only the distinct share index and identical /// unsigned payload (so ParSigDB groups the partials) matter. fn attester_partial(share_idx: u64, share: &pluto_crypto::types::PrivateKey) -> ParSignedData { - let sig = BlstImpl.sign(share, &[42u8; 32]).expect("sign share"); + let sig = tbls::sign(share, &[42u8; 32]).expect("sign share"); let attestation = phase0::Attestation { aggregation_bits: phase0::BitList::with_bits(8, &[0]), data: phase0::AttestationData { @@ -446,10 +446,9 @@ async fn wiring_connects_sign_path() { // Build two real BLS partial signatures (threshold 2 of 2) over the same // attestation so SigAgg's `threshold_aggregate` succeeds and the broadcaster // submits. - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).expect("secret"); - let shares = tbls.threshold_split(&secret, 2, 2).expect("split"); + let secret = tbls::generate_secret_key(&mut rng).expect("secret"); + let shares = tbls::threshold_split(&secret, 2, 2).expect("split"); let attester_duty = Duty::new_attester_duty(SlotNumber::new(1)); let mut share_iter = shares.into_iter(); @@ -564,16 +563,15 @@ async fn wiring_connects_sign_path_proposer() { .expect("wire did not deadlock") .expect("wire succeeded"); - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).expect("secret"); - let shares = tbls.threshold_split(&secret, 2, 2).expect("split"); + let secret = tbls::generate_secret_key(&mut rng).expect("secret"); + let shares = tbls::threshold_split(&secret, 2, 2).expect("split"); // Each partial signs an arbitrary message with its own share (permissive // verifier), swapping only the block signature onto an identical unsigned // block so ParSigDB's threshold-matching groups them. let make_par = |share_idx: u64, share: &pluto_crypto::types::PrivateKey| { - let sig = tbls.sign(share, &[42u8; 32]).expect("sign"); + let sig = tbls::sign(share, &[42u8; 32]).expect("sign"); pluto_core::signeddata::VersionedSignedProposal::new_partial( phase0_proposal(sig), share_idx, @@ -638,10 +636,9 @@ async fn wiring_connects_sign_path_sync_contribution() { .expect("wire did not deadlock") .expect("wire succeeded"); - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).expect("secret"); - let shares = tbls.threshold_split(&secret, 2, 2).expect("split"); + let secret = tbls::generate_secret_key(&mut rng).expect("secret"); + let shares = tbls::threshold_split(&secret, 2, 2).expect("split"); // Identical unsigned contribution across shares; each partial swaps only the // top-level signature (`set_signature`), preserving the payload so ParSigDB @@ -661,7 +658,7 @@ async fn wiring_connects_sign_path_sync_contribution() { signature: [0; 96], }; let make_par = |share_idx: u64, share: &pluto_crypto::types::PrivateKey| { - let sig = tbls.sign(share, &[42u8; 32]).expect("sign"); + let sig = tbls::sign(share, &[42u8; 32]).expect("sign"); let contribution = altair::SignedContributionAndProof { signature: sig, ..base_contribution.clone() @@ -720,12 +717,11 @@ async fn wiring_rejects_bad_partial_signature() { // Real BLS group key: the verifier parses this pubkey and verifies the // reconstructed group signature against the beacon attester signing domain. - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).expect("secret"); - let group_pubkey_bytes = tbls.secret_to_public_key(&secret).expect("group pubkey"); + let secret = tbls::generate_secret_key(&mut rng).expect("secret"); + let group_pubkey_bytes = tbls::secret_to_public_key(&secret).expect("group pubkey"); let pubkey = PubKey::new(group_pubkey_bytes); - let shares = tbls.threshold_split(&secret, 2, 2).expect("split"); + let shares = tbls::threshold_split(&secret, 2, 2).expect("split"); // REAL eth2 verifier (mirrors production `run`): BeaconMock serves the // signing domain via `/eth/v1/config/spec` + `/eth/v1/beacon/genesis`. @@ -769,7 +765,7 @@ async fn wiring_rejects_bad_partial_signature() { let attester_duty = Duty::new_attester_duty(SlotNumber::new(1)); let make_par = |share_idx: u64, share: &pluto_crypto::types::PrivateKey| { - let sig = tbls.sign(share, &[42u8; 32]).expect("sign"); + let sig = tbls::sign(share, &[42u8; 32]).expect("sign"); let attestation = phase0::Attestation { signature: sig, ..base_attestation.clone() @@ -996,12 +992,9 @@ async fn multinode_parsig_exchange_reaches_submission() { } // One real threshold-BLS keyset: N shares, any THRESHOLD reconstruct. - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).expect("secret"); - let shares = tbls - .threshold_split(&secret, N as u64, THRESHOLD) - .expect("split"); + let secret = tbls::generate_secret_key(&mut rng).expect("secret"); + let shares = tbls::threshold_split(&secret, N as u64, THRESHOLD).expect("split"); let attester_duty = Duty::new_attester_duty(SlotNumber::new(1)); // Each node stores its own partial internally; the router fans it out to diff --git a/crates/cli/src/commands/create_cluster.rs b/crates/cli/src/commands/create_cluster.rs index 770198f4..68c8e68f 100644 --- a/crates/cli/src/commands/create_cluster.rs +++ b/crates/cli/src/commands/create_cluster.rs @@ -25,8 +25,7 @@ use pluto_cluster::{ }; use pluto_consensus::protocols; use pluto_crypto::{ - blst_impl::BlstImpl, - tbls::Tbls, + tbls, types::{PrivateKey, PublicKey}, }; use pluto_eth1wrap as eth1wrap; @@ -674,7 +673,6 @@ fn create_validator_registrations( .try_into() .map_err(|_| CreateClusterError::InvalidForkVersionLength)?; - let tbls = BlstImpl; let mut registrations = Vec::with_capacity(secrets.len()); for (secret, fee_address) in secrets.iter().zip(fee_recipient_addresses.iter()) { @@ -684,7 +682,7 @@ fn create_validator_registrations( eth2util::network::fork_version_to_genesis_time(&fork_version)? }; - let pk = tbls.secret_to_public_key(secret)?; + let pk = tbls::secret_to_public_key(secret)?; let unsigned_reg = eth2util_registration::new_message( pk, @@ -695,7 +693,7 @@ fn create_validator_registrations( let sig_root = eth2util_registration::get_message_signing_root(&unsigned_reg, fork_version); - let sig = tbls.sign(secret, &sig_root)?; + let sig = tbls::sign(secret, &sig_root)?; registrations.push(BuilderRegistration { message: Registration { @@ -855,16 +853,15 @@ fn sign_deposit_datas( if deposit_amounts.is_empty() { return Err(CreateClusterError::EmptyDepositAmounts); } - let tbls = BlstImpl; let mut dd = Vec::new(); for &deposit_amount in deposit_amounts { let mut datas = Vec::new(); for (secret, withdrawal_addr) in secrets.iter().zip(withdrawal_addresses.iter()) { let withdrawal_addr = eth2util::helpers::checksum_address(withdrawal_addr)?; - let pk = tbls.secret_to_public_key(secret)?; + let pk = tbls::secret_to_public_key(secret)?; let msg = deposit::new_message(pk, &withdrawal_addr, deposit_amount, compounding)?; let sig_root = deposit::get_message_signing_root(&msg, network)?; - let sig = tbls.sign(secret, &sig_root)?; + let sig = tbls::sign(secret, &sig_root)?; datas.push(DepositData { pub_key: msg.pubkey, withdrawal_credentials: msg.withdrawal_credentials, @@ -878,11 +875,10 @@ fn sign_deposit_datas( } fn generate_keys(num_validators: u64) -> Result> { - let tbls = BlstImpl; let mut secrets = Vec::new(); for _ in 0..num_validators { - let secret = tbls.generate_secret_key(OsRng)?; + let secret = tbls::generate_secret_key(OsRng)?; secrets.push(secret); } @@ -993,12 +989,11 @@ fn get_tss_shares( threshold: u64, num_nodes: u64, ) -> Result<(Vec, Vec>)> { - let tbls = BlstImpl; let mut dvs = Vec::new(); let mut splits = Vec::new(); for secret in secrets { - let shares = tbls.threshold_split(secret, num_nodes, threshold)?; + let shares = tbls::threshold_split(secret, num_nodes, threshold)?; // Preserve order when transforming from map of private shares to array of // private keys @@ -1008,7 +1003,7 @@ fn get_tss_shares( splits.push(secret_set); - let pubkey = tbls.secret_to_public_key(secret)?; + let pubkey = tbls::secret_to_public_key(secret)?; dvs.push(pubkey); } @@ -1275,7 +1270,6 @@ fn get_validators( } let mut vals = Vec::with_capacity(dv_pubkeys.len()); - let tbls = BlstImpl; for (idx, dv_pubkey) in dv_pubkeys.iter().enumerate() { let pub_shares: Vec> = dv_priv_shares @@ -1283,7 +1277,7 @@ fn get_validators( .map(|shares| { shares .iter() - .map(|share| tbls.secret_to_public_key(share)) + .map(tbls::secret_to_public_key) .collect::, _>>() }) .transpose()? @@ -1320,12 +1314,11 @@ fn get_validators( fn agg_sign(secrets: &[Vec], message: &[u8]) -> Result> { use pluto_crypto::types::Signature; - let tbls = BlstImpl; let mut sigs: Vec = Vec::new(); for shares in secrets { for share in shares { - let sig = tbls.sign(share, message)?; + let sig = tbls::sign(share, message)?; sigs.push(sig); } } @@ -1334,7 +1327,7 @@ fn agg_sign(secrets: &[Vec], message: &[u8]) -> Result> { return Ok(Vec::new()); } - let agg = tbls.aggregate(&sigs)?; + let agg = tbls::aggregate(&sigs)?; Ok(agg.to_vec()) } @@ -1447,7 +1440,6 @@ mod tests { lock::Lock, version::versions::*, }; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls as _}; use pluto_eth1wrap::EthClient; use pluto_eth2util::{ deposit, @@ -1785,10 +1777,9 @@ mod tests { match prep { PrepKind::None => {} PrepKind::SplitKeys { num_keys } => { - let tbls = BlstImpl; let mut keys = Vec::new(); for _ in 0..num_keys { - keys.push(tbls.generate_secret_key(rand::thread_rng()).unwrap()); + keys.push(tbls::generate_secret_key(rand::thread_rng()).unwrap()); } keystore::store_keys_insecure( &keys, @@ -2094,10 +2085,9 @@ mod tests { let split_keys_temp = TempDir::new().unwrap(); // Generate and store split keys insecurely. - let tbls_impl = BlstImpl; let mut keys = Vec::new(); for _ in 0..num_split_keys { - keys.push(tbls_impl.generate_secret_key(rand::thread_rng()).unwrap()); + keys.push(tbls::generate_secret_key(rand::thread_rng()).unwrap()); } keystore::store_keys_insecure(&keys, split_keys_temp.path(), &CONFIRM_INSECURE_KEYS) .await @@ -2492,9 +2482,7 @@ mod tests { const TEST_AUTH_TOKEN: &str = "api-token-test"; - let tbls_impl = BlstImpl; - - let original_secret = tbls_impl.generate_secret_key(rand::thread_rng()).unwrap(); + let original_secret = tbls::generate_secret_key(rand::thread_rng()).unwrap(); let key_dir = TempDir::new().unwrap(); keystore::store_keys_insecure( std::slice::from_ref(&original_secret), @@ -2575,7 +2563,7 @@ mod tests { shares.insert(u64::try_from(i + 1).unwrap(), secret); } - let recovered = tbls_impl.recover_secret(&shares).unwrap(); + let recovered = tbls::recover_secret(&shares).unwrap(); assert_eq!(recovered, original_secret); } diff --git a/crates/cluster/src/helpers.rs b/crates/cluster/src/helpers.rs index d5b80404..8458b227 100644 --- a/crates/cluster/src/helpers.rs +++ b/crates/cluster/src/helpers.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use pluto_crypto::tbls::Tbls; +use pluto_crypto::tbls; use pluto_eth2util::helpers::{checksum_address, public_key_to_address}; use pluto_k1util::K1UtilError; use serde::{Deserialize, Deserializer, Serializer}; @@ -217,15 +217,13 @@ pub fn agg_sign( secrets: &[Vec], message: &[u8], ) -> Result { - let blst = pluto_crypto::blst_impl::BlstImpl; - let sigs = secrets .iter() .flat_map(|shares| shares.iter()) - .map(|share| blst.sign(share, message)) + .map(|share| tbls::sign(share, message)) .collect::, _>>()?; - blst.aggregate(&sigs) + tbls::aggregate(&sigs) } #[cfg(test)] diff --git a/crates/cluster/src/lock.rs b/crates/cluster/src/lock.rs index 92da90e5..08c4aa60 100644 --- a/crates/cluster/src/lock.rs +++ b/crates/cluster/src/lock.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, tblsconv}; +use pluto_crypto::{tbls, types}; use pluto_eth1wrap::EthClient; use pluto_eth2api::spec::phase0::{VERSION_LEN, Version}; use pluto_eth2util::registration; @@ -70,7 +70,7 @@ pub enum LockError { /// Failed to convert BLS bytes #[error("Failed to convert BLS bytes: {0}")] - FailedToConvertBLSBytes(#[from] pluto_crypto::tblsconv::ConvError), + FailedToConvertBLSBytes(#[from] pluto_crypto::types::ConvError), /// Failed to verify BLS signature #[error("Failed to verify BLS signature: {0}")] @@ -298,18 +298,18 @@ impl Lock { return Err(LockError::EmptyLockAggregateSignature); } - let signature = tblsconv::signature_from_bytes(&self.signature_aggregate)?; + let signature = types::signature_from_bytes(&self.signature_aggregate)?; let pubkeys = self .distributed_validators .iter() .flat_map(|v| v.pub_shares.iter()) - .map(|share| tblsconv::pubkey_from_bytes(share)) + .map(|share| types::pubkey_from_bytes(share)) .collect::, _>>()?; let hash = hash_lock(self)?; - BlstImpl.verify_aggregate(&pubkeys, signature, &hash)?; + tbls::verify_aggregate(&pubkeys, signature, &hash)?; self.verify_builder_registrations()?; self.verify_node_signatures() @@ -412,7 +412,7 @@ impl Lock { timestamp: validator.builder_registration.message.timestamp.timestamp(), })?; - let pubkey = tblsconv::pubkey_from_bytes(&validator.pub_key)?; + let pubkey = types::pubkey_from_bytes(&validator.pub_key)?; let registration_message = registration::new_message( pubkey, @@ -424,7 +424,7 @@ impl Lock { let signing_root = registration::get_message_signing_root(®istration_message, fork_version); - BlstImpl.verify( + tbls::verify( &pubkey, signing_root.as_ref(), &validator.builder_registration.signature, diff --git a/crates/cluster/src/test_cluster.rs b/crates/cluster/src/test_cluster.rs index e33f5318..9664ce85 100644 --- a/crates/cluster/src/test_cluster.rs +++ b/crates/cluster/src/test_cluster.rs @@ -2,7 +2,7 @@ use crate::{definition, distvalidator, helpers, lock, operator, registration, version}; use chrono::{TimeZone, Utc}; -use pluto_crypto::tbls::Tbls; +use pluto_crypto::tbls; use rand::{RngCore, SeedableRng}; /// Returns a new cluster lock with `dv` number of distributed validators, `k` @@ -37,12 +37,9 @@ pub fn new_for_test( let mut withdrawal_addresses = Vec::with_capacity(dv); for _ in 0..dv { - let blst = pluto_crypto::blst_impl::BlstImpl; - let root_secret = blst.generate_insecure_secret(&mut rng).unwrap(); - let root_public = blst.secret_to_public_key(&root_secret).unwrap(); - let shares = blst - .threshold_split_insecure(&root_secret, n, k, &mut rng) - .unwrap(); + let root_secret = tbls::generate_insecure_secret(&mut rng).unwrap(); + let root_public = tbls::secret_to_public_key(&root_secret).unwrap(); + let shares = tbls::threshold_split_insecure(&root_secret, n, k, &mut rng).unwrap(); let mut pub_shares: Vec = Vec::with_capacity(usize::try_from(n).expect("n fits in usize")); @@ -51,7 +48,7 @@ pub fn new_for_test( for i in 0..n { let share_priv_key = *shares.get(&i.checked_add(1).unwrap()).unwrap(); - let share_pub = blst.secret_to_public_key(&share_priv_key).unwrap(); + let share_pub = tbls::secret_to_public_key(&share_priv_key).unwrap(); pub_shares.push(share_pub); priv_shares.push(share_priv_key); @@ -172,12 +169,10 @@ fn get_signed_registration( fee_recipient: [u8; 20], network_name: impl AsRef, ) -> registration::BuilderRegistration { - let blst = pluto_crypto::blst_impl::BlstImpl; - let timestamp = pluto_eth2util::network::network_to_genesis_time(network_name.as_ref()).unwrap(); - let pubkey = blst.secret_to_public_key(secret).unwrap(); - let eth2pubkey = pluto_crypto::tblsconv::pubkey_to_eth2(pubkey); + let pubkey = tbls::secret_to_public_key(secret).unwrap(); + let eth2pubkey = pluto_crypto::types::pubkey_to_eth2(pubkey); let msg = pluto_eth2api::v1::ValidatorRegistration { fee_recipient, @@ -192,7 +187,7 @@ fn get_signed_registration( .unwrap(); let sig_root = pluto_eth2util::registration::get_message_signing_root(&msg, fork_version); - let signature = blst.sign(secret, &sig_root).unwrap(); + let signature = tbls::sign(secret, &sig_root).unwrap(); registration::BuilderRegistration { message: registration::Registration { diff --git a/crates/core/src/bcast/mod.rs b/crates/core/src/bcast/mod.rs index 7cec31e9..99a3f53e 100644 --- a/crates/core/src/bcast/mod.rs +++ b/crates/core/src/bcast/mod.rs @@ -6,7 +6,7 @@ mod recast; use std::{any::Any, error::Error as StdError}; use chrono::{DateTime, Duration, Utc}; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; +use pluto_crypto::tbls; use pluto_eth2api::{ AttesterDuty, BeaconNodeClient, EthBeaconNodeApiClient, GetStateValidatorsResponseResponseDatum, ValidatorStatus, data_version_is_before_electra, @@ -706,7 +706,7 @@ fn attestation_matches_duty( .0; let signature = payload.signature(); - match BlstImpl.verify(&attester_duty.pubkey, &signing_root, &signature) { + match tbls::verify(&attester_duty.pubkey, &signing_root, &signature) { Ok(()) => Ok(true), Err(pluto_crypto::types::Error::VerificationFailed(_)) => Ok(false), Err(source) => Err(Error::Crypto { @@ -799,7 +799,6 @@ mod tests { sync::{Arc, Mutex}, }; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; use pluto_eth2api::{ GetStateValidatorsResponseResponse, ValidatorResponseValidator, spec::{bellatrix, electra, phase0}, @@ -1016,7 +1015,7 @@ mod tests { } .tree_hash_root() .0; - let signature = BlstImpl.sign(secret, &signing_root).expect("sign"); + let signature = tbls::sign(secret, &signing_root).expect("sign"); versioned::VersionedAttestation { version: versioned::DataVersion::Electra, @@ -1218,10 +1217,8 @@ mod tests { #[tokio::test] async fn broadcast_attester_backfills_electra_validator_index() { - let secret = BlstImpl - .generate_insecure_secret(StdRng::seed_from_u64(42)) - .expect("secret"); - let public_key = BlstImpl.secret_to_public_key(&secret).expect("pubkey"); + let secret = tbls::generate_insecure_secret(StdRng::seed_from_u64(42)).expect("secret"); + let public_key = tbls::secret_to_public_key(&secret).expect("pubkey"); let beacon = BeaconMock::builder() .spec(deterministic_electra_spec()) .endpoint_overrides(vec![( diff --git a/crates/core/src/eth2signeddata.rs b/crates/core/src/eth2signeddata.rs index 729a1d57..22e663ab 100644 --- a/crates/core/src/eth2signeddata.rs +++ b/crates/core/src/eth2signeddata.rs @@ -282,7 +282,7 @@ impl Eth2SignedData for SyncCommitteeSelection { mod tests { use std::{fs, path::PathBuf}; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; + use pluto_crypto::tbls; use pluto_eth2api::spec::phase0; use pluto_testutil::BeaconMock; use serde::de::DeserializeOwned; @@ -338,15 +338,14 @@ mod tests { let epoch = data.epoch(client).await.unwrap(); let root = data.message_root().unwrap(); - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).unwrap(); - let pubkey = tbls.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_secret_key(&mut rng).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); let sig_data = signing::get_data_root(client, data.domain_name(), epoch, root) .await .unwrap(); - let sig: Signature = tbls.sign(&secret, &sig_data).unwrap(); + let sig: Signature = tbls::sign(&secret, &sig_data).unwrap(); let signed = data.set_signature(sig).unwrap(); @@ -463,16 +462,15 @@ mod tests { let epoch = data.epoch(client).await.unwrap(); let root = data.message_root().unwrap(); - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).unwrap(); - let wrong_secret = tbls.generate_secret_key(&mut rng).unwrap(); - let wrong_pubkey = tbls.secret_to_public_key(&wrong_secret).unwrap(); + let secret = tbls::generate_secret_key(&mut rng).unwrap(); + let wrong_secret = tbls::generate_secret_key(&mut rng).unwrap(); + let wrong_pubkey = tbls::secret_to_public_key(&wrong_secret).unwrap(); let sig_data = signing::get_data_root(client, data.domain_name(), epoch, root) .await .unwrap(); - let sig: Signature = tbls.sign(&secret, &sig_data).unwrap(); + let sig: Signature = tbls::sign(&secret, &sig_data).unwrap(); let signed = data.set_signature(sig).unwrap(); let err = verify_eth2_signed_data(client, &signed, &wrong_pubkey) diff --git a/crates/core/src/parsigex_codec.rs b/crates/core/src/parsigex_codec.rs index ac732cb7..0b3090aa 100644 --- a/crates/core/src/parsigex_codec.rs +++ b/crates/core/src/parsigex_codec.rs @@ -92,7 +92,7 @@ fn deserialize_signature(bytes: &[u8]) -> Result, ParSigExCo let raw = base64::engine::general_purpose::STANDARD .decode(encoded) .map_err(|e| ParSigExCodecError::SignedData(format!("invalid base64: {e}")))?; - let sig: Signature = pluto_crypto::tblsconv::signature_from_bytes(&raw) + let sig: Signature = pluto_crypto::types::signature_from_bytes(&raw) .map_err(|e| ParSigExCodecError::InvalidSignature(e.to_string()))?; Ok(Box::new(sig)) } diff --git a/crates/core/src/sigagg.rs b/crates/core/src/sigagg.rs index 944d7094..de1dfcdd 100644 --- a/crates/core/src/sigagg.rs +++ b/crates/core/src/sigagg.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::PublicKey}; +use pluto_crypto::{tbls, types::PublicKey}; use pluto_eth2api::client::EthBeaconNodeApiClient; use tracing::{debug, error, info_span}; @@ -196,10 +196,10 @@ impl Aggregator { return Err(SigAggError::InsufficientDistinctSignatures { pubkey: *pubkey }); } - let span = info_span!("BlstImpl::threshold_aggregate"); + let span = info_span!("tbls::threshold_aggregate"); let agg_bytes = { let _enter = span.enter(); - BlstImpl.threshold_aggregate(&bls_sigs) + tbls::threshold_aggregate(&bls_sigs) } .map_err(|e| { error!(parent: &span, error = %e, "threshold aggregate failed"); @@ -281,7 +281,6 @@ pub fn new_verifier(eth2_cl: Arc) -> VerifyFn { mod tests { use std::{fs, sync::Mutex}; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; use pluto_ssz::HashRoot; use super::*; @@ -420,16 +419,15 @@ mod tests { const PEERS: u64 = 4; const MSG: [u8; 32] = [42u8; 32]; - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).unwrap(); - let pubkey = tbls.secret_to_public_key(&secret).unwrap(); - let shares = tbls.threshold_split(&secret, PEERS, THRESHOLD).unwrap(); + let secret = tbls::generate_secret_key(&mut rng).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); + let shares = tbls::threshold_split(&secret, PEERS, THRESHOLD).unwrap(); let mut bls_map: HashMap = HashMap::new(); let mut sigs = Vec::new(); for (share_idx, share) in &shares { - let sig = tbls.sign(share, &MSG).unwrap(); + let sig = tbls::sign(share, &MSG).unwrap(); bls_map.insert(*share_idx, sig); sigs.push((*share_idx, sig)); } @@ -437,7 +435,7 @@ mod tests { BLSContext { pubkey, sigs, - expected_agg: tbls.threshold_aggregate(&bls_map).unwrap(), + expected_agg: tbls::threshold_aggregate(&bls_map).unwrap(), } } @@ -569,17 +567,16 @@ mod tests { const THRESHOLD: u64 = 3; const PEERS: u64 = 4; - let tbls = BlstImpl; let mut rng = rand::thread_rng(); - let secret = tbls.generate_secret_key(&mut rng).unwrap(); - let pubkey = tbls.secret_to_public_key(&secret).unwrap(); - let shares = tbls.threshold_split(&secret, PEERS, THRESHOLD).unwrap(); + let secret = tbls::generate_secret_key(&mut rng).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); + let shares = tbls::threshold_split(&secret, PEERS, THRESHOLD).unwrap(); let msg = [7u8; 32]; let mut par_sigs = Vec::new(); for (share_idx, share) in &shares { - let sig = tbls.sign(share, &msg).unwrap(); + let sig = tbls::sign(share, &msg).unwrap(); par_sigs.push(ParSignedData::new(MockSignedData { sig }, *share_idx)); } @@ -696,7 +693,6 @@ mod tests { const THRESHOLD: u64 = 3; const PEERS: u64 = 4; - let tbls = BlstImpl; let mut rng = rand::thread_rng(); let msg = [55u8; 32]; @@ -704,19 +700,19 @@ mod tests { let mut expected: HashMap = HashMap::new(); for _ in 0..2 { - let secret = tbls.generate_secret_key(&mut rng).unwrap(); - let pubkey_bytes = tbls.secret_to_public_key(&secret).unwrap(); - let shares = tbls.threshold_split(&secret, PEERS, THRESHOLD).unwrap(); + let secret = tbls::generate_secret_key(&mut rng).unwrap(); + let pubkey_bytes = tbls::secret_to_public_key(&secret).unwrap(); + let shares = tbls::threshold_split(&secret, PEERS, THRESHOLD).unwrap(); let mut par_sigs = Vec::new(); let mut bls_map: HashMap = HashMap::new(); for (share_idx, share) in &shares { - let sig = tbls.sign(share, &msg).unwrap(); + let sig = tbls::sign(share, &msg).unwrap(); bls_map.insert(*share_idx, sig); par_sigs.push(ParSignedData::new(MockSignedData { sig }, *share_idx)); } - let agg_sig = tbls.threshold_aggregate(&bls_map).unwrap(); + let agg_sig = tbls::threshold_aggregate(&bls_map).unwrap(); let pubkey = PubKey::new(pubkey_bytes); expected.insert(pubkey, agg_sig); agg_set.insert(pubkey, par_sigs); diff --git a/crates/core/src/signeddata.rs b/crates/core/src/signeddata.rs index e4bd8ed0..083dc49b 100644 --- a/crates/core/src/signeddata.rs +++ b/crates/core/src/signeddata.rs @@ -4,6 +4,7 @@ use alloy::primitives::U256; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tree_hash::TreeHash; +use pluto_crypto::types::sig_to_eth2; use pluto_eth2api::{ ConsensusVersion, ProduceBlockV3ResponseResponse, spec::{ @@ -117,10 +118,6 @@ pub fn sig_from_eth2(sig: phase0::BLSSignature) -> Signature { sig } -fn sig_to_eth2(sig: &Signature) -> phase0::BLSSignature { - *sig -} - impl SignedData for Signature { fn signature(&self) -> Result { Ok(*self) @@ -239,7 +236,7 @@ impl SignedData for VersionedSignedProposal { if proposal.version == versioned::DataVersion::Unknown { return Err(SignedDataError::UnknownVersion); } - let eth2_sig = sig_to_eth2(&signature); + let eth2_sig = sig_to_eth2(signature); proposal.block.set_signature(eth2_sig); Ok(out) @@ -386,7 +383,7 @@ impl SignedData for Attestation { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.signature = sig_to_eth2(&signature); + out.0.signature = sig_to_eth2(signature); Ok(out) } @@ -470,7 +467,7 @@ impl SignedData for VersionedAttestation { .attestation .as_mut() .ok_or(SignedDataError::MissingAttestation(version))? - .set_signature(sig_to_eth2(&signature)); + .set_signature(sig_to_eth2(signature)); Ok(out) } @@ -593,7 +590,7 @@ impl SignedData for SignedVoluntaryExit { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.signature = sig_to_eth2(&signature); + out.0.signature = sig_to_eth2(signature); Ok(out) } @@ -676,7 +673,7 @@ impl SignedData for VersionedSignedValidatorRegistration { let Some(v1) = out.0.v1.as_mut() else { return Err(SignedDataError::MissingV1Registration); }; - v1.signature = sig_to_eth2(&signature); + v1.signature = sig_to_eth2(signature); } versioned::BuilderVersion::Unknown => { return Err(SignedDataError::UnknownVersion); @@ -765,7 +762,7 @@ impl SignedData for SignedRandao { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.signature = sig_to_eth2(&signature); + out.0.signature = sig_to_eth2(signature); Ok(out) } @@ -815,7 +812,7 @@ impl SignedData for BeaconCommitteeSelection { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.selection_proof = sig_to_eth2(&signature); + out.0.selection_proof = sig_to_eth2(signature); Ok(out) } @@ -858,7 +855,7 @@ impl SignedData for SyncCommitteeSelection { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.selection_proof = sig_to_eth2(&signature); + out.0.selection_proof = sig_to_eth2(signature); Ok(out) } @@ -901,7 +898,7 @@ impl SignedData for SignedAggregateAndProof { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.signature = sig_to_eth2(&signature); + out.0.signature = sig_to_eth2(signature); Ok(out) } @@ -987,7 +984,7 @@ impl SignedData for VersionedSignedAggregateAndProof { } out.0 .aggregate_and_proof - .set_signature(sig_to_eth2(&signature)); + .set_signature(sig_to_eth2(signature)); Ok(out) } @@ -1081,7 +1078,7 @@ impl SignedData for SignedSyncMessage { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.signature = sig_to_eth2(&signature); + out.0.signature = sig_to_eth2(signature); Ok(out) } @@ -1124,7 +1121,7 @@ impl SignedData for SyncContributionAndProof { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.selection_proof = sig_to_eth2(&signature); + out.0.selection_proof = sig_to_eth2(signature); Ok(out) } @@ -1167,7 +1164,7 @@ impl SignedData for SignedSyncContributionAndProof { fn set_signature(&self, signature: Signature) -> Result { let mut out = self.clone(); - out.0.signature = sig_to_eth2(&signature); + out.0.signature = sig_to_eth2(signature); Ok(out) } diff --git a/crates/core/src/validatorapi/component.rs b/crates/core/src/validatorapi/component.rs index 9328a9a8..748aa40b 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -2736,7 +2736,7 @@ mod tests { use std::sync::Mutex; use chrono::{DateTime, Utc}; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; + use pluto_crypto::tbls; use pluto_eth2api::spec::altair::{ ContributionAndProof, SignedContributionAndProof as AltairSignedContributionAndProof, SyncCommitteeContribution as AltairSyncCommitteeContribution, @@ -3514,10 +3514,8 @@ mod tests { #[tokio::test] async fn verify_partial_sig_accepts_valid_and_rejects_invalid() { // Generate a BLS keypair to act as this node's public share. - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let pubshare = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let pubshare = tbls::secret_to_public_key(&secret).unwrap(); let dv_root = dv_pubkey(0xAA); let map = HashMap::from([(dv_root, pubshare)]); @@ -3534,7 +3532,7 @@ mod tests { pluto_eth2util::signing::get_data_root(mock.client(), domain, epoch, message_root) .await .unwrap(); - let good_signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let good_signature = tbls::sign(&secret, &signing_root).unwrap(); component .verify_partial_sig(&dv_root, domain, epoch, message_root, &good_signature) @@ -3605,10 +3603,8 @@ mod tests { /// the 400 the VC would misread as an invalid signature. #[tokio::test] async fn beacon_outage_during_verification_maps_to_502_not_400() { - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let pubshare = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let pubshare = tbls::secret_to_public_key(&secret).unwrap(); let dv_root = dv_pubkey(0xAB); // Unroutable beacon node: domain resolution fails before any BLS @@ -4467,10 +4463,8 @@ mod tests { const VAL_IDX: u64 = 5; const EPOCH: u64 = 3; - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let pubshare = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let pubshare = tbls::secret_to_public_key(&secret).unwrap(); let dv_root = dv_pubkey(0xCC); let map = HashMap::from([(dv_root, pubshare)]); let active = HashMap::from([(VAL_IDX, dv_root)]); @@ -4600,10 +4594,8 @@ mod tests { /// upstream + real BLS to drive the verification path. #[tokio::test] async fn submit_validator_registrations_rejects_bad_signature() { - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let pubshare = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let pubshare = tbls::secret_to_public_key(&secret).unwrap(); let dv_root = dv_pubkey(0xA5); let map = HashMap::from([(dv_root, pubshare)]); @@ -5023,10 +5015,8 @@ mod tests { /// share passes the outer partial-sig verify and the set fans out. #[tokio::test] async fn submit_sync_committee_messages_accepts_valid_partial_sig() { - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let pubshare = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let pubshare = tbls::secret_to_public_key(&secret).unwrap(); let dv_root = [0x77_u8; 48]; let slot: u64 = 1; @@ -5043,7 +5033,7 @@ mod tests { ) .await .unwrap(); - let signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let signature = tbls::sign(&secret, &signing_root).unwrap(); let map = HashMap::from([(dv_root, pubshare)]); let cancel = CancellationToken::new(); @@ -5096,10 +5086,8 @@ mod tests { /// the signing root. #[tokio::test] async fn verify_partial_sig_round_trips_sync_committee_domain() { - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let pubshare = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let pubshare = tbls::secret_to_public_key(&secret).unwrap(); let dv_root = [0xAB_u8; 48]; let map = HashMap::from([(dv_root, pubshare)]); @@ -5124,7 +5112,7 @@ mod tests { ) .await .unwrap(); - let signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let signature = tbls::sign(&secret, &signing_root).unwrap(); component .verify_partial_sig( @@ -5146,10 +5134,8 @@ mod tests { async fn submit_sync_committee_contributions_rejects_invalid_partial_sig() { // A valid BLS point: an unparseable root pubkey would map to 500 // (server-side state), not the 400 under test. - let secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let dv_root = BlstImpl.secret_to_public_key(&secret).unwrap(); + let secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let dv_root = tbls::secret_to_public_key(&secret).unwrap(); let mock = mock_beacon_for_signing().await; let cancel = CancellationToken::new(); let (deadliner, _deadliner_rx) = DeadlinerTask::start( @@ -5195,14 +5181,10 @@ mod tests { // Root secret signs the inner selection proof; share secret signs // the outer partial sig. Both pubkeys are derived from the BLS // secret keys and wired through the per-validator share map. - let root_secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let root_pubkey = BlstImpl.secret_to_public_key(&root_secret).unwrap(); - let share_secret = BlstImpl - .generate_insecure_secret(rand::rngs::OsRng) - .unwrap(); - let share_pubkey = BlstImpl.secret_to_public_key(&share_secret).unwrap(); + let root_secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let root_pubkey = tbls::secret_to_public_key(&root_secret).unwrap(); + let share_secret = tbls::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + let share_pubkey = tbls::secret_to_public_key(&share_secret).unwrap(); let slot: u64 = 1; let subcommittee_index: u64 = 3; @@ -5233,9 +5215,7 @@ mod tests { ) .await .unwrap(); - let selection_proof = BlstImpl - .sign(&root_secret, &selection_proof_signing_root) - .unwrap(); + let selection_proof = tbls::sign(&root_secret, &selection_proof_signing_root).unwrap(); // Outer: sign HTR(ContributionAndProof) — including the just-computed // selection_proof — with the share secret under @@ -5258,7 +5238,7 @@ mod tests { ) .await .unwrap(); - let outer_signature = BlstImpl.sign(&share_secret, &outer_signing_root).unwrap(); + let outer_signature = tbls::sign(&share_secret, &outer_signing_root).unwrap(); let map = HashMap::from([(root_pubkey, share_pubkey)]); let cancel = CancellationToken::new(); diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index f9929e9b..8d9f136d 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -16,8 +16,9 @@ thiserror.workspace = true zeroize.workspace = true [lints.rust] -# Allow unsafe code for blst C bindings (overrides workspace forbid) -unsafe_code = "allow" +# `deny` rather than the workspace `forbid` so `tbls::math` — the sole module +# wrapping the blst C bindings — can opt back in with `#![allow(unsafe_code)]`. +unsafe_code = "deny" missing_docs = "deny" [lints.clippy] diff --git a/crates/crypto/src/blst_impl.rs b/crates/crypto/src/blst_impl.rs deleted file mode 100644 index 62832f65..00000000 --- a/crates/crypto/src/blst_impl.rs +++ /dev/null @@ -1,1119 +0,0 @@ -//! # BLST implementation -//! -//! Implementation of threshold BLS signatures using the blst library. -//! This implementation is compatible with the Herumi BLS library used in the Go -//! implementation. -#![allow(unsafe_code)] - -use std::collections::{HashMap, HashSet}; - -use blst::{ - BLST_ERROR, - min_pk::{PublicKey as BlstPublicKey, SecretKey as BlstSecretKey, Signature as BlstSignature}, -}; -use rand_core::{CryptoRng, RngCore}; -use zeroize::Zeroizing; - -use crate::{ - tbls::Tbls, - types::{BlsError, Error, Index, PrivateKey, PublicKey, SIGNATURE_LENGTH, Signature}, -}; - -/// Domain Separation Tag for Ethereum 2.0 BLS signatures -const ETH2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; - -/// Serialized BLS12-381 G2 compressed point at infinity (the identity -/// signature). This is the value Charon's Herumi `Aggregate` returns for an -/// empty input slice: `sig.Serialize()` of a zero `bls.Sign`. The high byte -/// sets the compression bit (`0x80`) and the infinity bit (`0x40`); all other -/// bytes are zero. -const IDENTITY_SIGNATURE: Signature = { - let mut s = [0u8; SIGNATURE_LENGTH]; - s[0] = 0xc0; - s -}; - -/// BLST implementation of threshold BLS signatures. -/// -/// This implementation is compatible with the Herumi BLS library used in -/// the Go implementation of Charon. -#[derive(Default, Clone, Copy, PartialEq, Eq)] -pub struct BlstImpl; - -impl Tbls for BlstImpl { - fn generate_secret_key(&self, mut rng: impl RngCore + CryptoRng) -> Result { - // `ikm` is secret input key material; wipe it on drop. (`BlstSecretKey` - // itself is `#[zeroize(drop)]`, so the derived `sk` is wiped too.) - let mut ikm = Zeroizing::new([0u8; 32]); - rng.fill_bytes(ikm.as_mut()); - - let sk = BlstSecretKey::key_gen(ikm.as_ref(), &[]) - .map_err(|_| Error::InvalidSecretKey(BlsError::KeyGeneration))?; - - Ok(sk.to_bytes()) - } - - fn generate_insecure_secret( - &self, - mut rng: impl RngCore + CryptoRng, - ) -> Result { - for _ in 0..100 { - // Wipe the candidate buffer on every iteration; on success its value - // is copied into the returned key first. - let mut bytes = Zeroizing::new([0u8; 32]); - rng.fill_bytes(bytes.as_mut()); - - if BlstSecretKey::from_bytes(bytes.as_ref()).is_ok() { - return Ok(*bytes); - } - } - Err(Error::InvalidSecretKey(BlsError::KeyGeneration)) - } - - fn secret_to_public_key(&self, secret_key: &PrivateKey) -> Result { - let sk = - BlstSecretKey::from_bytes(secret_key).map_err(|e| Error::InvalidSecretKey(e.into()))?; - let pk = sk.sk_to_pk(); - Ok(pk.to_bytes()) - } - - fn threshold_split_insecure( - &self, - secret_key: &PrivateKey, - total: Index, - threshold: Index, - mut rng: impl RngCore + CryptoRng, - ) -> Result, Error> { - // Charon's Herumi backend only rejects `threshold <= 1` - // (see charon/tbls/herumi.go ThresholdSplit @ v1.7.1). We additionally - // reject `threshold > total`: such a (t, n) scheme is unrecoverable and - // is always a programming error. No Charon call site passes t > n, so - // this hardening never rejects an otherwise-valid split. - if threshold <= 1 || threshold > total { - return Err(Error::InvalidThreshold { threshold, total }); - } - - // `threshold` is bounded above by `total` here; the conversion is - // infallible on 64-bit targets and only fails on 32-bit targets for an - // implausibly large `total`. Map that to a dedicated overflow error - // rather than re-using InvalidThreshold (the value is in range). - let threshold_usize = - usize::try_from(threshold).map_err(|_| Error::ThresholdOverflow { threshold })?; - - let sk = - BlstSecretKey::from_bytes(secret_key).map_err(|e| Error::InvalidSecretKey(e.into()))?; - - // Create polynomial coefficients: a_0 = secret, a_1..a_{t-1} = random. - // `poly` holds `BlstSecretKey`s, each `#[zeroize(drop)]`, so the secret - // coefficients are wiped when `poly` is dropped. - let mut poly = Vec::with_capacity(threshold_usize); - poly.push(sk); - - for _ in 1..threshold { - let mut ikm = Zeroizing::new([0u8; 32]); - rng.fill_bytes(ikm.as_mut()); - let coeff = BlstSecretKey::key_gen(ikm.as_ref(), &[]) - .map_err(|_| Error::InvalidSecretKey(BlsError::KeyGeneration))?; - poly.push(coeff); - } - - // Evaluate polynomial at points 1..total to create shares - let mut shares = HashMap::new(); - for i in 1..=total { - let share = evaluate_polynomial(&poly, i)?; - shares.insert(i, share.to_bytes()); - } - - Ok(shares) - } - - fn threshold_split( - &self, - secret_key: &PrivateKey, - total: Index, - threshold: Index, - ) -> Result, Error> { - // Use OsRng for secure random number generation - self.threshold_split_insecure(secret_key, total, threshold, rand::rngs::OsRng) - } - - fn recover_secret(&self, shares: &HashMap) -> Result { - if shares.is_empty() { - return Err(Error::SharesAreEmpty); - } - - // Share indices are already 1-indexed (matching their polynomial evaluation - // points) - let share_points: Vec = shares.keys().copied().collect(); - - // The reconstructed master secret and the parsed share scalars are all - // `BlstSecretKey`s (`#[zeroize(drop)]`), so they are wiped on drop. - let share_secrets: Vec = shares - .values() - .map(|bytes| { - BlstSecretKey::from_bytes(bytes).map_err(|e| Error::InvalidSecretKey(e.into())) - }) - .collect::, _>>()?; - - // Lagrange interpolation at x=0 - let recovered = lagrange_interpolate_secret(&share_points, &share_secrets)?; - Ok(recovered.to_bytes()) - } - - fn aggregate(&self, signatures: &[Signature]) -> Result { - // Parity with Charon Herumi `Aggregate` (tbls/herumi.go:227): an empty - // input is NOT an error. Herumi aggregates into a zero `bls.Sign` and - // returns its serialized form, i.e. the G2 compressed point at infinity. - if signatures.is_empty() { - return Ok(IDENTITY_SIGNATURE); - } - - // Deserialize every input signature (matches the Herumi loop, which - // errors if any element fails to deserialize). Note: aggregation - // canonicalizes the output even for a single input (Herumi returns - // `sig.Serialize()`, never the input bytes verbatim). - let parsed_sigs: Vec = signatures - .iter() - .map(|sig_bytes| { - BlstSignature::from_bytes(sig_bytes).map_err(|e| Error::InvalidSignature(e.into())) - }) - .collect::, _>>()?; - - let sigs: Vec<&BlstSignature> = parsed_sigs.iter().collect(); - - let agg = blst::min_pk::AggregateSignature::aggregate(&sigs[..], true) - .map_err(|e| Error::AggregationFailed(e.into()))?; - - Ok(agg.to_signature().to_bytes()) - } - - fn threshold_aggregate( - &self, - partial_signatures_by_idx: &HashMap, - ) -> Result { - if partial_signatures_by_idx.is_empty() { - return Err(Error::EmptySignatureArray); - } - - // Signature indices are already 1-indexed (matching share evaluation points) - let indices: Vec = partial_signatures_by_idx.keys().copied().collect(); - - let signatures: Vec = partial_signatures_by_idx - .values() - .map(|sig_bytes| { - BlstSignature::from_bytes(sig_bytes).map_err(|e| Error::InvalidSignature(e.into())) - }) - .collect::, _>>()?; - - // Perform Lagrange interpolation on signatures at x=0 - let recovered_sig = lagrange_interpolate_signature(&indices, &signatures)?; - Ok(recovered_sig.to_bytes()) - } - - fn verify( - &self, - public_key: &PublicKey, - data: &[u8], - raw_signature: &Signature, - ) -> Result<(), Error> { - let pk = - BlstPublicKey::from_bytes(public_key).map_err(|e| Error::InvalidPublicKey(e.into()))?; - - let sig = BlstSignature::from_bytes(raw_signature) - .map_err(|e| Error::InvalidSignature(e.into()))?; - - let result = sig.verify(true, data, ETH2_DST, &[], &pk, true); - - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(Error::VerificationFailed(result.into())) - } - } - - fn sign(&self, private_key: &PrivateKey, data: &[u8]) -> Result { - let sk = BlstSecretKey::from_bytes(private_key) - .map_err(|e| Error::InvalidSecretKey(e.into()))?; - let sig = sk.sign(data, ETH2_DST, &[]); - Ok(sig.to_bytes()) - } - - fn verify_aggregate( - &self, - public_keys: &[PublicKey], - signature: Signature, - data: &[u8], - ) -> Result<(), Error> { - if public_keys.is_empty() { - return Err(Error::EmptyPublicKeyArray); - } - - let pks: Vec = public_keys - .iter() - .map(|pk_bytes| { - BlstPublicKey::from_bytes(pk_bytes).map_err(|e| Error::InvalidPublicKey(e.into())) - }) - .collect::, _>>()?; - - let sig = - BlstSignature::from_bytes(&signature).map_err(|e| Error::InvalidSignature(e.into()))?; - - // Aggregate public keys using blst point addition - let agg_pk = aggregate_public_keys(&pks)?; - - let result = sig.verify(true, data, ETH2_DST, &[], &agg_pk, true); - - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(Error::VerificationFailed(result.into())) - } - } -} - -/// Aggregate public keys -fn aggregate_public_keys(pks: &[BlstPublicKey]) -> Result { - if pks.is_empty() { - return Err(Error::EmptyPublicKeyArray); - } - - let mut agg = blst::blst_p1::default(); - - unsafe { - // Convert first key to projective form - let first_affine: &blst::blst_p1_affine = (&pks[0]).into(); - blst::blst_p1_from_affine(&mut agg, first_affine); - - for pk in pks.iter().skip(1) { - let pk_affine: &blst::blst_p1_affine = pk.into(); - blst::blst_p1_add_or_double_affine(&mut agg, &agg, pk_affine); - } - - // Convert back to affine - let mut agg_affine = blst::blst_p1_affine::default(); - blst::blst_p1_to_affine(&mut agg_affine, &agg); - Ok(BlstPublicKey::from(agg_affine)) - } -} - -/// Evaluate polynomial at point x -/// poly(x) = a_0 + a_1*x + a_2*x^2 + ... + a_n*x^n -fn evaluate_polynomial(poly: &[BlstSecretKey], x: Index) -> Result { - if poly.is_empty() { - return Err(Error::PolynomialIsEmpty); - } - - // Start with the constant term - let mut result = poly[0].clone(); - - // Compute powers of x and accumulate - let mut x_power = scalar_from_u64(x); - - for coeff in poly.iter().skip(1) { - // result += coeff * x_power - let term = scalar_mult_secret(coeff, &x_power)?; - result = scalar_add_secret(&result, &term)?; - - // x_power *= x for next iteration - if poly.len() > 2 { - let x_scalar = scalar_from_u64(x); - x_power = scalar_mult_scalars(&x_power, &x_scalar)?; - } - } - - Ok(result) -} - -/// Lagrange interpolation of secret keys at x=0 -/// Recovers f(0) from points (x_i, y_i) where y_i are secret keys -fn lagrange_interpolate_secret( - indices: &[Index], - shares: &[BlstSecretKey], -) -> Result { - if indices.len() != shares.len() || indices.is_empty() { - return Err(Error::IndicesSharesMismatch); - } - - // Compute Lagrange coefficients and interpolate - let coeffs = compute_lagrange_coefficients(indices)?; - - let mut result = BlstSecretKey::default(); - - for i in 0..shares.len() { - let term = scalar_mult_secret(&shares[i], &coeffs[i])?; - result = scalar_add_secret(&result, &term)?; - } - - Ok(result) -} - -/// Lagrange interpolation of signatures at x=0 -/// Recovers f(0) from points (x_i, σ_i) where σ_i are signatures -fn lagrange_interpolate_signature( - indices: &[Index], - signatures: &[BlstSignature], -) -> Result { - if indices.len() != signatures.len() || indices.is_empty() { - return Err(Error::EmptySignatureArray); - } - - // Compute Lagrange coefficients - let coeffs = compute_lagrange_coefficients(indices)?; - - // Multiply each signature by its Lagrange coefficient and aggregate - let first_sig_scaled = signature_mult(&signatures[0], &coeffs[0])?; - let mut result_p2 = blst::blst_p2::default(); - - unsafe { - // Convert first scaled signature to projective - let first_affine: &blst::blst_p2_affine = (&first_sig_scaled).into(); - blst::blst_p2_from_affine(&mut result_p2, first_affine); - - for i in 1..signatures.len() { - let sig_scaled = signature_mult(&signatures[i], &coeffs[i])?; - let sig_affine: &blst::blst_p2_affine = (&sig_scaled).into(); - blst::blst_p2_add_or_double_affine(&mut result_p2, &result_p2, sig_affine); - } - - // Convert back to affine - let mut result_affine = blst::blst_p2_affine::default(); - blst::blst_p2_to_affine(&mut result_affine, &result_p2); - Ok(BlstSignature::from(result_affine)) - } -} - -/// Compute Lagrange coefficients for interpolation at x=0 -/// λ_i = ∏_{j≠i} (0 - x_j) / (x_i - x_j) = ∏_{j≠i} x_j / (x_j - x_i) -fn compute_lagrange_coefficients(indices: &[Index]) -> Result, Error> { - // Check if indices are unique - if indices.len() != indices.iter().collect::>().len() { - return Err(Error::IndicesNotUnique); - } - - let mut coeffs = Vec::with_capacity(indices.len()); - - for (i, &x_i) in indices.iter().enumerate() { - let mut numerator = scalar_from_u64(1); - let mut denominator = scalar_from_u64(1); - - for (j, &x_j) in indices.iter().enumerate() { - if i == j { - continue; - } - - // numerator *= x_j - let x_j_scalar = scalar_from_u64(x_j); - numerator = scalar_mult_scalars(&numerator, &x_j_scalar)?; - - // denominator *= (x_j - x_i) - let diff = if x_j > x_i { - scalar_from_u64(x_j.abs_diff(x_i)) - } else { - // For negative differences, we need to work in the scalar field - // x_j - x_i (mod r) where r is the curve order - scalar_negate(&scalar_from_u64(x_i.abs_diff(x_j)))? - }; - - denominator = scalar_mult_scalars(&denominator, &diff)?; - } - - // Compute numerator / denominator = numerator * denominator^{-1} - let coeff = scalar_div(&numerator, &denominator)?; - coeffs.push(coeff); - } - - Ok(coeffs) -} - -/// Convert u64 to blst scalar -fn scalar_from_u64(val: u64) -> blst::blst_scalar { - let mut scalar = blst::blst_scalar::default(); - let limbs: [u64; 4] = [val, 0, 0, 0]; - unsafe { - blst::blst_scalar_from_uint64(&mut scalar, limbs.as_ptr()); - } - scalar -} - -/// Multiply secret key by scalar -fn scalar_mult_secret( - sk: &BlstSecretKey, - scalar: &blst::blst_scalar, -) -> Result { - let sk_scalar = sk.into(); - let result_scalar = scalar_mult_scalars(sk_scalar, scalar)?; - let sk: &BlstSecretKey = (&result_scalar) - .try_into() - .map_err(|_| Error::FailedToConvertSkToBlstScalar)?; - Ok(sk.clone()) -} - -/// Add two secret keys -fn scalar_add_secret(sk1: &BlstSecretKey, sk2: &BlstSecretKey) -> Result { - let result = scalar_add(sk1.into(), sk2.into())?; - let sk: &BlstSecretKey = (&result) - .try_into() - .map_err(|_| Error::FailedToConvertScalarToSecretKey)?; - Ok(sk.clone()) -} - -/// Multiply signature by scalar -fn signature_mult(sig: &BlstSignature, scalar: &blst::blst_scalar) -> Result { - let mut sig_proj = blst::blst_p2::default(); - let mut result_p2 = blst::blst_p2::default(); - let mut result_affine = blst::blst_p2_affine::default(); - - unsafe { - // Convert affine to projective - let sig_affine: &blst::blst_p2_affine = sig.into(); - blst::blst_p2_from_affine(&mut sig_proj, sig_affine); - // Multiply - blst::blst_p2_mult(&mut result_p2, &sig_proj, scalar.b.as_ptr(), 255); - // Convert back to affine - blst::blst_p2_to_affine(&mut result_affine, &result_p2); - } - - Ok(BlstSignature::from(result_affine)) -} - -/// Add two scalars -fn scalar_add(a: &blst::blst_scalar, b: &blst::blst_scalar) -> Result { - let mut result = blst::blst_scalar::default(); - unsafe { - if blst::blst_sk_add_n_check(&mut result, a, b) { - Ok(result) - } else { - Err(Error::FailedToAddScalars) - } - } -} - -/// Multiply two scalars -fn scalar_mult_scalars( - a: &blst::blst_scalar, - b: &blst::blst_scalar, -) -> Result { - let mut result = blst::blst_scalar::default(); - unsafe { - if blst::blst_sk_mul_n_check(&mut result, a, b) { - Ok(result) - } else { - Err(Error::FailedToMultiplyScalars) - } - } -} - -/// Negate a scalar -fn scalar_negate(a: &blst::blst_scalar) -> Result { - // To negate in the field, we compute (r - a) where r is the curve order - // But blst doesn't expose this directly, so we use: -a ≡ r - a - // We can compute this as: 0 - a - let zero = scalar_from_u64(0); - let mut result_scalar = blst::blst_scalar::default(); - - unsafe { - // Convert scalars to fr for arithmetic - let mut a_fr = blst::blst_fr::default(); - let mut zero_fr = blst::blst_fr::default(); - - blst::blst_fr_from_scalar(&mut a_fr, a); - blst::blst_fr_from_scalar(&mut zero_fr, &zero); - - let mut result_fr = blst::blst_fr::default(); - blst::blst_fr_sub(&mut result_fr, &zero_fr, &a_fr); - - blst::blst_scalar_from_fr(&mut result_scalar, &result_fr); - } - - Ok(result_scalar) -} - -/// Divide two scalars (multiply by inverse) -fn scalar_div( - numerator: &blst::blst_scalar, - denominator: &blst::blst_scalar, -) -> Result { - let zero = blst::blst_scalar::default(); - if *denominator == zero { - return Err(Error::DivisionByZero); - } - - let mut inv_scalar = blst::blst_scalar::default(); - - unsafe { - blst::blst_sk_inverse(&mut inv_scalar, denominator); - } - - scalar_mult_scalars(numerator, &inv_scalar) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn setup() -> BlstImpl { - BlstImpl - } - - #[test] - fn generate_insecure_secret() { - let blst = setup(); - let sk = blst.generate_insecure_secret(rand::rngs::OsRng).unwrap(); - assert_eq!(sk.len(), 32); - } - - #[test] - fn verify_aggregate_from_data() { - let blst = setup(); - let data = b"hello obol!"; - - // Decode the secret key from hex - let secret_bytes = - hex::decode("7356c7dab0220088158a8bba45894b164c04cf7de83149e2c4fab381e765ff38") - .unwrap(); - assert_eq!(secret_bytes.len(), 32); - - let mut secret = [0u8; 32]; - secret.copy_from_slice(&secret_bytes); - assert!(!secret.is_empty()); - - // Split the secret into shares (total=5, threshold=3) - let shares = blst.threshold_split(&secret, 5, 3).unwrap(); - assert_eq!(shares.len(), 5); - - // Create signatures for each share - let mut signatures = HashMap::new(); - for (idx, key) in shares.iter() { - let signature = blst.sign(key, data).unwrap(); - signatures.insert(*idx, signature); - } - - // Aggregate the threshold signatures - let total_sig = blst.threshold_aggregate(&signatures).unwrap(); - - // Expected signature from the Go implementation - let expected_sig = hex::decode("b46736c3a1fb5d7977acc6abf3cb3a10fd1a5aed301437022f28cf616326186654d747fda7cd530c2bf18c640e4c024b01d7ba38d90e4abe0cc5356ef63b8e20f717ef0a1f68c3292bd62b4f891345ecafa89a8604f8f6c3ce193dc239215adf").unwrap(); - - // Compare the aggregated signature with the expected one - assert_eq!( - expected_sig, - &total_sig[..], - "Aggregated signature does not match expected signature from Go implementation" - ); - } - - #[test] - fn generate_and_derive_key() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - assert_eq!(sk.len(), 32); - - let pk = blst.secret_to_public_key(&sk).unwrap(); - assert_eq!(pk.len(), 48); - } - - #[test] - fn sign_and_verify() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - let pk = blst.secret_to_public_key(&sk).unwrap(); - let data = b"test message"; - - let sig = blst.sign(&sk, data).unwrap(); - assert_eq!(sig.len(), 96); - - let result = blst.verify(&pk, data, &sig); - assert!(result.is_ok()); - } - - #[test] - fn threshold_split_and_recover() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - let threshold = 3; - let total = 5; - - let shares = blst.threshold_split(&sk, total, threshold).unwrap(); - assert_eq!(shares.len(), usize::try_from(total).unwrap()); - - // Take exactly threshold shares - let subset: HashMap = shares - .iter() - .take(usize::try_from(threshold).unwrap()) - .map(|(k, v)| (*k, *v)) - .collect(); - - let recovered_sk = blst.recover_secret(&subset).unwrap(); - assert_eq!(sk, recovered_sk); - } - - #[test] - fn recover_secret_with_all_shares() { - use rand::rngs::OsRng; - - let blst = setup(); - let secret = blst.generate_secret_key(OsRng).unwrap(); - let threshold = 3; - let total = 5; - - let shares = blst.threshold_split(&secret, total, threshold).unwrap(); - assert_eq!(shares.len(), usize::try_from(total).unwrap()); - - // Recover using all shares - let recovered = blst.recover_secret(&shares).unwrap(); - assert_eq!( - secret, recovered, - "Secret recovered from all shares should match original" - ); - } - - #[test] - fn threshold_aggregate_matches_direct_sign() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"hello obol!"; - - let secret = blst.generate_secret_key(OsRng).unwrap(); - - // Sign directly with the secret - let direct_sig = blst.sign(&secret, data).unwrap(); - - // Split into shares and sign with each - let shares = blst.threshold_split(&secret, 5, 3).unwrap(); - let mut signatures = HashMap::new(); - for (idx, key) in shares.iter() { - let signature = blst.sign(key, data).unwrap(); - signatures.insert(*idx, signature); - } - - // Aggregate threshold signatures - let aggregated_sig = blst.threshold_aggregate(&signatures).unwrap(); - - // Both signatures should be identical - assert_eq!( - direct_sig, aggregated_sig, - "Threshold aggregated signature should match direct signature" - ); - } - - #[test] - fn verify_with_correct_signature() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"hello obol!"; - - let secret = blst.generate_secret_key(OsRng).unwrap(); - let pubkey = blst.secret_to_public_key(&secret).unwrap(); - let signature = blst.sign(&secret, data).unwrap(); - - let result = blst.verify(&pubkey, data, &signature); - assert!( - result.is_ok(), - "Verification should succeed with correct signature" - ); - } - - #[test] - fn verify_fails_with_wrong_message() { - use rand::rngs::OsRng; - - let blst = setup(); - let data1 = b"hello obol!"; - let data2 = b"goodbye obol!"; - - let secret = blst.generate_secret_key(OsRng).unwrap(); - let pubkey = blst.secret_to_public_key(&secret).unwrap(); - let signature = blst.sign(&secret, data1).unwrap(); - - let result = blst.verify(&pubkey, data2, &signature); - assert!( - result.is_err(), - "Verification should fail with wrong message" - ); - } - - #[test] - fn verify_fails_with_wrong_public_key() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"hello obol!"; - - let secret1 = blst.generate_secret_key(OsRng).unwrap(); - let secret2 = blst.generate_secret_key(OsRng).unwrap(); - let pubkey2 = blst.secret_to_public_key(&secret2).unwrap(); - let signature1 = blst.sign(&secret1, data).unwrap(); - - let result = blst.verify(&pubkey2, data, &signature1); - assert!( - result.is_err(), - "Verification should fail with wrong public key" - ); - } - - #[test] - fn verify_aggregate_success() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"hello obol!"; - - // Generate 10 key pairs - let mut keys = Vec::new(); - for _ in 0..10 { - let secret = blst.generate_secret_key(OsRng).unwrap(); - let pubkey = blst.secret_to_public_key(&secret).unwrap(); - keys.push((secret, pubkey)); - } - - // Sign with each key - let mut signatures = Vec::new(); - let mut public_keys = Vec::new(); - for (secret, pubkey) in &keys { - let sig = blst.sign(secret, data).unwrap(); - signatures.push(sig); - public_keys.push(*pubkey); - } - - // Aggregate signatures - let aggregated_sig = blst.aggregate(&signatures).unwrap(); - - // Verify aggregate - let result = blst.verify_aggregate(&public_keys, aggregated_sig, data); - assert!(result.is_ok(), "Aggregate verification should succeed"); - } - - #[test] - fn verify_aggregate_fails_with_wrong_data() { - use rand::rngs::OsRng; - - let blst = setup(); - let data1 = b"hello obol!"; - let data2 = b"goodbye obol!"; - - // Generate 5 key pairs - let mut keys = Vec::new(); - for _ in 0..5 { - let secret = blst.generate_secret_key(OsRng).unwrap(); - let pubkey = blst.secret_to_public_key(&secret).unwrap(); - keys.push((secret, pubkey)); - } - - // Sign with each key using data1 - let mut signatures = Vec::new(); - let mut public_keys = Vec::new(); - for (secret, pubkey) in &keys { - let sig = blst.sign(secret, data1).unwrap(); - signatures.push(sig); - public_keys.push(*pubkey); - } - - // Aggregate signatures - let aggregated_sig = blst.aggregate(&signatures).unwrap(); - - // Verify with data2 (wrong data) - let result = blst.verify_aggregate(&public_keys, aggregated_sig, data2); - assert!( - result.is_err(), - "Aggregate verification should fail with wrong data" - ); - } - - #[test] - fn aggregate_single_signature_is_canonical() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"test message"; - - let sk = blst.generate_secret_key(OsRng).unwrap(); - let sig = blst.sign(&sk, data).unwrap(); - - // Charon Herumi Aggregate deserializes+re-serializes even for one element, - // so the output is the canonical encoding of the parsed point (not the - // input bytes verbatim). For a signature produced by `sign` these coincide. - let aggregated = blst.aggregate(&[sig]).unwrap(); - assert_eq!(sig, aggregated); - - // Canonical round-trip: re-serializing the parsed aggregate is idempotent. - let reparsed = BlstSignature::from_bytes(&aggregated).unwrap(); - assert_eq!(aggregated, reparsed.to_bytes()); - } - - #[test] - fn aggregate_single_malformed_signature_errors() { - let blst = setup(); - // All-zero 96 bytes is not a valid compressed signature; Herumi's - // Deserialize fails, so Aggregate returns an error. - let bad = [0u8; 96]; - assert!(blst.aggregate(&[bad]).is_err()); - } - - #[test] - fn aggregate_multiple_signatures() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"test message"; - - // Generate 3 signatures - let mut signatures = Vec::new(); - for _ in 0..3 { - let sk = blst.generate_secret_key(OsRng).unwrap(); - let sig = blst.sign(&sk, data).unwrap(); - signatures.push(sig); - } - - let aggregated = blst.aggregate(&signatures).unwrap(); - assert_eq!( - aggregated.len(), - 96, - "Aggregated signature should be 96 bytes" - ); - } - - #[test] - fn threshold_split_minimum_threshold() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - - // Minimum valid threshold is 2 - let shares = blst.threshold_split(&sk, 3, 2).unwrap(); - assert_eq!(shares.len(), 3); - - // Recover with exactly 2 shares - let subset: HashMap = - shares.iter().take(2).map(|(k, v)| (*k, *v)).collect(); - - let recovered = blst.recover_secret(&subset).unwrap(); - assert_eq!(sk, recovered); - } - - #[test] - fn threshold_split_invalid_threshold() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - - // Threshold of 1 is invalid - let err = blst.threshold_split(&sk, 5, 1).unwrap_err(); - assert!(matches!( - err, - Error::InvalidThreshold { - threshold: 1, - total: 5 - } - )); - - // Threshold greater than total is invalid - let err = blst.threshold_split(&sk, 3, 5).unwrap_err(); - assert!(matches!( - err, - Error::InvalidThreshold { - threshold: 5, - total: 3 - } - )); - - // threshold == 0 is also rejected (<= 1) - let err = blst.threshold_split(&sk, 5, 0).unwrap_err(); - assert!(matches!( - err, - Error::InvalidThreshold { - threshold: 0, - total: 5 - } - )); - } - - #[test] - fn threshold_equal_total_is_valid() { - let blst = setup(); - let sk = blst.generate_secret_key(rand::rngs::OsRng).unwrap(); - // threshold == total is a valid (n, n) scheme. - let shares = blst.threshold_split(&sk, 4, 4).unwrap(); - assert_eq!(shares.len(), 4); - let recovered = blst.recover_secret(&shares).unwrap(); - assert_eq!(sk, recovered); - } - - #[test] - fn invalid_secret_key_error_is_consistent() { - let blst = setup(); - // All-0xff bytes are >= the scalar field order => from_bytes fails. - let bad: PrivateKey = [0xff; 32]; - - assert!(matches!( - blst.secret_to_public_key(&bad), - Err(Error::InvalidSecretKey(_)) - )); - assert!(matches!( - blst.sign(&bad, b"data"), - Err(Error::InvalidSecretKey(_)) - )); - assert!(matches!( - blst.threshold_split(&bad, 5, 3), - Err(Error::InvalidSecretKey(_)) - )); - - let mut shares = HashMap::new(); - shares.insert(1u64, bad); - shares.insert(2u64, bad); - assert!(matches!( - blst.recover_secret(&shares), - Err(Error::InvalidSecretKey(_)) - )); - } - - #[test] - fn different_keys_produce_different_signatures() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"test message"; - - let sk1 = blst.generate_secret_key(OsRng).unwrap(); - let sk2 = blst.generate_secret_key(OsRng).unwrap(); - - let sig1 = blst.sign(&sk1, data).unwrap(); - let sig2 = blst.sign(&sk2, data).unwrap(); - - assert_ne!( - sig1, sig2, - "Different keys should produce different signatures" - ); - } - - #[test] - fn same_key_produces_same_signature() { - use rand::rngs::OsRng; - - let blst = setup(); - let data = b"test message"; - - let sk = blst.generate_secret_key(OsRng).unwrap(); - - let sig1 = blst.sign(&sk, data).unwrap(); - let sig2 = blst.sign(&sk, data).unwrap(); - - assert_eq!( - sig1, sig2, - "Same key should produce same signature for same data" - ); - } - - #[test] - fn aggregate_empty_returns_identity_signature() { - let blst = setup(); - - // Parity with Charon Herumi Aggregate: empty input is NOT an error; it - // returns the serialized G2 point at infinity. Fixture: `0xc0` followed by - // 95 zero bytes (Herumi `sig.Serialize()` of a zero `bls.Sign`). - let agg = blst - .aggregate(&[]) - .expect("empty aggregate must not error (Herumi parity)"); - - let mut expected = [0u8; 96]; - expected[0] = 0xc0; - assert_eq!( - agg, expected, - "empty aggregate must equal the serialized identity signature" - ); - } - - #[test] - fn identity_signature_matches_go_fixture() { - // Hex of Herumi `bls.Sign{}.Serialize()` for the BLS12-381 G2 compressed - // point at infinity (eth2/ZCash compressed encoding): `c0` followed by - // 190 hex zeros (96 bytes total). - let go_fixture_hex = format!("c0{}", "0".repeat(190)); - let go_fixture = hex::decode(go_fixture_hex).unwrap(); - assert_eq!(go_fixture.len(), 96); - assert_eq!(&IDENTITY_SIGNATURE[..], &go_fixture[..]); - } - - #[test] - fn public_key_is_deterministic() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - - let pk1 = blst.secret_to_public_key(&sk).unwrap(); - let pk2 = blst.secret_to_public_key(&sk).unwrap(); - - assert_eq!(pk1, pk2, "Public key derivation should be deterministic"); - } - - #[test] - fn different_secrets_produce_different_public_keys() { - use rand::rngs::OsRng; - - let blst = setup(); - - let sk1 = blst.generate_secret_key(OsRng).unwrap(); - let sk2 = blst.generate_secret_key(OsRng).unwrap(); - - let pk1 = blst.secret_to_public_key(&sk1).unwrap(); - let pk2 = blst.secret_to_public_key(&sk2).unwrap(); - - assert_ne!( - pk1, pk2, - "Different secrets should produce different public keys" - ); - } - - #[test] - fn threshold_split_returns_1_indexed_keys() { - use rand::rngs::OsRng; - - let blst = setup(); - let sk = blst.generate_secret_key(OsRng).unwrap(); - - // Split into 5 shares - let shares = blst.threshold_split(&sk, 5, 3).unwrap(); - assert_eq!(shares.len(), 5); - - // Verify keys are 1-indexed (1, 2, 3, 4, 5) - assert!(shares.contains_key(&1), "Should contain key 1"); - assert!(shares.contains_key(&2), "Should contain key 2"); - assert!(shares.contains_key(&3), "Should contain key 3"); - assert!(shares.contains_key(&4), "Should contain key 4"); - assert!(shares.contains_key(&5), "Should contain key 5"); - - // Verify no 0-indexed key exists - assert!(!shares.contains_key(&0), "Should not contain key 0"); - } - - #[test] - fn scalar_from_u64_upper_limbs_are_zero() { - // blst_scalar_from_uint64 reads 4 consecutive u64s (4 × 8 = 32 bytes); - // passing &val instead of &[val, 0, 0, 0] reads 3 extra u64s from the - // stack. The scalar is stored little-endian: the value occupies the first - // u64 (bytes 0–7) and the remaining three limbs (bytes 8–31) must be zero. - for val in [0u64, 1, 2, 3, 4, 255, u64::from(u32::MAX)] { - let scalar = scalar_from_u64(val); - let expected = val.to_le_bytes(); - assert_eq!( - &scalar.b[..8], - &expected, - "lower 8 bytes should encode {val}" - ); - assert!( - scalar.b[8..].iter().all(|&b| b == 0), - "upper 24 bytes must be zero for val={val}" - ); - } - } -} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 9189a9d0..19489496 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -8,14 +8,8 @@ //! BLS library used in the Go implementation, using the BLST library which //! provides high-performance BLS12-381 cryptography. -/// BLST implementation of TBLS (Herumi-compatible) -pub mod blst_impl; - -/// TBLS trait definition +/// Threshold BLS signatures (Herumi-compatible) pub mod tbls; -/// Conversions between crypto (tbls) and eth2 BLS types. -pub mod tblsconv; - -/// Error types and constants +/// Key and signature types, their conversions, error types and constants pub mod types; diff --git a/crates/crypto/src/tbls.rs b/crates/crypto/src/tbls.rs index 7800e4ac..80081efc 100644 --- a/crates/crypto/src/tbls.rs +++ b/crates/crypto/src/tbls.rs @@ -1,127 +1,834 @@ //! # tbls //! -//! tbls is an implementation of tbls. +//! Threshold BLS signatures over the blst library, compatible with the Herumi +//! BLS library used in the Go implementation of Charon. use std::collections::HashMap; +use blst::{ + BLST_ERROR, + min_pk::{PublicKey as BlstPublicKey, SecretKey as BlstSecretKey, Signature as BlstSignature}, +}; use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroizing; -use crate::types::{Error, Index, PrivateKey, PublicKey, Signature}; - -/// Tbls trait -pub trait Tbls { - /// Generates a secret key and returns its compressed - /// serialized representation. - fn generate_secret_key(&self, rng: impl RngCore + CryptoRng) -> Result; - - /// Generates a secret that is not cryptographically - /// secure using the provided random number generator. This is useful - /// for testing. - fn generate_insecure_secret(&self, rng: impl RngCore + CryptoRng) -> Result; - - /// Extracts the public key associated with the secret - /// passed in input, and returns its compressed serialized - /// representation. - fn secret_to_public_key(&self, secret_key: &PrivateKey) -> Result; - - /// Splits a compressed secret into total units of - /// secret keys, with the given threshold. It returns a map that - /// associates each private, compressed private key to its ID. - /// - /// **Important:** Share IDs are 1-indexed (1, 2, 3, ..., n), matching - /// the Go implementation and TBLS polynomial evaluation points. - /// - /// # Limitations - /// - /// Maximum of 255 shares (total <= 255) due to underlying BLS library - /// constraints. - /// - /// # Errors - /// - /// Returns [`Error::InvalidThreshold`] if `threshold < 2` or - /// `threshold > total`. (Charon only enforces `threshold > 1`; Pluto - /// additionally rejects `threshold > total` as an unrecoverable, always-bug - /// configuration.) Returns [`Error::InvalidSecretKey`] if `secret_key` is - /// not a valid BLS scalar, and [`Error::ThresholdOverflow`] if `threshold` - /// does not fit in `usize` on this platform. - fn threshold_split_insecure( - &self, - secret_key: &PrivateKey, - total: Index, - threshold: Index, - rng: impl RngCore + CryptoRng, - ) -> Result, Error>; - - /// ThresholdSplit splits a compressed secret into total units of secret - /// keys, with the given threshold. It returns a map that associates - /// each private, compressed private key to its ID. - /// - /// **Important:** Share IDs are 1-indexed (1, 2, 3, ..., n), matching - /// the Go implementation and TBLS polynomial evaluation points. - /// - /// # Limitations - /// - /// Maximum of 255 shares (total <= 255) due to underlying BLS library - /// constraints. - /// - /// # Errors - /// - /// Returns [`Error::InvalidThreshold`] if `threshold < 2` or - /// `threshold > total`. (Charon only enforces `threshold > 1`; Pluto - /// additionally rejects `threshold > total` as an unrecoverable, always-bug - /// configuration.) Returns [`Error::InvalidSecretKey`] if `secret_key` is - /// not a valid BLS scalar, and [`Error::ThresholdOverflow`] if `threshold` - /// does not fit in `usize` on this platform. - fn threshold_split( - &self, - secret_key: &PrivateKey, - total: Index, - threshold: Index, - ) -> Result, Error>; - - /// Recovers a secret from a set of shares - /// - /// **Important:** Share IDs in the input HashMap must be 1-indexed - /// (1, 2, 3, ..., n), matching the IDs returned by threshold_split. - /// - /// # Limitations - /// - /// Share IDs must be < 255 due to underlying BLS library constraints. - fn recover_secret(&self, shares: &HashMap) -> Result; - - /// Aggregates a set of signatures into a single signature - fn aggregate(&self, signatures: &[Signature]) -> Result; - - /// Aggregates a set of partial signatures into a single - /// signature - /// - /// **Important:** Share IDs in the input HashMap must be 1-indexed - /// (1, 2, 3, ..., n), matching the share IDs used for key splitting. - /// - /// # Limitations - /// - /// Share IDs must be < 255 due to underlying BLS library constraints. - fn threshold_aggregate( - &self, - partial_signatures_by_idx: &HashMap, - ) -> Result; - - /// Verify verifies a signature - fn verify( - &self, - public_key: &PublicKey, - data: &[u8], - raw_signature: &Signature, - ) -> Result<(), Error>; - - /// Signs a message with a private key - fn sign(&self, private_key: &PrivateKey, data: &[u8]) -> Result; - - /// Verifies an aggregate signature - fn verify_aggregate( - &self, - public_keys: &[PublicKey], - signature: Signature, - data: &[u8], - ) -> Result<(), Error>; +use crate::types::{BlsError, Error, Index, PrivateKey, PublicKey, SIGNATURE_LENGTH, Signature}; + +mod math; + +/// Domain Separation Tag for Ethereum 2.0 BLS signatures +const ETH2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; + +/// Serialized BLS12-381 G2 compressed point at infinity (the identity +/// signature). This is the value Charon's Herumi `Aggregate` returns for an +/// empty input slice: `sig.Serialize()` of a zero `bls.Sign`. The high byte +/// sets the compression bit (`0x80`) and the infinity bit (`0x40`); all other +/// bytes are zero. +const IDENTITY_SIGNATURE: Signature = { + let mut s = [0u8; SIGNATURE_LENGTH]; + s[0] = 0xc0; + s +}; + +/// Generates a secret key and returns its compressed +/// serialized representation. +pub fn generate_secret_key(mut rng: impl RngCore + CryptoRng) -> Result { + // `ikm` is secret input key material; wipe it on drop. (`BlstSecretKey` + // itself is `#[zeroize(drop)]`, so the derived `sk` is wiped too.) + let mut ikm = Zeroizing::new([0u8; 32]); + rng.fill_bytes(ikm.as_mut()); + + let sk = BlstSecretKey::key_gen(ikm.as_ref(), &[]) + .map_err(|_| Error::InvalidSecretKey(BlsError::KeyGeneration))?; + + Ok(sk.to_bytes()) +} + +/// Generates a secret that is not cryptographically +/// secure using the provided random number generator. This is useful +/// for testing. +pub fn generate_insecure_secret(mut rng: impl RngCore + CryptoRng) -> Result { + for _ in 0..100 { + // Wipe the candidate buffer on every iteration; on success its value + // is copied into the returned key first. + let mut bytes = Zeroizing::new([0u8; 32]); + rng.fill_bytes(bytes.as_mut()); + + if BlstSecretKey::from_bytes(bytes.as_ref()).is_ok() { + return Ok(*bytes); + } + } + Err(Error::InvalidSecretKey(BlsError::KeyGeneration)) +} + +/// Extracts the public key associated with the secret +/// passed in input, and returns its compressed serialized +/// representation. +pub fn secret_to_public_key(secret_key: &PrivateKey) -> Result { + let sk = + BlstSecretKey::from_bytes(secret_key).map_err(|e| Error::InvalidSecretKey(e.into()))?; + let pk = sk.sk_to_pk(); + Ok(pk.to_bytes()) +} + +/// Splits a compressed secret into total units of +/// secret keys, with the given threshold. It returns a map that +/// associates each private, compressed private key to its ID. +/// +/// **Important:** Share IDs are 1-indexed (1, 2, 3, ..., n), matching +/// the Go implementation and TBLS polynomial evaluation points. +/// +/// # Limitations +/// +/// Maximum of 255 shares (total <= 255) due to underlying BLS library +/// constraints. +/// +/// # Errors +/// +/// Returns [`Error::InvalidThreshold`] if `threshold < 2` or +/// `threshold > total`. (Charon only enforces `threshold > 1`; Pluto +/// additionally rejects `threshold > total` as an unrecoverable, always-bug +/// configuration.) Returns [`Error::InvalidSecretKey`] if `secret_key` is +/// not a valid BLS scalar, and [`Error::ThresholdOverflow`] if `threshold` +/// does not fit in `usize` on this platform. +pub fn threshold_split_insecure( + secret_key: &PrivateKey, + total: Index, + threshold: Index, + mut rng: impl RngCore + CryptoRng, +) -> Result, Error> { + // Charon's Herumi backend only rejects `threshold <= 1` + // (see charon/tbls/herumi.go ThresholdSplit @ v1.7.1). We additionally + // reject `threshold > total`: such a (t, n) scheme is unrecoverable and + // is always a programming error. No Charon call site passes t > n, so + // this hardening never rejects an otherwise-valid split. + if threshold <= 1 || threshold > total { + return Err(Error::InvalidThreshold { threshold, total }); + } + + // `threshold` is bounded above by `total` here; the conversion is + // infallible on 64-bit targets and only fails on 32-bit targets for an + // implausibly large `total`. Map that to a dedicated overflow error + // rather than re-using InvalidThreshold (the value is in range). + let threshold_usize = + usize::try_from(threshold).map_err(|_| Error::ThresholdOverflow { threshold })?; + + let sk = + BlstSecretKey::from_bytes(secret_key).map_err(|e| Error::InvalidSecretKey(e.into()))?; + + // Create polynomial coefficients: a_0 = secret, a_1..a_{t-1} = random. + // `poly` holds `BlstSecretKey`s, each `#[zeroize(drop)]`, so the secret + // coefficients are wiped when `poly` is dropped. + let mut poly = Vec::with_capacity(threshold_usize); + poly.push(sk); + + for _ in 1..threshold { + let mut ikm = Zeroizing::new([0u8; 32]); + rng.fill_bytes(ikm.as_mut()); + let coeff = BlstSecretKey::key_gen(ikm.as_ref(), &[]) + .map_err(|_| Error::InvalidSecretKey(BlsError::KeyGeneration))?; + poly.push(coeff); + } + + // Evaluate polynomial at points 1..total to create shares + let mut shares = HashMap::new(); + for i in 1..=total { + let share = math::evaluate_polynomial(&poly, i)?; + shares.insert(i, share.to_bytes()); + } + + Ok(shares) +} + +/// ThresholdSplit splits a compressed secret into total units of secret +/// keys, with the given threshold. It returns a map that associates +/// each private, compressed private key to its ID. +/// +/// **Important:** Share IDs are 1-indexed (1, 2, 3, ..., n), matching +/// the Go implementation and TBLS polynomial evaluation points. +/// +/// # Limitations +/// +/// Maximum of 255 shares (total <= 255) due to underlying BLS library +/// constraints. +/// +/// # Errors +/// +/// Returns [`Error::InvalidThreshold`] if `threshold < 2` or +/// `threshold > total`. (Charon only enforces `threshold > 1`; Pluto +/// additionally rejects `threshold > total` as an unrecoverable, always-bug +/// configuration.) Returns [`Error::InvalidSecretKey`] if `secret_key` is +/// not a valid BLS scalar, and [`Error::ThresholdOverflow`] if `threshold` +/// does not fit in `usize` on this platform. +pub fn threshold_split( + secret_key: &PrivateKey, + total: Index, + threshold: Index, +) -> Result, Error> { + // Use OsRng for secure random number generation + threshold_split_insecure(secret_key, total, threshold, rand::rngs::OsRng) +} + +/// Recovers a secret from a set of shares +/// +/// **Important:** Share IDs in the input HashMap must be 1-indexed +/// (1, 2, 3, ..., n), matching the IDs returned by threshold_split. +/// +/// # Limitations +/// +/// Share IDs must be < 255 due to underlying BLS library constraints. +pub fn recover_secret(shares: &HashMap) -> Result { + if shares.is_empty() { + return Err(Error::SharesAreEmpty); + } + + // Share indices are already 1-indexed (matching their polynomial evaluation + // points) + let share_points: Vec = shares.keys().copied().collect(); + + // The reconstructed master secret and the parsed share scalars are all + // `BlstSecretKey`s (`#[zeroize(drop)]`), so they are wiped on drop. + let share_secrets: Vec = shares + .values() + .map(|bytes| { + BlstSecretKey::from_bytes(bytes).map_err(|e| Error::InvalidSecretKey(e.into())) + }) + .collect::, _>>()?; + + // Lagrange interpolation at x=0 + let recovered = math::lagrange_interpolate_secret(&share_points, &share_secrets)?; + Ok(recovered.to_bytes()) +} + +/// Aggregates a set of signatures into a single signature +pub fn aggregate(signatures: &[Signature]) -> Result { + // Parity with Charon Herumi `Aggregate` (tbls/herumi.go:227): an empty + // input is NOT an error. Herumi aggregates into a zero `bls.Sign` and + // returns its serialized form, i.e. the G2 compressed point at infinity. + if signatures.is_empty() { + return Ok(IDENTITY_SIGNATURE); + } + + // Deserialize every input signature (matches the Herumi loop, which + // errors if any element fails to deserialize). Note: aggregation + // canonicalizes the output even for a single input (Herumi returns + // `sig.Serialize()`, never the input bytes verbatim). + let parsed_sigs: Vec = signatures + .iter() + .map(|sig_bytes| { + BlstSignature::from_bytes(sig_bytes).map_err(|e| Error::InvalidSignature(e.into())) + }) + .collect::, _>>()?; + + let sigs: Vec<&BlstSignature> = parsed_sigs.iter().collect(); + + let agg = blst::min_pk::AggregateSignature::aggregate(&sigs[..], true) + .map_err(|e| Error::AggregationFailed(e.into()))?; + + Ok(agg.to_signature().to_bytes()) +} + +/// Aggregates a set of partial signatures into a single +/// signature +/// +/// **Important:** Share IDs in the input HashMap must be 1-indexed +/// (1, 2, 3, ..., n), matching the share IDs used for key splitting. +/// +/// # Limitations +/// +/// Share IDs must be < 255 due to underlying BLS library constraints. +pub fn threshold_aggregate( + partial_signatures_by_idx: &HashMap, +) -> Result { + if partial_signatures_by_idx.is_empty() { + return Err(Error::EmptySignatureArray); + } + + // Signature indices are already 1-indexed (matching share evaluation points) + let indices: Vec = partial_signatures_by_idx.keys().copied().collect(); + + let signatures: Vec = partial_signatures_by_idx + .values() + .map(|sig_bytes| { + BlstSignature::from_bytes(sig_bytes).map_err(|e| Error::InvalidSignature(e.into())) + }) + .collect::, _>>()?; + + // Perform Lagrange interpolation on signatures at x=0 + let recovered_sig = math::lagrange_interpolate_signature(&indices, &signatures)?; + Ok(recovered_sig.to_bytes()) +} + +/// Verify verifies a signature +pub fn verify(public_key: &PublicKey, data: &[u8], raw_signature: &Signature) -> Result<(), Error> { + let pk = + BlstPublicKey::from_bytes(public_key).map_err(|e| Error::InvalidPublicKey(e.into()))?; + + let sig = + BlstSignature::from_bytes(raw_signature).map_err(|e| Error::InvalidSignature(e.into()))?; + + let result = sig.verify(true, data, ETH2_DST, &[], &pk, true); + + if result == BLST_ERROR::BLST_SUCCESS { + Ok(()) + } else { + Err(Error::VerificationFailed(result.into())) + } +} + +/// Signs a message with a private key +pub fn sign(private_key: &PrivateKey, data: &[u8]) -> Result { + let sk = + BlstSecretKey::from_bytes(private_key).map_err(|e| Error::InvalidSecretKey(e.into()))?; + let sig = sk.sign(data, ETH2_DST, &[]); + Ok(sig.to_bytes()) +} + +/// Verifies an aggregate signature +pub fn verify_aggregate( + public_keys: &[PublicKey], + signature: Signature, + data: &[u8], +) -> Result<(), Error> { + if public_keys.is_empty() { + return Err(Error::EmptyPublicKeyArray); + } + + let pks: Vec = public_keys + .iter() + .map(|pk_bytes| { + BlstPublicKey::from_bytes(pk_bytes).map_err(|e| Error::InvalidPublicKey(e.into())) + }) + .collect::, _>>()?; + + let sig = + BlstSignature::from_bytes(&signature).map_err(|e| Error::InvalidSignature(e.into()))?; + + // Aggregate public keys using blst point addition + let agg_pk = math::aggregate_public_keys(&pks)?; + + let result = sig.verify(true, data, ETH2_DST, &[], &agg_pk, true); + + if result == BLST_ERROR::BLST_SUCCESS { + Ok(()) + } else { + Err(Error::VerificationFailed(result.into())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_insecure_secret() { + let sk = super::generate_insecure_secret(rand::rngs::OsRng).unwrap(); + assert_eq!(sk.len(), 32); + } + + #[test] + fn verify_aggregate_from_data() { + let data = b"hello obol!"; + + // Decode the secret key from hex + let secret_bytes = + hex::decode("7356c7dab0220088158a8bba45894b164c04cf7de83149e2c4fab381e765ff38") + .unwrap(); + assert_eq!(secret_bytes.len(), 32); + + let mut secret = [0u8; 32]; + secret.copy_from_slice(&secret_bytes); + assert!(!secret.is_empty()); + + // Split the secret into shares (total=5, threshold=3) + let shares = threshold_split(&secret, 5, 3).unwrap(); + assert_eq!(shares.len(), 5); + + // Create signatures for each share + let mut signatures = HashMap::new(); + for (idx, key) in shares.iter() { + let signature = sign(key, data).unwrap(); + signatures.insert(*idx, signature); + } + + // Aggregate the threshold signatures + let total_sig = threshold_aggregate(&signatures).unwrap(); + + // Expected signature from the Go implementation + let expected_sig = hex::decode("b46736c3a1fb5d7977acc6abf3cb3a10fd1a5aed301437022f28cf616326186654d747fda7cd530c2bf18c640e4c024b01d7ba38d90e4abe0cc5356ef63b8e20f717ef0a1f68c3292bd62b4f891345ecafa89a8604f8f6c3ce193dc239215adf").unwrap(); + + // Compare the aggregated signature with the expected one + assert_eq!( + expected_sig, + &total_sig[..], + "Aggregated signature does not match expected signature from Go implementation" + ); + } + + #[test] + fn generate_and_derive_key() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + assert_eq!(sk.len(), 32); + + let pk = secret_to_public_key(&sk).unwrap(); + assert_eq!(pk.len(), 48); + } + + #[test] + fn sign_and_verify() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + let pk = secret_to_public_key(&sk).unwrap(); + let data = b"test message"; + + let sig = sign(&sk, data).unwrap(); + assert_eq!(sig.len(), 96); + + let result = verify(&pk, data, &sig); + assert!(result.is_ok()); + } + + #[test] + fn threshold_split_and_recover() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + let threshold = 3; + let total = 5; + + let shares = threshold_split(&sk, total, threshold).unwrap(); + assert_eq!(shares.len(), usize::try_from(total).unwrap()); + + // Take exactly threshold shares + let subset: HashMap = shares + .iter() + .take(usize::try_from(threshold).unwrap()) + .map(|(k, v)| (*k, *v)) + .collect(); + + let recovered_sk = recover_secret(&subset).unwrap(); + assert_eq!(sk, recovered_sk); + } + + #[test] + fn recover_secret_with_all_shares() { + use rand::rngs::OsRng; + + let secret = generate_secret_key(OsRng).unwrap(); + let threshold = 3; + let total = 5; + + let shares = threshold_split(&secret, total, threshold).unwrap(); + assert_eq!(shares.len(), usize::try_from(total).unwrap()); + + // Recover using all shares + let recovered = recover_secret(&shares).unwrap(); + assert_eq!( + secret, recovered, + "Secret recovered from all shares should match original" + ); + } + + #[test] + fn threshold_aggregate_matches_direct_sign() { + use rand::rngs::OsRng; + + let data = b"hello obol!"; + + let secret = generate_secret_key(OsRng).unwrap(); + + // Sign directly with the secret + let direct_sig = sign(&secret, data).unwrap(); + + // Split into shares and sign with each + let shares = threshold_split(&secret, 5, 3).unwrap(); + let mut signatures = HashMap::new(); + for (idx, key) in shares.iter() { + let signature = sign(key, data).unwrap(); + signatures.insert(*idx, signature); + } + + // Aggregate threshold signatures + let aggregated_sig = threshold_aggregate(&signatures).unwrap(); + + // Both signatures should be identical + assert_eq!( + direct_sig, aggregated_sig, + "Threshold aggregated signature should match direct signature" + ); + } + + #[test] + fn verify_with_correct_signature() { + use rand::rngs::OsRng; + + let data = b"hello obol!"; + + let secret = generate_secret_key(OsRng).unwrap(); + let pubkey = secret_to_public_key(&secret).unwrap(); + let signature = sign(&secret, data).unwrap(); + + let result = verify(&pubkey, data, &signature); + assert!( + result.is_ok(), + "Verification should succeed with correct signature" + ); + } + + #[test] + fn verify_fails_with_wrong_message() { + use rand::rngs::OsRng; + + let data1 = b"hello obol!"; + let data2 = b"goodbye obol!"; + + let secret = generate_secret_key(OsRng).unwrap(); + let pubkey = secret_to_public_key(&secret).unwrap(); + let signature = sign(&secret, data1).unwrap(); + + let result = verify(&pubkey, data2, &signature); + assert!( + result.is_err(), + "Verification should fail with wrong message" + ); + } + + #[test] + fn verify_fails_with_wrong_public_key() { + use rand::rngs::OsRng; + + let data = b"hello obol!"; + + let secret1 = generate_secret_key(OsRng).unwrap(); + let secret2 = generate_secret_key(OsRng).unwrap(); + let pubkey2 = secret_to_public_key(&secret2).unwrap(); + let signature1 = sign(&secret1, data).unwrap(); + + let result = verify(&pubkey2, data, &signature1); + assert!( + result.is_err(), + "Verification should fail with wrong public key" + ); + } + + #[test] + fn verify_aggregate_success() { + use rand::rngs::OsRng; + + let data = b"hello obol!"; + + // Generate 10 key pairs + let mut keys = Vec::new(); + for _ in 0..10 { + let secret = generate_secret_key(OsRng).unwrap(); + let pubkey = secret_to_public_key(&secret).unwrap(); + keys.push((secret, pubkey)); + } + + // Sign with each key + let mut signatures = Vec::new(); + let mut public_keys = Vec::new(); + for (secret, pubkey) in &keys { + let sig = sign(secret, data).unwrap(); + signatures.push(sig); + public_keys.push(*pubkey); + } + + // Aggregate signatures + let aggregated_sig = aggregate(&signatures).unwrap(); + + // Verify aggregate + let result = verify_aggregate(&public_keys, aggregated_sig, data); + assert!(result.is_ok(), "Aggregate verification should succeed"); + } + + #[test] + fn verify_aggregate_fails_with_wrong_data() { + use rand::rngs::OsRng; + + let data1 = b"hello obol!"; + let data2 = b"goodbye obol!"; + + // Generate 5 key pairs + let mut keys = Vec::new(); + for _ in 0..5 { + let secret = generate_secret_key(OsRng).unwrap(); + let pubkey = secret_to_public_key(&secret).unwrap(); + keys.push((secret, pubkey)); + } + + // Sign with each key using data1 + let mut signatures = Vec::new(); + let mut public_keys = Vec::new(); + for (secret, pubkey) in &keys { + let sig = sign(secret, data1).unwrap(); + signatures.push(sig); + public_keys.push(*pubkey); + } + + // Aggregate signatures + let aggregated_sig = aggregate(&signatures).unwrap(); + + // Verify with data2 (wrong data) + let result = verify_aggregate(&public_keys, aggregated_sig, data2); + assert!( + result.is_err(), + "Aggregate verification should fail with wrong data" + ); + } + + #[test] + fn aggregate_single_signature_is_canonical() { + use rand::rngs::OsRng; + + let data = b"test message"; + + let sk = generate_secret_key(OsRng).unwrap(); + let sig = sign(&sk, data).unwrap(); + + // Charon Herumi Aggregate deserializes+re-serializes even for one element, + // so the output is the canonical encoding of the parsed point (not the + // input bytes verbatim). For a signature produced by `sign` these coincide. + let aggregated = aggregate(&[sig]).unwrap(); + assert_eq!(sig, aggregated); + + // Canonical round-trip: re-serializing the parsed aggregate is idempotent. + let reparsed = BlstSignature::from_bytes(&aggregated).unwrap(); + assert_eq!(aggregated, reparsed.to_bytes()); + } + + #[test] + fn aggregate_single_malformed_signature_errors() { + // All-zero 96 bytes is not a valid compressed signature; Herumi's + // Deserialize fails, so Aggregate returns an error. + let bad = [0u8; 96]; + assert!(aggregate(&[bad]).is_err()); + } + + #[test] + fn aggregate_multiple_signatures() { + use rand::rngs::OsRng; + + let data = b"test message"; + + // Generate 3 signatures + let mut signatures = Vec::new(); + for _ in 0..3 { + let sk = generate_secret_key(OsRng).unwrap(); + let sig = sign(&sk, data).unwrap(); + signatures.push(sig); + } + + let aggregated = aggregate(&signatures).unwrap(); + assert_eq!( + aggregated.len(), + 96, + "Aggregated signature should be 96 bytes" + ); + } + + #[test] + fn threshold_split_minimum_threshold() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + + // Minimum valid threshold is 2 + let shares = threshold_split(&sk, 3, 2).unwrap(); + assert_eq!(shares.len(), 3); + + // Recover with exactly 2 shares + let subset: HashMap = + shares.iter().take(2).map(|(k, v)| (*k, *v)).collect(); + + let recovered = recover_secret(&subset).unwrap(); + assert_eq!(sk, recovered); + } + + #[test] + fn threshold_split_invalid_threshold() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + + // Threshold of 1 is invalid + let err = threshold_split(&sk, 5, 1).unwrap_err(); + assert!(matches!( + err, + Error::InvalidThreshold { + threshold: 1, + total: 5 + } + )); + + // Threshold greater than total is invalid + let err = threshold_split(&sk, 3, 5).unwrap_err(); + assert!(matches!( + err, + Error::InvalidThreshold { + threshold: 5, + total: 3 + } + )); + + // threshold == 0 is also rejected (<= 1) + let err = threshold_split(&sk, 5, 0).unwrap_err(); + assert!(matches!( + err, + Error::InvalidThreshold { + threshold: 0, + total: 5 + } + )); + } + + #[test] + fn threshold_equal_total_is_valid() { + let sk = generate_secret_key(rand::rngs::OsRng).unwrap(); + // threshold == total is a valid (n, n) scheme. + let shares = threshold_split(&sk, 4, 4).unwrap(); + assert_eq!(shares.len(), 4); + let recovered = recover_secret(&shares).unwrap(); + assert_eq!(sk, recovered); + } + + #[test] + fn invalid_secret_key_error_is_consistent() { + // All-0xff bytes are >= the scalar field order => from_bytes fails. + let bad: PrivateKey = [0xff; 32]; + + assert!(matches!( + secret_to_public_key(&bad), + Err(Error::InvalidSecretKey(_)) + )); + assert!(matches!( + sign(&bad, b"data"), + Err(Error::InvalidSecretKey(_)) + )); + assert!(matches!( + threshold_split(&bad, 5, 3), + Err(Error::InvalidSecretKey(_)) + )); + + let mut shares = HashMap::new(); + shares.insert(1u64, bad); + shares.insert(2u64, bad); + assert!(matches!( + recover_secret(&shares), + Err(Error::InvalidSecretKey(_)) + )); + } + + #[test] + fn different_keys_produce_different_signatures() { + use rand::rngs::OsRng; + + let data = b"test message"; + + let sk1 = generate_secret_key(OsRng).unwrap(); + let sk2 = generate_secret_key(OsRng).unwrap(); + + let sig1 = sign(&sk1, data).unwrap(); + let sig2 = sign(&sk2, data).unwrap(); + + assert_ne!( + sig1, sig2, + "Different keys should produce different signatures" + ); + } + + #[test] + fn same_key_produces_same_signature() { + use rand::rngs::OsRng; + + let data = b"test message"; + + let sk = generate_secret_key(OsRng).unwrap(); + + let sig1 = sign(&sk, data).unwrap(); + let sig2 = sign(&sk, data).unwrap(); + + assert_eq!( + sig1, sig2, + "Same key should produce same signature for same data" + ); + } + + #[test] + fn aggregate_empty_returns_identity_signature() { + // Parity with Charon Herumi Aggregate: empty input is NOT an error; it + // returns the serialized G2 point at infinity. Fixture: `0xc0` followed by + // 95 zero bytes (Herumi `sig.Serialize()` of a zero `bls.Sign`). + let agg = aggregate(&[]).expect("empty aggregate must not error (Herumi parity)"); + + let mut expected = [0u8; 96]; + expected[0] = 0xc0; + assert_eq!( + agg, expected, + "empty aggregate must equal the serialized identity signature" + ); + } + + #[test] + fn identity_signature_matches_go_fixture() { + // Hex of Herumi `bls.Sign{}.Serialize()` for the BLS12-381 G2 compressed + // point at infinity (eth2/ZCash compressed encoding): `c0` followed by + // 190 hex zeros (96 bytes total). + let go_fixture_hex = format!("c0{}", "0".repeat(190)); + let go_fixture = hex::decode(go_fixture_hex).unwrap(); + assert_eq!(go_fixture.len(), 96); + assert_eq!(&IDENTITY_SIGNATURE[..], &go_fixture[..]); + } + + #[test] + fn public_key_is_deterministic() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + + let pk1 = secret_to_public_key(&sk).unwrap(); + let pk2 = secret_to_public_key(&sk).unwrap(); + + assert_eq!(pk1, pk2, "Public key derivation should be deterministic"); + } + + #[test] + fn different_secrets_produce_different_public_keys() { + use rand::rngs::OsRng; + + let sk1 = generate_secret_key(OsRng).unwrap(); + let sk2 = generate_secret_key(OsRng).unwrap(); + + let pk1 = secret_to_public_key(&sk1).unwrap(); + let pk2 = secret_to_public_key(&sk2).unwrap(); + + assert_ne!( + pk1, pk2, + "Different secrets should produce different public keys" + ); + } + + #[test] + fn threshold_split_returns_1_indexed_keys() { + use rand::rngs::OsRng; + + let sk = generate_secret_key(OsRng).unwrap(); + + // Split into 5 shares + let shares = threshold_split(&sk, 5, 3).unwrap(); + assert_eq!(shares.len(), 5); + + // Verify keys are 1-indexed (1, 2, 3, 4, 5) + assert!(shares.contains_key(&1), "Should contain key 1"); + assert!(shares.contains_key(&2), "Should contain key 2"); + assert!(shares.contains_key(&3), "Should contain key 3"); + assert!(shares.contains_key(&4), "Should contain key 4"); + assert!(shares.contains_key(&5), "Should contain key 5"); + + // Verify no 0-indexed key exists + assert!(!shares.contains_key(&0), "Should not contain key 0"); + } } diff --git a/crates/crypto/src/tbls/math.rs b/crates/crypto/src/tbls/math.rs new file mode 100644 index 00000000..3b79573e --- /dev/null +++ b/crates/crypto/src/tbls/math.rs @@ -0,0 +1,314 @@ +//! Scalar-field and curve-point arithmetic over the blst FFI. +//! +//! Every `unsafe` block in this crate lives here. +#![allow(unsafe_code)] + +use std::collections::HashSet; + +use blst::min_pk::{ + PublicKey as BlstPublicKey, SecretKey as BlstSecretKey, Signature as BlstSignature, +}; + +use crate::types::{Error, Index}; + +/// Aggregate public keys +pub(super) fn aggregate_public_keys(pks: &[BlstPublicKey]) -> Result { + if pks.is_empty() { + return Err(Error::EmptyPublicKeyArray); + } + + let mut agg = blst::blst_p1::default(); + + unsafe { + // Convert first key to projective form + let first_affine: &blst::blst_p1_affine = (&pks[0]).into(); + blst::blst_p1_from_affine(&mut agg, first_affine); + + for pk in pks.iter().skip(1) { + let pk_affine: &blst::blst_p1_affine = pk.into(); + blst::blst_p1_add_or_double_affine(&mut agg, &agg, pk_affine); + } + + // Convert back to affine + let mut agg_affine = blst::blst_p1_affine::default(); + blst::blst_p1_to_affine(&mut agg_affine, &agg); + Ok(BlstPublicKey::from(agg_affine)) + } +} + +/// Evaluate polynomial at point x +/// poly(x) = a_0 + a_1*x + a_2*x^2 + ... + a_n*x^n +pub(super) fn evaluate_polynomial( + poly: &[BlstSecretKey], + x: Index, +) -> Result { + if poly.is_empty() { + return Err(Error::PolynomialIsEmpty); + } + + // Start with the constant term + let mut result = poly[0].clone(); + + // Horner-free evaluation: `x_power` holds x^i entering iteration i. + let x_scalar = scalar_from_u64(x); + let mut x_power = x_scalar.clone(); + + for coeff in poly.iter().skip(1) { + // result += coeff * x_power + let term = scalar_mult_secret(coeff, &x_power)?; + result = scalar_add_secret(&result, &term)?; + + x_power = scalar_mult_scalars(&x_power, &x_scalar)?; + } + + Ok(result) +} + +/// Lagrange interpolation of secret keys at x=0 +/// Recovers f(0) from points (x_i, y_i) where y_i are secret keys +pub(super) fn lagrange_interpolate_secret( + indices: &[Index], + shares: &[BlstSecretKey], +) -> Result { + if indices.len() != shares.len() || indices.is_empty() { + return Err(Error::IndicesSharesMismatch); + } + + // Compute Lagrange coefficients and interpolate + let coeffs = compute_lagrange_coefficients(indices)?; + + let mut result = BlstSecretKey::default(); + + for i in 0..shares.len() { + let term = scalar_mult_secret(&shares[i], &coeffs[i])?; + result = scalar_add_secret(&result, &term)?; + } + + Ok(result) +} + +/// Lagrange interpolation of signatures at x=0 +/// Recovers f(0) from points (x_i, σ_i) where σ_i are signatures +pub(super) fn lagrange_interpolate_signature( + indices: &[Index], + signatures: &[BlstSignature], +) -> Result { + if indices.len() != signatures.len() || indices.is_empty() { + return Err(Error::EmptySignatureArray); + } + + // Compute Lagrange coefficients + let coeffs = compute_lagrange_coefficients(indices)?; + + // Multiply each signature by its Lagrange coefficient and aggregate + let first_sig_scaled = signature_mult(&signatures[0], &coeffs[0])?; + let mut result_p2 = blst::blst_p2::default(); + + unsafe { + // Convert first scaled signature to projective + let first_affine: &blst::blst_p2_affine = (&first_sig_scaled).into(); + blst::blst_p2_from_affine(&mut result_p2, first_affine); + + for i in 1..signatures.len() { + let sig_scaled = signature_mult(&signatures[i], &coeffs[i])?; + let sig_affine: &blst::blst_p2_affine = (&sig_scaled).into(); + blst::blst_p2_add_or_double_affine(&mut result_p2, &result_p2, sig_affine); + } + + // Convert back to affine + let mut result_affine = blst::blst_p2_affine::default(); + blst::blst_p2_to_affine(&mut result_affine, &result_p2); + Ok(BlstSignature::from(result_affine)) + } +} + +/// Compute Lagrange coefficients for interpolation at x=0 +/// λ_i = ∏_{j≠i} (0 - x_j) / (x_i - x_j) = ∏_{j≠i} x_j / (x_j - x_i) +fn compute_lagrange_coefficients(indices: &[Index]) -> Result, Error> { + // Check if indices are unique + if indices.len() != indices.iter().collect::>().len() { + return Err(Error::IndicesNotUnique); + } + + let mut coeffs = Vec::with_capacity(indices.len()); + + for (i, &x_i) in indices.iter().enumerate() { + let mut numerator = scalar_from_u64(1); + let mut denominator = scalar_from_u64(1); + + for (j, &x_j) in indices.iter().enumerate() { + if i == j { + continue; + } + + // numerator *= x_j + let x_j_scalar = scalar_from_u64(x_j); + numerator = scalar_mult_scalars(&numerator, &x_j_scalar)?; + + // denominator *= (x_j - x_i) + let diff = if x_j > x_i { + scalar_from_u64(x_j.abs_diff(x_i)) + } else { + // For negative differences, we need to work in the scalar field + // x_j - x_i (mod r) where r is the curve order + scalar_negate(&scalar_from_u64(x_i.abs_diff(x_j)))? + }; + + denominator = scalar_mult_scalars(&denominator, &diff)?; + } + + // Compute numerator / denominator = numerator * denominator^{-1} + let coeff = scalar_div(&numerator, &denominator)?; + coeffs.push(coeff); + } + + Ok(coeffs) +} + +/// Convert u64 to blst scalar +fn scalar_from_u64(val: u64) -> blst::blst_scalar { + let mut scalar = blst::blst_scalar::default(); + let limbs: [u64; 4] = [val, 0, 0, 0]; + unsafe { + blst::blst_scalar_from_uint64(&mut scalar, limbs.as_ptr()); + } + scalar +} + +/// Multiply secret key by scalar +fn scalar_mult_secret( + sk: &BlstSecretKey, + scalar: &blst::blst_scalar, +) -> Result { + let sk_scalar = sk.into(); + let result_scalar = scalar_mult_scalars(sk_scalar, scalar)?; + let sk: &BlstSecretKey = (&result_scalar) + .try_into() + .map_err(|_| Error::FailedToConvertSkToBlstScalar)?; + Ok(sk.clone()) +} + +/// Add two secret keys +fn scalar_add_secret(sk1: &BlstSecretKey, sk2: &BlstSecretKey) -> Result { + let result = scalar_add(sk1.into(), sk2.into())?; + let sk: &BlstSecretKey = (&result) + .try_into() + .map_err(|_| Error::FailedToConvertScalarToSecretKey)?; + Ok(sk.clone()) +} + +/// Multiply signature by scalar +fn signature_mult(sig: &BlstSignature, scalar: &blst::blst_scalar) -> Result { + let mut sig_proj = blst::blst_p2::default(); + let mut result_p2 = blst::blst_p2::default(); + let mut result_affine = blst::blst_p2_affine::default(); + + unsafe { + // Convert affine to projective + let sig_affine: &blst::blst_p2_affine = sig.into(); + blst::blst_p2_from_affine(&mut sig_proj, sig_affine); + // Multiply + blst::blst_p2_mult(&mut result_p2, &sig_proj, scalar.b.as_ptr(), 255); + // Convert back to affine + blst::blst_p2_to_affine(&mut result_affine, &result_p2); + } + + Ok(BlstSignature::from(result_affine)) +} + +/// Add two scalars +fn scalar_add(a: &blst::blst_scalar, b: &blst::blst_scalar) -> Result { + let mut result = blst::blst_scalar::default(); + unsafe { + if blst::blst_sk_add_n_check(&mut result, a, b) { + Ok(result) + } else { + Err(Error::FailedToAddScalars) + } + } +} + +/// Multiply two scalars +fn scalar_mult_scalars( + a: &blst::blst_scalar, + b: &blst::blst_scalar, +) -> Result { + let mut result = blst::blst_scalar::default(); + unsafe { + if blst::blst_sk_mul_n_check(&mut result, a, b) { + Ok(result) + } else { + Err(Error::FailedToMultiplyScalars) + } + } +} + +/// Negate a scalar +fn scalar_negate(a: &blst::blst_scalar) -> Result { + // To negate in the field, we compute (r - a) where r is the curve order + // But blst doesn't expose this directly, so we use: -a ≡ r - a + // We can compute this as: 0 - a + let zero = scalar_from_u64(0); + let mut result_scalar = blst::blst_scalar::default(); + + unsafe { + // Convert scalars to fr for arithmetic + let mut a_fr = blst::blst_fr::default(); + let mut zero_fr = blst::blst_fr::default(); + + blst::blst_fr_from_scalar(&mut a_fr, a); + blst::blst_fr_from_scalar(&mut zero_fr, &zero); + + let mut result_fr = blst::blst_fr::default(); + blst::blst_fr_sub(&mut result_fr, &zero_fr, &a_fr); + + blst::blst_scalar_from_fr(&mut result_scalar, &result_fr); + } + + Ok(result_scalar) +} + +/// Divide two scalars (multiply by inverse) +fn scalar_div( + numerator: &blst::blst_scalar, + denominator: &blst::blst_scalar, +) -> Result { + let zero = blst::blst_scalar::default(); + if *denominator == zero { + return Err(Error::DivisionByZero); + } + + let mut inv_scalar = blst::blst_scalar::default(); + + unsafe { + blst::blst_sk_inverse(&mut inv_scalar, denominator); + } + + scalar_mult_scalars(numerator, &inv_scalar) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scalar_from_u64_upper_limbs_are_zero() { + // blst_scalar_from_uint64 reads 4 consecutive u64s (4 × 8 = 32 bytes); + // passing &val instead of &[val, 0, 0, 0] reads 3 extra u64s from the + // stack. The scalar is stored little-endian: the value occupies the first + // u64 (bytes 0–7) and the remaining three limbs (bytes 8–31) must be zero. + for val in [0u64, 1, 2, 3, 4, 255, u64::from(u32::MAX)] { + let scalar = scalar_from_u64(val); + let expected = val.to_le_bytes(); + assert_eq!( + &scalar.b[..8], + &expected, + "lower 8 bytes should encode {val}" + ); + assert!( + scalar.b[8..].iter().all(|&b| b == 0), + "upper 24 bytes must be zero for val={val}" + ); + } + } +} diff --git a/crates/crypto/src/tblsconv.rs b/crates/crypto/src/tblsconv.rs deleted file mode 100644 index 6be230e4..00000000 --- a/crates/crypto/src/tblsconv.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Conversions between crypto (tbls) and eth2 BLS types. -//! -//! Core-type conversion helpers are intentionally excluded here to avoid a -//! `core -> eth2util -> crypto -> core` crate dependency cycle. - -use pluto_eth2api::spec::phase0; - -use crate::types::{self, PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; - -/// 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::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_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_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[..]); - } -} diff --git a/crates/crypto/src/types.rs b/crates/crypto/src/types.rs index ebe5297c..16bfe9c8 100644 --- a/crates/crypto/src/types.rs +++ b/crates/crypto/src/types.rs @@ -1,6 +1,13 @@ //! # pluto-crypto types +//! +//! The BLS key and signature types, together with the conversions between them, +//! their raw byte encodings, and their eth2 counterparts. +//! +//! Conversions for core types are intentionally excluded here to avoid a +//! `core -> eth2util -> crypto -> core` crate dependency cycle. use blst::BLST_ERROR; +use pluto_eth2api::spec::phase0; /// Public key length pub const PUBLIC_KEY_LENGTH: usize = 48; @@ -22,6 +29,49 @@ pub type Signature = [u8; SIGNATURE_LENGTH]; /// Index type & total shares / threshold pub type Index = u64; +/// Converts a [`Signature`] into an eth2 phase0 [`phase0::BLSSignature`]. +pub fn sig_to_eth2(sig: Signature) -> phase0::BLSSignature { + sig +} + +/// Converts a [`PublicKey`] into an eth2 phase0 [`phase0::BLSPubKey`]. +pub fn pubkey_to_eth2(pk: PublicKey) -> phase0::BLSPubKey { + pk +} + +/// Returns a [`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 [`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 [`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) +} + /// Error type for charon-crypto operations. /// /// This enum represents all possible errors that can occur during cryptographic @@ -34,11 +84,7 @@ pub enum Error { /// This error occurs when the provided bytes don't represent a valid /// BLS secret key (e.g., out of valid scalar field range). #[error("Failed to deserialize secret key: {0}")] - InvalidSecretKey(#[from] BlsError), - - /// BLST error. - #[error("BLST error: {0}")] - BlsError(BlsError), + InvalidSecretKey(BlsError), /// Failed to deserialize a public key from bytes. #[error("Failed to deserialize public key: {0}")] @@ -185,6 +231,19 @@ pub enum BlsError { Unknown, } +/// 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, + }, +} + impl From for BlsError { fn from(err: BLST_ERROR) -> Self { match err { @@ -200,8 +259,76 @@ impl From for BlsError { } } -impl From for Error { - fn from(err: BLST_ERROR) -> Self { - Error::BlsError(BlsError::from(err)) +#[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_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_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[..]); } } diff --git a/crates/dkg/src/aggregate.rs b/crates/dkg/src/aggregate.rs index e79d743f..6baa3bfd 100644 --- a/crates/dkg/src/aggregate.rs +++ b/crates/dkg/src/aggregate.rs @@ -5,10 +5,8 @@ use pluto_core::{ types::{ParSignedData, PubKey, SignedData}, }; use pluto_crypto::{ - blst_impl::BlstImpl, - tbls::Tbls, - tblsconv::signature_from_bytes, - types::{PublicKey, Signature}, + tbls, + types::{PublicKey, Signature, signature_from_bytes}, }; use pluto_eth2api::spec::phase0; use pluto_eth2util::{deposit, registration}; @@ -23,7 +21,7 @@ pub type Result = std::result::Result; pub enum AggregateError { /// Failed to convert raw bytes into a threshold signature. #[error(transparent)] - SignatureBytes(#[from] pluto_crypto::tblsconv::ConvError), + SignatureBytes(#[from] pluto_crypto::types::ConvError), /// Failed to verify or aggregate threshold signatures. #[error(transparent)] @@ -143,7 +141,7 @@ pub fn agg_lock_hash_sig( .get(&partial.share_idx) .ok_or(AggregateError::InvalidPubshare)?; - BlstImpl.verify(pubshare, hash, &sig).map_err(|source| { + tbls::verify(pubshare, hash, &sig).map_err(|source| { AggregateError::InvalidLockHashPartialSignature { share_idx: partial.share_idx, pub_key: pub_key_hex.clone(), @@ -156,7 +154,7 @@ pub fn agg_lock_hash_sig( } } - Ok((BlstImpl.aggregate(&sigs)?, pubkeys)) + Ok((tbls::aggregate(&sigs)?, pubkeys)) } /// Aggregates threshold deposit-data signatures per validator. @@ -188,9 +186,8 @@ pub fn agg_deposit_data( } })?; - let agg_sig = BlstImpl.threshold_aggregate(&partial_sigs)?; - BlstImpl - .verify(&share.pub_key, &sig_root, &agg_sig) + let agg_sig = tbls::threshold_aggregate(&partial_sigs)?; + tbls::verify(&share.pub_key, &sig_root, &agg_sig) .map_err(AggregateError::InvalidDepositAggregatedSignature)?; res.push(phase0::DepositData { @@ -237,9 +234,8 @@ pub fn agg_validator_registrations( } })?; - let agg_sig = BlstImpl.threshold_aggregate(&partial_sigs)?; - BlstImpl - .verify(&share.pub_key, &sig_root, &agg_sig) + let agg_sig = tbls::threshold_aggregate(&partial_sigs)?; + tbls::verify(&share.pub_key, &sig_root, &agg_sig) .map_err(AggregateError::InvalidValidatorRegistrationAggregatedSignature)?; res.push(msg.set_signature(agg_sig)?); @@ -295,8 +291,7 @@ fn verify_threshold_partials( .get(&partial.share_idx) .ok_or(AggregateError::InvalidPubshare)?; - BlstImpl - .verify(pubshare, message, &sig) + tbls::verify(pubshare, message, &sig) .map_err(|_| invalid_signature_error(partial.share_idx))?; res.insert(partial.share_idx, sig); @@ -310,7 +305,7 @@ mod tests { use super::*; use pluto_core::signeddata::VersionedSignedValidatorRegistration as CoreRegistration; - use pluto_crypto::tblsconv::pubkey_to_eth2; + use pluto_crypto::types::pubkey_to_eth2; use pluto_eth2api::{ v1, versioned::{BuilderVersion, VersionedSignedValidatorRegistration}, @@ -319,22 +314,18 @@ mod tests { use rand::SeedableRng; fn build_share_fixture() -> (Share, HashMap) { - let tbls = BlstImpl; - let secret = tbls - .generate_insecure_secret(rand::rngs::StdRng::seed_from_u64(7)) + let secret = tbls::generate_insecure_secret(rand::rngs::StdRng::seed_from_u64(7)) .expect("secret generation should succeed"); - let pub_key = tbls - .secret_to_public_key(&secret) - .expect("public key derivation should succeed"); - let secret_shares = tbls - .threshold_split(&secret, 4, 3) - .expect("threshold split should succeed"); + let pub_key = + tbls::secret_to_public_key(&secret).expect("public key derivation should succeed"); + let secret_shares = + tbls::threshold_split(&secret, 4, 3).expect("threshold split should succeed"); let public_shares = secret_shares .iter() .map(|(idx, share)| { ( *idx, - tbls.secret_to_public_key(share) + tbls::secret_to_public_key(share) .expect("public share derivation should succeed"), ) }) @@ -384,12 +375,11 @@ mod tests { .into_iter() .map(|idx| { partial_signature( - BlstImpl - .sign( - secret_shares.get(&idx).expect("share should exist"), - &sig_root, - ) - .expect("partial signing should succeed"), + tbls::sign( + secret_shares.get(&idx).expect("share should exist"), + &sig_root, + ) + .expect("partial signing should succeed"), idx, ) }) @@ -405,8 +395,7 @@ mod tests { assert_eq!(res.len(), 1); let agg = res[0].0.v1.as_ref().expect("v1 registration should exist"); assert_eq!(agg.message, reg.0.v1.as_ref().expect("v1 reg").message); - BlstImpl - .verify(&share.pub_key, &sig_root, &agg.signature) + tbls::verify(&share.pub_key, &sig_root, &agg.signature) .expect("aggregate signature should verify"); } @@ -431,12 +420,11 @@ mod tests { } else { &sig_root }; - let sig = BlstImpl - .sign( - secret_shares.get(&idx).expect("share should exist"), - message, - ) - .expect("signing should succeed"); + let sig = tbls::sign( + secret_shares.get(&idx).expect("share should exist"), + message, + ) + .expect("signing should succeed"); partials.push(partial_signature(sig, idx)); } @@ -467,12 +455,11 @@ mod tests { } else { hash }; - let sig = BlstImpl - .sign( - secret_shares.get(&idx).expect("share should exist"), - message, - ) - .expect("signing should succeed"); + let sig = tbls::sign( + secret_shares.get(&idx).expect("share should exist"), + message, + ) + .expect("signing should succeed"); partials.push(partial_signature(sig, idx)); } @@ -506,12 +493,11 @@ mod tests { .into_iter() .map(|idx| { partial_signature( - BlstImpl - .sign( - secret_shares.get(&idx).expect("share should exist"), - &sig_root, - ) - .expect("signing should succeed"), + tbls::sign( + secret_shares.get(&idx).expect("share should exist"), + &sig_root, + ) + .expect("signing should succeed"), idx, ) }) @@ -537,8 +523,7 @@ mod tests { .into_iter() .map(|idx| { partial_signature( - BlstImpl - .sign(secret_shares.get(&idx).expect("share should exist"), hash) + tbls::sign(secret_shares.get(&idx).expect("share should exist"), hash) .expect("signing should succeed"), idx, ) @@ -579,12 +564,11 @@ mod tests { .into_iter() .map(|idx| { partial_signature( - BlstImpl - .sign( - secret_shares.get(&idx).expect("share should exist"), - &sig_root, - ) - .expect("signing should succeed"), + tbls::sign( + secret_shares.get(&idx).expect("share should exist"), + &sig_root, + ) + .expect("signing should succeed"), idx, ) }) diff --git a/crates/dkg/src/frost.rs b/crates/dkg/src/frost.rs index 42c0b600..3598ad5e 100644 --- a/crates/dkg/src/frost.rs +++ b/crates/dkg/src/frost.rs @@ -1,10 +1,7 @@ use std::collections::{BTreeMap, HashMap}; use async_trait::async_trait; -use pluto_crypto::{ - tblsconv::{privkey_from_bytes, pubkey_from_bytes}, - types::PublicKey, -}; +use pluto_crypto::types::{PublicKey, privkey_from_bytes, pubkey_from_bytes}; use pluto_frost::{ G1Affine, G1Projective, KeyPackage, kryptology::{self, Round1Bcast, Round1Secret, Round2Bcast, ShamirShare}, @@ -138,7 +135,7 @@ pub enum FrostError { ChannelClosed(&'static str), /// Failed to convert public key bytes. #[error("public key conversion: {0}")] - PublicKey(#[from] pluto_crypto::tblsconv::ConvError), + PublicKey(#[from] pluto_crypto::types::ConvError), /// Failed to decode a compressed G1 public key point. #[error("invalid compressed G1 public key point")] InvalidPublicKeyPoint, @@ -484,7 +481,7 @@ fn dkg_context_byte(dkg_ctx: &str) -> u8 { mod tests { use std::sync::Arc; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::Index}; + use pluto_crypto::{tbls, types::Index}; use tokio::sync::{Mutex, Notify}; use super::*; @@ -945,18 +942,14 @@ mod tests { .expect("node index should not overflow"), ) .expect("node index should fit in Index"); - let sig = BlstImpl - .sign(&shares[val_idx].secret_share, msg) + let sig = tbls::sign(&shares[val_idx].secret_share, msg) .expect("partial signature should succeed"); partials.insert(share_id, sig); } - let sig = BlstImpl - .threshold_aggregate(&partials) - .expect("threshold aggregation should succeed"); - BlstImpl - .verify(&pub_key, msg, &sig) - .expect("aggregated signature should verify"); + let sig = + tbls::threshold_aggregate(&partials).expect("threshold aggregation should succeed"); + tbls::verify(&pub_key, msg, &sig).expect("aggregated signature should verify"); } } } diff --git a/crates/dkg/src/frostp2p_integ_test.rs b/crates/dkg/src/frostp2p_integ_test.rs index 6a407e19..4fefd82f 100644 --- a/crates/dkg/src/frostp2p_integ_test.rs +++ b/crates/dkg/src/frostp2p_integ_test.rs @@ -9,7 +9,7 @@ use libp2p::{ Multiaddr, PeerId, swarm::{NetworkBehaviour, SwarmEvent}, }; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::Index}; +use pluto_crypto::{tbls, types::Index}; use pluto_p2p::{ behaviours::pluto::PlutoBehaviourEvent, config::P2PConfig, @@ -520,18 +520,14 @@ fn verify_returned_shares(node_shares: &[Vec]) { .expect("share index should not overflow"), ) .expect("share index should fit Index"); - let sig = BlstImpl - .sign(&shares[val_idx].secret_share, msg) + let sig = tbls::sign(&shares[val_idx].secret_share, msg) .expect("partial signature should succeed"); partials.insert(share_id, sig); } } - let sig = BlstImpl - .threshold_aggregate(&partials) - .expect("threshold aggregation should succeed"); - BlstImpl - .verify(&pub_key, msg, &sig) - .expect("aggregated signature should verify"); + let sig = + tbls::threshold_aggregate(&partials).expect("threshold aggregation should succeed"); + tbls::verify(&pub_key, msg, &sig).expect("aggregated signature should verify"); } } diff --git a/crates/dkg/src/signing.rs b/crates/dkg/src/signing.rs index a99182d2..ab489a94 100644 --- a/crates/dkg/src/signing.rs +++ b/crates/dkg/src/signing.rs @@ -15,7 +15,7 @@ use pluto_core::{ signeddata::VersionedSignedValidatorRegistration, types::{ParSignedData, ParSignedDataSet, PubKey}, }; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, tblsconv::pubkey_to_eth2}; +use pluto_crypto::{tbls, types::pubkey_to_eth2}; use pluto_eth2api::{spec::phase0, v1, versioned}; use pluto_eth2util::{deposit, network, registration}; use tracing::{info, warn}; @@ -112,7 +112,7 @@ pub fn sign_lock_hash(share_idx: u64, shares: &[Share], hash: &[u8]) -> Result

) -> Result> { mod tests { use std::path::{Path, PathBuf}; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::PrivateKey}; + use pluto_crypto::{tbls, types::PrivateKey}; use tempfile::TempDir; use test_case::test_case; @@ -324,8 +324,7 @@ mod tests { /// Generates a random BLS secret key for testing. fn generate_secret_key() -> PrivateKey { - let tbls = BlstImpl; - tbls.generate_secret_key(rand::thread_rng()).unwrap() + tbls::generate_secret_key(rand::thread_rng()).unwrap() } /// Helper: generates a new key, stores it insecurely, then renames the diff --git a/crates/eth2util/src/keystore/store.rs b/crates/eth2util/src/keystore/store.rs index ac0373a7..4733ce7a 100644 --- a/crates/eth2util/src/keystore/store.rs +++ b/crates/eth2util/src/keystore/store.rs @@ -7,7 +7,7 @@ use std::path::Path; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::PrivateKey}; +use pluto_crypto::{tbls, types::PrivateKey}; use rand::RngCore; use serde::{Deserialize, Serialize}; use uuid::Builder; @@ -120,9 +120,7 @@ pub fn encrypt( pbkdf2_c: Option, rng: &mut impl rand::RngCore, ) -> Result { - let tbls = BlstImpl; - let pub_key = tbls - .secret_to_public_key(secret) + let pub_key = tbls::secret_to_public_key(secret) .map_err(|e| KeystoreError::Encrypt(format!("marshal pubkey: {e}")))?; let crypto = keystorev4::encrypt(secret, password.as_ref(), pbkdf2_c, rng)?; @@ -242,7 +240,6 @@ async fn write_file(path: impl AsRef, data: &[u8], mode: u32) -> Result<() mod tests { use std::path::PathBuf; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::PrivateKey}; use rand::SeedableRng; use tempfile::TempDir; @@ -251,8 +248,7 @@ mod tests { /// Generates a random BLS secret key for testing. fn generate_secret_key() -> PrivateKey { - let tbls = BlstImpl; - tbls.generate_secret_key(rand::thread_rng()).unwrap() + tbls::generate_secret_key(rand::thread_rng()).unwrap() } #[tokio::test] diff --git a/crates/eth2util/src/registration.rs b/crates/eth2util/src/registration.rs index 7ac4e276..64009e67 100644 --- a/crates/eth2util/src/registration.rs +++ b/crates/eth2util/src/registration.rs @@ -59,7 +59,7 @@ pub fn get_message_signing_root( #[cfg(test)] mod tests { use super::*; - use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls}; + use pluto_crypto::tbls; #[test] fn new_message_works() { @@ -134,7 +134,7 @@ mod tests { .unwrap(); let secret: pluto_crypto::types::PrivateKey = sk_bytes.as_slice().try_into().unwrap(); - let pubkey = BlstImpl.secret_to_public_key(&secret).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); let registration_json = r#" { @@ -204,8 +204,7 @@ mod tests { .try_into() .unwrap(); - BlstImpl - .verify(&pubkey, &signing_root, &signature) + tbls::verify(&pubkey, &signing_root, &signature) .expect("BLS signature verification failed"); } } diff --git a/crates/eth2util/src/signing.rs b/crates/eth2util/src/signing.rs index 57c01c81..8fa54a68 100644 --- a/crates/eth2util/src/signing.rs +++ b/crates/eth2util/src/signing.rs @@ -1,6 +1,5 @@ use pluto_crypto::{ - blst_impl::BlstImpl, - tbls::Tbls, + tbls, types::{PublicKey, Signature}, }; use pluto_eth2api::{ @@ -148,7 +147,7 @@ pub async fn verify( let signing_root = get_data_root(client, domain_name, epoch, message_root).await?; - BlstImpl.verify(pubkey, &signing_root, signature)?; + tbls::verify(pubkey, &signing_root, signature)?; Ok(()) } @@ -169,7 +168,7 @@ pub fn verify_with_domain( let signing_root = compute_signing_root(message_root, domain); - BlstImpl.verify(pubkey, &signing_root, signature)?; + tbls::verify(pubkey, &signing_root, signature)?; Ok(()) } @@ -204,7 +203,6 @@ pub async fn verify_aggregate_and_proof_selection( mod tests { use super::*; use chrono::DateTime; - use pluto_crypto::tbls::Tbls; use pluto_eth2api::{ compute_builder_domain, compute_domain, spec::{bellatrix::ExecutionAddress, phase0::Version}, @@ -358,7 +356,7 @@ mod tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let pubkey = BlstImpl.secret_to_public_key(&secret).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); let fee_recipient: ExecutionAddress = hex::decode("000000000000000000000000000000000000dead") .unwrap() @@ -375,7 +373,7 @@ mod tests { let signing_root = get_data_root(client, DomainName::ApplicationBuilder, 0, message_root) .await .unwrap(); - let signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let signature = tbls::sign(&secret, &signing_root).unwrap(); verify( client, @@ -414,13 +412,13 @@ mod tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let pubkey = BlstImpl.secret_to_public_key(&secret).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); let message_root = [0x55; 32]; let domain = get_domain(client, DomainName::ApplicationBuilder, 0) .await .unwrap(); let signing_root = compute_signing_root(message_root, domain); - let signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let signature = tbls::sign(&secret, &signing_root).unwrap(); verify_with_domain(domain, message_root, &signature, &pubkey).unwrap(); } @@ -447,12 +445,12 @@ mod tests { let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); let wrong_secret = secret_key("01477d4bfbbcebe1fef8d4d6f624ecbb6e3178558bb1b0d6286c816c66842a6d"); - let pubkey = BlstImpl.secret_to_public_key(&wrong_secret).unwrap(); + let pubkey = tbls::secret_to_public_key(&wrong_secret).unwrap(); let message_root = [0x55; 32]; let signing_root = get_data_root(client, DomainName::ApplicationBuilder, 0, message_root) .await .unwrap(); - let signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let signature = tbls::sign(&secret, &signing_root).unwrap(); let err = verify( client, @@ -474,7 +472,7 @@ mod tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let pubkey = BlstImpl.secret_to_public_key(&secret).unwrap(); + let pubkey = tbls::secret_to_public_key(&secret).unwrap(); let signed_message_root = [0x55; 32]; let verified_message_root = [0x66; 32]; let signing_root = get_data_root( @@ -485,7 +483,7 @@ mod tests { ) .await .unwrap(); - let signature = BlstImpl.sign(&secret, &signing_root).unwrap(); + let signature = tbls::sign(&secret, &signing_root).unwrap(); let err = verify( client, diff --git a/crates/parsigex/src/behaviour.rs b/crates/parsigex/src/behaviour.rs index 8dc6806c..4505150e 100644 --- a/crates/parsigex/src/behaviour.rs +++ b/crates/parsigex/src/behaviour.rs @@ -593,8 +593,7 @@ mod eth2_verifier_tests { types::{Duty, ParSignedData, PubKey, SignedData}, }; use pluto_crypto::{ - blst_impl::BlstImpl, - tbls::Tbls, + tbls, types::{Index, PrivateKey, PublicKey}, }; use pluto_eth2api::{EthBeaconNodeApiClient, spec::phase0}; @@ -650,19 +649,17 @@ mod eth2_verifier_tests { let signing_root = get_data_root(client, domain, epoch, message_root) .await .unwrap(); - let signature = BlstImpl.sign(secret, &signing_root).unwrap(); + let signature = tbls::sign(secret, &signing_root).unwrap(); data.set_signature(signature).unwrap() } /// Splits `secret` into threshold BLS shares and returns each share's /// private key alongside the public-share map keyed by 1-indexed share id. fn split_shares(secret: &PrivateKey) -> (HashMap, HashMap) { - let shares = BlstImpl - .threshold_split(secret, TOTAL_SHARES, THRESHOLD) - .unwrap(); + let shares = tbls::threshold_split(secret, TOTAL_SHARES, THRESHOLD).unwrap(); let pub_shares = shares .iter() - .map(|(idx, share)| (*idx, BlstImpl.secret_to_public_key(share).unwrap())) + .map(|(idx, share)| (*idx, tbls::secret_to_public_key(share).unwrap())) .collect(); (shares, pub_shares) } @@ -677,7 +674,7 @@ mod eth2_verifier_tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let group_pubkey = PubKey::new(BlstImpl.secret_to_public_key(&secret).unwrap()); + let group_pubkey = PubKey::new(tbls::secret_to_public_key(&secret).unwrap()); let (shares, pub_shares) = split_shares(&secret); // Sign the attestation with the private share for index 2. @@ -708,7 +705,7 @@ mod eth2_verifier_tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let group_pubkey = PubKey::new(BlstImpl.secret_to_public_key(&secret).unwrap()); + let group_pubkey = PubKey::new(tbls::secret_to_public_key(&secret).unwrap()); let (shares, pub_shares) = split_shares(&secret); // Sign with share 2's secret but claim share index 3, so the verifier @@ -734,7 +731,7 @@ mod eth2_verifier_tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let group_pubkey = PubKey::new(BlstImpl.secret_to_public_key(&secret).unwrap()); + let group_pubkey = PubKey::new(tbls::secret_to_public_key(&secret).unwrap()); let (shares, _pub_shares) = split_shares(&secret); let att = sample_attestation(4); @@ -758,7 +755,7 @@ mod eth2_verifier_tests { let client = mock.client(); let secret = secret_key("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b"); - let group_pubkey = PubKey::new(BlstImpl.secret_to_public_key(&secret).unwrap()); + let group_pubkey = PubKey::new(tbls::secret_to_public_key(&secret).unwrap()); let (shares, pub_shares) = split_shares(&secret); let att = sample_attestation(4); diff --git a/crates/parsigex/tests/parsigex_e2e.rs b/crates/parsigex/tests/parsigex_e2e.rs index 491900b2..a20c9fe9 100644 --- a/crates/parsigex/tests/parsigex_e2e.rs +++ b/crates/parsigex/tests/parsigex_e2e.rs @@ -21,8 +21,7 @@ use pluto_core::{ types::{Duty, DutyType, ParSignedDataSet, PubKey, SlotNumber}, }; use pluto_crypto::{ - blst_impl::BlstImpl, - tbls::Tbls, + tbls, types::{PrivateKey, PublicKey, Signature}, }; use pluto_p2p::{ @@ -61,13 +60,11 @@ impl ClusterKey { /// Deals a fresh group key into [`NODES`] shares with a [`THRESHOLD`]. fn deal() -> Result { let secret = generate_test_bls_key(42); - let group_pub = BlstImpl - .secret_to_public_key(&secret) - .context("failed to derive group public key")?; + let group_pub = + tbls::secret_to_public_key(&secret).context("failed to derive group public key")?; let total = u64::try_from(NODES).context("node count should fit u64")?; let threshold = u64::try_from(THRESHOLD).context("threshold should fit u64")?; - let shares = BlstImpl - .threshold_split(&secret, total, threshold) + let shares = tbls::threshold_split(&secret, total, threshold) .context("failed to split group secret into shares")?; let group_pub_core = PubKey::new(group_pub); @@ -234,9 +231,8 @@ impl Harness { let mut tasks = JoinSet::new(); for node in &self.running { - let signature = BlstImpl - .sign(&node.share_priv, MSG) - .context("failed to sign with share")?; + let signature = + tbls::sign(&node.share_priv, MSG).context("failed to sign with share")?; let partial = SignedRandao::new_partial(EPOCH, signature, node.share_idx); let mut data_set = ParSignedDataSet::new(); data_set.insert(self.cluster.group_pub_core, partial); @@ -281,11 +277,9 @@ impl Harness { /// Aggregates `partials` and verifies the result against the group key. fn aggregate_and_verify(&self, partials: &HashMap) -> Result<()> { - let group_sig = BlstImpl - .threshold_aggregate(partials) + let group_sig = tbls::threshold_aggregate(partials) .context("threshold aggregation of received partials failed")?; - BlstImpl - .verify(&self.cluster.group_pub, MSG, &group_sig) + tbls::verify(&self.cluster.group_pub, MSG, &group_sig) .context("aggregated signature did not verify against the group public key") } diff --git a/crates/testutil/src/random.rs b/crates/testutil/src/random.rs index 0f878ee4..03a82c95 100644 --- a/crates/testutil/src/random.rs +++ b/crates/testutil/src/random.rs @@ -6,7 +6,7 @@ use k256::{ SecretKey, elliptic_curve::rand_core::{CryptoRng, Error, RngCore}, }; -use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::PrivateKey}; +use pluto_crypto::{tbls, types::PrivateKey}; use pluto_eth2api::{ spec::phase0, types::{ @@ -68,12 +68,10 @@ pub fn random_bytes32_seed(seed: u8) -> Vec { /// Generates a deterministic BLS private key for testing. pub fn generate_test_bls_key(seed: u64) -> PrivateKey { - let tbls = BlstImpl; let mut seed_bytes = [0u8; 32]; seed_bytes[..8].copy_from_slice(&seed.to_le_bytes()); let rng = StdRng::from_seed(seed_bytes); - tbls.generate_secret_key(rng) - .expect("deterministic key generation should not fail") + tbls::generate_secret_key(rng).expect("deterministic key generation should not fail") } /// Generates a random BLS signature as a hex string for testing. diff --git a/crates/testutil/src/validatormock/sign.rs b/crates/testutil/src/validatormock/sign.rs index 8270c558..e11ea3c5 100644 --- a/crates/testutil/src/validatormock/sign.rs +++ b/crates/testutil/src/validatormock/sign.rs @@ -8,10 +8,8 @@ use std::{collections::HashMap, sync::Arc}; use pluto_crypto::{ - blst_impl::BlstImpl, - tbls::Tbls, - tblsconv::{pubkey_to_eth2, sig_to_eth2}, - types::PrivateKey, + tbls, + types::{PrivateKey, pubkey_to_eth2, sig_to_eth2}, }; use pluto_eth2api::spec::phase0::{BLSPubKey, BLSSignature}; @@ -31,8 +29,8 @@ pub trait Sign: Send + Sync + std::fmt::Debug + 'static { /// component that needs to sign. pub type SignFunc = Arc; -/// Concrete BLS signer backed by [`BlstImpl`]. Registers a set of secrets by -/// their derived eth2 public key. +/// Concrete BLS signer backed by [`pluto_crypto::tbls`]. Registers a set of +/// secrets by their derived eth2 public key. #[derive(Debug, Clone)] pub struct Signer { secrets: HashMap, @@ -40,12 +38,12 @@ pub struct Signer { impl Signer { /// Builds a [`Signer`] from `secrets`, deriving each public key with - /// [`BlstImpl`]. Fails fast if any secret is rejected by the BLS backend. + /// [`tbls::secret_to_public_key`]. Fails fast if any secret is rejected by + /// the BLS backend. pub fn new(secrets: &[PrivateKey]) -> Result { - let tbls = BlstImpl; let mut map = HashMap::with_capacity(secrets.len()); for secret in secrets { - let pk = tbls.secret_to_public_key(secret)?; + let pk = tbls::secret_to_public_key(secret)?; map.insert(pubkey_to_eth2(pk), *secret); } Ok(Self { secrets: map }) @@ -60,7 +58,7 @@ impl Signer { impl Sign for Signer { fn sign(&self, pubkey: &BLSPubKey, data: &[u8]) -> Result { let secret = self.secrets.get(pubkey).ok_or(SignError::UnknownPubkey)?; - let sig = BlstImpl.sign(secret, data)?; + let sig = tbls::sign(secret, data)?; Ok(sig_to_eth2(sig)) } } @@ -74,17 +72,13 @@ mod tests { let mut bytes = [0u8; 32]; bytes[0] = seed; let rng = StdRng::from_seed(bytes); - BlstImpl.generate_insecure_secret(rng).expect("generate") + tbls::generate_insecure_secret(rng).expect("generate") } #[test] fn round_trip_known_pubkey() { let secret = deterministic_secret(1); - let pubkey = pubkey_to_eth2( - BlstImpl - .secret_to_public_key(&secret) - .expect("derive pubkey"), - ); + let pubkey = pubkey_to_eth2(tbls::secret_to_public_key(&secret).expect("derive pubkey")); let signer = Signer::new(&[secret]).expect("build signer"); let sig = signer.sign(&pubkey, b"msg").expect("sign");