diff --git a/Cargo.lock b/Cargo.lock index cbfad377d..37a3b21a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1272,7 +1272,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1281,7 +1281,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1304,11 +1304,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bls_lagrange" +version = "0.1.0" +dependencies = [ + "bls", + "blst", + "blstrs_plus", + "rand", + "vsss-rs", + "zeroize", +] + [[package]] name = "blst" version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +source = "git+https://github.com/dknopik/blst?branch=sk-conversion#19e5f3494fd8c7efe92a66b968423641ff93b2eb" dependencies = [ "cc", "glob", @@ -1332,6 +1343,23 @@ dependencies = [ "subtle", ] +[[package]] +name = "blstrs_plus" +version = "0.8.18" +source = "git+https://github.com/dknopik/blstrs?branch=pls#76852cb871b39ffea9ae45e68cf6d773b67acf12" +dependencies = [ + "arrayref", + "blst", + "elliptic-curve", + "ff", + "group", + "pairing", + "rand_core", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1469,7 +1497,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1789,8 +1817,9 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core", + "serdect", "subtle", "zeroize", ] @@ -1801,7 +1830,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core", "typenum", ] @@ -1812,7 +1841,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ - "generic-array", + "generic-array 0.14.7", "subtle", ] @@ -1822,7 +1851,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ - "generic-array", + "generic-array 0.14.7", "subtle", ] @@ -2130,7 +2159,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -2328,13 +2357,29 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array", + "generic-array 0.14.7", "group", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core", "sec1", "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "elliptic-curve-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48843edfbd0a370b3dd14cdbb4e446e9a8855311e6b2b57bf9a1fd1367bc317" +dependencies = [ + "elliptic-curve", + "heapless", + "hex", + "multiexp", + "serde", "zeroize", ] @@ -2995,6 +3040,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" +dependencies = [ + "serde", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -3141,6 +3196,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -3198,6 +3262,16 @@ dependencies = [ "psutil", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -3328,7 +3402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" dependencies = [ "digest 0.9.0", - "generic-array", + "generic-array 0.14.7", "hmac 0.8.1", ] @@ -3838,7 +3912,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -4873,6 +4947,19 @@ dependencies = [ "data-encoding-macro", ] +[[package]] +name = "multiexp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a383da1ae933078ddb1e4141f1dd617b512b4183779d6977e6451b0e644806" +dependencies = [ + "ff", + "group", + "rustversion", + "std-shims", + "zeroize", +] + [[package]] name = "multihash" version = "0.19.3" @@ -5049,6 +5136,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -5057,6 +5158,8 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "rand", + "serde", ] [[package]] @@ -5077,6 +5180,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "rand", + "serde", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -5103,6 +5217,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -6512,7 +6638,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array", + "generic-array 0.14.7", "pkcs8", "subtle", "zeroize", @@ -6658,6 +6784,16 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6751,6 +6887,7 @@ dependencies = [ name = "signature_collector" version = "0.1.0" dependencies = [ + "bls_lagrange", "dashmap", "processor", "slot_clock", @@ -6951,6 +7088,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -7021,6 +7161,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "std-shims" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e49360f31b0b75a6a82a5205c6103ea07a79a60808d44f5cc879d303337926" +dependencies = [ + "hashbrown 0.14.5", + "spin 0.9.8", +] + [[package]] name = "strsim" version = "0.10.0" @@ -8047,6 +8197,25 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vsss-rs" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec4ebcc5594130c31b49594d55c0583fe80621f252f570b222ca4845cafd3cf" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "elliptic-curve-tools", + "generic-array 1.2.0", + "hex", + "num", + "rand_core", + "serde", + "sha3", + "subtle", + "zeroize", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1c8c2fca7..5564daf14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "anchor", "anchor/client", + "anchor/common/bls_lagrange", "anchor/common/qbft", "anchor/common/ssv_types", "anchor/common/version", @@ -24,6 +25,7 @@ edition = "2021" # This table has three subsections: first the internal dependencies, then the lighthouse dependencies, then all other. [workspace.dependencies] anchor_validator_store = { path = "anchor/validator_store" } +bls_lagrange = { path = "anchor/common/bls_lagrange" } client = { path = "anchor/client" } database = { path = "anchor/database" } eth = { path = "anchor/eth" } @@ -71,6 +73,9 @@ alloy = { version = "0.6.4", features = [ async-channel = "1.9" axum = "0.7.7" base64 = "0.22.1" +blst = { git = "https://github.com/dknopik/blst", branch = "sk-conversion" } +# the custom repo is needed because they fix to a specific version of blst, which conflicts with the line above +blstrs_plus = { git = "https://github.com/dknopik/blstrs", branch = "pls" } clap = { version = "4.5.15", features = ["derive", "wrap_help"] } dashmap = "6.1.0" derive_more = { version = "1.0.0", features = ["full"] } @@ -83,6 +88,7 @@ indexmap = "2.7.0" num_cpus = "1" openssl = "0.10.68" parking_lot = "0.12" +rand = "0.8.5" reqwest = "0.12.12" rusqlite = "0.28.0" serde = { version = "1.0.208", features = ["derive"] } @@ -99,8 +105,13 @@ tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } tree_hash = "0.8" tree_hash_derive = "0.8" +vsss-rs = "5.1.0" zeroize = "1.8.1" +[patch.crates-io] +# todo: remove when https://github.com/supranational/blst/pull/248 is merged +blst = { git = "https://github.com/dknopik/blst", branch = "sk-conversion" } + [profile.maxperf] inherits = "release" diff --git a/anchor/common/bls_lagrange/Cargo.toml b/anchor/common/bls_lagrange/Cargo.toml new file mode 100644 index 000000000..b3955601f --- /dev/null +++ b/anchor/common/bls_lagrange/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bls_lagrange" +version = "0.1.0" +edition = "2021" + +[dependencies] +bls = { workspace = true } +blst = { workspace = true, optional = true } +blstrs_plus = { workspace = true, optional = true } +rand = { workspace = true } +vsss-rs = { workspace = true, optional = true } +zeroize = { workspace = true } + +[dev-dependencies] +blst = { workspace = true } + +[features] +default = ["blsful"] +blsful = ["dep:blstrs_plus", "dep:vsss-rs"] +blst = ["dep:blst"] +blst_single_thread = ["blst"] diff --git a/anchor/common/bls_lagrange/src/blsful.rs b/anchor/common/bls_lagrange/src/blsful.rs new file mode 100644 index 000000000..0ca7ff9f3 --- /dev/null +++ b/anchor/common/bls_lagrange/src/blsful.rs @@ -0,0 +1,126 @@ +use crate::Error; +use blstrs_plus::{G2Projective, Scalar}; +use rand::{CryptoRng, Rng}; +use std::num::NonZeroU64; +use vsss_rs::{ + shamir, IdentifierPrimeField, ParticipantIdGeneratorType, ReadableShareSet, ValueGroup, +}; +use zeroize::Zeroizing; + +#[derive(Debug, Clone)] +pub struct KeyId { + num: u64, + identifier: IdentifierPrimeField, +} + +impl TryFrom for KeyId { + type Error = Error; + + fn try_from(value: u64) -> Result { + if value != 0 { + Ok(KeyId { + num: value, + identifier: IdentifierPrimeField(Scalar::from(value)), + }) + } else { + Err(Error::ZeroId) + } + } +} +impl From for KeyId { + fn from(value: NonZeroU64) -> Self { + KeyId { + num: value.get(), + identifier: IdentifierPrimeField(Scalar::from(value.get())), + } + } +} + +impl From for u64 { + fn from(value: KeyId) -> Self { + value.num + } +} + +pub fn split_with_rng( + key: bls::SecretKey, + threshold: u64, + ids: impl IntoIterator, + rng: &mut (impl CryptoRng + Rng), +) -> Result, Error> { + let result = Scalar::from_be_bytes( + key.serialize() + .as_bytes() + .try_into() + .map_err(|_| Error::InternalError)?, + ); + let key = if result.is_some().into() { + IdentifierPrimeField(result.unwrap()) + } else { + return Err(Error::InternalError); + }; + + let ids = ids.into_iter().map(|k| k.identifier).collect::>(); + + let result = Zeroizing::new( + shamir::split_secret_with_participant_generator( + threshold as usize, + ids.len(), + &key, + rng, + &[ParticipantIdGeneratorType::List { list: &ids }], + ) + .map_err(|_| Error::InternalError)?, + ); + + result + .iter() + .map(|(identifier, share)| { + bls::SecretKey::deserialize(&share.0.to_be_bytes()) + .map_err(|_| Error::InternalError) + .map(move |sk| { + let bytes = identifier.0.to_be_bytes(); + debug_assert_eq!(bytes[..24], [0; 24]); + ( + KeyId { + num: u64::from_be_bytes((&bytes[24..]).try_into().unwrap()), + identifier: *identifier, + }, + sk, + ) + }) + }) + .collect() +} + +pub fn combine_signatures( + signatures: &[bls::Signature], + ids: &[KeyId], +) -> Result { + if signatures.len() < 2 { + return Err(Error::LessThanTwoSignatures); + } + if signatures.len() != ids.len() { + return Err(Error::NotOneIdPerSignature); + } + + let share_set = signatures + .iter() + .zip(ids) + .map(|(sig, id)| { + let Some(bytes) = sig.serialize_uncompressed() else { + return Err(Error::InternalError); + }; + let g2 = G2Projective::from_uncompressed(&bytes); + if g2.is_some().into() { + Ok((id.identifier, ValueGroup(g2.unwrap()))) + } else { + Err(Error::InternalError) + } + }) + .collect::, _>>()?; + + let result = share_set.combine().map_err(|_| Error::InternalError)?; + bls::Signature::deserialize_uncompressed(&result.0.to_uncompressed()) + .map_err(|_| Error::InternalError) +} diff --git a/anchor/common/bls_lagrange/src/blst.rs b/anchor/common/bls_lagrange/src/blst.rs new file mode 100644 index 000000000..861d110a3 --- /dev/null +++ b/anchor/common/bls_lagrange/src/blst.rs @@ -0,0 +1,204 @@ +// from https://github.com/herumi/mcl/blob/3462cf0983bffb703a6e9f4623e47a26ec6e7fe5/include/mcl/lagrange.hpp +use crate::{random_key, Error}; +use bls::Signature; +use blst::min_pk::SecretKey; +use blst::*; +use rand::prelude::*; +use std::iter::{once, repeat_with}; +use std::mem::MaybeUninit; +use std::num::NonZeroU64; +use std::sync::LazyLock; +use zeroize::Zeroizing; + +static WARNING: LazyLock<()> = LazyLock::new(|| { + eprintln!( + r#" +####################################################################################### +### YOU ARE USING AN UNAUDITED, UNSAFE IMPLEMENTATION OF BLS LAGRANGE INTERPOLATION ### +### ### +### !!! DO NOT USE IN PRODUCTION !!! ### +####################################################################################### +"# + ) +}); + +#[derive(Debug, Clone)] +pub struct KeyId { + num: u64, + fr: blst_fr, +} + +impl TryFrom for KeyId { + type Error = Error; + + fn try_from(value: u64) -> Result { + if value != 0 { + unsafe { + let mut id = MaybeUninit::::uninit(); + blst_fr_from_uint64(id.as_mut_ptr(), &value); + Ok(KeyId { + num: value, + fr: id.assume_init(), + }) + } + } else { + Err(Error::ZeroId) + } + } +} +impl From for KeyId { + fn from(value: NonZeroU64) -> Self { + unsafe { + let mut id = MaybeUninit::::uninit(); + blst_fr_from_uint64(id.as_mut_ptr(), &value.get()); + KeyId { + num: value.get(), + fr: id.assume_init(), + } + } + } +} + +impl From for u64 { + fn from(value: KeyId) -> Self { + value.num + } +} + +#[inline] +fn key_to_fr(key: &SecretKey) -> blst_fr { + let mut key_fr = MaybeUninit::::uninit(); + unsafe { + blst_fr_from_scalar(key_fr.as_mut_ptr(), <&blst_scalar>::from(key)); + key_fr.assume_init() + } +} + +pub fn split_with_rng( + key: bls::SecretKey, + threshold: u64, + ids: impl IntoIterator, + rng: &mut (impl CryptoRng + Rng), +) -> Result, Error> { + LazyLock::force(&WARNING); + let key = key.point(); + + if threshold <= 1 { + return Err(Error::InvalidThreshold); + } + + // MaybeUninit needed to make `Zeroizing` work + let msk = Zeroizing::new( + once(Ok(MaybeUninit::new(key_to_fr(key)))) + .chain( + repeat_with(|| random_key(rng).map(|sk| MaybeUninit::new(key_to_fr(sk.point())))) + .take((threshold - 1) as usize), + ) + .collect::, _>>()?, + ); + ids.into_iter() + .map(|id| { + let mut intermediate = MaybeUninit::::uninit(); + unsafe { + let mut y = (*msk).last().copied().unwrap(); + for i in (0..=(threshold - 2)).rev() { + blst_fr_mul(intermediate.as_mut_ptr(), y.as_ptr(), &id.fr); + blst_fr_add( + y.as_mut_ptr(), + intermediate.as_ptr(), + msk[i as usize].as_ptr(), + ); + } + let mut scalar = blst_scalar::default(); + blst_scalar_from_fr(&mut scalar, y.as_ptr()); + Ok(( + id, + bls::SecretKey::from_point(SecretKey::from_scalar_unchecked(scalar)), + )) + } + }) + .collect() +} + +pub fn combine_signatures(signatures: &[Signature], ids: &[KeyId]) -> Result { + LazyLock::force(&WARNING); + if signatures.len() < 2 { + return Err(Error::LessThanTwoSignatures); + } + if signatures.len() != ids.len() { + return Err(Error::NotOneIdPerSignature); + } + + let signatures = signatures + .iter() + .map(|sig| sig.point().cloned().ok_or(Error::InvalidSignature)) + .collect::, _>>()?; + + // intermediates. + let mut ifr = blst_fr::default(); + let mut is = blst_scalar::default(); + + let zero = unsafe { + blst_fr_from_uint64(&mut ifr, &0); + ifr + }; + + let mut numerator = ids[0].clone().fr; + unsafe { + for id in &ids[1..] { + blst_fr_mul(&mut numerator, &numerator, &id.fr); + } + } + if numerator == zero { + return Err(Error::ZeroId); + } + + let mut d = Vec::with_capacity(ids.len() * 32); + unsafe { + for id_i in ids { + let mut denominator = id_i.fr; + for id_j in ids.iter() { + if id_i as *const KeyId != id_j as *const KeyId { + blst_fr_sub(&mut ifr, &id_j.fr, &id_i.fr); + if ifr == zero { + return Err(Error::RepeatedId); + } + blst_fr_mul(&mut denominator, &denominator, &ifr); + } + } + blst_fr_inverse(&mut denominator, &denominator); + blst_fr_mul(&mut ifr, &denominator, &numerator); + blst_scalar_from_fr(&mut is, &ifr); + d.extend(is.b); + } + } + + Ok(Signature::from_point(mult(&signatures, &d), false)) +} + +#[cfg(not(feature = "blst_single_thread"))] +fn mult(signatures: &[min_pk::Signature], d: &[u8]) -> min_pk::Signature { + signatures.mult(d, 255).to_signature() +} + +#[cfg(feature = "blst_single_thread")] +fn mult(signatures: &[min_pk::Signature], d: &[u8]) -> min_pk::Signature { + let mut ret = blst_p2::default(); + let mut ret_affine = blst_p2_affine::default(); + let p: [*const blst_p2_affine; 2] = [<&blst_p2_affine>::from(&signatures[0]), std::ptr::null()]; + let s: [*const u8; 2] = [&d[0], std::ptr::null()]; + unsafe { + let mut scratch: Vec = + Vec::with_capacity(blst_p2s_mult_pippenger_scratch_sizeof(signatures.len()) / 8); + blst_p2s_mult_pippenger( + &mut ret, + &p[0], + signatures.len(), + &s[0], + 255, + scratch.as_mut_ptr(), + ); + blst_p2_to_affine(&mut ret_affine, &ret) + } + ret_affine.into() +} diff --git a/anchor/common/bls_lagrange/src/lib.rs b/anchor/common/bls_lagrange/src/lib.rs new file mode 100644 index 000000000..dfba386cf --- /dev/null +++ b/anchor/common/bls_lagrange/src/lib.rs @@ -0,0 +1,145 @@ +//! THIS CRATE IS NOT READY FOR PRODUCTION USE! DO *NOT* USE IN PRODUCTION CODE! + +use bls::SecretKey; +use rand::prelude::*; + +#[cfg(feature = "blst")] +pub mod blst; +#[cfg(all(not(feature = "blsful"), feature = "blst"))] +pub use self::blst::*; +#[cfg(feature = "blsful")] +pub mod blsful; +#[cfg(feature = "blsful")] +pub use self::blsful::*; + +#[derive(Debug, Clone, Copy)] +pub enum Error { + InternalError, + InvalidThreshold, + LessThanTwoSignatures, + NotOneIdPerSignature, + ZeroId, + RepeatedId, + InvalidSignature, +} + +pub fn split( + key: SecretKey, + threshold: u64, + ids: impl IntoIterator, +) -> Result, Error> { + split_with_rng(key, threshold, ids, &mut thread_rng()) +} + +#[cfg(any(feature = "blst", test))] +pub(crate) fn random_key(rng: &mut (impl CryptoRng + Rng)) -> Result { + let ikm: [u8; 32] = rng.gen(); + let sk = ::blst::min_pk::SecretKey::key_gen(&ikm, &[]).map_err(|_| Error::InternalError)?; + Ok(SecretKey::from_point(sk)) +} + +#[cfg(test)] +mod tests { + use super::*; + use bls::Hash256; + use std::hint::black_box; + use std::time::Instant; + + #[test] + fn test_basic_often() { + let mut rng = &mut StdRng::seed_from_u64(0x12345EED00000000); + for _ in 0..1000 { + test_basic(&mut rng); + } + } + + fn test_basic(rng: &mut (impl CryptoRng + Rng)) { + let total = rng.gen_range(2..=13); + let threshold = rng.gen_range(2..=total); + + let master = random_key(rng).unwrap(); + let pk = master.public_key(); + + let mut keys = split_with_rng( + master, + threshold as u64, + (1..=total).map(|x| KeyId::try_from(x as u64).unwrap()), + rng, + ) + .unwrap(); + + // shuffle to sign with varying key indices + keys.shuffle(rng); + + let (ids, keys): (Vec<_>, Vec<_>) = keys.into_iter().unzip(); + + assert_eq!(keys.len(), total); + + let mut data = [0u8; 32]; + rng.fill(&mut data); + + let signers = rng.gen_range(2..=total); + + let signatures = keys + .into_iter() + .take(signers) + .map(|key| key.sign(Hash256::from(data))) + .collect::>(); + + let combined = combine_signatures(&signatures, &ids[..signers]).unwrap(); + + let result = combined.verify(&pk, data.into()); + if signers >= threshold { + assert!(result); + } else { + assert!(!result); + } + } + + #[test] + fn bench_basic() { + println!("1000 iterations each"); + (1..=4).for_each(do_bench_basic) + } + + fn do_bench_basic(f: usize) { + let rng = &mut StdRng::seed_from_u64(0x12345EED00000000); + let total = 3 * f + 1; + let threshold = 2 * f + 1; + + let master = random_key(rng).unwrap(); + + let mut keys = split_with_rng( + master, + threshold as u64, + (1..=total).map(|x| KeyId::try_from(x as u64).unwrap()), + rng, + ) + .unwrap(); + + // shuffle to sign with varying key indices + keys.shuffle(rng); + + let (ids, keys): (Vec<_>, Vec<_>) = keys.into_iter().unzip(); + + assert_eq!(keys.len(), total); + + let mut data = [0u8; 32]; + rng.fill(&mut data); + + let signatures = keys + .into_iter() + .take(threshold) + .map(|key| key.sign(Hash256::from(data))) + .collect::>(); + + let timing = Instant::now(); + for _ in 0..1_000 { + black_box(combine_signatures(&signatures, &ids[..threshold]).unwrap()); + } + println!( + "took {} ms for threshold = {threshold}", + timing.elapsed().as_millis() + ); + } +} diff --git a/anchor/signature_collector/Cargo.toml b/anchor/signature_collector/Cargo.toml index 4e7e2613c..5fff325e9 100644 --- a/anchor/signature_collector/Cargo.toml +++ b/anchor/signature_collector/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +bls_lagrange = { workspace = true } dashmap = { workspace = true } processor = { workspace = true } slot_clock = { workspace = true } diff --git a/anchor/signature_collector/src/lib.rs b/anchor/signature_collector/src/lib.rs index 6fbc2fd44..f98d38fef 100644 --- a/anchor/signature_collector/src/lib.rs +++ b/anchor/signature_collector/src/lib.rs @@ -1,3 +1,4 @@ +use bls_lagrange::KeyId; use dashmap::DashMap; use processor::{DropOnFinish, Senders, WorkItem}; use slot_clock::SlotClock; @@ -177,6 +178,8 @@ pub enum CollectionError { QueueClosedError, QueueFullError, CollectionTimeout, + EmptySignature, + RecoverError(bls_lagrange::Error), } impl From> for CollectionError { @@ -194,6 +197,12 @@ impl From for CollectionError { } } +impl From for CollectionError { + fn from(err: bls_lagrange::Error) -> Self { + CollectionError::RecoverError(err) + } +} + async fn signature_collector( mut rx: mpsc::UnboundedReceiver, request: SignatureRequest, @@ -223,10 +232,10 @@ async fn signature_collector( // always insert to make sure we're not duplicated match signature_share.entry(operator_id) { hash_map::Entry::Vacant(entry) => { - entry.insert(signature); + entry.insert(*signature); } hash_map::Entry::Occupied(entry) => { - if entry.get() != &signature { + if entry.get() != &*signature { error!( ?operator_id, "received conflicting signatures from operator" @@ -238,13 +247,13 @@ async fn signature_collector( if signature_share.len() as u64 >= request.threshold { // TODO move to blocking threadpool? - // TODO do magic crypto to actually restore signature, for now hackily take first one - let signature = mem::take(&mut signature_share) - .into_iter() - .next() - .unwrap() - .1 - .into(); + let signature = match combine_signatures(mem::take(&mut signature_share)) { + Ok(signature) => Arc::new(signature), + Err(err) => { + error!(?err, "Failed to recover signature"); + return; + } + }; for notifier in mem::take(&mut notifiers) { let _ = notifier.send(Arc::clone(&signature)); @@ -255,3 +264,16 @@ async fn signature_collector( } } } + +fn combine_signatures( + shares: HashMap, +) -> Result { + let (ids, signatures): (Vec<_>, Vec<_>) = shares + .into_iter() + .map(|(k, s)| KeyId::try_from(*k).map(|k| (k, s))) + .collect::, _>>()? + .into_iter() + .unzip(); + + Ok(bls_lagrange::combine_signatures(&signatures, &ids)?) +}