Skip to content
Closed
255 changes: 224 additions & 31 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ members = [
"big-mod-exp",
"bincode",
"blake3-hasher",
"bls",
"bn254",
"borsh",
"client-traits",
Expand Down Expand Up @@ -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 }
Expand All @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -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"
Expand Down
43 changes: 43 additions & 0 deletions bls/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions bls/src/error.rs
Original file line number Diff line number Diff line change
@@ -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
}
232 changes: 232 additions & 0 deletions bls/src/keypair.rs
Original file line number Diff line number Diff line change
@@ -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<Self, BlsError> {
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<Self, BlsError> {
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<Item = &'a PubkeyProjective>,
{
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<PubkeyProjective, BlsError>
where
I: IntoIterator<Item = &'a PubkeyProjective>,
{
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<PubkeyProjective> for Pubkey {
fn from(proof: PubkeyProjective) -> Self {
Self(proof.0.to_uncompressed())
}
}

impl TryFrom<Pubkey> for PubkeyProjective {
type Error = BlsError;

fn try_from(proof: Pubkey) -> Result<Self, Self::Error> {
(&proof).try_into()
}
}

impl TryFrom<&Pubkey> for PubkeyProjective {
type Error = BlsError;

fn try_from(proof: &Pubkey) -> Result<Self, Self::Error> {
let maybe_uncompressed: Option<G1Affine> = 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<Self, BlsError> {
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<Self, BlsError> {
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);
}
}
Loading