diff --git a/Cargo.lock b/Cargo.lock index 356d411cb..f3cf180e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.5.5" @@ -351,6 +363,34 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "subtle", +] + [[package]] name = "borsh" version = "0.10.4" @@ -465,6 +505,12 @@ dependencies = [ "serde", ] +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytemuck" version = "1.21.0" @@ -985,6 +1031,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1055,6 +1112,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1174,6 +1237,25 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand 0.8.5", + "rand_core 0.6.4", + "rand_xorshift", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -1239,6 +1321,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1557,7 +1645,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1774,6 +1862,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -1876,6 +1974,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -2046,6 +2153,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -2117,6 +2230,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -2656,7 +2778,27 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-hash", - "solana-sanitize", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-bls" +version = "0.1.0" +dependencies = [ + "bincode", + "blst", + "blstrs", + "bytemuck", + "ff", + "group", + "rand 0.8.5", + "serde", + "serde_with", + "solana-keypair", + "solana-signature", + "solana-signer 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle", + "thiserror 2.0.11", ] [[package]] @@ -2698,10 +2840,10 @@ dependencies = [ "solana-message", "solana-pubkey", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", "solana-system-interface", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 2.2.1", ] [[package]] @@ -2804,7 +2946,7 @@ dependencies = [ "solana-precompile-error", "solana-sdk", "solana-sdk-ids", - "solana-signer", + "solana-signer 2.2.1", ] [[package]] @@ -3009,7 +3151,7 @@ dependencies = [ "solana-sdk-ids", "solana-sha256-hasher", "solana-shred-version", - "solana-signer", + "solana-signer 2.2.1", "solana-time-utils", ] @@ -3038,7 +3180,7 @@ dependencies = [ "solana-atomic-u64", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-sanitize", + "solana-sanitize 2.2.1", "wasm-bindgen", ] @@ -3081,7 +3223,7 @@ dependencies = [ "solana-instruction", "solana-program-error", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-serialize-utils", "solana-sysvar-id", @@ -3099,7 +3241,7 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-hash", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -3116,7 +3258,7 @@ dependencies = [ "solana-seed-derivable", "solana-seed-phrase", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", "static_assertions", "tiny-bip39", "wasm-bindgen", @@ -3214,13 +3356,13 @@ dependencies = [ "solana-nonce", "solana-program", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sha256-hasher", "solana-short-vec", "solana-system-interface", "solana-sysvar", - "solana-transaction-error", + "solana-transaction-error 2.2.1", "static_assertions", "wasm-bindgen", ] @@ -3272,10 +3414,10 @@ dependencies = [ "solana-offchain-message", "solana-packet", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sha256-hasher", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", "static_assertions", ] @@ -3355,7 +3497,7 @@ dependencies = [ "solana-keypair", "solana-pubkey", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", ] [[package]] @@ -3424,7 +3566,7 @@ dependencies = [ "solana-program-pack", "solana-pubkey", "solana-rent", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-secp256k1-recover", @@ -3513,7 +3655,7 @@ dependencies = [ "solana-frozen-abi-macro", "solana-program", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sha256-hasher", "strum", "strum_macros", @@ -3597,6 +3739,12 @@ dependencies = [ name = "solana-sanitize" version = "2.2.1" +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + [[package]] name = "solana-sdk" version = "2.2.2" @@ -3650,7 +3798,7 @@ dependencies = [ "solana-rent-debits", "solana-reserved-account-keys", "solana-reward-info", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk", "solana-sdk-ids", "solana-sdk-macro", @@ -3664,12 +3812,12 @@ dependencies = [ "solana-short-vec", "solana-shred-version", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", "solana-system-transaction", "solana-time-utils", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 2.2.1", "solana-validator-exit", "thiserror 2.0.11", "wasm-bindgen", @@ -3720,7 +3868,7 @@ dependencies = [ "solana-sdk-ids", "solana-secp256k1-program", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", ] [[package]] @@ -3797,7 +3945,7 @@ dependencies = [ "serde", "solana-instruction", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -3847,7 +3995,7 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-short-vec", "solana-signature", ] @@ -3858,7 +4006,18 @@ version = "2.2.1" dependencies = [ "solana-pubkey", "solana-signature", - "solana-transaction-error", + "solana-transaction-error 2.2.1", +] + +[[package]] +name = "solana-signer" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" +dependencies = [ + "solana-pubkey", + "solana-signature", + "solana-transaction-error 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3940,7 +4099,7 @@ dependencies = [ "solana-keypair", "solana-message", "solana-pubkey", - "solana-signer", + "solana-signer 2.2.1", "solana-system-interface", "solana-transaction", ] @@ -3977,7 +4136,7 @@ dependencies = [ "solana-program-memory", "solana-pubkey", "solana-rent", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk", "solana-sdk-ids", "solana-sdk-macro", @@ -4026,16 +4185,16 @@ dependencies = [ "solana-presigner", "solana-program", "solana-pubkey", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk", "solana-sdk-ids", "solana-sha256-hasher", "solana-short-vec", "solana-signature", - "solana-signer", + "solana-signer 2.2.1", "solana-system-interface", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 2.2.1", "static_assertions", "wasm-bindgen", ] @@ -4065,7 +4224,17 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-instruction", - "solana-sanitize", + "solana-sanitize 2.2.1", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "solana-instruction", + "solana-sanitize 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4205,6 +4374,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.4.1" @@ -4287,6 +4462,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tiny-bip39" version = "0.8.2" @@ -4681,7 +4865,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4869,6 +5053,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 521cc5da7..b2cd0aa07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "big-mod-exp", "bincode", "blake3-hasher", + "bls", "bn254", "borsh", "client-traits", @@ -150,6 +151,8 @@ base64 = "0.22.1" bincode = "1.3.3" bitflags = { version = "2.8.0" } blake3 = "1.5.5" +blst = "0.3.14" +blstrs = "0.7.1" borsh = { version = "1.5.5", features = ["derive", "unstable__schema"] } borsh0-10 = { package = "borsh", version = "0.10.3" } bs58 = { version = "0.5.1", default-features = false } @@ -169,9 +172,11 @@ digest = "0.10.7" ed25519-dalek = "=1.0.1" ed25519-dalek-bip32 = "0.2.0" env_logger = "0.9.3" +ff = "0.13.1" five8 = "0.2.1" five8_const = "0.1.3" getrandom = "0.2.10" +group = "0.13.0" hex = "0.4.3" hmac = "0.12.1" im = "15.1.0" @@ -222,6 +227,7 @@ solana-bincode = { path = "bincode", version = "2.2.1" } solana-blake3-hasher = { path = "blake3-hasher", version = "2.2.1" } solana-bn254 = { path = "bn254", version = "2.2.2" } solana-borsh = { path = "borsh", version = "2.2.1" } +solana-bls = { path = "bls", version = "0.1.0" } solana-client-traits = { path = "client-traits", version = "2.2.1" } solana-clock = { path = "clock", version = "2.2.1" } solana-cluster-type = { path = "cluster-type", version = "2.2.1" } @@ -318,6 +324,7 @@ solana-vote-interface = { path = "vote-interface", version = "2.2.1" } static_assertions = "1.1.0" strum = "0.24" strum_macros = "0.24" +subtle = "2.6.1" syn = "2.0.87" test-case = "3.3.1" thiserror = "2.0.11" diff --git a/bls/Cargo.toml b/bls/Cargo.toml new file mode 100644 index 000000000..186a83a0a --- /dev/null +++ b/bls/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "solana-bls" +description = "Solana BLS" +documentation = "https://docs.rs/solana-bls" +version = "0.1.0" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +bytemuck = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true, features = ["macros"] } + +[target.'cfg(not(target_os = "solana"))'.dependencies] +blst = { workspace = true } +blstrs = { workspace = true } +ff = { workspace = true } +group = { workspace = true } +rand = { workspace = true } +solana-signature = { version = "2.2.1", optional = true } +solana-signer = { version = "2.2.1", optional = true } +subtle = { workspace = true, optional = true } +thiserror = { workspace = true } + +[dev-dependencies] +bincode = { workspace = true } +solana-keypair = { workspace = true } + +[features] +solana-signer-derive = [ + "dep:solana-signer", + "dep:solana-signature", + "dep:subtle", +] + +[lib] +crate-type = ["rlib"] + +[lints] +workspace = true diff --git a/bls/src/error.rs b/bls/src/error.rs new file mode 100644 index 000000000..fca55b6df --- /dev/null +++ b/bls/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +pub enum BlsError { + #[error("Field decode failed")] + FieldDecode, + #[error("Empty aggregation attempted")] + EmptyAggregation, + #[error("Key derivation failed")] + KeyDerivation, + #[error("Point representation conversion failed")] + PointConversion, // TODO: could be more specific here +} diff --git a/bls/src/keypair.rs b/bls/src/keypair.rs new file mode 100644 index 000000000..30edb0092 --- /dev/null +++ b/bls/src/keypair.rs @@ -0,0 +1,232 @@ +use { + crate::{ + error::BlsError, pod::Pubkey, proof_of_possession::ProofOfPossessionProjective, + signature::SignatureProjective, Bls, + }, + blst::{blst_keygen, blst_scalar}, + blstrs::{G1Affine, G1Projective, Scalar}, + ff::Field, + group::Group, + rand::rngs::OsRng, + std::ptr, +}; +#[cfg(feature = "solana-signer-derive")] +use {solana_signature::Signature, solana_signer::Signer, subtle::ConstantTimeEq}; + +/// Size of BLS secret key in bytes +pub const BLS_SECRET_KEY_SIZE: usize = 32; + +/// A BLS secret key +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SecretKey(pub(crate) Scalar); + +impl SecretKey { + /// Constructs a new, random `BlsSecretKey` using `OsRng` + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut rng = OsRng; + Self(Scalar::random(&mut rng)) + } + + /// Derive a `BlsSecretKey` from a seed (input key material) + pub fn derive(ikm: &[u8]) -> Result { + let mut scalar = blst_scalar::default(); + unsafe { + blst_keygen( + &mut scalar as *mut blst_scalar, + ikm.as_ptr(), + ikm.len(), + ptr::null(), + 0, + ); + } + scalar + .try_into() + .map(Self) + .map_err(|_| BlsError::FieldDecode) + } + + /// Derive a `BlsSecretKey` from a Solana signer + #[cfg(feature = "solana-signer-derive")] + pub fn derive_from_signer(signer: &dyn Signer, public_seed: &[u8]) -> Result { + let message = [b"bls-key-derive-", public_seed].concat(); + let signature = signer + .try_sign_message(&message) + .map_err(|_| BlsError::KeyDerivation)?; + + // Some `Signer` implementations return the default signature, which is not suitable for + // use as key material + if bool::from(signature.as_ref().ct_eq(Signature::default().as_ref())) { + return Err(BlsError::KeyDerivation); + } + + Self::derive(signature.as_ref()) + } + + /// Generate a proof of possession for the corresponding pubkey + pub fn proof_of_possession(&self) -> ProofOfPossessionProjective { + let pubkey = PubkeyProjective::from_secret(self); + Bls::generate_proof_of_possession(self, &pubkey) + } + + /// Sign a message using the provided secret key + pub fn sign(&self, message: &[u8]) -> SignatureProjective { + Bls::sign(self, message) + } +} + +/// A BLS public key in a projective point representation +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PubkeyProjective(pub(crate) G1Projective); + +impl Default for PubkeyProjective { + fn default() -> Self { + Self(G1Projective::identity()) + } +} + +impl PubkeyProjective { + /// Construct a corresponding `BlsPubkey` for a `BlsSecretKey` + #[allow(clippy::arithmetic_side_effects)] + pub fn from_secret(secret: &SecretKey) -> Self { + Self(G1Projective::generator() * secret.0) + } + + /// Verify a signature against a message and a public key + pub fn verify(&self, signature: &SignatureProjective, message: &[u8]) -> bool { + Bls::verify(self, signature, message) + } + + /// Verify a proof of possession against a public key + pub fn verify_proof_of_possession(&self, proof: &ProofOfPossessionProjective) -> bool { + Bls::verify_proof_of_possession(self, proof) + } + + /// Aggregate a list of public keys into an existing aggregate + #[allow(clippy::arithmetic_side_effects)] + pub fn aggregate_with<'a, I>(&mut self, pubkeys: I) + where + I: IntoIterator, + { + self.0 = pubkeys.into_iter().fold(self.0, |mut acc, pubkey| { + acc += &pubkey.0; + acc + }); + } + + /// Aggregate a list of public keys + #[allow(clippy::arithmetic_side_effects)] + pub fn aggregate<'a, I>(pubkeys: I) -> Result + where + I: IntoIterator, + { + let mut iter = pubkeys.into_iter(); + if let Some(acc) = iter.next() { + let aggregate_point = iter.fold(acc.0, |mut acc, pubkey| { + acc += &pubkey.0; + acc + }); + Ok(Self(aggregate_point)) + } else { + Err(BlsError::EmptyAggregation) + } + } +} + +impl From for Pubkey { + fn from(proof: PubkeyProjective) -> Self { + Self(proof.0.to_uncompressed()) + } +} + +impl TryFrom for PubkeyProjective { + type Error = BlsError; + + fn try_from(proof: Pubkey) -> Result { + (&proof).try_into() + } +} + +impl TryFrom<&Pubkey> for PubkeyProjective { + type Error = BlsError; + + fn try_from(proof: &Pubkey) -> Result { + let maybe_uncompressed: Option = G1Affine::from_uncompressed(&proof.0).into(); + let uncompressed = maybe_uncompressed.ok_or(BlsError::PointConversion)?; + Ok(Self(uncompressed.into())) + } +} + +/// A BLS keypair +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Keypair { + pub secret: SecretKey, + pub public: PubkeyProjective, +} + +impl Keypair { + /// Constructs a new, random `Keypair` using `OsRng` + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let secret = SecretKey::new(); + let public = PubkeyProjective::from_secret(&secret); + Self { secret, public } + } + + /// Derive a `Keypair` from a seed (input key material) + pub fn derive(ikm: &[u8]) -> Result { + let secret = SecretKey::derive(ikm)?; + let public = PubkeyProjective::from_secret(&secret); + Ok(Self { secret, public }) + } + + /// Derive a `BlsSecretKey` from a Solana signer + #[cfg(feature = "solana-signer-derive")] + pub fn derive_from_signer(signer: &dyn Signer, public_seed: &[u8]) -> Result { + let secret = SecretKey::derive_from_signer(signer, public_seed)?; + let public = PubkeyProjective::from_secret(&secret); + Ok(Self { secret, public }) + } + + /// Generate a proof of possession for the given keypair + pub fn proof_of_possession(&self) -> ProofOfPossessionProjective { + Bls::generate_proof_of_possession(&self.secret, &self.public) + } + + /// Sign a message using the provided secret key + pub fn sign(&self, message: &[u8]) -> SignatureProjective { + Bls::sign(&self.secret, message) + } + + /// Verify a signature against a message and a public key + pub fn verify(&self, signature: &SignatureProjective, message: &[u8]) -> bool { + Bls::verify(&self.public, signature, message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keygen_derive() { + let ikm = b"test_ikm"; + let secret = SecretKey::derive(ikm).unwrap(); + let public = PubkeyProjective::from_secret(&secret); + let keypair = Keypair::derive(ikm).unwrap(); + assert_eq!(keypair.secret, secret); + assert_eq!(keypair.public, public); + } + + #[test] + #[cfg(feature = "solana-signer-derive")] + fn test_keygen_derive_from_signer() { + let solana_keypair = solana_keypair::Keypair::new(); + let secret = SecretKey::derive_from_signer(&solana_keypair, b"alpenglow-vote").unwrap(); + let public = PubkeyProjective::from_secret(&secret); + let keypair = Keypair::derive_from_signer(&solana_keypair, b"alpenglow-vote").unwrap(); + + assert_eq!(keypair.secret, secret); + assert_eq!(keypair.public, public); + } +} diff --git a/bls/src/lib.rs b/bls/src/lib.rs new file mode 100644 index 000000000..1aa290d60 --- /dev/null +++ b/bls/src/lib.rs @@ -0,0 +1,199 @@ +pub use crate::pod::{ + ProofOfPossession, ProofOfPossessionCompressed, Pubkey, PubkeyCompressed, Signature, + SignatureCompressed, BLS_PROOF_OF_POSSESSION_AFFINE_SIZE, + BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE, BLS_PUBLIC_KEY_AFFINE_SIZE, + BLS_PUBLIC_KEY_COMPRESSED_SIZE, BLS_SIGNATURE_AFFINE_SIZE, BLS_SIGNATURE_COMPRESSED_SIZE, +}; +#[cfg(not(target_os = "solana"))] +pub use crate::{ + error::BlsError, + keypair::{PubkeyProjective, SecretKey, BLS_SECRET_KEY_SIZE}, + proof_of_possession::ProofOfPossessionProjective, + signature::SignatureProjective, +}; +#[cfg(not(target_os = "solana"))] +use { + blstrs::{pairing, G1Affine, G2Projective}, + group::prime::PrimeCurveAffine, +}; + +// TODO: add conversion between compressed and uncompressed representation of +// signatures, pubkeys, and proof of possessions + +#[cfg(not(target_os = "solana"))] +pub mod error; +#[cfg(not(target_os = "solana"))] +pub mod keypair; +pub mod pod; +#[cfg(not(target_os = "solana"))] +pub mod proof_of_possession; +#[cfg(not(target_os = "solana"))] +pub mod signature; + +/// Domain separation tag used for hashing messages to curve points to prevent +/// potential conflicts between different BLS implementations. This is defined +/// as the ciphersuite ID string as recommended in the standard +/// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-4.2.1. +pub const HASH_TO_POINT_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + +/// Domain separation tag used when hashing public keys to G2 in the proof of +/// possession signing and verification functions. See +/// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05#section-4.2.3. +pub const POP_DST: &[u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_"; + +#[cfg(not(target_os = "solana"))] +pub struct Bls; +#[cfg(not(target_os = "solana"))] +impl Bls { + /// Sign a message using the provided secret key + #[allow(clippy::arithmetic_side_effects)] + pub(crate) fn sign(secret_key: &SecretKey, message: &[u8]) -> SignatureProjective { + let hashed_message = Bls::hash_message_to_point(message); + SignatureProjective(hashed_message * secret_key.0) + } + + /// Verify a signature against a message and a public key + /// + /// TODO: Verify by invoking pairing just once + pub(crate) fn verify( + public_key: &PubkeyProjective, + signature: &SignatureProjective, + message: &[u8], + ) -> bool { + let hashed_message = Bls::hash_message_to_point(message); + pairing(&public_key.0.into(), &hashed_message.into()) + == pairing(&G1Affine::generator(), &signature.0.into()) + } + + /// Generate a proof of possession for the given keypair + #[allow(clippy::arithmetic_side_effects)] + pub(crate) fn generate_proof_of_possession( + secret_key: &SecretKey, + public_key: &PubkeyProjective, + ) -> ProofOfPossessionProjective { + let hashed_pubkey_bytes = Self::hash_pubkey_to_g2(public_key); + ProofOfPossessionProjective(hashed_pubkey_bytes * secret_key.0) + } + + /// Verify a proof of possession against a public key + pub(crate) fn verify_proof_of_possession( + public_key: &PubkeyProjective, + proof: &ProofOfPossessionProjective, + ) -> bool { + let hashed_pubkey_bytes = Self::hash_pubkey_to_g2(public_key); + pairing(&public_key.0.into(), &hashed_pubkey_bytes.into()) + == pairing(&G1Affine::generator(), &proof.0.into()) + } + + /// Verify a list of signatures against a message and a list of public keys + pub fn aggregate_verify<'a, I, J>( + public_keys: I, + signatures: J, + message: &[u8], + ) -> Result + where + I: IntoIterator, + J: IntoIterator, + { + let aggregate_pubkey = PubkeyProjective::aggregate(public_keys)?; + let aggregate_signature = SignatureProjective::aggregate(signatures)?; + + Ok(Self::verify( + &aggregate_pubkey, + &aggregate_signature, + message, + )) + } + + /// Hash a message to a G2 point + pub fn hash_message_to_point(message: &[u8]) -> G2Projective { + G2Projective::hash_to_curve(message, HASH_TO_POINT_DST, &[]) + } + + /// Hash a pubkey to a G2 point + pub(crate) fn hash_pubkey_to_g2(public_key: &PubkeyProjective) -> G2Projective { + let pubkey_bytes = public_key.0.to_compressed(); + G2Projective::hash_to_curve(&pubkey_bytes, POP_DST, &[]) + } +} + +#[cfg(test)] +mod tests { + use {super::*, crate::keypair::Keypair}; + + #[test] + fn test_verify() { + let keypair = Keypair::new(); + let test_message = b"test message"; + let signature = Bls::sign(&keypair.secret, test_message); + assert!(Bls::verify(&keypair.public, &signature, test_message)); + } + + #[test] + fn test_aggregate_verify() { + let test_message = b"test message"; + + let keypair0 = Keypair::new(); + let signature0 = Bls::sign(&keypair0.secret, test_message); + assert!(Bls::verify(&keypair0.public, &signature0, test_message)); + + let keypair1 = Keypair::new(); + let signature1 = Bls::sign(&keypair1.secret, test_message); + assert!(Bls::verify(&keypair1.public, &signature1, test_message)); + + // basic case + assert!(Bls::aggregate_verify( + vec![&keypair0.public, &keypair1.public], + vec![&signature0, &signature1], + test_message, + ) + .unwrap()); + + // pre-aggregate the signatures + let aggregate_signature = + SignatureProjective::aggregate([&signature0, &signature1]).unwrap(); + assert!(Bls::aggregate_verify( + vec![&keypair0.public, &keypair1.public], + vec![&aggregate_signature], + test_message, + ) + .unwrap()); + + // pre-aggregate the public keys + let aggregate_pubkey = + PubkeyProjective::aggregate([&keypair0.public, &keypair1.public]).unwrap(); + assert!(Bls::aggregate_verify( + vec![&aggregate_pubkey], + vec![&signature0, &signature1], + test_message, + ) + .unwrap()); + + // empty set of public keys or signatures + let err = Bls::aggregate_verify(vec![], vec![&signature0, &signature1], test_message) + .unwrap_err(); + assert_eq!(err, BlsError::EmptyAggregation); + + let err = Bls::aggregate_verify( + vec![&keypair0.public, &keypair1.public], + vec![], + test_message, + ) + .unwrap_err(); + assert_eq!(err, BlsError::EmptyAggregation); + } + + #[test] + fn test_proof_of_possession() { + let keypair = Keypair::new(); + let proof = Bls::generate_proof_of_possession(&keypair.secret, &keypair.public); + assert!(Bls::verify_proof_of_possession(&keypair.public, &proof)); + + let invalid_secret_key = SecretKey::new(); + let invalid_proof = Bls::generate_proof_of_possession(&invalid_secret_key, &keypair.public); + assert!(!Bls::verify_proof_of_possession( + &keypair.public, + &invalid_proof + )); + } +} diff --git a/bls/src/pod.rs b/bls/src/pod.rs new file mode 100644 index 000000000..5ca798d9a --- /dev/null +++ b/bls/src/pod.rs @@ -0,0 +1,226 @@ +use { + bytemuck::{Pod, PodInOption, Zeroable, ZeroableInOption}, + serde::{Deserialize, Serialize}, + serde_with::serde_as, +}; + +/// Size of a BLS signature in a compressed point representation +pub const BLS_SIGNATURE_COMPRESSED_SIZE: usize = 96; + +/// Size of a BLS signature in an affine point representation +pub const BLS_SIGNATURE_AFFINE_SIZE: usize = 192; + +/// A serialized BLS signature in a compressed point representation +#[serde_with::serde_as] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +pub struct SignatureCompressed( + #[serde_as(as = "[_; BLS_SIGNATURE_COMPRESSED_SIZE]")] pub [u8; BLS_SIGNATURE_COMPRESSED_SIZE], +); + +impl Default for SignatureCompressed { + fn default() -> Self { + Self([0; BLS_SIGNATURE_COMPRESSED_SIZE]) + } +} + +/// A serialized BLS signature in an affine point representation +#[serde_with::serde_as] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +pub struct Signature( + #[serde_as(as = "[_; BLS_SIGNATURE_AFFINE_SIZE]")] pub [u8; BLS_SIGNATURE_AFFINE_SIZE], +); + +impl Default for Signature { + fn default() -> Self { + Self([0; BLS_SIGNATURE_AFFINE_SIZE]) + } +} + +/// Size of a BLS proof of possession in a compressed point representation +pub const BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE: usize = 96; + +/// Size of a BLS proof of possession in an affine point representation +pub const BLS_PROOF_OF_POSSESSION_AFFINE_SIZE: usize = 192; + +/// A serialized BLS signature in a compressed point representation +#[serde_as] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +pub struct ProofOfPossessionCompressed( + #[serde_as(as = "[_; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]")] + pub [u8; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE], +); + +impl Default for ProofOfPossessionCompressed { + fn default() -> Self { + Self([0; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]) + } +} + +/// A serialized BLS signature in an affine point representation +#[serde_as] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +pub struct ProofOfPossession( + #[serde_as(as = "[_; BLS_PROOF_OF_POSSESSION_AFFINE_SIZE]")] + pub [u8; BLS_PROOF_OF_POSSESSION_AFFINE_SIZE], +); + +impl Default for ProofOfPossession { + fn default() -> Self { + Self([0; BLS_PROOF_OF_POSSESSION_AFFINE_SIZE]) + } +} + +/// Size of a BLS public key in a compressed point representation +pub const BLS_PUBLIC_KEY_COMPRESSED_SIZE: usize = 48; + +/// Size of a BLS public key in an affine point representation +pub const BLS_PUBLIC_KEY_AFFINE_SIZE: usize = 96; + +/// A serialized BLS public key in a compressed point representation +#[serde_as] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +pub struct PubkeyCompressed( + #[serde_as(as = "[_; BLS_PUBLIC_KEY_COMPRESSED_SIZE]")] pub [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE], +); + +impl Default for PubkeyCompressed { + fn default() -> Self { + Self([0; BLS_PUBLIC_KEY_COMPRESSED_SIZE]) + } +} + +/// A serialized BLS public key in an affine point representation +#[serde_as] +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +pub struct Pubkey( + #[serde_as(as = "[_; BLS_PUBLIC_KEY_AFFINE_SIZE]")] pub [u8; BLS_PUBLIC_KEY_AFFINE_SIZE], +); + +impl Default for Pubkey { + fn default() -> Self { + Self([0; BLS_PUBLIC_KEY_AFFINE_SIZE]) + } +} + +// Byte arrays are both `Pod` and `Zeraoble`, but the traits `bytemuck::Pod` and +// `bytemuck::Zeroable` can only be derived for power-of-two length byte arrays. +// Directly implement these traits for types that are simple wrappers around +// byte arrays. +unsafe impl Zeroable for PubkeyCompressed {} +unsafe impl Pod for PubkeyCompressed {} +unsafe impl ZeroableInOption for PubkeyCompressed {} +unsafe impl PodInOption for PubkeyCompressed {} + +unsafe impl Zeroable for Pubkey {} +unsafe impl Pod for Pubkey {} +unsafe impl ZeroableInOption for Pubkey {} +unsafe impl PodInOption for Pubkey {} + +unsafe impl Zeroable for Signature {} +unsafe impl Pod for Signature {} +unsafe impl ZeroableInOption for Signature {} +unsafe impl PodInOption for Signature {} + +unsafe impl Zeroable for SignatureCompressed {} +unsafe impl Pod for SignatureCompressed {} +unsafe impl ZeroableInOption for SignatureCompressed {} +unsafe impl PodInOption for SignatureCompressed {} + +unsafe impl Zeroable for ProofOfPossessionCompressed {} +unsafe impl Pod for ProofOfPossessionCompressed {} +unsafe impl ZeroableInOption for ProofOfPossessionCompressed {} +unsafe impl PodInOption for ProofOfPossessionCompressed {} + +unsafe impl Zeroable for ProofOfPossession {} +unsafe impl Pod for ProofOfPossession {} +unsafe impl ZeroableInOption for ProofOfPossession {} +unsafe impl PodInOption for ProofOfPossession {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize_and_deserialize_pubkey() { + let original = Pubkey::default(); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: Pubkey = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + + let original = Pubkey([1; BLS_PUBLIC_KEY_AFFINE_SIZE]); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: Pubkey = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_and_deserialize_pubkey_compressed() { + let original = PubkeyCompressed::default(); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: PubkeyCompressed = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + + let original = PubkeyCompressed([1; BLS_PUBLIC_KEY_COMPRESSED_SIZE]); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: PubkeyCompressed = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_and_deserialize_signature() { + let original = Signature::default(); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: Signature = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + + let original = Signature([1; BLS_SIGNATURE_AFFINE_SIZE]); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: Signature = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_and_deserialize_signature_compressed() { + let original = SignatureCompressed::default(); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: SignatureCompressed = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + + let original = SignatureCompressed([1; BLS_SIGNATURE_COMPRESSED_SIZE]); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: SignatureCompressed = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_and_deserialize_proof_of_possession() { + let original = ProofOfPossession::default(); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: ProofOfPossession = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + + let original = ProofOfPossession([1; BLS_PROOF_OF_POSSESSION_AFFINE_SIZE]); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: ProofOfPossession = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_and_deserialize_proof_of_possession_compressed() { + let original = ProofOfPossessionCompressed::default(); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: ProofOfPossessionCompressed = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + + let original = ProofOfPossessionCompressed([1; BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE]); + let serialized = bincode::serialize(&original).unwrap(); + let deserialized: ProofOfPossessionCompressed = bincode::deserialize(&serialized).unwrap(); + assert_eq!(original, deserialized); + } +} diff --git a/bls/src/proof_of_possession.rs b/bls/src/proof_of_possession.rs new file mode 100644 index 000000000..35c34836c --- /dev/null +++ b/bls/src/proof_of_possession.rs @@ -0,0 +1,38 @@ +use { + crate::{error::BlsError, keypair::PubkeyProjective, pod::ProofOfPossession, Bls}, + blstrs::{G2Affine, G2Projective}, +}; + +/// A BLS proof of possession in a projective point representation +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProofOfPossessionProjective(pub(crate) G2Projective); +impl ProofOfPossessionProjective { + /// Verify a proof of possession against a public key + pub fn verify(&self, public_key: &PubkeyProjective) -> bool { + Bls::verify_proof_of_possession(public_key, self) + } +} + +impl From for ProofOfPossession { + fn from(proof: ProofOfPossessionProjective) -> Self { + Self(proof.0.to_uncompressed()) + } +} + +impl TryFrom for ProofOfPossessionProjective { + type Error = BlsError; + + fn try_from(proof: ProofOfPossession) -> Result { + (&proof).try_into() + } +} + +impl TryFrom<&ProofOfPossession> for ProofOfPossessionProjective { + type Error = BlsError; + + fn try_from(proof: &ProofOfPossession) -> Result { + let maybe_uncompressed: Option = G2Affine::from_uncompressed(&proof.0).into(); + let uncompressed = maybe_uncompressed.ok_or(BlsError::PointConversion)?; + Ok(Self(uncompressed.into())) + } +} diff --git a/bls/src/signature.rs b/bls/src/signature.rs new file mode 100644 index 000000000..68c30c36a --- /dev/null +++ b/bls/src/signature.rs @@ -0,0 +1,102 @@ +use { + crate::{error::BlsError, keypair::PubkeyProjective, pod::Signature, Bls}, + blstrs::{G2Affine, G2Projective}, + group::Group, +}; + +/// A BLS signature in a projective point representation +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SignatureProjective(pub(crate) G2Projective); + +impl Default for SignatureProjective { + fn default() -> Self { + Self(G2Projective::identity()) + } +} + +impl SignatureProjective { + /// Verify a signature against a message and a public key + pub fn verify(&self, pubkey: &PubkeyProjective, message: &[u8]) -> bool { + Bls::verify(pubkey, self, message) + } + + /// Aggregate a list of signatures into an existing aggregate + #[allow(clippy::arithmetic_side_effects)] + pub fn aggregate_with<'a, I>(&mut self, signatures: I) + where + I: IntoIterator, + { + self.0 = signatures.into_iter().fold(self.0, |mut acc, signature| { + acc += &signature.0; + acc + }); + } + + /// Aggregate a list of public keys + #[allow(clippy::arithmetic_side_effects)] + pub fn aggregate<'a, I>(signatures: I) -> Result + where + I: IntoIterator, + { + let mut iter = signatures.into_iter(); + if let Some(acc) = iter.next() { + let aggregate_point = iter.fold(acc.0, |mut acc, signature| { + acc += &signature.0; + acc + }); + Ok(Self(aggregate_point)) + } else { + Err(BlsError::EmptyAggregation) + } + } +} + +impl From for Signature { + fn from(proof: SignatureProjective) -> Self { + Self(proof.0.to_uncompressed()) + } +} + +impl TryFrom for SignatureProjective { + type Error = BlsError; + + fn try_from(proof: Signature) -> Result { + let maybe_uncompressed: Option = G2Affine::from_uncompressed(&proof.0).into(); + let uncompressed = maybe_uncompressed.ok_or(BlsError::PointConversion)?; + Ok(Self(uncompressed.into())) + } +} + +impl TryFrom<&Signature> for SignatureProjective { + type Error = BlsError; + + fn try_from(proof: &Signature) -> Result { + let maybe_uncompressed: Option = G2Affine::from_uncompressed(&proof.0).into(); + let uncompressed = maybe_uncompressed.ok_or(BlsError::PointConversion)?; + Ok(Self(uncompressed.into())) + } +} + +#[cfg(test)] +mod tests { + use {super::*, crate::keypair::Keypair}; + + #[test] + fn test_signature_aggregate() { + let test_message = b"test message"; + let keypair0 = Keypair::new(); + let signature0 = keypair0.sign(test_message); + + let test_message = b"test message"; + let keypair1 = Keypair::new(); + let signature1 = keypair1.sign(test_message); + + let aggregate_signature = + SignatureProjective::aggregate([&signature0, &signature1]).unwrap(); + + let mut aggregate_signature_with = signature0; + aggregate_signature_with.aggregate_with([&signature1]); + + assert_eq!(aggregate_signature, aggregate_signature_with); + } +}