#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use sha2::{Digest, Sha512};
pub mod curve;
mod error;
mod lagrange;
pub mod liveness;
pub mod reshare;
mod types;
pub use curve::{OsstCurve, OsstPoint, OsstScalar};
pub use error::OsstError;
pub use lagrange::compute_lagrange_coefficients;
pub use types::*;
#[cfg(feature = "ristretto255")]
pub use curve::ristretto::Ristretto255;
#[cfg(feature = "pallas")]
pub use curve::pallas::PallasCurve;
#[cfg(feature = "secp256k1")]
pub use curve::secp256k1::Secp256k1Curve;
#[cfg(feature = "decaf377")]
pub use curve::decaf377::Decaf377Curve;
pub fn hash_to_challenge<S: OsstScalar, P: OsstPoint<Scalar = S>>(
commitment: &P,
payload: &[u8],
) -> S {
let mut hasher = Sha512::new();
hasher.update(commitment.compress());
hasher.update(payload);
let hash: [u8; 64] = hasher.finalize().into();
S::from_bytes_wide(&hash)
}
#[derive(Clone, Debug)]
pub struct SecretShare<S: OsstScalar> {
pub index: u32,
pub scalar: S,
}
impl<S: OsstScalar> SecretShare<S> {
pub fn new(index: u32, scalar: S) -> Self {
assert!(index > 0, "index must be 1-indexed");
Self { index, scalar }
}
pub fn contribute<P: OsstPoint<Scalar = S>, R: rand_core::RngCore + rand_core::CryptoRng>(
&self,
rng: &mut R,
payload: &[u8],
) -> Contribution<P> {
let r = S::random(rng);
let commitment = P::generator().mul_scalar(&r);
let challenge: S = hash_to_challenge(&commitment, payload);
let response = r.add(&challenge.mul(&self.scalar));
Contribution {
index: self.index,
commitment,
response,
}
}
pub fn public_share<P: OsstPoint<Scalar = S>>(&self) -> P {
P::generator().mul_scalar(&self.scalar)
}
}
#[derive(Clone, Debug)]
pub struct Contribution<P: OsstPoint> {
pub index: u32,
pub commitment: P,
pub response: P::Scalar,
}
impl<P: OsstPoint> Contribution<P> {
pub fn new(index: u32, commitment: P, response: P::Scalar) -> Self {
Self {
index,
commitment,
response,
}
}
pub fn to_bytes(&self) -> [u8; 68] {
let mut buf = [0u8; 68];
buf[0..4].copy_from_slice(&self.index.to_le_bytes());
buf[4..36].copy_from_slice(&self.commitment.compress());
buf[36..68].copy_from_slice(&self.response.to_bytes());
buf
}
pub fn from_bytes(bytes: &[u8; 68]) -> Result<Self, OsstError> {
let index = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
let point_bytes: [u8; 32] = bytes[4..36].try_into().unwrap();
let commitment = P::decompress(&point_bytes).ok_or(OsstError::InvalidCommitment)?;
let response_bytes: [u8; 32] = bytes[36..68].try_into().unwrap();
let response =
P::Scalar::from_canonical_bytes(&response_bytes).ok_or(OsstError::InvalidResponse)?;
Ok(Self {
index,
commitment,
response,
})
}
}
pub fn compute_weights<P: OsstPoint>(
contributions: &[Contribution<P>],
payload: &[u8],
) -> Result<(P::Scalar, Vec<P::Scalar>), OsstError> {
if contributions.is_empty() {
return Err(OsstError::EmptyContributions);
}
let k = contributions.len();
let challenges: Vec<P::Scalar> = contributions
.iter()
.map(|c| hash_to_challenge(&c.commitment, payload))
.collect();
for c in &challenges {
if c == &P::Scalar::zero() {
return Err(OsstError::ZeroChallenge);
}
}
let normalizer: P::Scalar = challenges
.iter()
.fold(P::Scalar::one(), |acc, c| acc.mul(c));
let indices: Vec<u32> = contributions.iter().map(|c| c.index).collect();
let lagrange = compute_lagrange_coefficients::<P::Scalar>(&indices)?;
let mut weights: Vec<P::Scalar> = Vec::with_capacity(k);
for (i, lambda_i) in lagrange.iter().enumerate().take(k) {
let mut weight = lambda_i.clone();
for (j, c_j) in challenges.iter().enumerate() {
if i != j {
weight = weight.mul(c_j);
}
}
weights.push(weight);
}
Ok((normalizer, weights))
}
pub fn verify<P: OsstPoint>(
group_pubkey: &P,
contributions: &[Contribution<P>],
threshold: u32,
payload: &[u8],
) -> Result<bool, OsstError> {
if contributions.len() < threshold as usize {
return Err(OsstError::InsufficientContributions {
got: contributions.len(),
need: threshold as usize,
});
}
let mut indices: Vec<u32> = contributions.iter().map(|c| c.index).collect();
indices.sort();
for i in 1..indices.len() {
if indices[i] == indices[i - 1] {
return Err(OsstError::DuplicateIndex(indices[i]));
}
}
let (normalizer, weights) = compute_weights(contributions, payload)?;
let mut lhs_exponent = P::Scalar::zero();
for (c, μ) in contributions.iter().zip(weights.iter()) {
let term = μ.mul(&c.response);
lhs_exponent = lhs_exponent.add(&term);
}
let lhs = P::generator().mul_scalar(&lhs_exponent);
let mut scalars = vec![normalizer];
let mut points = vec![group_pubkey.clone()];
for (c, μ) in contributions.iter().zip(weights.iter()) {
scalars.push(μ.clone());
points.push(c.commitment.clone());
}
let rhs = P::multiscalar_mul(&scalars, &points);
Ok(lhs == rhs)
}
pub fn verify_incremental<P: OsstPoint>(
group_pubkey: &P,
existing: &[Contribution<P>],
new_contribution: &Contribution<P>,
threshold: u32,
payload: &[u8],
) -> Result<bool, OsstError> {
for c in existing {
if c.index == new_contribution.index {
return Err(OsstError::DuplicateIndex(new_contribution.index));
}
}
let mut all: Vec<Contribution<P>> = existing.to_vec();
all.push(new_contribution.clone());
verify(group_pubkey, &all, threshold, payload)
}
#[cfg(feature = "ristretto255")]
pub mod ristretto255 {
use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar};
pub type SecretShare = super::SecretShare<Scalar>;
pub type Contribution = super::Contribution<RistrettoPoint>;
pub const G: RistrettoPoint = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
}
#[cfg(feature = "ristretto255")]
pub use ristretto255::{
Contribution as RistrettoContribution, SecretShare as RistrettoSecretShare,
};
#[cfg(feature = "pallas")]
pub mod pallas {
use pasta_curves::pallas::{Point, Scalar};
pub type SecretShare = super::SecretShare<Scalar>;
pub type Contribution = super::Contribution<Point>;
}
#[cfg(feature = "pallas")]
pub use pallas::{Contribution as PallasContribution, SecretShare as PallasSecretShare};
#[cfg(all(test, feature = "pallas"))]
mod pallas_tests {
use super::*;
use pasta_curves::group::ff::Field;
use pasta_curves::pallas::{Point, Scalar};
use rand::rngs::OsRng;
use crate::curve::OsstPoint;
fn shamir_split(secret: &Scalar, n: u32, t: u32) -> Vec<SecretShare<Scalar>> {
assert!(t <= n);
assert!(t > 0);
let mut rng = OsRng;
let mut coeffs = vec![*secret];
for _ in 1..t {
coeffs.push(<Scalar as Field>::random(&mut rng));
}
(1..=n)
.map(|i| {
let x = Scalar::from(i as u64);
let mut y = Scalar::ZERO;
let mut x_pow = Scalar::ONE;
for coeff in &coeffs {
y += coeff * x_pow;
x_pow *= x;
}
SecretShare::new(i, y)
})
.collect()
}
#[test]
fn test_pallas_basic_osst() {
let mut rng = OsRng;
let secret = <Scalar as Field>::random(&mut rng);
let group_pubkey: Point = Point::generator().mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"test pallas osst verification";
let contributions: Vec<Contribution<Point>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(result.unwrap(), "Pallas OSST verification should succeed");
}
#[test]
fn test_pallas_wrong_payload() {
let mut rng = OsRng;
let secret = <Scalar as Field>::random(&mut rng);
let group_pubkey: Point = Point::generator().mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"correct payload";
let wrong_payload = b"wrong payload";
let contributions: Vec<Contribution<Point>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, wrong_payload);
assert!(result.is_ok());
assert!(
!result.unwrap(),
"Pallas verification with wrong payload should fail"
);
}
#[test]
fn test_pallas_serialization() {
let mut rng = OsRng;
let secret = <Scalar as Field>::random(&mut rng);
let shares = shamir_split(&secret, 3, 2);
let payload = b"pallas serialization test";
let original: Contribution<Point> = shares[0].contribute(&mut rng, payload);
let bytes = original.to_bytes();
let recovered = Contribution::<Point>::from_bytes(&bytes).unwrap();
assert_eq!(original.index, recovered.index);
assert_eq!(original.commitment, recovered.commitment);
assert_eq!(original.response, recovered.response);
}
}
#[cfg(all(test, feature = "ristretto255"))]
mod tests {
use super::*;
use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar};
use rand::rngs::OsRng;
fn shamir_split(secret: &Scalar, n: u32, t: u32) -> Vec<SecretShare<Scalar>> {
assert!(t <= n);
assert!(t > 0);
let mut rng = OsRng;
let mut coeffs = vec![*secret];
for _ in 1..t {
coeffs.push(Scalar::random(&mut rng));
}
(1..=n)
.map(|i| {
let x = Scalar::from(i);
let mut y = Scalar::ZERO;
let mut x_pow = Scalar::ONE;
for coeff in &coeffs {
y += coeff * x_pow;
x_pow *= x;
}
SecretShare::new(i, y)
})
.collect()
}
#[test]
fn test_basic_osst() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"test payload for osst verification";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(result.unwrap(), "OSST verification should succeed");
}
#[test]
fn test_osst_with_more_than_threshold() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let n = 7u32;
let t = 4u32;
let shares = shamir_split(&secret, n, t);
let payload = b"threshold exceeded test";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..5]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(
result.unwrap(),
"verification with >t contributors should work"
);
}
#[test]
fn test_osst_insufficient_threshold() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"insufficient threshold test";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..2]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(matches!(
result,
Err(OsstError::InsufficientContributions { .. })
));
}
#[test]
fn test_osst_wrong_payload() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"correct payload";
let wrong_payload = b"wrong payload";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, wrong_payload);
assert!(result.is_ok());
assert!(
!result.unwrap(),
"verification with wrong payload should fail"
);
}
#[test]
fn test_osst_wrong_pubkey() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let wrong_secret = Scalar::random(&mut rng);
let wrong_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&wrong_secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"test";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&wrong_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(
!result.unwrap(),
"verification with wrong pubkey should fail"
);
}
#[test]
fn test_incremental_verification() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split(&secret, n, t);
let payload = b"incremental test";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
assert!(verify(&group_pubkey, &contributions, t, payload).unwrap());
let new_contrib = shares[t as usize].contribute(&mut rng, payload);
let result = verify_incremental(&group_pubkey, &contributions, &new_contrib, t, payload);
assert!(result.is_ok());
assert!(result.unwrap(), "incremental verification should succeed");
}
#[test]
fn test_contribution_serialization() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let shares = shamir_split(&secret, 3, 2);
let payload = b"serialization test";
let original: Contribution<RistrettoPoint> = shares[0].contribute(&mut rng, payload);
let bytes = original.to_bytes();
let recovered = Contribution::<RistrettoPoint>::from_bytes(&bytes).unwrap();
assert_eq!(original.index, recovered.index);
assert_eq!(original.commitment, recovered.commitment);
assert_eq!(original.response, recovered.response);
}
#[test]
fn test_large_threshold() {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
let group_pubkey: RistrettoPoint = RistrettoPoint::generator().mul_scalar(&secret);
let n = 100u32;
let t = 67u32;
let shares = shamir_split(&secret, n, t);
let payload = b"large threshold test";
let contributions: Vec<Contribution<RistrettoPoint>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(
result.unwrap(),
"large threshold verification should succeed"
);
}
}
#[cfg(all(test, feature = "secp256k1"))]
mod secp256k1_tests {
use super::*;
use k256::ProjectivePoint;
use k256::Scalar;
use rand::rngs::OsRng;
fn shamir_split_secp(secret: &Scalar, n: u32, t: u32) -> Vec<SecretShare<Scalar>> {
let mut rng = OsRng;
let mut coefficients = vec![*secret];
for _ in 1..t {
coefficients.push(<Scalar as OsstScalar>::random(&mut rng));
}
(1..=n)
.map(|i| {
let x = <Scalar as OsstScalar>::from_u32(i);
let mut y = <Scalar as OsstScalar>::zero();
let mut x_pow = <Scalar as OsstScalar>::one();
for coeff in &coefficients {
y = y.add(&coeff.mul(&x_pow));
x_pow = x_pow.mul(&x);
}
SecretShare::new(i, y)
})
.collect()
}
#[test]
fn test_secp256k1_basic_osst() {
let mut rng = OsRng;
let secret = <Scalar as OsstScalar>::random(&mut rng);
let group_pubkey: ProjectivePoint = ProjectivePoint::GENERATOR.mul_scalar(&secret);
let n = 5u32;
let t = 3u32;
let shares = shamir_split_secp(&secret, n, t);
let payload = b"bitcoin custody test";
let contributions: Vec<Contribution<ProjectivePoint>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok(), "secp256k1 verification should not error");
assert!(result.unwrap(), "secp256k1 verification should succeed");
}
#[test]
fn test_secp256k1_point_serialization() {
let mut rng = OsRng;
let scalar = <Scalar as OsstScalar>::random(&mut rng);
let point: ProjectivePoint = ProjectivePoint::GENERATOR.mul_scalar(&scalar);
let compressed = point.compress();
let decompressed = ProjectivePoint::decompress(&compressed);
assert!(decompressed.is_some(), "should decompress 32-byte form");
let full_compressed = point.compress_vec();
assert_eq!(full_compressed.len(), 33, "secp256k1 should be 33 bytes");
let full_decompressed = ProjectivePoint::decompress_slice(&full_compressed);
assert!(
full_decompressed.is_some(),
"should decompress 33-byte form"
);
assert_eq!(point, full_decompressed.unwrap(), "roundtrip should match");
}
#[test]
fn test_secp256k1_threshold_2_of_3() {
let mut rng = OsRng;
let secret = <Scalar as OsstScalar>::random(&mut rng);
let group_pubkey: ProjectivePoint = ProjectivePoint::GENERATOR.mul_scalar(&secret);
let n = 3u32;
let t = 2u32;
let shares = shamir_split_secp(&secret, n, t);
let payload = b"2-of-3 multisig";
let contributions: Vec<Contribution<ProjectivePoint>> = vec![
shares[0].contribute(&mut rng, payload),
shares[2].contribute(&mut rng, payload),
];
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(result.unwrap(), "2-of-3 secp256k1 should verify");
}
}
#[cfg(all(test, feature = "decaf377"))]
mod decaf377_tests {
use super::*;
use ::decaf377::{Element, Fr};
use rand::rngs::OsRng;
fn shamir_split_decaf(secret: &Fr, n: u32, t: u32) -> Vec<SecretShare<Fr>> {
let mut rng = OsRng;
let mut coefficients = vec![*secret];
for _ in 1..t {
coefficients.push(<Fr as OsstScalar>::random(&mut rng));
}
(1..=n)
.map(|i| {
let x = <Fr as OsstScalar>::from_u32(i);
let mut y = <Fr as OsstScalar>::zero();
let mut x_pow = <Fr as OsstScalar>::one();
for coeff in &coefficients {
y = y.add(&coeff.mul(&x_pow));
x_pow = x_pow.mul(&x);
}
SecretShare::new(i, y)
})
.collect()
}
#[test]
fn test_decaf377_basic_osst() {
let mut rng = OsRng;
let secret = <Fr as OsstScalar>::random(&mut rng);
let group_pubkey: Element = Element::GENERATOR * secret;
let n = 5u32;
let t = 3u32;
let shares = shamir_split_decaf(&secret, n, t);
let payload = b"penumbra custody test";
let contributions: Vec<Contribution<Element>> = shares[0..t as usize]
.iter()
.map(|s| s.contribute(&mut rng, payload))
.collect();
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok(), "decaf377 verification should not error");
assert!(result.unwrap(), "decaf377 verification should succeed");
}
#[test]
fn test_decaf377_point_serialization() {
let mut rng = OsRng;
let scalar = <Fr as OsstScalar>::random(&mut rng);
let point: Element = Element::GENERATOR * scalar;
let compressed = point.compress();
let decompressed = Element::decompress(&compressed);
assert!(decompressed.is_some(), "should decompress");
assert_eq!(point, decompressed.unwrap(), "roundtrip should match");
}
#[test]
fn test_decaf377_threshold_2_of_3() {
let mut rng = OsRng;
let secret = <Fr as OsstScalar>::random(&mut rng);
let group_pubkey: Element = Element::GENERATOR * secret;
let n = 3u32;
let t = 2u32;
let shares = shamir_split_decaf(&secret, n, t);
let payload = b"2-of-3 penumbra multisig";
let contributions: Vec<Contribution<Element>> = vec![
shares[0].contribute(&mut rng, payload),
shares[2].contribute(&mut rng, payload),
];
let result = verify(&group_pubkey, &contributions, t, payload);
assert!(result.is_ok());
assert!(result.unwrap(), "2-of-3 decaf377 should verify");
}
}