mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Merge branch 'develop' into next
This resolves the conflicts and gets the workspace `Cargo.toml`s to not be invalid. It doesn't actually get clippy to pass again yet. Does move `crypto/dkg/src/evrf` into a new `crypto/dkg/evrf` crate (which does not yet compile).
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "ciphersuite"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
description = "Ciphersuites built around ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ciphersuite"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["ciphersuite", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.66"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -24,22 +24,12 @@ rand_core = { version = "0.6", default-features = false }
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["derive"] }
|
||||
subtle = { version = "^2.4", default-features = false }
|
||||
|
||||
digest = { version = "0.10", default-features = false }
|
||||
digest = { version = "0.10", default-features = false, features = ["core-api"] }
|
||||
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false }
|
||||
sha2 = { version = "0.10", default-features = false, optional = true }
|
||||
sha3 = { version = "0.10", default-features = false, optional = true }
|
||||
|
||||
ff = { version = "0.13", default-features = false, features = ["bits"] }
|
||||
group = { version = "0.13", default-features = false }
|
||||
|
||||
dalek-ff-group = { path = "../dalek-ff-group", version = "0.4", default-features = false, optional = true }
|
||||
|
||||
elliptic-curve = { version = "0.13", default-features = false, features = ["hash2curve"], optional = true }
|
||||
p256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits", "hash2curve"], optional = true }
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits", "hash2curve"], optional = true }
|
||||
|
||||
minimal-ed448 = { path = "../ed448", version = "0.4", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||
|
||||
@@ -59,27 +49,8 @@ std = [
|
||||
|
||||
"digest/std",
|
||||
"transcript/std",
|
||||
"sha2?/std",
|
||||
"sha3?/std",
|
||||
|
||||
"ff/std",
|
||||
|
||||
"dalek-ff-group?/std",
|
||||
|
||||
"elliptic-curve?/std",
|
||||
"p256?/std",
|
||||
"k256?/std",
|
||||
"minimal-ed448?/std",
|
||||
]
|
||||
|
||||
dalek = ["sha2", "dalek-ff-group"]
|
||||
ed25519 = ["dalek"]
|
||||
ristretto = ["dalek"]
|
||||
|
||||
kp256 = ["sha2", "elliptic-curve"]
|
||||
p256 = ["kp256", "dep:p256"]
|
||||
secp256k1 = ["kp256", "k256"]
|
||||
|
||||
ed448 = ["sha3", "minimal-ed448"]
|
||||
|
||||
default = ["std"]
|
||||
|
||||
@@ -21,6 +21,8 @@ Their `hash_to_F` is the
|
||||
[IETF's hash to curve](https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html),
|
||||
yet applied to their scalar field.
|
||||
|
||||
Please see the [`ciphersuite-kp256`](https://docs.rs/ciphersuite-kp256) crate for more info.
|
||||
|
||||
### Ed25519/Ristretto
|
||||
|
||||
Ed25519/Ristretto are offered via
|
||||
@@ -33,6 +35,8 @@ the draft
|
||||
[RFC-RISTRETTO](https://www.ietf.org/archive/id/draft-irtf-cfrg-ristretto255-decaf448-05.html).
|
||||
The domain-separation tag is naively prefixed to the message.
|
||||
|
||||
Please see the [`dalek-ff-group`](https://docs.rs/dalek-ff-group) crate for more info.
|
||||
|
||||
### Ed448
|
||||
|
||||
Ed448 is offered via [minimal-ed448](https://crates.io/crates/minimal-ed448), an
|
||||
@@ -42,3 +46,5 @@ to its prime-order subgroup.
|
||||
Its `hash_to_F` is the wide reduction of SHAKE256, with a 114-byte output, as
|
||||
used in [RFC-8032](https://www.rfc-editor.org/rfc/rfc8032). The
|
||||
domain-separation tag is naively prefixed to the message.
|
||||
|
||||
Please see the [`minimal-ed448`](https://docs.rs/minimal-ed448) crate for more info.
|
||||
|
||||
55
crypto/ciphersuite/kp256/Cargo.toml
Normal file
55
crypto/ciphersuite/kp256/Cargo.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[package]
|
||||
name = "ciphersuite-kp256"
|
||||
version = "0.4.0"
|
||||
description = "Ciphersuites built around ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ciphersuite/kp256"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["ciphersuite", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["derive"] }
|
||||
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
|
||||
elliptic-curve = { version = "0.13", default-features = false, features = ["hash2curve"] }
|
||||
p256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits", "hash2curve"] }
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits", "hash2curve"] }
|
||||
|
||||
ciphersuite = { path = "../", version = "0.4", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
ff-group-tests = { version = "0.13", path = "../../ff-group-tests" }
|
||||
|
||||
[features]
|
||||
alloc = ["ciphersuite/alloc"]
|
||||
std = [
|
||||
"rand_core/std",
|
||||
|
||||
"zeroize/std",
|
||||
|
||||
"sha2/std",
|
||||
|
||||
"elliptic-curve/std",
|
||||
"p256/std",
|
||||
"k256/std",
|
||||
|
||||
"ciphersuite/std",
|
||||
]
|
||||
|
||||
default = ["std"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023-2024 Luke Parker
|
||||
Copyright (c) 2021-2023 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
3
crypto/ciphersuite/kp256/README.md
Normal file
3
crypto/ciphersuite/kp256/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ciphersuite {k, p}256
|
||||
|
||||
SECP256k1 and P-256 Ciphersuites around k256 and p256.
|
||||
@@ -1,16 +1,17 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use sha2::Sha256;
|
||||
|
||||
use group::ff::PrimeField;
|
||||
|
||||
use elliptic_curve::{
|
||||
generic_array::GenericArray,
|
||||
bigint::{NonZero, CheckedAdd, Encoding, U384, U512},
|
||||
bigint::{NonZero, CheckedAdd, Encoding, U384},
|
||||
hash2curve::{Expander, ExpandMsg, ExpandMsgXmd},
|
||||
};
|
||||
|
||||
use crate::Ciphersuite;
|
||||
use ciphersuite::{group::ff::PrimeField, Ciphersuite};
|
||||
|
||||
macro_rules! kp_curve {
|
||||
(
|
||||
@@ -31,22 +32,6 @@ macro_rules! kp_curve {
|
||||
$lib::ProjectivePoint::GENERATOR
|
||||
}
|
||||
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
let mut modulus = [0; 64];
|
||||
modulus[32 ..].copy_from_slice(&(Self::F::ZERO - Self::F::ONE).to_bytes());
|
||||
let modulus = U512::from_be_slice(&modulus).checked_add(&U512::ONE).unwrap();
|
||||
|
||||
let mut wide =
|
||||
U512::from_be_bytes(scalar).rem(&NonZero::new(modulus).unwrap()).to_be_bytes();
|
||||
|
||||
let mut array = *GenericArray::from_slice(&wide[32 ..]);
|
||||
let res = $lib::Scalar::from_repr(array).unwrap();
|
||||
|
||||
wide.zeroize();
|
||||
array.zeroize();
|
||||
res
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
|
||||
// While one of these two libraries does support directly hashing to the Scalar field, the
|
||||
// other doesn't. While that's probably an oversight, this is a universally working method
|
||||
@@ -123,12 +108,9 @@ fn test_oversize_dst<C: Ciphersuite>() {
|
||||
/// Ciphersuite for Secp256k1.
|
||||
///
|
||||
/// hash_to_F is implemented via the IETF draft for hash to curve's hash_to_field (v16).
|
||||
#[cfg(feature = "secp256k1")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Secp256k1;
|
||||
#[cfg(feature = "secp256k1")]
|
||||
kp_curve!("secp256k1", k256, Secp256k1, b"secp256k1");
|
||||
#[cfg(feature = "secp256k1")]
|
||||
#[test]
|
||||
fn test_secp256k1() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, k256::ProjectivePoint>(&mut rand_core::OsRng);
|
||||
@@ -161,12 +143,9 @@ fn test_secp256k1() {
|
||||
/// Ciphersuite for P-256.
|
||||
///
|
||||
/// hash_to_F is implemented via the IETF draft for hash to curve's hash_to_field (v16).
|
||||
#[cfg(feature = "p256")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct P256;
|
||||
#[cfg(feature = "p256")]
|
||||
kp_curve!("p256", p256, P256, b"P-256");
|
||||
#[cfg(feature = "p256")]
|
||||
#[test]
|
||||
fn test_p256() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, p256::ProjectivePoint>(&mut rand_core::OsRng);
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Ciphersuites for elliptic curves premised on ff/group.
|
||||
|
||||
This library, except for the not recommended Ed448 ciphersuite, was
|
||||
This library was
|
||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
||||
culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
use core::fmt::Debug;
|
||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||
#[allow(unused_imports)]
|
||||
use std_shims::prelude::*;
|
||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||
use std_shims::io::{self, Read};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
@@ -23,25 +26,6 @@ use group::{
|
||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||
use group::GroupEncoding;
|
||||
|
||||
#[cfg(feature = "dalek")]
|
||||
mod dalek;
|
||||
#[cfg(feature = "ristretto")]
|
||||
pub use dalek::Ristretto;
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub use dalek::Ed25519;
|
||||
|
||||
#[cfg(feature = "kp256")]
|
||||
mod kp256;
|
||||
#[cfg(feature = "secp256k1")]
|
||||
pub use kp256::Secp256k1;
|
||||
#[cfg(feature = "p256")]
|
||||
pub use kp256::P256;
|
||||
|
||||
#[cfg(feature = "ed448")]
|
||||
mod ed448;
|
||||
#[cfg(feature = "ed448")]
|
||||
pub use ed448::*;
|
||||
|
||||
/// Unified trait defining a ciphersuite around an elliptic curve.
|
||||
pub trait Ciphersuite:
|
||||
'static + Send + Sync + Clone + Copy + PartialEq + Eq + Debug + Zeroize
|
||||
@@ -62,12 +46,6 @@ pub trait Ciphersuite:
|
||||
// While group does provide this in its API, privacy coins may want to use a custom basepoint
|
||||
fn generator() -> Self::G;
|
||||
|
||||
/// Reduce 512 bits into a uniform scalar.
|
||||
///
|
||||
/// If 512 bits is insufficient to perform a reduction into a uniform scalar, the ciphersuite
|
||||
/// will perform a hash to sample the necessary bits.
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F;
|
||||
|
||||
/// Hash the provided domain-separation tag and message to a scalar. Ciphersuites MAY naively
|
||||
/// prefix the tag to the message, enabling transpotion between the two. Accordingly, this
|
||||
/// function should NOT be used in any scheme where one tag is a valid substring of another
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "dalek-ff-group"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
description = "ff/group bindings around curve25519-dalek"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-group"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.71"
|
||||
rust-version = "1.65"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -25,18 +25,22 @@ subtle = { version = "^2.4", default-features = false }
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
digest = { version = "0.10", default-features = false }
|
||||
sha2 = { version = "0.10", default-features = false }
|
||||
|
||||
ff = { version = "0.13", default-features = false, features = ["bits"] }
|
||||
group = { version = "0.13", default-features = false }
|
||||
ciphersuite = { path = "../ciphersuite", default-features = false }
|
||||
|
||||
crypto-bigint = { version = "0.5", default-features = false, features = ["zeroize"] }
|
||||
|
||||
curve25519-dalek = { version = ">= 4.0, < 4.2", default-features = false, features = ["alloc", "zeroize", "digest", "group", "precomputed-tables"] }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
ff-group-tests = { path = "../ff-group-tests" }
|
||||
|
||||
[features]
|
||||
std = ["zeroize/std", "subtle/std", "rand_core/std", "digest/std"]
|
||||
alloc = ["zeroize/alloc", "ciphersuite/alloc"]
|
||||
std = ["alloc", "zeroize/std", "subtle/std", "rand_core/std", "digest/std", "sha2/std", "ciphersuite/std"]
|
||||
default = ["std"]
|
||||
|
||||
@@ -3,9 +3,9 @@ use zeroize::Zeroize;
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
use group::Group;
|
||||
use dalek_ff_group::Scalar;
|
||||
use crate::Scalar;
|
||||
|
||||
use crate::Ciphersuite;
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
macro_rules! dalek_curve {
|
||||
(
|
||||
@@ -15,7 +15,7 @@ macro_rules! dalek_curve {
|
||||
$Point: ident,
|
||||
$ID: literal
|
||||
) => {
|
||||
use dalek_ff_group::$Point;
|
||||
use crate::$Point;
|
||||
|
||||
impl Ciphersuite for $Ciphersuite {
|
||||
type F = Scalar;
|
||||
@@ -28,12 +28,6 @@ macro_rules! dalek_curve {
|
||||
$Point::generator()
|
||||
}
|
||||
|
||||
fn reduce_512(mut scalar: [u8; 64]) -> Self::F {
|
||||
let res = Scalar::from_bytes_mod_order_wide(&scalar);
|
||||
scalar.zeroize();
|
||||
res
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
Scalar::from_hash(Sha512::new_with_prefix(&[dst, data].concat()))
|
||||
}
|
||||
@@ -46,12 +40,9 @@ macro_rules! dalek_curve {
|
||||
/// hash_to_F is implemented with a naive concatenation of the dst and data, allowing transposition
|
||||
/// between the two. This means `dst: b"abc", data: b"def"`, will produce the same scalar as
|
||||
/// `dst: "abcdef", data: b""`. Please use carefully, not letting dsts be substrings of each other.
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Ristretto;
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
dalek_curve!("ristretto", Ristretto, RistrettoPoint, b"ristretto");
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
#[test]
|
||||
fn test_ristretto() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, RistrettoPoint>(&mut rand_core::OsRng);
|
||||
@@ -77,12 +68,9 @@ fn test_ristretto() {
|
||||
/// hash_to_F is implemented with a naive concatenation of the dst and data, allowing transposition
|
||||
/// between the two. This means `dst: b"abc", data: b"def"`, will produce the same scalar as
|
||||
/// `dst: "abcdef", data: b""`. Please use carefully, not letting dsts be substrings of each other.
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Ed25519;
|
||||
#[cfg(feature = "ed25519")]
|
||||
dalek_curve!("ed25519", Ed25519, EdwardsPoint, b"edwards25519");
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[test]
|
||||
fn test_ed25519() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, EdwardsPoint>(&mut rand_core::OsRng);
|
||||
@@ -17,7 +17,7 @@ use crypto_bigint::{
|
||||
impl_modulus,
|
||||
};
|
||||
|
||||
use group::ff::{Field, PrimeField, FieldBits, PrimeFieldBits};
|
||||
use group::ff::{Field, PrimeField, FieldBits, PrimeFieldBits, FromUniformBytes};
|
||||
|
||||
use crate::{u8_from_bool, constant_time, math_op, math};
|
||||
|
||||
@@ -36,6 +36,7 @@ type ResidueType = Residue<FieldModulus, { FieldModulus::LIMBS }>;
|
||||
|
||||
/// A constant-time implementation of the Ed25519 field.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Zeroize)]
|
||||
#[repr(transparent)]
|
||||
pub struct FieldElement(ResidueType);
|
||||
|
||||
// Square root of -1.
|
||||
@@ -216,10 +217,18 @@ impl PrimeFieldBits for FieldElement {
|
||||
}
|
||||
|
||||
impl FieldElement {
|
||||
/// Interpret the value as a little-endian integer, square it, and reduce it into a FieldElement.
|
||||
pub fn from_square(value: [u8; 32]) -> FieldElement {
|
||||
let value = U256::from_le_bytes(value);
|
||||
FieldElement(reduce(U512::from(value.mul_wide(&value))))
|
||||
/// Create a FieldElement from a `crypto_bigint::U256`.
|
||||
///
|
||||
/// This will reduce the `U256` by the modulus, into a member of the field.
|
||||
pub const fn from_u256(u256: &U256) -> Self {
|
||||
FieldElement(Residue::new(u256))
|
||||
}
|
||||
|
||||
/// Create a `FieldElement` from the reduction of a 512-bit number.
|
||||
///
|
||||
/// The bytes are interpreted in little-endian format.
|
||||
pub fn wide_reduce(value: [u8; 64]) -> Self {
|
||||
FieldElement(reduce(U512::from_le_bytes(value)))
|
||||
}
|
||||
|
||||
/// Perform an exponentiation.
|
||||
@@ -297,6 +306,12 @@ impl FieldElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUniformBytes<64> for FieldElement {
|
||||
fn from_uniform_bytes(bytes: &[u8; 64]) -> Self {
|
||||
Self::wide_reduce(*bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sum<FieldElement> for FieldElement {
|
||||
fn sum<I: Iterator<Item = FieldElement>>(iter: I) -> FieldElement {
|
||||
let mut res = FieldElement::ZERO;
|
||||
|
||||
@@ -30,7 +30,7 @@ use dalek::{
|
||||
pub use constants::{ED25519_BASEPOINT_TABLE, RISTRETTO_BASEPOINT_TABLE};
|
||||
|
||||
use group::{
|
||||
ff::{Field, PrimeField, FieldBits, PrimeFieldBits},
|
||||
ff::{Field, PrimeField, FieldBits, PrimeFieldBits, FromUniformBytes},
|
||||
Group, GroupEncoding,
|
||||
prime::PrimeGroup,
|
||||
};
|
||||
@@ -38,13 +38,24 @@ use group::{
|
||||
mod field;
|
||||
pub use field::FieldElement;
|
||||
|
||||
mod ciphersuite;
|
||||
pub use crate::ciphersuite::{Ed25519, Ristretto};
|
||||
|
||||
// Use black_box when possible
|
||||
#[rustversion::since(1.66)]
|
||||
use core::hint::black_box;
|
||||
#[rustversion::before(1.66)]
|
||||
fn black_box<T>(val: T) -> T {
|
||||
val
|
||||
mod black_box {
|
||||
pub(crate) fn black_box<T>(val: T) -> T {
|
||||
#[allow(clippy::incompatible_msrv)]
|
||||
core::hint::black_box(val)
|
||||
}
|
||||
}
|
||||
#[rustversion::before(1.66)]
|
||||
mod black_box {
|
||||
pub(crate) fn black_box<T>(val: T) -> T {
|
||||
val
|
||||
}
|
||||
}
|
||||
use black_box::black_box;
|
||||
|
||||
fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
||||
let bit_ref = black_box(bit_ref);
|
||||
@@ -314,6 +325,12 @@ impl PrimeFieldBits for Scalar {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUniformBytes<64> for Scalar {
|
||||
fn from_uniform_bytes(bytes: &[u8; 64]) -> Self {
|
||||
Self::from_bytes_mod_order_wide(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sum<Scalar> for Scalar {
|
||||
fn sum<I: Iterator<Item = Scalar>>(iter: I) -> Scalar {
|
||||
Self(DScalar::sum(iter))
|
||||
@@ -351,7 +368,12 @@ macro_rules! dalek_group {
|
||||
$BASEPOINT_POINT: ident,
|
||||
$BASEPOINT_TABLE: ident
|
||||
) => {
|
||||
/// Wrapper around the dalek Point type. For Ed25519, this is restricted to the prime subgroup.
|
||||
/// Wrapper around the dalek Point type.
|
||||
///
|
||||
/// All operations will be restricted to a prime-order subgroup (equivalent to the group itself
|
||||
/// in the case of Ristretto). The exposure of the internal element does allow bypassing this
|
||||
/// however, which may lead to undefined/computationally-unsafe behavior, and is entirely at
|
||||
/// the user's risk.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct $Point(pub $DPoint);
|
||||
deref_borrow!($Point, $DPoint);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "dkg"
|
||||
version = "0.5.1"
|
||||
version = "0.6.1"
|
||||
description = "Distributed key generation over ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.81"
|
||||
rust-version = "1.66"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -17,82 +17,25 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive", "alloc"] }
|
||||
|
||||
thiserror = { version = "2", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
std-shims = { version = "0.1", path = "../../common/std-shims", default-features = false }
|
||||
|
||||
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, features = ["recommended"] }
|
||||
chacha20 = { version = "0.9", default-features = false, features = ["zeroize"] }
|
||||
|
||||
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false }
|
||||
multiexp = { path = "../multiexp", version = "0.4", default-features = false }
|
||||
|
||||
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1", default-features = false }
|
||||
dleq = { path = "../dleq", version = "^0.4.1", default-features = false }
|
||||
|
||||
# eVRF DKG dependencies
|
||||
generic-array = { version = "1", default-features = false, features = ["alloc"], optional = true }
|
||||
blake2 = { version = "0.10", default-features = false, features = ["std"], optional = true }
|
||||
rand_chacha = { version = "0.3", default-features = false, features = ["std"], optional = true }
|
||||
generalized-bulletproofs = { path = "../evrf/generalized-bulletproofs", default-features = false, optional = true }
|
||||
ec-divisors = { path = "../evrf/divisors", default-features = false, optional = true }
|
||||
generalized-bulletproofs-circuit-abstraction = { path = "../evrf/circuit-abstraction", optional = true }
|
||||
generalized-bulletproofs-ec-gadgets = { path = "../evrf/ec-gadgets", optional = true }
|
||||
|
||||
secq256k1 = { path = "../evrf/secq256k1", optional = true }
|
||||
embedwards25519 = { path = "../evrf/embedwards25519", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||
rand = { version = "0.8", default-features = false, features = ["std"] }
|
||||
ciphersuite = { path = "../ciphersuite", default-features = false, features = ["ristretto"] }
|
||||
generalized-bulletproofs = { path = "../evrf/generalized-bulletproofs", features = ["tests"] }
|
||||
ec-divisors = { path = "../evrf/divisors", features = ["pasta"] }
|
||||
pasta_curves = { git = "https://github.com/kayabaNerve/pasta_curves", rev = "a46b5be95cacbff54d06aad8d3bbcba42e05d616" }
|
||||
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["alloc"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"thiserror/std",
|
||||
|
||||
"rand_core/std",
|
||||
|
||||
"std-shims/std",
|
||||
|
||||
"borsh?/std",
|
||||
|
||||
"transcript/std",
|
||||
"chacha20/std",
|
||||
|
||||
"ciphersuite/std",
|
||||
"multiexp/std",
|
||||
"multiexp/batch",
|
||||
|
||||
"schnorr/std",
|
||||
"dleq/std",
|
||||
"dleq/serialize"
|
||||
]
|
||||
borsh = ["dep:borsh"]
|
||||
evrf = [
|
||||
"std",
|
||||
|
||||
"dep:generic-array",
|
||||
|
||||
"dep:blake2",
|
||||
"dep:rand_chacha",
|
||||
|
||||
"dep:generalized-bulletproofs",
|
||||
"dep:ec-divisors",
|
||||
"dep:generalized-bulletproofs-circuit-abstraction",
|
||||
"dep:generalized-bulletproofs-ec-gadgets",
|
||||
]
|
||||
evrf-secp256k1 = ["evrf", "ciphersuite/secp256k1", "secq256k1"]
|
||||
evrf-ed25519 = ["evrf", "ciphersuite/ed25519", "embedwards25519"]
|
||||
evrf-ristretto = ["evrf", "ciphersuite/ristretto", "embedwards25519"]
|
||||
tests = ["rand_core/getrandom"]
|
||||
default = ["std"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2023 Luke Parker
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# Distributed Key Generation
|
||||
|
||||
A collection of implementations of various distributed key generation protocols.
|
||||
A crate implementing a type for keys, presumably the result of a distributed
|
||||
key generation protocol, and utilities from there.
|
||||
|
||||
All included protocols resolve into the provided `Threshold` types, intended to
|
||||
enable their modularity. Additional utilities around these types, such as
|
||||
promotion from one generator to another, are also provided.
|
||||
This crate used to host implementations of distributed key generation protocols
|
||||
as well (hence the name). Those have been smashed into their own crates, such
|
||||
as [`dkg-musig`](https://docs.rs/dkg-musig) and
|
||||
[`dkg-pedpop`](https://docs.rs/dkg-pedpop).
|
||||
|
||||
Currently, the only included protocol is the two-round protocol from the
|
||||
[FROST paper](https://eprint.iacr.org/2020/852).
|
||||
|
||||
This library was
|
||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
||||
culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
|
||||
Any subsequent changes have not undergone auditing.
|
||||
Before being smashed, this crate was [audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit [669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
|
||||
36
crypto/dkg/dealer/Cargo.toml
Normal file
36
crypto/dkg/dealer/Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "dkg-dealer"
|
||||
version = "0.6.0"
|
||||
description = "Produce dkg::ThresholdKeys with a dealer key generation"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/dealer"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
zeroize = { version = "^1.5", default-features = false }
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false }
|
||||
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||
dkg = { path = "../", version = "0.6", default-features = false }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"zeroize/std",
|
||||
"rand_core/std",
|
||||
"std-shims/std",
|
||||
"ciphersuite/std",
|
||||
"dkg/std",
|
||||
]
|
||||
default = ["std"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Luke Parker
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
13
crypto/dkg/dealer/README.md
Normal file
13
crypto/dkg/dealer/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Distributed Key Generation - Dealer
|
||||
|
||||
This crate implements a dealer key generation protocol for the
|
||||
[`dkg`](https://docs.rs/dkg) crate's types. This provides a single point of
|
||||
failure when the key is being generated and is NOT recommended for use outside
|
||||
of tests.
|
||||
|
||||
This crate was originally part of (in some form) the `dkg` crate, which was
|
||||
[audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit [669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
68
crypto/dkg/dealer/src/lib.rs
Normal file
68
crypto/dkg/dealer/src/lib.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![no_std]
|
||||
|
||||
use core::ops::Deref;
|
||||
use std_shims::{vec::Vec, collections::HashMap};
|
||||
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{
|
||||
group::ff::{Field, PrimeField},
|
||||
Ciphersuite,
|
||||
};
|
||||
pub use dkg::*;
|
||||
|
||||
/// Create a key via a dealer key generation protocol.
|
||||
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
threshold: u16,
|
||||
participants: u16,
|
||||
) -> Result<HashMap<Participant, ThresholdKeys<C>>, DkgError> {
|
||||
let mut coefficients = Vec::with_capacity(usize::from(participants));
|
||||
// `.max(1)` so we always generate the 0th coefficient which we'll share
|
||||
for _ in 0 .. threshold.max(1) {
|
||||
coefficients.push(Zeroizing::new(C::F::random(&mut *rng)));
|
||||
}
|
||||
|
||||
fn polynomial<F: PrimeField + Zeroize>(
|
||||
coefficients: &[Zeroizing<F>],
|
||||
l: Participant,
|
||||
) -> Zeroizing<F> {
|
||||
let l = F::from(u64::from(u16::from(l)));
|
||||
// This should never be reached since Participant is explicitly non-zero
|
||||
assert!(l != F::ZERO, "zero participant passed to polynomial");
|
||||
let mut share = Zeroizing::new(F::ZERO);
|
||||
for (idx, coefficient) in coefficients.iter().rev().enumerate() {
|
||||
*share += coefficient.deref();
|
||||
if idx != (coefficients.len() - 1) {
|
||||
*share *= l;
|
||||
}
|
||||
}
|
||||
share
|
||||
}
|
||||
|
||||
let group_key = C::generator() * coefficients[0].deref();
|
||||
let mut secret_shares = HashMap::with_capacity(participants as usize);
|
||||
let mut verification_shares = HashMap::with_capacity(participants as usize);
|
||||
for i in 1 ..= participants {
|
||||
let i = Participant::new(i).expect("non-zero u16 wasn't a valid Participant index");
|
||||
let secret_share = polynomial(&coefficients, i);
|
||||
secret_shares.insert(i, secret_share.clone());
|
||||
verification_shares.insert(i, C::generator() * *secret_share);
|
||||
}
|
||||
|
||||
let mut res = HashMap::with_capacity(participants as usize);
|
||||
for (i, secret_share) in secret_shares {
|
||||
let keys = ThresholdKeys::new(
|
||||
ThresholdParams::new(threshold, participants, i)?,
|
||||
Interpolation::Lagrange,
|
||||
secret_share,
|
||||
verification_shares.clone(),
|
||||
)?;
|
||||
debug_assert_eq!(keys.group_key(), group_key);
|
||||
res.insert(i, keys);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
68
crypto/dkg/evrf/Cargo.toml
Normal file
68
crypto/dkg/evrf/Cargo.toml
Normal file
@@ -0,0 +1,68 @@
|
||||
[package]
|
||||
name = "dkg-evrf"
|
||||
version = "0.1.0"
|
||||
description = "Distributed key generation over ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/evrf"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.81"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.2", default-features = false, features = ["recommended"] }
|
||||
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||
multiexp = { path = "../../multiexp", version = "0.4", default-features = false }
|
||||
|
||||
generic-array = { version = "1", default-features = false, features = ["alloc"] }
|
||||
blake2 = { version = "0.10", default-features = false, features = ["std"] }
|
||||
rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
|
||||
generalized-bulletproofs = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
|
||||
ec-divisors = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
|
||||
generalized-bulletproofs-circuit-abstraction = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e" }
|
||||
generalized-bulletproofs-ec-gadgets = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e" }
|
||||
|
||||
dkg = { path = ".." }
|
||||
|
||||
secq256k1 = { path = "../../evrf/secq256k1", optional = true }
|
||||
embedwards25519 = { path = "../../evrf/embedwards25519", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||
rand = { version = "0.8", default-features = false, features = ["std"] }
|
||||
ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["std"] }
|
||||
dalek-ff-group = { path = "../../dalek-ff-group", default-features = false, features = ["std"] }
|
||||
generalized-bulletproofs = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", features = ["tests"] }
|
||||
ec-divisors = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e" }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"thiserror/std",
|
||||
|
||||
"rand_core/std",
|
||||
|
||||
"transcript/std",
|
||||
|
||||
"ciphersuite/std",
|
||||
"multiexp/std",
|
||||
"multiexp/batch",
|
||||
]
|
||||
secp256k1 = ["secq256k1"]
|
||||
ed25519 = ["embedwards25519"]
|
||||
ristretto = ["embedwards25519"]
|
||||
tests = ["rand_core/getrandom"]
|
||||
default = ["std"]
|
||||
@@ -88,7 +88,7 @@ use multiexp::multiexp_vartime;
|
||||
use generalized_bulletproofs::{Generators, arithmetic_circuit_proof::*};
|
||||
use ec_divisors::DivisorCurve;
|
||||
|
||||
use crate::{Participant, ThresholdParams, Interpolation, ThresholdCore, ThresholdKeys};
|
||||
use dkg::{Participant, ThresholdParams, Interpolation, ThresholdKeys};
|
||||
|
||||
pub(crate) mod proof;
|
||||
use proof::*;
|
||||
@@ -35,9 +35,6 @@ impl Ciphersuite for Pallas {
|
||||
// This is solely test code so it's fine
|
||||
Self::F::from_uniform_bytes(&Self::H::digest([dst, msg].concat()).into())
|
||||
}
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
Self::F::from_uniform_bytes(&scalar)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
@@ -55,9 +52,6 @@ impl Ciphersuite for Vesta {
|
||||
// This is solely test code so it's fine
|
||||
Self::F::from_uniform_bytes(&Self::H::digest([dst, msg].concat()).into())
|
||||
}
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
Self::F::from_uniform_bytes(&scalar)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VestaParams;
|
||||
49
crypto/dkg/musig/Cargo.toml
Normal file
49
crypto/dkg/musig/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "dkg-musig"
|
||||
version = "0.6.0"
|
||||
description = "The MuSig key aggregation protocol"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/musig"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.79"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false }
|
||||
|
||||
multiexp = { path = "../../multiexp", version = "0.4", default-features = false }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||
dkg = { path = "../", version = "0.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||
dalek-ff-group = { path = "../../dalek-ff-group" }
|
||||
dkg-recovery = { path = "../recovery", default-features = false, features = ["std"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"thiserror/std",
|
||||
|
||||
"rand_core/std",
|
||||
|
||||
"std-shims/std",
|
||||
|
||||
"multiexp/std",
|
||||
"ciphersuite/std",
|
||||
"dkg/std",
|
||||
]
|
||||
default = ["std"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2024 Luke Parker
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
12
crypto/dkg/musig/README.md
Normal file
12
crypto/dkg/musig/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Distributed Key Generation - MuSig
|
||||
|
||||
This implements the MuSig key aggregation protocol for the
|
||||
[`dkg`](https://docs.rs/dkg) crate's types.
|
||||
|
||||
This crate was originally part of (in some form) the `dkg` crate, which was
|
||||
[audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
162
crypto/dkg/musig/src/lib.rs
Normal file
162
crypto/dkg/musig/src/lib.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::ops::Deref;
|
||||
use std_shims::{
|
||||
vec,
|
||||
vec::Vec,
|
||||
collections::{HashSet, HashMap},
|
||||
};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||
|
||||
pub use dkg::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Errors encountered when working with threshold keys.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||
pub enum MusigError<C: Ciphersuite> {
|
||||
/// No keys were provided.
|
||||
#[error("no keys provided")]
|
||||
NoKeysProvided,
|
||||
/// Too many keys were provided.
|
||||
#[error("too many keys (allowed {max}, provided {provided})")]
|
||||
TooManyKeysProvided {
|
||||
/// The maximum amount of keys allowed.
|
||||
max: u16,
|
||||
/// The amount of keys provided.
|
||||
provided: usize,
|
||||
},
|
||||
/// A participant was duplicated.
|
||||
#[error("a participant was duplicated")]
|
||||
DuplicatedParticipant(C::G),
|
||||
/// Participating, yet our public key wasn't found in the list of keys.
|
||||
#[error("private key's public key wasn't present in the list of public keys")]
|
||||
NotPresent,
|
||||
/// An error propagated from the underlying `dkg` crate.
|
||||
#[error("error from dkg ({0})")]
|
||||
DkgError(DkgError),
|
||||
}
|
||||
|
||||
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, MusigError<C>> {
|
||||
if keys.is_empty() {
|
||||
Err(MusigError::NoKeysProvided)?;
|
||||
}
|
||||
|
||||
let keys_len = u16::try_from(keys.len())
|
||||
.map_err(|_| MusigError::TooManyKeysProvided { max: u16::MAX, provided: keys.len() })?;
|
||||
|
||||
let mut set = HashSet::with_capacity(keys.len());
|
||||
for key in keys {
|
||||
let bytes = key.to_bytes().as_ref().to_vec();
|
||||
if !set.insert(bytes) {
|
||||
Err(MusigError::DuplicatedParticipant(*key))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys_len)
|
||||
}
|
||||
|
||||
fn binding_factor_transcript<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
keys_len: u16,
|
||||
keys: &[C::G],
|
||||
) -> Vec<u8> {
|
||||
debug_assert_eq!(usize::from(keys_len), keys.len());
|
||||
|
||||
let mut transcript = vec![];
|
||||
transcript.extend(&context);
|
||||
transcript.extend(keys_len.to_le_bytes());
|
||||
for key in keys {
|
||||
transcript.extend(key.to_bytes().as_ref());
|
||||
}
|
||||
transcript
|
||||
}
|
||||
|
||||
fn binding_factor<C: Ciphersuite>(mut transcript: Vec<u8>, i: u16) -> C::F {
|
||||
transcript.extend(i.to_le_bytes());
|
||||
C::hash_to_F(b"dkg-musig", &transcript)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn musig_key_multiexp<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
keys: &[C::G],
|
||||
) -> Result<Vec<(C::F, C::G)>, MusigError<C>> {
|
||||
let keys_len = check_keys::<C>(keys)?;
|
||||
let transcript = binding_factor_transcript::<C>(context, keys_len, keys);
|
||||
let mut multiexp = Vec::with_capacity(keys.len());
|
||||
for i in 1 ..= keys_len {
|
||||
multiexp.push((binding_factor::<C>(transcript.clone(), i), keys[usize::from(i - 1)]));
|
||||
}
|
||||
Ok(multiexp)
|
||||
}
|
||||
|
||||
/// The group key resulting from using this library's MuSig key aggregation.
|
||||
///
|
||||
/// This function executes in variable time and MUST NOT be used with secret data.
|
||||
pub fn musig_key_vartime<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
keys: &[C::G],
|
||||
) -> Result<C::G, MusigError<C>> {
|
||||
Ok(multiexp::multiexp_vartime(&musig_key_multiexp(context, keys)?))
|
||||
}
|
||||
|
||||
/// The group key resulting from using this library's MuSig key aggregation.
|
||||
pub fn musig_key<C: Ciphersuite>(context: [u8; 32], keys: &[C::G]) -> Result<C::G, MusigError<C>> {
|
||||
Ok(multiexp::multiexp(&musig_key_multiexp(context, keys)?))
|
||||
}
|
||||
|
||||
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
||||
pub fn musig<C: Ciphersuite>(
|
||||
context: [u8; 32],
|
||||
private_key: Zeroizing<C::F>,
|
||||
keys: &[C::G],
|
||||
) -> Result<ThresholdKeys<C>, MusigError<C>> {
|
||||
let our_pub_key = C::generator() * private_key.deref();
|
||||
let Some(our_i) = keys.iter().position(|key| *key == our_pub_key) else {
|
||||
Err(MusigError::DkgError(DkgError::NotParticipating))?
|
||||
};
|
||||
|
||||
let keys_len: u16 = check_keys::<C>(keys)?;
|
||||
|
||||
let params = ThresholdParams::new(
|
||||
keys_len,
|
||||
keys_len,
|
||||
// The `+ 1` won't fail as `keys.len() <= u16::MAX`, so any index is `< u16::MAX`
|
||||
Participant::new(
|
||||
u16::try_from(our_i).expect("keys.len() <= u16::MAX yet index of keys > u16::MAX?") + 1,
|
||||
)
|
||||
.expect("i + 1 != 0"),
|
||||
)
|
||||
.map_err(MusigError::DkgError)?;
|
||||
|
||||
let transcript = binding_factor_transcript::<C>(context, keys_len, keys);
|
||||
let mut binding_factors = Vec::with_capacity(keys.len());
|
||||
let mut multiexp = Vec::with_capacity(keys.len());
|
||||
let mut verification_shares = HashMap::with_capacity(keys.len());
|
||||
for (i, key) in (1 ..= keys_len).zip(keys.iter().copied()) {
|
||||
let binding_factor = binding_factor::<C>(transcript.clone(), i);
|
||||
binding_factors.push(binding_factor);
|
||||
multiexp.push((binding_factor, key));
|
||||
|
||||
let i = Participant::new(i).expect("non-zero u16 wasn't a valid Participant index?");
|
||||
verification_shares.insert(i, key);
|
||||
}
|
||||
let group_key = multiexp::multiexp(&multiexp);
|
||||
debug_assert_eq!(our_pub_key, verification_shares[¶ms.i()]);
|
||||
debug_assert_eq!(musig_key_vartime::<C>(context, keys).unwrap(), group_key);
|
||||
|
||||
ThresholdKeys::new(
|
||||
params,
|
||||
Interpolation::Constant(binding_factors),
|
||||
private_key,
|
||||
verification_shares,
|
||||
)
|
||||
.map_err(MusigError::DkgError)
|
||||
}
|
||||
71
crypto/dkg/musig/src/tests.rs
Normal file
71
crypto/dkg/musig/src/tests.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use dalek_ff_group::Ristretto;
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use dkg_recovery::recover_key;
|
||||
use crate::*;
|
||||
|
||||
/// Tests MuSig key generation.
|
||||
#[test]
|
||||
pub fn test_musig() {
|
||||
const PARTICIPANTS: u16 = 5;
|
||||
|
||||
let mut keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
for _ in 0 .. PARTICIPANTS {
|
||||
let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
pub_keys.push(<Ristretto as Ciphersuite>::generator() * *key);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
const CONTEXT: [u8; 32] = *b"MuSig Test ";
|
||||
|
||||
// Empty signing set
|
||||
musig::<Ristretto>(CONTEXT, Zeroizing::new(<Ristretto as Ciphersuite>::F::ZERO), &[])
|
||||
.unwrap_err();
|
||||
// Signing set we're not part of
|
||||
musig::<Ristretto>(
|
||||
CONTEXT,
|
||||
Zeroizing::new(<Ristretto as Ciphersuite>::F::ZERO),
|
||||
&[<Ristretto as Ciphersuite>::generator()],
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// Test with n keys
|
||||
{
|
||||
let mut created_keys = HashMap::new();
|
||||
let mut verification_shares = HashMap::new();
|
||||
let group_key = musig_key::<Ristretto>(CONTEXT, &pub_keys).unwrap();
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let these_keys = musig::<Ristretto>(CONTEXT, key.clone(), &pub_keys).unwrap();
|
||||
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
||||
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
||||
assert_eq!(usize::from(u16::from(these_keys.params().i())), i + 1);
|
||||
|
||||
verification_shares.insert(
|
||||
these_keys.params().i(),
|
||||
<Ristretto as Ciphersuite>::generator() * **these_keys.original_secret_share(),
|
||||
);
|
||||
|
||||
assert_eq!(these_keys.group_key(), group_key);
|
||||
|
||||
created_keys.insert(these_keys.params().i(), these_keys);
|
||||
}
|
||||
|
||||
for keys in created_keys.values() {
|
||||
for (l, verification_share) in &verification_shares {
|
||||
assert_eq!(keys.original_verification_share(*l), *verification_share);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
<Ristretto as Ciphersuite>::generator() *
|
||||
*recover_key(&created_keys.values().cloned().collect::<Vec<_>>()).unwrap(),
|
||||
group_key
|
||||
);
|
||||
}
|
||||
}
|
||||
37
crypto/dkg/pedpop/Cargo.toml
Normal file
37
crypto/dkg/pedpop/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "dkg-pedpop"
|
||||
version = "0.6.0"
|
||||
description = "The PedPoP distributed key generation protocol"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/pedpop"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2", default-features = false, features = ["std"] }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.3", default-features = false, features = ["std", "recommended"] }
|
||||
chacha20 = { version = "0.9", default-features = false, features = ["std", "zeroize"] }
|
||||
|
||||
multiexp = { path = "../../multiexp", version = "0.4", default-features = false, features = ["std"] }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
||||
schnorr = { package = "schnorr-signatures", path = "../../schnorr", version = "^0.5.1", default-features = false, features = ["std"] }
|
||||
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
||||
|
||||
dkg = { path = "../", version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||
dalek-ff-group = { path = "../../dalek-ff-group", default-features = false }
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Luke Parker
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
12
crypto/dkg/pedpop/README.md
Normal file
12
crypto/dkg/pedpop/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Distributed Key Generation - PedPoP
|
||||
|
||||
This implements the PedPoP distributed key generation protocol for the
|
||||
[`dkg`](https://docs.rs/dkg) crate's types.
|
||||
|
||||
This crate was originally part of the `dkg` crate, which was
|
||||
[audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
@@ -21,7 +21,7 @@ use multiexp::BatchVerifier;
|
||||
use schnorr::SchnorrSignature;
|
||||
use dleq::DLEqProof;
|
||||
|
||||
use crate::{Participant, ThresholdParams};
|
||||
use dkg::{Participant, ThresholdParams};
|
||||
|
||||
mod sealed {
|
||||
use super::*;
|
||||
@@ -69,7 +69,7 @@ impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
|
||||
buf
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "tests"))]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn enc_key(&self) -> C::G {
|
||||
self.enc_key
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
// This crate requires `dleq` which doesn't support no-std via std-shims
|
||||
// #![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::{marker::PhantomData, ops::Deref, fmt};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
||||
use ciphersuite::{
|
||||
group::{
|
||||
ff::{Field, PrimeField},
|
||||
@@ -17,29 +22,75 @@ use ciphersuite::{
|
||||
},
|
||||
Ciphersuite,
|
||||
};
|
||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
||||
|
||||
use schnorr::SchnorrSignature;
|
||||
|
||||
use crate::{
|
||||
Participant, DkgError, ThresholdParams, Interpolation, ThresholdCore, validate_map,
|
||||
encryption::{
|
||||
ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, Decryption, EncryptionKeyProof,
|
||||
DecryptionError,
|
||||
},
|
||||
};
|
||||
pub use dkg::*;
|
||||
|
||||
type FrostError<C> = DkgError<EncryptionKeyProof<C>>;
|
||||
mod encryption;
|
||||
pub use encryption::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Errors possible during key generation.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||
pub enum PedPoPError<C: Ciphersuite> {
|
||||
/// An incorrect amount of participants was provided.
|
||||
#[error("incorrect amount of participants (expected {expected}, found {found})")]
|
||||
IncorrectAmountOfParticipants { expected: usize, found: usize },
|
||||
/// An invalid proof of knowledge was provided.
|
||||
#[error("invalid proof of knowledge (participant {0})")]
|
||||
InvalidCommitments(Participant),
|
||||
/// An invalid DKG share was provided.
|
||||
#[error("invalid share (participant {participant}, blame {blame})")]
|
||||
InvalidShare { participant: Participant, blame: Option<EncryptionKeyProof<C>> },
|
||||
/// A participant was missing.
|
||||
#[error("missing participant {0}")]
|
||||
MissingParticipant(Participant),
|
||||
/// An error propagated from the underlying `dkg` crate.
|
||||
#[error("error from dkg ({0})")]
|
||||
DkgError(DkgError),
|
||||
}
|
||||
|
||||
// Validate a map of values to have the expected included participants
|
||||
fn validate_map<T, C: Ciphersuite>(
|
||||
map: &HashMap<Participant, T>,
|
||||
included: &[Participant],
|
||||
ours: Participant,
|
||||
) -> Result<(), PedPoPError<C>> {
|
||||
if (map.len() + 1) != included.len() {
|
||||
Err(PedPoPError::IncorrectAmountOfParticipants {
|
||||
expected: included.len(),
|
||||
found: map.len() + 1,
|
||||
})?;
|
||||
}
|
||||
|
||||
for included in included {
|
||||
if *included == ours {
|
||||
if map.contains_key(included) {
|
||||
Err(PedPoPError::DkgError(DkgError::DuplicatedParticipant(*included)))?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !map.contains_key(included) {
|
||||
Err(PedPoPError::MissingParticipant(*included))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn challenge<C: Ciphersuite>(context: [u8; 32], l: Participant, R: &[u8], Am: &[u8]) -> C::F {
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2");
|
||||
let mut transcript = RecommendedTranscript::new(b"DKG PedPoP v0.2");
|
||||
transcript.domain_separate(b"schnorr_proof_of_knowledge");
|
||||
transcript.append_message(b"context", context);
|
||||
transcript.append_message(b"participant", l.to_bytes());
|
||||
transcript.append_message(b"nonce", R);
|
||||
transcript.append_message(b"commitments", Am);
|
||||
C::hash_to_F(b"DKG-FROST-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
||||
C::hash_to_F(b"DKG-PedPoP-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
||||
}
|
||||
|
||||
/// The commitments message, intended to be broadcast to all other parties.
|
||||
@@ -98,7 +149,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
|
||||
KeyGenMachine { params, context, _curve: PhantomData }
|
||||
}
|
||||
|
||||
/// Start generating a key according to the FROST DKG spec.
|
||||
/// Start generating a key according to the PedPoP DKG specification present in the FROST paper.
|
||||
///
|
||||
/// Returns a commitments message to be sent to all parties over an authenticated channel. If any
|
||||
/// party submits multiple sets of commitments, they MUST be treated as malicious.
|
||||
@@ -106,7 +157,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
|
||||
let t = usize::from(self.params.t);
|
||||
let t = usize::from(self.params.t());
|
||||
let mut coefficients = Vec::with_capacity(t);
|
||||
let mut commitments = Vec::with_capacity(t);
|
||||
let mut cached_msg = vec![];
|
||||
@@ -133,7 +184,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
|
||||
);
|
||||
|
||||
// Additionally create an encryption mechanism to protect the secret shares
|
||||
let encryption = Encryption::new(self.context, self.params.i, rng);
|
||||
let encryption = Encryption::new(self.context, self.params.i(), rng);
|
||||
|
||||
// Step 4: Broadcast
|
||||
let msg =
|
||||
@@ -250,21 +301,21 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
) -> Result<HashMap<Participant, Vec<C::G>>, FrostError<C>> {
|
||||
) -> Result<HashMap<Participant, Vec<C::G>>, PedPoPError<C>> {
|
||||
validate_map(
|
||||
&commitment_msgs,
|
||||
&(1 ..= self.params.n()).map(Participant).collect::<Vec<_>>(),
|
||||
&self.params.all_participant_indexes().collect::<Vec<_>>(),
|
||||
self.params.i(),
|
||||
)?;
|
||||
|
||||
let mut batch = BatchVerifier::<Participant, C::G>::new(commitment_msgs.len());
|
||||
let mut commitments = HashMap::new();
|
||||
for l in (1 ..= self.params.n()).map(Participant) {
|
||||
for l in self.params.all_participant_indexes() {
|
||||
let Some(msg) = commitment_msgs.remove(&l) else { continue };
|
||||
let mut msg = self.encryption.register(l, msg);
|
||||
|
||||
if msg.commitments.len() != self.params.t().into() {
|
||||
Err(FrostError::InvalidCommitments(l))?;
|
||||
Err(PedPoPError::InvalidCommitments(l))?;
|
||||
}
|
||||
|
||||
// Step 5: Validate each proof of knowledge
|
||||
@@ -280,9 +331,9 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||
commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
batch.verify_vartime_with_vartime_blame().map_err(FrostError::InvalidCommitments)?;
|
||||
batch.verify_vartime_with_vartime_blame().map_err(PedPoPError::InvalidCommitments)?;
|
||||
|
||||
commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
|
||||
commitments.insert(self.params.i(), self.our_commitments.drain(..).collect());
|
||||
Ok(commitments)
|
||||
}
|
||||
|
||||
@@ -299,13 +350,13 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
||||
commitments: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
) -> Result<
|
||||
(KeyMachine<C>, HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>),
|
||||
FrostError<C>,
|
||||
PedPoPError<C>,
|
||||
> {
|
||||
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
||||
|
||||
// Step 1: Generate secret shares for all other parties
|
||||
let mut res = HashMap::new();
|
||||
for l in (1 ..= self.params.n()).map(Participant) {
|
||||
for l in self.params.all_participant_indexes() {
|
||||
// Don't insert our own shares to the byte buffer which is meant to be sent around
|
||||
// An app developer could accidentally send it. Best to keep this black boxed
|
||||
if l == self.params.i() {
|
||||
@@ -413,10 +464,10 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
mut self,
|
||||
rng: &mut R,
|
||||
mut shares: HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>,
|
||||
) -> Result<BlameMachine<C>, FrostError<C>> {
|
||||
) -> Result<BlameMachine<C>, PedPoPError<C>> {
|
||||
validate_map(
|
||||
&shares,
|
||||
&(1 ..= self.params.n()).map(Participant).collect::<Vec<_>>(),
|
||||
&self.params.all_participant_indexes().collect::<Vec<_>>(),
|
||||
self.params.i(),
|
||||
)?;
|
||||
|
||||
@@ -427,7 +478,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes);
|
||||
let share =
|
||||
Zeroizing::new(Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or_else(|| {
|
||||
FrostError::InvalidShare { participant: l, blame: Some(blame.clone()) }
|
||||
PedPoPError::InvalidShare { participant: l, blame: Some(blame.clone()) }
|
||||
})?);
|
||||
share_bytes.zeroize();
|
||||
*self.secret += share.deref();
|
||||
@@ -444,7 +495,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
BatchId::Decryption(l) => (l, None),
|
||||
BatchId::Share(l) => (l, Some(blames.remove(&l).unwrap())),
|
||||
};
|
||||
FrostError::InvalidShare { participant: l, blame }
|
||||
PedPoPError::InvalidShare { participant: l, blame }
|
||||
})?;
|
||||
|
||||
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
||||
@@ -458,7 +509,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
|
||||
// Calculate each user's verification share
|
||||
let mut verification_shares = HashMap::new();
|
||||
for i in (1 ..= self.params.n()).map(Participant) {
|
||||
for i in self.params.all_participant_indexes() {
|
||||
verification_shares.insert(
|
||||
i,
|
||||
if i == self.params.i() {
|
||||
@@ -473,13 +524,10 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
Ok(BlameMachine {
|
||||
commitments,
|
||||
encryption: encryption.into_decryption(),
|
||||
result: Some(ThresholdCore {
|
||||
params,
|
||||
interpolation: Interpolation::Lagrange,
|
||||
secret_share: secret,
|
||||
group_key: stripes[0],
|
||||
verification_shares,
|
||||
}),
|
||||
result: Some(
|
||||
ThresholdKeys::new(params, Interpolation::Lagrange, secret, verification_shares)
|
||||
.map_err(PedPoPError::DkgError)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -488,7 +536,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
||||
pub struct BlameMachine<C: Ciphersuite> {
|
||||
commitments: HashMap<Participant, Vec<C::G>>,
|
||||
encryption: Decryption<C>,
|
||||
result: Option<ThresholdCore<C>>,
|
||||
result: Option<ThresholdKeys<C>>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for BlameMachine<C> {
|
||||
@@ -520,7 +568,7 @@ impl<C: Ciphersuite> BlameMachine<C> {
|
||||
/// territory of consensus protocols. This library does not handle that nor does it provide any
|
||||
/// tooling to do so. This function is solely intended to force users to acknowledge they're
|
||||
/// completing the protocol, not processing any blame.
|
||||
pub fn complete(self) -> ThresholdCore<C> {
|
||||
pub fn complete(self) -> ThresholdKeys<C> {
|
||||
self.result.unwrap()
|
||||
}
|
||||
|
||||
@@ -602,12 +650,12 @@ impl<C: Ciphersuite> AdditionalBlameMachine<C> {
|
||||
context: [u8; 32],
|
||||
n: u16,
|
||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
) -> Result<Self, FrostError<C>> {
|
||||
) -> Result<Self, PedPoPError<C>> {
|
||||
let mut commitments = HashMap::new();
|
||||
let mut encryption = Decryption::new(context);
|
||||
for i in 1 ..= n {
|
||||
let i = Participant::new(i).unwrap();
|
||||
let Some(msg) = commitment_msgs.remove(&i) else { Err(DkgError::MissingParticipant(i))? };
|
||||
let Some(msg) = commitment_msgs.remove(&i) else { Err(PedPoPError::MissingParticipant(i))? };
|
||||
commitments.insert(i, encryption.register(i, msg).commitments);
|
||||
}
|
||||
Ok(AdditionalBlameMachine(BlameMachine { commitments, encryption, result: None }))
|
||||
346
crypto/dkg/pedpop/src/tests.rs
Normal file
346
crypto/dkg/pedpop/src/tests.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng, OsRng};
|
||||
|
||||
use dalek_ff_group::Ristretto;
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
use crate::*;
|
||||
|
||||
const THRESHOLD: u16 = 3;
|
||||
const PARTICIPANTS: u16 = 5;
|
||||
|
||||
/// Clone a map without a specific value.
|
||||
fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
map: &HashMap<K, V>,
|
||||
without: &K,
|
||||
) -> HashMap<K, V> {
|
||||
let mut res = map.clone();
|
||||
res.remove(without).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
||||
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
|
||||
|
||||
const CONTEXT: [u8; 32] = *b"DKG Test Key Generation ";
|
||||
|
||||
// Commit, then return commitment messages, enc keys, and shares
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
HashMap<Participant, KeyMachine<C>>,
|
||||
HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
HashMap<Participant, C::G>,
|
||||
HashMap<Participant, PedPoPSecretShares<C>>,
|
||||
) {
|
||||
let mut machines = HashMap::new();
|
||||
let mut commitments = HashMap::new();
|
||||
let mut enc_keys = HashMap::new();
|
||||
for i in (1 ..= PARTICIPANTS).map(|i| Participant::new(i).unwrap()) {
|
||||
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
|
||||
let machine = KeyGenMachine::<C>::new(params, CONTEXT);
|
||||
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
||||
machines.insert(i, machine);
|
||||
|
||||
commitments.insert(
|
||||
i,
|
||||
EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params)
|
||||
.unwrap(),
|
||||
);
|
||||
enc_keys.insert(i, commitments[&i].enc_key());
|
||||
}
|
||||
|
||||
let mut secret_shares = HashMap::new();
|
||||
let machines = machines
|
||||
.drain()
|
||||
.map(|(l, machine)| {
|
||||
let (machine, mut shares) =
|
||||
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
|
||||
let shares = shares
|
||||
.drain()
|
||||
.map(|(l, share)| {
|
||||
(
|
||||
l,
|
||||
EncryptedMessage::read::<&[u8]>(
|
||||
&mut share.serialize().as_ref(),
|
||||
// Only t/n actually matters, so hardcode i to 1 here
|
||||
ThresholdParams::new(THRESHOLD, PARTICIPANTS, Participant::new(1).unwrap()).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
secret_shares.insert(l, shares);
|
||||
(l, machine)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
(machines, commitments, enc_keys, secret_shares)
|
||||
}
|
||||
|
||||
fn generate_secret_shares<C: Ciphersuite>(
|
||||
shares: &HashMap<Participant, PedPoPSecretShares<C>>,
|
||||
recipient: Participant,
|
||||
) -> PedPoPSecretShares<C> {
|
||||
let mut our_secret_shares = HashMap::new();
|
||||
for (i, shares) in shares {
|
||||
if recipient == *i {
|
||||
continue;
|
||||
}
|
||||
our_secret_shares.insert(*i, shares[&recipient].clone());
|
||||
}
|
||||
our_secret_shares
|
||||
}
|
||||
|
||||
/// Fully perform the PedPoP key generation algorithm.
|
||||
fn pedpop_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||
let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
||||
|
||||
let mut verification_shares = None;
|
||||
let mut group_key = None;
|
||||
machines
|
||||
.drain()
|
||||
.map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
||||
|
||||
// Verify the verification_shares are agreed upon
|
||||
if verification_shares.is_none() {
|
||||
verification_shares = Some(
|
||||
these_keys
|
||||
.params()
|
||||
.all_participant_indexes()
|
||||
.map(|i| (i, these_keys.original_verification_share(i)))
|
||||
.collect::<HashMap<_, _>>(),
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
verification_shares.as_ref().unwrap(),
|
||||
&these_keys
|
||||
.params()
|
||||
.all_participant_indexes()
|
||||
.map(|i| (i, these_keys.original_verification_share(i)))
|
||||
.collect::<HashMap<_, _>>()
|
||||
);
|
||||
|
||||
// Verify the group keys are agreed upon
|
||||
if group_key.is_none() {
|
||||
group_key = Some(these_keys.group_key());
|
||||
}
|
||||
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
||||
|
||||
(i, these_keys)
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
const ONE: Participant = Participant::new(1).unwrap();
|
||||
const TWO: Participant = Participant::new(2).unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_pedpop() {
|
||||
let _ = core::hint::black_box(pedpop_gen::<_, Ristretto>(&mut OsRng));
|
||||
}
|
||||
|
||||
fn test_blame(
|
||||
commitment_msgs: &HashMap<Participant, EncryptionKeyMessage<Ristretto, Commitments<Ristretto>>>,
|
||||
machines: Vec<BlameMachine<Ristretto>>,
|
||||
msg: &PedPoPEncryptedMessage<Ristretto>,
|
||||
blame: &Option<EncryptionKeyProof<Ristretto>>,
|
||||
) {
|
||||
for machine in machines {
|
||||
let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone());
|
||||
assert_eq!(blamed, ONE);
|
||||
// Verify additional blame also works
|
||||
assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE);
|
||||
|
||||
// Verify machines constructed with AdditionalBlameMachine::new work
|
||||
assert_eq!(
|
||||
AdditionalBlameMachine::new(CONTEXT, PARTICIPANTS, commitment_msgs.clone()).unwrap().blame(
|
||||
ONE,
|
||||
TWO,
|
||||
msg.clone(),
|
||||
blame.clone()
|
||||
),
|
||||
ONE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Write a macro which expands to the following
|
||||
#[test]
|
||||
fn invalid_encryption_pop_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
// Mutate the PoP of the encrypted message from 1 to 2
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop();
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
assert_eq!(
|
||||
machine.err(),
|
||||
Some(PedPoPError::InvalidShare { participant: ONE, blame: None })
|
||||
);
|
||||
// Explicitly declare we have a blame object, which happens to be None since invalid PoP
|
||||
// is self-explainable
|
||||
blame = Some(None);
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ecdh_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
// Mutate the share to trigger a blame event
|
||||
// Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass
|
||||
// While here, 2 is malicious, this is so 1 creates the blame proof
|
||||
// We then malleate 1's blame proof, so 1 ends up malicious
|
||||
// Doesn't simply invalidate the PoP as that won't have a blame statement
|
||||
// By mutating the encrypted data, we do ensure a blame statement is created
|
||||
secret_shares
|
||||
.get_mut(&TWO)
|
||||
.unwrap()
|
||||
.get_mut(&ONE)
|
||||
.unwrap()
|
||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == ONE {
|
||||
blame = Some(match machine.err() {
|
||||
Some(PedPoPError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
// This should be largely equivalent to the prior test
|
||||
#[test]
|
||||
fn invalid_dleq_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares
|
||||
.get_mut(&TWO)
|
||||
.unwrap()
|
||||
.get_mut(&ONE)
|
||||
.unwrap()
|
||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == ONE {
|
||||
blame = Some(match machine.err() {
|
||||
Some(PedPoPError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_share_serialization_blame() {
|
||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization(
|
||||
&mut OsRng,
|
||||
CONTEXT,
|
||||
ONE,
|
||||
enc_keys[&TWO],
|
||||
);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
blame = Some(match machine.err() {
|
||||
Some(PedPoPError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_share_value_blame() {
|
||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value(
|
||||
&mut OsRng,
|
||||
CONTEXT,
|
||||
ONE,
|
||||
enc_keys[&TWO],
|
||||
);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
blame = Some(match machine.err() {
|
||||
Some(PedPoPError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
34
crypto/dkg/promote/Cargo.toml
Normal file
34
crypto/dkg/promote/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "dkg-promote"
|
||||
version = "0.6.1"
|
||||
description = "Promotions for keys from the dkg crate"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/promote"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2", default-features = false, features = ["std"] }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.2", default-features = false, features = ["std", "recommended"] }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
||||
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
||||
|
||||
dkg = { path = "../", version = "0.6.1", default-features = false, features = ["std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||
dalek-ff-group = { path = "../../dalek-ff-group" }
|
||||
dkg-recovery = { path = "../recovery", default-features = false, features = ["std"] }
|
||||
21
crypto/dkg/promote/LICENSE
Normal file
21
crypto/dkg/promote/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
13
crypto/dkg/promote/README.md
Normal file
13
crypto/dkg/promote/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Distributed Key Generation - Promote
|
||||
|
||||
This crate implements 'promotions' for keys from the
|
||||
[`dkg`](https://docs.rs/dkg) crate. A promotion takes a set of keys and maps it
|
||||
to a different `Ciphersuite`.
|
||||
|
||||
This crate was originally part of the `dkg` crate, which was
|
||||
[audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
@@ -1,7 +1,11 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
// This crate requires `dleq` which doesn't support no-std via std-shims
|
||||
// #![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::{marker::PhantomData, ops::Deref};
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
sync::Arc,
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
@@ -12,11 +16,37 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
use dleq::DLEqProof;
|
||||
|
||||
use crate::{Participant, DkgError, ThresholdCore, ThresholdKeys, validate_map};
|
||||
pub use dkg::*;
|
||||
|
||||
/// Promote a set of keys to another Ciphersuite definition.
|
||||
pub trait CiphersuitePromote<C2: Ciphersuite> {
|
||||
fn promote(self) -> ThresholdKeys<C2>;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Errors encountered when promoting keys.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||
pub enum PromotionError {
|
||||
/// Invalid participant identifier.
|
||||
#[error("invalid participant (1 <= participant <= {n}, yet participant is {participant})")]
|
||||
InvalidParticipant {
|
||||
/// The total amount of participants.
|
||||
n: u16,
|
||||
/// The specified participant.
|
||||
participant: Participant,
|
||||
},
|
||||
|
||||
/// An incorrect amount of participants was specified.
|
||||
#[error("incorrect amount of participants. {t} <= amount <= {n}, yet amount is {amount}")]
|
||||
IncorrectAmountOfParticipants {
|
||||
/// The threshold required.
|
||||
t: u16,
|
||||
/// The total amount of participants.
|
||||
n: u16,
|
||||
/// The amount of participants specified.
|
||||
amount: usize,
|
||||
},
|
||||
|
||||
/// Participant provided an invalid proof.
|
||||
#[error("invalid proof {0}")]
|
||||
InvalidProof(Participant),
|
||||
}
|
||||
|
||||
fn transcript<G: GroupEncoding>(key: &G, i: Participant) -> RecommendedTranscript {
|
||||
@@ -65,20 +95,21 @@ pub struct GeneratorPromotion<C1: Ciphersuite, C2: Ciphersuite> {
|
||||
}
|
||||
|
||||
impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<C1, C2> {
|
||||
/// Begin promoting keys from one generator to another. Returns a proof this share was properly
|
||||
/// promoted.
|
||||
/// Begin promoting keys from one generator to another.
|
||||
///
|
||||
/// Returns a proof this share was properly promoted.
|
||||
pub fn promote<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
base: ThresholdKeys<C1>,
|
||||
) -> (GeneratorPromotion<C1, C2>, GeneratorProof<C1>) {
|
||||
// Do a DLEqProof for the new generator
|
||||
let proof = GeneratorProof {
|
||||
share: C2::generator() * base.secret_share().deref(),
|
||||
share: C2::generator() * base.original_secret_share().deref(),
|
||||
proof: DLEqProof::prove(
|
||||
rng,
|
||||
&mut transcript(&base.core.group_key(), base.params().i),
|
||||
&mut transcript(&base.original_group_key(), base.params().i()),
|
||||
&[C1::generator(), C2::generator()],
|
||||
base.secret_share(),
|
||||
base.original_secret_share(),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -89,35 +120,49 @@ impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<
|
||||
pub fn complete(
|
||||
self,
|
||||
proofs: &HashMap<Participant, GeneratorProof<C1>>,
|
||||
) -> Result<ThresholdKeys<C2>, DkgError<()>> {
|
||||
) -> Result<ThresholdKeys<C2>, PromotionError> {
|
||||
let params = self.base.params();
|
||||
validate_map(proofs, &(1 ..= params.n).map(Participant).collect::<Vec<_>>(), params.i)?;
|
||||
|
||||
let original_shares = self.base.verification_shares();
|
||||
if proofs.len() != (usize::from(params.n()) - 1) {
|
||||
Err(PromotionError::IncorrectAmountOfParticipants {
|
||||
t: params.n(),
|
||||
n: params.n(),
|
||||
amount: proofs.len() + 1,
|
||||
})?;
|
||||
}
|
||||
for i in proofs.keys().copied() {
|
||||
if u16::from(i) > params.n() {
|
||||
Err(PromotionError::InvalidParticipant { n: params.n(), participant: i })?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut verification_shares = HashMap::new();
|
||||
verification_shares.insert(params.i, self.proof.share);
|
||||
for (i, proof) in proofs {
|
||||
let i = *i;
|
||||
verification_shares.insert(params.i(), self.proof.share);
|
||||
for i in 1 ..= params.n() {
|
||||
let i = Participant::new(i).unwrap();
|
||||
if i == params.i() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let proof = proofs.get(&i).unwrap();
|
||||
proof
|
||||
.proof
|
||||
.verify(
|
||||
&mut transcript(&self.base.core.group_key(), i),
|
||||
&mut transcript(&self.base.original_group_key(), i),
|
||||
&[C1::generator(), C2::generator()],
|
||||
&[original_shares[&i], proof.share],
|
||||
&[self.base.original_verification_share(i), proof.share],
|
||||
)
|
||||
.map_err(|_| DkgError::InvalidCommitments(i))?;
|
||||
.map_err(|_| PromotionError::InvalidProof(i))?;
|
||||
verification_shares.insert(i, proof.share);
|
||||
}
|
||||
|
||||
Ok(ThresholdKeys {
|
||||
core: Arc::new(ThresholdCore::new(
|
||||
Ok(
|
||||
ThresholdKeys::new(
|
||||
params,
|
||||
self.base.core.interpolation.clone(),
|
||||
self.base.secret_share().clone(),
|
||||
self.base.interpolation().clone(),
|
||||
self.base.original_secret_share().clone(),
|
||||
verification_shares,
|
||||
)),
|
||||
offset: None,
|
||||
})
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
113
crypto/dkg/promote/src/tests.rs
Normal file
113
crypto/dkg/promote/src/tests.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use core::marker::PhantomData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
use rand_core::OsRng;
|
||||
|
||||
use dalek_ff_group::Ristretto;
|
||||
use ciphersuite::{
|
||||
group::{ff::Field, Group},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
use dkg::*;
|
||||
use dkg_recovery::recover_key;
|
||||
use crate::{GeneratorPromotion, GeneratorProof};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
struct AltGenerator<C: Ciphersuite> {
|
||||
_curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
||||
type F = C::F;
|
||||
type G = C::G;
|
||||
type H = C::H;
|
||||
|
||||
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
||||
|
||||
fn generator() -> Self::G {
|
||||
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
<C as Ciphersuite>::hash_to_F(dst, data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone a map without a specific value.
|
||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
map: &HashMap<K, V>,
|
||||
without: &K,
|
||||
) -> HashMap<K, V> {
|
||||
let mut res = map.clone();
|
||||
res.remove(without).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
// Test promotion of threshold keys to another generator
|
||||
#[test]
|
||||
fn test_generator_promotion() {
|
||||
// Generate a set of `ThresholdKeys`
|
||||
const PARTICIPANTS: u16 = 5;
|
||||
let keys: [ThresholdKeys<_>; PARTICIPANTS as usize] = {
|
||||
let shares: [<Ristretto as Ciphersuite>::F; PARTICIPANTS as usize] =
|
||||
core::array::from_fn(|_| <Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
let verification_shares = (0 .. PARTICIPANTS)
|
||||
.map(|i| {
|
||||
(
|
||||
Participant::new(i + 1).unwrap(),
|
||||
<Ristretto as Ciphersuite>::generator() * shares[usize::from(i)],
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
core::array::from_fn(|i| {
|
||||
ThresholdKeys::new(
|
||||
ThresholdParams::new(
|
||||
PARTICIPANTS,
|
||||
PARTICIPANTS,
|
||||
Participant::new(u16::try_from(i + 1).unwrap()).unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
Interpolation::Constant(vec![<Ristretto as Ciphersuite>::F::ONE; PARTICIPANTS as usize]),
|
||||
Zeroizing::new(shares[i]),
|
||||
verification_shares.clone(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
};
|
||||
|
||||
// Perform the promotion
|
||||
let mut promotions = HashMap::new();
|
||||
let mut proofs = HashMap::new();
|
||||
for keys in &keys {
|
||||
let i = keys.params().i();
|
||||
let (promotion, proof) =
|
||||
GeneratorPromotion::<_, AltGenerator<Ristretto>>::promote(&mut OsRng, keys.clone());
|
||||
promotions.insert(i, promotion);
|
||||
proofs.insert(
|
||||
i,
|
||||
GeneratorProof::<Ristretto>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Complete the promotion, and verify it worked
|
||||
let new_group_key = AltGenerator::<Ristretto>::generator() * *recover_key(&keys).unwrap();
|
||||
for (i, promoting) in promotions.drain() {
|
||||
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
||||
assert_eq!(keys[usize::from(u16::from(i) - 1)].params(), promoted.params());
|
||||
assert_eq!(
|
||||
keys[usize::from(u16::from(i) - 1)].original_secret_share(),
|
||||
promoted.original_secret_share()
|
||||
);
|
||||
assert_eq!(new_group_key, promoted.group_key());
|
||||
for l in 0 .. PARTICIPANTS {
|
||||
let verification_share =
|
||||
promoted.original_verification_share(Participant::new(l + 1).unwrap());
|
||||
assert_eq!(
|
||||
AltGenerator::<Ristretto>::generator() * **keys[usize::from(l)].original_secret_share(),
|
||||
verification_share
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
crypto/dkg/recovery/Cargo.toml
Normal file
34
crypto/dkg/recovery/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "dkg-recovery"
|
||||
version = "0.6.0"
|
||||
description = "Recover a secret-shared key from a collection of dkg::ThresholdKeys"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/recovery"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
zeroize = { version = "^1.5", default-features = false }
|
||||
|
||||
thiserror = { version = "2", default-features = false }
|
||||
|
||||
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||
dkg = { path = "../", version = "0.6", default-features = false }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"zeroize/std",
|
||||
"thiserror/std",
|
||||
"ciphersuite/std",
|
||||
"dkg/std",
|
||||
]
|
||||
default = ["std"]
|
||||
21
crypto/dkg/recovery/LICENSE
Normal file
21
crypto/dkg/recovery/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-2025 Luke Parker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
14
crypto/dkg/recovery/README.md
Normal file
14
crypto/dkg/recovery/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Distributed Key Generation - Recovery
|
||||
|
||||
A utility function to recover a key from its secret shares.
|
||||
|
||||
Keys likely SHOULD NOT ever be recovered, making this primarily intended for
|
||||
testing purposes. Instead, the shares of the key should be used to produce
|
||||
shares for the desired action, allowing using the key while never
|
||||
reconstructing it.
|
||||
|
||||
Before being smashed, this crate was [audited by Cypher Stack in March 2023](
|
||||
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||
), culminating in commit [669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||
). Any subsequent changes have not undergone auditing.
|
||||
85
crypto/dkg/recovery/src/lib.rs
Normal file
85
crypto/dkg/recovery/src/lib.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![no_std]
|
||||
|
||||
use core::ops::{Deref, DerefMut};
|
||||
extern crate alloc;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
pub use dkg::*;
|
||||
|
||||
/// Errors encountered when recovering a secret-shared key from a collection of
|
||||
/// `dkg::ThresholdKeys`.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||
pub enum RecoveryError {
|
||||
/// No keys were provided.
|
||||
#[error("no keys provided")]
|
||||
NoKeysProvided,
|
||||
/// Not enough keys were provided.
|
||||
#[error("not enough keys provided (threshold required {required}, provided {provided})")]
|
||||
NotEnoughKeysProvided { required: u16, provided: usize },
|
||||
/// The keys had inconsistent parameters.
|
||||
#[error("keys had inconsistent parameters")]
|
||||
InconsistentParameters,
|
||||
/// The keys are from distinct secret-sharing sessions or otherwise corrupt.
|
||||
#[error("recovery failed")]
|
||||
Failure,
|
||||
/// An error propagated from the underlying `dkg` crate.
|
||||
#[error("error from dkg ({0})")]
|
||||
DkgError(DkgError),
|
||||
}
|
||||
|
||||
/// Recover a shared secret from a collection of `dkg::ThresholdKeys`.
|
||||
pub fn recover_key<C: Ciphersuite>(
|
||||
keys: &[ThresholdKeys<C>],
|
||||
) -> Result<Zeroizing<C::F>, RecoveryError> {
|
||||
let included = keys.iter().map(|keys| keys.params().i()).collect::<Vec<_>>();
|
||||
|
||||
let keys_len = keys.len();
|
||||
let mut keys = keys.iter();
|
||||
let first_keys = keys.next().ok_or(RecoveryError::NoKeysProvided)?;
|
||||
{
|
||||
let t = first_keys.params().t();
|
||||
if keys_len < usize::from(t) {
|
||||
Err(RecoveryError::NotEnoughKeysProvided { required: t, provided: keys_len })?;
|
||||
}
|
||||
}
|
||||
{
|
||||
let first_params = (
|
||||
first_keys.params().t(),
|
||||
first_keys.params().n(),
|
||||
first_keys.group_key(),
|
||||
first_keys.current_scalar(),
|
||||
first_keys.current_offset(),
|
||||
);
|
||||
for keys in keys.clone() {
|
||||
let params = (
|
||||
keys.params().t(),
|
||||
keys.params().n(),
|
||||
keys.group_key(),
|
||||
keys.current_scalar(),
|
||||
keys.current_offset(),
|
||||
);
|
||||
if params != first_params {
|
||||
Err(RecoveryError::InconsistentParameters)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut res: Zeroizing<_> =
|
||||
first_keys.view(included.clone()).map_err(RecoveryError::DkgError)?.secret_share().clone();
|
||||
for keys in keys {
|
||||
*res.deref_mut() +=
|
||||
keys.view(included.clone()).map_err(RecoveryError::DkgError)?.secret_share().deref();
|
||||
}
|
||||
|
||||
if (C::generator() * res.deref()) != first_keys.group_key() {
|
||||
Err(RecoveryError::Failure)?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
||||
#[cfg(feature = "std")]
|
||||
use core::ops::Deref;
|
||||
use std_shims::{vec, vec::Vec, collections::HashSet};
|
||||
#[cfg(feature = "std")]
|
||||
use std_shims::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::{
|
||||
group::{Group, GroupEncoding},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
use crate::DkgError;
|
||||
#[cfg(feature = "std")]
|
||||
use crate::{Participant, ThresholdParams, Interpolation, ThresholdCore};
|
||||
|
||||
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, DkgError<()>> {
|
||||
if keys.is_empty() {
|
||||
Err(DkgError::InvalidSigningSet)?;
|
||||
}
|
||||
// Too many signers
|
||||
let keys_len = u16::try_from(keys.len()).map_err(|_| DkgError::InvalidSigningSet)?;
|
||||
|
||||
// Duplicated public keys
|
||||
if keys.iter().map(|key| key.to_bytes().as_ref().to_vec()).collect::<HashSet<_>>().len() !=
|
||||
keys.len()
|
||||
{
|
||||
Err(DkgError::InvalidSigningSet)?;
|
||||
}
|
||||
|
||||
Ok(keys_len)
|
||||
}
|
||||
|
||||
// This function panics if called with keys whose length exceed 2**16.
|
||||
// This is fine since it's internal and all calls occur after calling check_keys, which does check
|
||||
// the keys' length.
|
||||
fn binding_factor_transcript<C: Ciphersuite>(
|
||||
context: &[u8],
|
||||
keys: &[C::G],
|
||||
) -> Result<Vec<u8>, DkgError<()>> {
|
||||
let mut transcript = vec![];
|
||||
transcript.push(u8::try_from(context.len()).map_err(|_| DkgError::InvalidSigningSet)?);
|
||||
transcript.extend(context);
|
||||
transcript.extend(u16::try_from(keys.len()).unwrap().to_le_bytes());
|
||||
for key in keys {
|
||||
transcript.extend(key.to_bytes().as_ref());
|
||||
}
|
||||
Ok(transcript)
|
||||
}
|
||||
|
||||
fn binding_factor<C: Ciphersuite>(mut transcript: Vec<u8>, i: u16) -> C::F {
|
||||
transcript.extend(i.to_le_bytes());
|
||||
C::hash_to_F(b"musig", &transcript)
|
||||
}
|
||||
|
||||
/// The group key resulting from using this library's MuSig key gen.
|
||||
///
|
||||
/// This function will return an error if the context is longer than 255 bytes.
|
||||
///
|
||||
/// Creating an aggregate key with a list containing duplicated public keys will return an error.
|
||||
pub fn musig_key<C: Ciphersuite>(context: &[u8], keys: &[C::G]) -> Result<C::G, DkgError<()>> {
|
||||
let keys_len = check_keys::<C>(keys)?;
|
||||
let transcript = binding_factor_transcript::<C>(context, keys)?;
|
||||
let mut res = C::G::identity();
|
||||
for i in 1 ..= keys_len {
|
||||
// TODO: Calculate this with a multiexp
|
||||
res += keys[usize::from(i - 1)] * binding_factor::<C>(transcript.clone(), i);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
||||
///
|
||||
/// Creating an aggregate key with a list containing duplicated public keys returns an error.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn musig<C: Ciphersuite>(
|
||||
context: &[u8],
|
||||
private_key: &Zeroizing<C::F>,
|
||||
keys: &[C::G],
|
||||
) -> Result<ThresholdCore<C>, DkgError<()>> {
|
||||
let keys_len = check_keys::<C>(keys)?;
|
||||
|
||||
let our_pub_key = C::generator() * private_key.deref();
|
||||
let Some(pos) = keys.iter().position(|key| *key == our_pub_key) else {
|
||||
// Not present in signing set
|
||||
Err(DkgError::InvalidSigningSet)?
|
||||
};
|
||||
let params = ThresholdParams::new(
|
||||
keys_len,
|
||||
keys_len,
|
||||
// These errors shouldn't be possible, as pos is bounded to len - 1
|
||||
// Since len is prior guaranteed to be within u16::MAX, pos + 1 must also be
|
||||
Participant::new((pos + 1).try_into().map_err(|_| DkgError::InvalidSigningSet)?)
|
||||
.ok_or(DkgError::InvalidSigningSet)?,
|
||||
)?;
|
||||
|
||||
// Calculate the binding factor per-key
|
||||
let transcript = binding_factor_transcript::<C>(context, keys)?;
|
||||
let mut binding = Vec::with_capacity(keys.len());
|
||||
for i in 1 ..= keys_len {
|
||||
binding.push(binding_factor::<C>(transcript.clone(), i));
|
||||
}
|
||||
|
||||
// Our secret share is our private key
|
||||
let secret_share = private_key.clone();
|
||||
|
||||
// Calculate verification shares
|
||||
let mut verification_shares = HashMap::new();
|
||||
let mut group_key = C::G::identity();
|
||||
for l in 1 ..= keys_len {
|
||||
let key = keys[usize::from(l) - 1];
|
||||
group_key += key * binding[usize::from(l - 1)];
|
||||
|
||||
// These errors also shouldn't be possible, for the same reasons as documented above
|
||||
verification_shares.insert(Participant::new(l).ok_or(DkgError::InvalidSigningSet)?, key);
|
||||
}
|
||||
debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]);
|
||||
debug_assert_eq!(musig_key::<C>(context, keys).unwrap(), group_key);
|
||||
|
||||
Ok(ThresholdCore::new(
|
||||
params,
|
||||
Interpolation::Constant(binding),
|
||||
secret_share,
|
||||
verification_shares,
|
||||
))
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use core::ops::Deref;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::{Participant, ThresholdCore, ThresholdKeys, musig::musig as musig_fn};
|
||||
|
||||
mod musig;
|
||||
pub use musig::test_musig;
|
||||
|
||||
/// FROST key generation testing utility.
|
||||
pub mod pedpop;
|
||||
use pedpop::pedpop_gen;
|
||||
|
||||
// Promotion test.
|
||||
mod promote;
|
||||
use promote::test_generator_promotion;
|
||||
|
||||
#[cfg(all(test, feature = "evrf"))]
|
||||
mod evrf;
|
||||
|
||||
/// Constant amount of participants to use when testing.
|
||||
pub const PARTICIPANTS: u16 = 5;
|
||||
/// Constant threshold of participants to use when testing.
|
||||
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
||||
|
||||
/// Clone a map without a specific value.
|
||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
map: &HashMap<K, V>,
|
||||
without: &K,
|
||||
) -> HashMap<K, V> {
|
||||
let mut res = map.clone();
|
||||
res.remove(without).unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
/// Recover the secret from a collection of keys.
|
||||
///
|
||||
/// This will panic if no keys, an insufficient amount of keys, or the wrong keys are provided.
|
||||
pub fn recover_key<C: Ciphersuite>(keys: &HashMap<Participant, ThresholdKeys<C>>) -> C::F {
|
||||
let first = keys.values().next().expect("no keys provided");
|
||||
assert!(keys.len() >= first.params().t().into(), "not enough keys provided");
|
||||
let included = keys.keys().copied().collect::<Vec<_>>();
|
||||
|
||||
let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| {
|
||||
accum +
|
||||
(first.core.interpolation.interpolation_factor(*i, &included) * keys.secret_share().deref())
|
||||
});
|
||||
assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys");
|
||||
group_private
|
||||
}
|
||||
|
||||
/// Generate threshold keys for tests.
|
||||
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||
let res = pedpop_gen(rng)
|
||||
.drain()
|
||||
.map(|(i, core)| {
|
||||
assert_eq!(
|
||||
&ThresholdCore::<C>::read::<&[u8]>(&mut core.serialize().as_ref()).unwrap(),
|
||||
&core
|
||||
);
|
||||
(i, ThresholdKeys::new(core))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
||||
res
|
||||
}
|
||||
|
||||
/// Generate MuSig keys for tests.
|
||||
pub fn musig_key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||
let mut keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
for _ in 0 .. PARTICIPANTS {
|
||||
let key = Zeroizing::new(C::F::random(&mut *rng));
|
||||
pub_keys.push(C::generator() * *key);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
let mut res = HashMap::new();
|
||||
for key in keys {
|
||||
let these_keys = musig_fn::<C>(b"Test MuSig Key Gen", &key, &pub_keys).unwrap();
|
||||
res.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
||||
}
|
||||
|
||||
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
||||
res
|
||||
}
|
||||
|
||||
/// Run the test suite on a ciphersuite.
|
||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||
key_gen::<_, C>(rng);
|
||||
test_generator_promotion::<_, C>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_ristretto() {
|
||||
test_ciphersuite::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::{
|
||||
ThresholdKeys,
|
||||
musig::{musig_key, musig},
|
||||
tests::{PARTICIPANTS, recover_key},
|
||||
};
|
||||
|
||||
/// Tests MuSig key generation.
|
||||
pub fn test_musig<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||
let mut keys = vec![];
|
||||
let mut pub_keys = vec![];
|
||||
for _ in 0 .. PARTICIPANTS {
|
||||
let key = Zeroizing::new(C::F::random(&mut *rng));
|
||||
pub_keys.push(C::generator() * *key);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
const CONTEXT: &[u8] = b"MuSig Test";
|
||||
|
||||
// Empty signing set
|
||||
musig::<C>(CONTEXT, &Zeroizing::new(C::F::ZERO), &[]).unwrap_err();
|
||||
// Signing set we're not part of
|
||||
musig::<C>(CONTEXT, &Zeroizing::new(C::F::ZERO), &[C::generator()]).unwrap_err();
|
||||
|
||||
// Test with n keys
|
||||
{
|
||||
let mut created_keys = HashMap::new();
|
||||
let mut verification_shares = HashMap::new();
|
||||
let group_key = musig_key::<C>(CONTEXT, &pub_keys).unwrap();
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let these_keys = musig::<C>(CONTEXT, key, &pub_keys).unwrap();
|
||||
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
||||
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
||||
assert_eq!(usize::from(these_keys.params().i().0), i + 1);
|
||||
|
||||
verification_shares
|
||||
.insert(these_keys.params().i(), C::generator() * **these_keys.secret_share());
|
||||
|
||||
assert_eq!(these_keys.group_key(), group_key);
|
||||
|
||||
created_keys.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
||||
}
|
||||
|
||||
for keys in created_keys.values() {
|
||||
assert_eq!(keys.verification_shares(), verification_shares);
|
||||
}
|
||||
|
||||
assert_eq!(C::generator() * recover_key(&created_keys), group_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn musig_literal() {
|
||||
test_musig::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng)
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
use crate::{
|
||||
Participant, ThresholdParams, ThresholdCore,
|
||||
pedpop::{Commitments, KeyGenMachine, SecretShare, KeyMachine},
|
||||
encryption::{EncryptionKeyMessage, EncryptedMessage},
|
||||
tests::{THRESHOLD, PARTICIPANTS, clone_without},
|
||||
};
|
||||
|
||||
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
||||
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
|
||||
|
||||
const CONTEXT: [u8; 32] = *b"DKG Test Key Generation ";
|
||||
|
||||
// Commit, then return commitment messages, enc keys, and shares
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
HashMap<Participant, KeyMachine<C>>,
|
||||
HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||
HashMap<Participant, C::G>,
|
||||
HashMap<Participant, PedPoPSecretShares<C>>,
|
||||
) {
|
||||
let mut machines = HashMap::new();
|
||||
let mut commitments = HashMap::new();
|
||||
let mut enc_keys = HashMap::new();
|
||||
for i in (1 ..= PARTICIPANTS).map(Participant) {
|
||||
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
|
||||
let machine = KeyGenMachine::<C>::new(params, CONTEXT);
|
||||
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
||||
machines.insert(i, machine);
|
||||
|
||||
commitments.insert(
|
||||
i,
|
||||
EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params)
|
||||
.unwrap(),
|
||||
);
|
||||
enc_keys.insert(i, commitments[&i].enc_key());
|
||||
}
|
||||
|
||||
let mut secret_shares = HashMap::new();
|
||||
let machines = machines
|
||||
.drain()
|
||||
.map(|(l, machine)| {
|
||||
let (machine, mut shares) =
|
||||
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
|
||||
let shares = shares
|
||||
.drain()
|
||||
.map(|(l, share)| {
|
||||
(
|
||||
l,
|
||||
EncryptedMessage::read::<&[u8]>(
|
||||
&mut share.serialize().as_ref(),
|
||||
// Only t/n actually matters, so hardcode i to 1 here
|
||||
ThresholdParams { t: THRESHOLD, n: PARTICIPANTS, i: Participant(1) },
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
secret_shares.insert(l, shares);
|
||||
(l, machine)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
(machines, commitments, enc_keys, secret_shares)
|
||||
}
|
||||
|
||||
fn generate_secret_shares<C: Ciphersuite>(
|
||||
shares: &HashMap<Participant, PedPoPSecretShares<C>>,
|
||||
recipient: Participant,
|
||||
) -> PedPoPSecretShares<C> {
|
||||
let mut our_secret_shares = HashMap::new();
|
||||
for (i, shares) in shares {
|
||||
if recipient == *i {
|
||||
continue;
|
||||
}
|
||||
our_secret_shares.insert(*i, shares[&recipient].clone());
|
||||
}
|
||||
our_secret_shares
|
||||
}
|
||||
|
||||
/// Fully perform the PedPoP key generation algorithm.
|
||||
pub fn pedpop_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdCore<C>> {
|
||||
let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
||||
|
||||
let mut verification_shares = None;
|
||||
let mut group_key = None;
|
||||
machines
|
||||
.drain()
|
||||
.map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
||||
|
||||
// Verify the verification_shares are agreed upon
|
||||
if verification_shares.is_none() {
|
||||
verification_shares = Some(these_keys.verification_shares());
|
||||
}
|
||||
assert_eq!(verification_shares.as_ref().unwrap(), &these_keys.verification_shares());
|
||||
|
||||
// Verify the group keys are agreed upon
|
||||
if group_key.is_none() {
|
||||
group_key = Some(these_keys.group_key());
|
||||
}
|
||||
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
||||
|
||||
(i, these_keys)
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod literal {
|
||||
use rand_core::OsRng;
|
||||
|
||||
use ciphersuite::Ristretto;
|
||||
|
||||
use crate::{
|
||||
DkgError,
|
||||
encryption::EncryptionKeyProof,
|
||||
pedpop::{BlameMachine, AdditionalBlameMachine},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const ONE: Participant = Participant(1);
|
||||
const TWO: Participant = Participant(2);
|
||||
|
||||
fn test_blame(
|
||||
commitment_msgs: &HashMap<Participant, EncryptionKeyMessage<Ristretto, Commitments<Ristretto>>>,
|
||||
machines: Vec<BlameMachine<Ristretto>>,
|
||||
msg: &PedPoPEncryptedMessage<Ristretto>,
|
||||
blame: &Option<EncryptionKeyProof<Ristretto>>,
|
||||
) {
|
||||
for machine in machines {
|
||||
let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone());
|
||||
assert_eq!(blamed, ONE);
|
||||
// Verify additional blame also works
|
||||
assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE);
|
||||
|
||||
// Verify machines constructed with AdditionalBlameMachine::new work
|
||||
assert_eq!(
|
||||
AdditionalBlameMachine::new(CONTEXT, PARTICIPANTS, commitment_msgs.clone()).unwrap().blame(
|
||||
ONE,
|
||||
TWO,
|
||||
msg.clone(),
|
||||
blame.clone()
|
||||
),
|
||||
ONE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Write a macro which expands to the following
|
||||
#[test]
|
||||
fn invalid_encryption_pop_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
// Mutate the PoP of the encrypted message from 1 to 2
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop();
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
assert_eq!(machine.err(), Some(DkgError::InvalidShare { participant: ONE, blame: None }));
|
||||
// Explicitly declare we have a blame object, which happens to be None since invalid PoP
|
||||
// is self-explainable
|
||||
blame = Some(None);
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ecdh_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
// Mutate the share to trigger a blame event
|
||||
// Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass
|
||||
// While here, 2 is malicious, this is so 1 creates the blame proof
|
||||
// We then malleate 1's blame proof, so 1 ends up malicious
|
||||
// Doesn't simply invalidate the PoP as that won't have a blame statement
|
||||
// By mutating the encrypted data, we do ensure a blame statement is created
|
||||
secret_shares
|
||||
.get_mut(&TWO)
|
||||
.unwrap()
|
||||
.get_mut(&ONE)
|
||||
.unwrap()
|
||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == ONE {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
// This should be largely equivalent to the prior test
|
||||
#[test]
|
||||
fn invalid_dleq_blame() {
|
||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares
|
||||
.get_mut(&TWO)
|
||||
.unwrap()
|
||||
.get_mut(&ONE)
|
||||
.unwrap()
|
||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == ONE {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_share_serialization_blame() {
|
||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization(
|
||||
&mut OsRng,
|
||||
CONTEXT,
|
||||
ONE,
|
||||
enc_keys[&TWO],
|
||||
);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_share_value_blame() {
|
||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||
|
||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value(
|
||||
&mut OsRng,
|
||||
CONTEXT,
|
||||
ONE,
|
||||
enc_keys[&TWO],
|
||||
);
|
||||
|
||||
let mut blame = None;
|
||||
let machines = machines
|
||||
.drain()
|
||||
.filter_map(|(i, machine)| {
|
||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||
if i == TWO {
|
||||
blame = Some(match machine.err() {
|
||||
Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||
_ => panic!(),
|
||||
});
|
||||
None
|
||||
} else {
|
||||
Some(machine.unwrap())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use core::{marker::PhantomData, ops::Deref};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use ciphersuite::{group::Group, Ciphersuite};
|
||||
|
||||
use crate::{
|
||||
promote::{GeneratorPromotion, GeneratorProof},
|
||||
tests::{clone_without, key_gen, recover_key},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
struct AltGenerator<C: Ciphersuite> {
|
||||
_curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
||||
type F = C::F;
|
||||
type G = C::G;
|
||||
type H = C::H;
|
||||
|
||||
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
||||
|
||||
fn generator() -> Self::G {
|
||||
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
||||
}
|
||||
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
<C as Ciphersuite>::reduce_512(scalar)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
<C as Ciphersuite>::hash_to_F(dst, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Test promotion of threshold keys to another generator
|
||||
pub(crate) fn test_generator_promotion<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
||||
let keys = key_gen::<_, C>(&mut *rng);
|
||||
|
||||
let mut promotions = HashMap::new();
|
||||
let mut proofs = HashMap::new();
|
||||
for (i, keys) in &keys {
|
||||
let (promotion, proof) =
|
||||
GeneratorPromotion::<_, AltGenerator<C>>::promote(&mut *rng, keys.clone());
|
||||
promotions.insert(*i, promotion);
|
||||
proofs.insert(*i, GeneratorProof::<C>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap());
|
||||
}
|
||||
|
||||
let new_group_key = AltGenerator::<C>::generator() * recover_key(&keys);
|
||||
for (i, promoting) in promotions.drain() {
|
||||
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
||||
assert_eq!(keys[&i].params(), promoted.params());
|
||||
assert_eq!(keys[&i].secret_share(), promoted.secret_share());
|
||||
assert_eq!(new_group_key, promoted.group_key());
|
||||
for (l, verification_share) in promoted.verification_shares() {
|
||||
assert_eq!(
|
||||
AltGenerator::<C>::generator() * keys[&l].secret_share().deref(),
|
||||
verification_share
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "minimal-ed448"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
description = "Unaudited, inefficient implementation of Ed448 in Rust"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ed448"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["ed448", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.71"
|
||||
rust-version = "1.65"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -24,8 +24,11 @@ rand_core = { version = "0.6", default-features = false }
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
subtle = { version = "^2.4", default-features = false }
|
||||
|
||||
sha3 = { version = "0.10", default-features = false }
|
||||
|
||||
ff = { version = "0.13", default-features = false, features = ["bits"] }
|
||||
group = { version = "0.13", default-features = false }
|
||||
ciphersuite = { path = "../ciphersuite", default-features = false }
|
||||
|
||||
generic-array = { version = "1", default-features = false }
|
||||
crypto-bigint = { version = "0.5", default-features = false, features = ["zeroize"] }
|
||||
@@ -38,5 +41,6 @@ rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
ff-group-tests = { path = "../ff-group-tests" }
|
||||
|
||||
[features]
|
||||
std = ["rand_core/std", "zeroize/std", "subtle/std", "ff/std"]
|
||||
alloc = ["zeroize/alloc", "ciphersuite/alloc"]
|
||||
std = ["alloc", "rand_core/std", "zeroize/std", "subtle/std", "sha3/std", "ff/std", "ciphersuite/std"]
|
||||
default = ["std"]
|
||||
|
||||
@@ -2,11 +2,19 @@ use zeroize::Zeroize;
|
||||
|
||||
// Use black_box when possible
|
||||
#[rustversion::since(1.66)]
|
||||
use core::hint::black_box;
|
||||
#[rustversion::before(1.66)]
|
||||
fn black_box<T>(val: T) -> T {
|
||||
val
|
||||
mod black_box {
|
||||
pub(crate) fn black_box<T>(val: T) -> T {
|
||||
#[allow(clippy::incompatible_msrv)]
|
||||
core::hint::black_box(val)
|
||||
}
|
||||
}
|
||||
#[rustversion::before(1.66)]
|
||||
mod black_box {
|
||||
pub(crate) fn black_box<T>(val: T) -> T {
|
||||
val
|
||||
}
|
||||
}
|
||||
use black_box::black_box;
|
||||
|
||||
pub(crate) fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
||||
let bit_ref = black_box(bit_ref);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use digest::{
|
||||
typenum::U114, core_api::BlockSizeUser, Update, Output, OutputSizeUser, FixedOutput,
|
||||
ExtendableOutput, XofReader, HashMarker, Digest,
|
||||
use sha3::{
|
||||
digest::{
|
||||
typenum::U114, core_api::BlockSizeUser, Update, Output, OutputSizeUser, FixedOutput,
|
||||
ExtendableOutput, XofReader, HashMarker, Digest,
|
||||
},
|
||||
Shake256,
|
||||
};
|
||||
use sha3::Shake256;
|
||||
|
||||
use group::Group;
|
||||
use minimal_ed448::{Scalar, Point};
|
||||
use crate::{Scalar, Point};
|
||||
|
||||
use crate::Ciphersuite;
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
/// Shake256, fixed to a 114-byte output, as used by Ed448.
|
||||
#[derive(Clone, Default)]
|
||||
@@ -66,12 +68,6 @@ impl Ciphersuite for Ed448 {
|
||||
Point::generator()
|
||||
}
|
||||
|
||||
fn reduce_512(mut scalar: [u8; 64]) -> Self::F {
|
||||
let res = Self::hash_to_F(b"Ciphersuite-reduce_512", &scalar);
|
||||
scalar.zeroize();
|
||||
res
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_ref().try_into().unwrap())
|
||||
}
|
||||
@@ -14,3 +14,6 @@ pub use field::FieldElement;
|
||||
|
||||
mod point;
|
||||
pub use point::Point;
|
||||
|
||||
mod ciphersuite;
|
||||
pub use crate::ciphersuite::Ed448;
|
||||
|
||||
@@ -50,13 +50,25 @@ fn recover_x(y: FieldElement) -> CtOption<FieldElement> {
|
||||
}
|
||||
|
||||
/// Ed448 point.
|
||||
#[derive(Clone, Copy, Debug, Zeroize)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Point {
|
||||
x: FieldElement,
|
||||
y: FieldElement,
|
||||
z: FieldElement,
|
||||
}
|
||||
|
||||
impl Zeroize for Point {
|
||||
fn zeroize(&mut self) {
|
||||
self.x.zeroize();
|
||||
self.y.zeroize();
|
||||
self.z.zeroize();
|
||||
let identity = Self::identity();
|
||||
self.x = identity.x;
|
||||
self.y = identity.y;
|
||||
self.z = identity.z;
|
||||
}
|
||||
}
|
||||
|
||||
const G: Point = Point { x: G_X, y: G_Y, z: FieldElement::ONE };
|
||||
|
||||
impl ConstantTimeEq for Point {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "generalized-bulletproofs-circuit-abstraction"
|
||||
version = "0.1.0"
|
||||
description = "An abstraction for arithmetic circuits over Generalized Bulletproofs"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/fcmps/circuit-abstraction"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["bulletproofs", "circuit"]
|
||||
edition = "2021"
|
||||
rust-version = "1.69"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false }
|
||||
|
||||
generalized-bulletproofs = { path = "../generalized-bulletproofs", default-features = false }
|
||||
|
||||
[features]
|
||||
std = ["std-shims/std", "zeroize/std", "ciphersuite/std", "generalized-bulletproofs/std"]
|
||||
default = ["std"]
|
||||
@@ -1,3 +0,0 @@
|
||||
# Generalized Bulletproofs Circuit Abstraction
|
||||
|
||||
A circuit abstraction around `generalized-bulletproofs`.
|
||||
@@ -1,39 +0,0 @@
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<C: Ciphersuite> Circuit<C> {
|
||||
/// Constrain two linear combinations to be equal.
|
||||
pub fn equality(&mut self, a: LinComb<C::F>, b: &LinComb<C::F>) {
|
||||
self.constrain_equal_to_zero(a - b);
|
||||
}
|
||||
|
||||
/// Calculate (and constrain) the inverse of a value.
|
||||
///
|
||||
/// A linear combination may optionally be passed as a constraint for the value being inverted.
|
||||
/// A reference to the inverted value and its inverse is returned.
|
||||
///
|
||||
/// May panic if any linear combinations reference non-existent terms, the witness isn't provided
|
||||
/// when proving/is provided when verifying, or if the witness is 0 (and accordingly doesn't have
|
||||
/// an inverse).
|
||||
pub fn inverse(
|
||||
&mut self,
|
||||
lincomb: Option<LinComb<C::F>>,
|
||||
witness: Option<C::F>,
|
||||
) -> (Variable, Variable) {
|
||||
let (l, r, o) = self.mul(lincomb, None, witness.map(|f| (f, f.invert().unwrap())));
|
||||
// The output of a value multiplied by its inverse is 1
|
||||
// Constrain `1 o - 1 = 0`
|
||||
self.constrain_equal_to_zero(LinComb::from(o).constant(-C::F::ONE));
|
||||
(l, r)
|
||||
}
|
||||
|
||||
/// Constrain two linear combinations as inequal.
|
||||
///
|
||||
/// May panic if any linear combinations reference non-existent terms.
|
||||
pub fn inequality(&mut self, a: LinComb<C::F>, b: &LinComb<C::F>, witness: Option<(C::F, C::F)>) {
|
||||
let l_constraint = a - b;
|
||||
// The existence of a multiplicative inverse means a-b != 0, which means a != b
|
||||
self.inverse(Some(l_constraint), witness.map(|(a, b)| a - b));
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use generalized_bulletproofs::{
|
||||
ScalarVector, PedersenCommitment, PedersenVectorCommitment, ProofGenerators,
|
||||
transcript::{Transcript as ProverTranscript, VerifierTranscript, Commitments},
|
||||
arithmetic_circuit_proof::{AcError, ArithmeticCircuitStatement, ArithmeticCircuitWitness},
|
||||
};
|
||||
pub use generalized_bulletproofs::arithmetic_circuit_proof::{Variable, LinComb};
|
||||
|
||||
mod gadgets;
|
||||
|
||||
/// A trait for the transcript, whether proving for verifying, as necessary for sampling
|
||||
/// challenges.
|
||||
pub trait Transcript {
|
||||
/// Sample a challenge from the transacript.
|
||||
///
|
||||
/// It is the caller's responsibility to have properly transcripted all variables prior to
|
||||
/// sampling this challenge.
|
||||
fn challenge<C: Ciphersuite>(&mut self) -> C::F;
|
||||
|
||||
/// Sample a challenge as a byte array.
|
||||
///
|
||||
/// It is the caller's responsibility to have properly transcripted all variables prior to
|
||||
/// sampling this challenge.
|
||||
fn challenge_bytes(&mut self) -> [u8; 64];
|
||||
}
|
||||
impl Transcript for ProverTranscript {
|
||||
fn challenge<C: Ciphersuite>(&mut self) -> C::F {
|
||||
self.challenge::<C>()
|
||||
}
|
||||
fn challenge_bytes(&mut self) -> [u8; 64] {
|
||||
self.challenge_bytes()
|
||||
}
|
||||
}
|
||||
impl Transcript for VerifierTranscript<'_> {
|
||||
fn challenge<C: Ciphersuite>(&mut self) -> C::F {
|
||||
self.challenge::<C>()
|
||||
}
|
||||
fn challenge_bytes(&mut self) -> [u8; 64] {
|
||||
self.challenge_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
/// The witness for the satisfaction of this circuit.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||
struct ProverData<C: Ciphersuite> {
|
||||
aL: Vec<C::F>,
|
||||
aR: Vec<C::F>,
|
||||
C: Vec<PedersenVectorCommitment<C>>,
|
||||
V: Vec<PedersenCommitment<C>>,
|
||||
}
|
||||
|
||||
/// A struct representing a circuit.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Circuit<C: Ciphersuite> {
|
||||
muls: usize,
|
||||
// A series of linear combinations which must evaluate to 0.
|
||||
constraints: Vec<LinComb<C::F>>,
|
||||
prover: Option<ProverData<C>>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Circuit<C> {
|
||||
/// Returns the amount of multiplications used by this circuit.
|
||||
pub fn muls(&self) -> usize {
|
||||
self.muls
|
||||
}
|
||||
|
||||
/// Create an instance to prove satisfaction of a circuit with.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn prove(
|
||||
vector_commitments: Vec<PedersenVectorCommitment<C>>,
|
||||
commitments: Vec<PedersenCommitment<C>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
muls: 0,
|
||||
constraints: vec![],
|
||||
prover: Some(ProverData { aL: vec![], aR: vec![], C: vector_commitments, V: commitments }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an instance to verify a proof with.
|
||||
pub fn verify() -> Self {
|
||||
Self { muls: 0, constraints: vec![], prover: None }
|
||||
}
|
||||
|
||||
/// Evaluate a linear combination.
|
||||
///
|
||||
/// Yields WL aL + WR aR + WO aO + WCG CG + WV V + c.
|
||||
///
|
||||
/// May panic if the linear combination references non-existent terms.
|
||||
///
|
||||
/// Returns None if not a prover.
|
||||
pub fn eval(&self, lincomb: &LinComb<C::F>) -> Option<C::F> {
|
||||
self.prover.as_ref().map(|prover| {
|
||||
let mut res = lincomb.c();
|
||||
for (index, weight) in lincomb.WL() {
|
||||
res += prover.aL[*index] * weight;
|
||||
}
|
||||
for (index, weight) in lincomb.WR() {
|
||||
res += prover.aR[*index] * weight;
|
||||
}
|
||||
for (index, weight) in lincomb.WO() {
|
||||
res += prover.aL[*index] * prover.aR[*index] * weight;
|
||||
}
|
||||
for (WCG, C) in lincomb.WCG().iter().zip(&prover.C) {
|
||||
for (j, weight) in WCG {
|
||||
res += C.g_values[*j] * weight;
|
||||
}
|
||||
}
|
||||
for (index, weight) in lincomb.WV() {
|
||||
res += prover.V[*index].value * weight;
|
||||
}
|
||||
res
|
||||
})
|
||||
}
|
||||
|
||||
/// Multiply two values, optionally constrained, returning the constrainable left/right/out
|
||||
/// terms.
|
||||
///
|
||||
/// May panic if any linear combinations reference non-existent terms or if the witness isn't
|
||||
/// provided when proving/is provided when verifying.
|
||||
pub fn mul(
|
||||
&mut self,
|
||||
a: Option<LinComb<C::F>>,
|
||||
b: Option<LinComb<C::F>>,
|
||||
witness: Option<(C::F, C::F)>,
|
||||
) -> (Variable, Variable, Variable) {
|
||||
let l = Variable::aL(self.muls);
|
||||
let r = Variable::aR(self.muls);
|
||||
let o = Variable::aO(self.muls);
|
||||
self.muls += 1;
|
||||
|
||||
debug_assert_eq!(self.prover.is_some(), witness.is_some());
|
||||
if let Some(witness) = witness {
|
||||
let prover = self.prover.as_mut().unwrap();
|
||||
prover.aL.push(witness.0);
|
||||
prover.aR.push(witness.1);
|
||||
}
|
||||
|
||||
if let Some(a) = a {
|
||||
self.constrain_equal_to_zero(a.term(-C::F::ONE, l));
|
||||
}
|
||||
if let Some(b) = b {
|
||||
self.constrain_equal_to_zero(b.term(-C::F::ONE, r));
|
||||
}
|
||||
|
||||
(l, r, o)
|
||||
}
|
||||
|
||||
/// Constrain a linear combination to be equal to 0.
|
||||
///
|
||||
/// May panic if the linear combination references non-existent terms.
|
||||
pub fn constrain_equal_to_zero(&mut self, lincomb: LinComb<C::F>) {
|
||||
self.constraints.push(lincomb);
|
||||
}
|
||||
|
||||
/// Obtain the statement for this circuit.
|
||||
///
|
||||
/// If configured as the prover, the witness to use is also returned.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn statement(
|
||||
self,
|
||||
generators: ProofGenerators<'_, C>,
|
||||
commitments: Commitments<C>,
|
||||
) -> Result<(ArithmeticCircuitStatement<'_, C>, Option<ArithmeticCircuitWitness<C>>), AcError> {
|
||||
let statement = ArithmeticCircuitStatement::new(generators, self.constraints, commitments)?;
|
||||
|
||||
let witness = self
|
||||
.prover
|
||||
.map(|mut prover| {
|
||||
// We can't deconstruct the witness as it implements Drop (per ZeroizeOnDrop)
|
||||
// Accordingly, we take the values within it and move forward with those
|
||||
let mut aL = vec![];
|
||||
core::mem::swap(&mut prover.aL, &mut aL);
|
||||
let mut aR = vec![];
|
||||
core::mem::swap(&mut prover.aR, &mut aR);
|
||||
let mut C = vec![];
|
||||
core::mem::swap(&mut prover.C, &mut C);
|
||||
let mut V = vec![];
|
||||
core::mem::swap(&mut prover.V, &mut V);
|
||||
ArithmeticCircuitWitness::new(ScalarVector::from(aL), ScalarVector::from(aR), C, V)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok((statement, witness))
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
[package]
|
||||
name = "ec-divisors"
|
||||
version = "0.1.0"
|
||||
description = "A library for calculating elliptic curve divisors"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/divisors"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["ciphersuite", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.69"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
subtle = { version = "2", default-features = false }
|
||||
ff = { version = "0.13", default-features = false, features = ["bits"] }
|
||||
group = { version = "0.13", default-features = false }
|
||||
|
||||
hex = { version = "0.4", default-features = false, optional = true }
|
||||
dalek-ff-group = { path = "../../dalek-ff-group", default-features = false, optional = true }
|
||||
pasta_curves = { version = "0.5", git = "https://github.com/kayabaNerve/pasta_curves.git", rev = "a46b5be95cacbff54d06aad8d3bbcba42e05d616", default-features = false, features = ["bits", "alloc"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
|
||||
hex = "0.4"
|
||||
dalek-ff-group = { path = "../../dalek-ff-group", features = ["std"] }
|
||||
pasta_curves = { version = "0.5", git = "https://github.com/kayabaNerve/pasta_curves.git", rev = "a46b5be95cacbff54d06aad8d3bbcba42e05d616", default-features = false, features = ["bits", "alloc"] }
|
||||
|
||||
[features]
|
||||
std = ["std-shims/std", "zeroize/std", "subtle/std", "ff/std", "dalek-ff-group?/std"]
|
||||
ed25519 = ["hex/alloc", "dalek-ff-group"]
|
||||
pasta = ["pasta_curves"]
|
||||
default = ["std"]
|
||||
@@ -1,4 +0,0 @@
|
||||
# Elliptic Curve Divisors
|
||||
|
||||
An implementation of a representation for and construction of elliptic curve
|
||||
divisors, intended for Eagen's [EC IP work](https://eprint.iacr.org/2022/596).
|
||||
@@ -1,581 +0,0 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use subtle::{Choice, ConstantTimeEq, ConstantTimeGreater, ConditionallySelectable};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use group::{
|
||||
ff::{Field, PrimeField, PrimeFieldBits},
|
||||
Group,
|
||||
};
|
||||
|
||||
mod poly;
|
||||
pub use poly::Poly;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// A curve usable with this library.
|
||||
pub trait DivisorCurve: Group + ConstantTimeEq + ConditionallySelectable + Zeroize {
|
||||
/// An element of the field this curve is defined over.
|
||||
type FieldElement: Zeroize + PrimeField + ConditionallySelectable;
|
||||
|
||||
/// The A in the curve equation y^2 = x^3 + A x + B.
|
||||
fn a() -> Self::FieldElement;
|
||||
/// The B in the curve equation y^2 = x^3 + A x + B.
|
||||
fn b() -> Self::FieldElement;
|
||||
|
||||
/// y^2 - x^3 - A x - B
|
||||
///
|
||||
/// Section 2 of the security proofs define this modulus.
|
||||
///
|
||||
/// This MUST NOT be overriden.
|
||||
// TODO: Move to an extension trait
|
||||
fn divisor_modulus() -> Poly<Self::FieldElement> {
|
||||
Poly {
|
||||
// 0 y**1, 1 y*2
|
||||
y_coefficients: vec![Self::FieldElement::ZERO, Self::FieldElement::ONE],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![
|
||||
// - A x
|
||||
-Self::a(),
|
||||
// 0 x^2
|
||||
Self::FieldElement::ZERO,
|
||||
// - x^3
|
||||
-Self::FieldElement::ONE,
|
||||
],
|
||||
// - B
|
||||
zero_coefficient: -Self::b(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a point to its x and y coordinates.
|
||||
///
|
||||
/// Returns None if passed the point at infinity.
|
||||
///
|
||||
/// This function may run in time variable to if the point is the identity.
|
||||
fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)>;
|
||||
}
|
||||
|
||||
/// Calculate the slope and intercept between two points.
|
||||
///
|
||||
/// This function panics when `a @ infinity`, `b @ infinity`, `a == b`, or when `a == -b`.
|
||||
pub(crate) fn slope_intercept<C: DivisorCurve>(a: C, b: C) -> (C::FieldElement, C::FieldElement) {
|
||||
let (ax, ay) = C::to_xy(a).unwrap();
|
||||
debug_assert_eq!(C::divisor_modulus().eval(ax, ay), C::FieldElement::ZERO);
|
||||
let (bx, by) = C::to_xy(b).unwrap();
|
||||
debug_assert_eq!(C::divisor_modulus().eval(bx, by), C::FieldElement::ZERO);
|
||||
let slope = (by - ay) *
|
||||
Option::<C::FieldElement>::from((bx - ax).invert())
|
||||
.expect("trying to get slope/intercept of points sharing an x coordinate");
|
||||
let intercept = by - (slope * bx);
|
||||
debug_assert!(bool::from((ay - (slope * ax) - intercept).is_zero()));
|
||||
debug_assert!(bool::from((by - (slope * bx) - intercept).is_zero()));
|
||||
(slope, intercept)
|
||||
}
|
||||
|
||||
// The line interpolating two points.
|
||||
fn line<C: DivisorCurve>(a: C, b: C) -> Poly<C::FieldElement> {
|
||||
#[derive(Clone, Copy)]
|
||||
struct LinesRes<F: ConditionallySelectable> {
|
||||
y_coefficient: F,
|
||||
x_coefficient: F,
|
||||
zero_coefficient: F,
|
||||
}
|
||||
impl<F: ConditionallySelectable> ConditionallySelectable for LinesRes<F> {
|
||||
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
|
||||
Self {
|
||||
y_coefficient: <_>::conditional_select(&a.y_coefficient, &b.y_coefficient, choice),
|
||||
x_coefficient: <_>::conditional_select(&a.x_coefficient, &b.x_coefficient, choice),
|
||||
zero_coefficient: <_>::conditional_select(&a.zero_coefficient, &b.zero_coefficient, choice),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let a_is_identity = a.is_identity();
|
||||
let b_is_identity = b.is_identity();
|
||||
|
||||
// If they're both the point at infinity, we simply set the line to one
|
||||
let both_are_identity = a_is_identity & b_is_identity;
|
||||
let if_both_are_identity = LinesRes {
|
||||
y_coefficient: C::FieldElement::ZERO,
|
||||
x_coefficient: C::FieldElement::ZERO,
|
||||
zero_coefficient: C::FieldElement::ONE,
|
||||
};
|
||||
|
||||
// If either point is the point at infinity, or these are additive inverses, the line is
|
||||
// `1 * x - x`. The first `x` is a term in the polynomial, the `x` is the `x` coordinate of these
|
||||
// points (of which there is one, as the second point is either at infinity or has a matching `x`
|
||||
// coordinate).
|
||||
let one_is_identity = a_is_identity | b_is_identity;
|
||||
let additive_inverses = a.ct_eq(&-b);
|
||||
let one_is_identity_or_additive_inverses = one_is_identity | additive_inverses;
|
||||
let if_one_is_identity_or_additive_inverses = {
|
||||
// If both are identity, set `a` to the generator so we can safely evaluate the following
|
||||
// (which we won't select at the end of this function)
|
||||
let a = <_>::conditional_select(&a, &C::generator(), both_are_identity);
|
||||
// If `a` is identity, this selects `b`. If `a` isn't identity, this selects `a`
|
||||
let non_identity = <_>::conditional_select(&a, &b, a.is_identity());
|
||||
let (x, _) = C::to_xy(non_identity).unwrap();
|
||||
LinesRes {
|
||||
y_coefficient: C::FieldElement::ZERO,
|
||||
x_coefficient: C::FieldElement::ONE,
|
||||
zero_coefficient: -x,
|
||||
}
|
||||
};
|
||||
|
||||
// The following calculation assumes neither point is the point at infinity
|
||||
// If either are, we use a prior result
|
||||
// To ensure we can calculcate a result here, set any points at infinity to the generator
|
||||
let a = <_>::conditional_select(&a, &C::generator(), a_is_identity);
|
||||
let b = <_>::conditional_select(&b, &C::generator(), b_is_identity);
|
||||
// It also assumes a, b aren't additive inverses which is also covered by a prior result
|
||||
let b = <_>::conditional_select(&b, &a.double(), additive_inverses);
|
||||
|
||||
// If the points are equal, we use the line interpolating the sum of these points with the point
|
||||
// at infinity
|
||||
let b = <_>::conditional_select(&b, &-a.double(), a.ct_eq(&b));
|
||||
|
||||
let (slope, intercept) = slope_intercept::<C>(a, b);
|
||||
|
||||
// Section 4 of the proofs explicitly state the line `L = y - lambda * x - mu`
|
||||
// y - (slope * x) - intercept
|
||||
let mut res = LinesRes {
|
||||
y_coefficient: C::FieldElement::ONE,
|
||||
x_coefficient: -slope,
|
||||
zero_coefficient: -intercept,
|
||||
};
|
||||
|
||||
res = <_>::conditional_select(
|
||||
&res,
|
||||
&if_one_is_identity_or_additive_inverses,
|
||||
one_is_identity_or_additive_inverses,
|
||||
);
|
||||
res = <_>::conditional_select(&res, &if_both_are_identity, both_are_identity);
|
||||
|
||||
Poly {
|
||||
y_coefficients: vec![res.y_coefficient],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![res.x_coefficient],
|
||||
zero_coefficient: res.zero_coefficient,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a divisor interpolating the following points.
|
||||
///
|
||||
/// Returns None if:
|
||||
/// - No points were passed in
|
||||
/// - The points don't sum to the point at infinity
|
||||
/// - A passed in point was the point at infinity
|
||||
///
|
||||
/// If the arguments were valid, this function executes in an amount of time constant to the amount
|
||||
/// of points.
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new_divisor<C: DivisorCurve>(points: &[C]) -> Option<Poly<C::FieldElement>> {
|
||||
// No points were passed in, this is the point at infinity, or the single point isn't infinity
|
||||
// and accordingly doesn't sum to infinity. All three cause us to return None
|
||||
// Checks a bit other than the first bit is set, meaning this is >= 2
|
||||
let mut invalid_args = (points.len() & (!1)).ct_eq(&0);
|
||||
// The points don't sum to the point at infinity
|
||||
invalid_args |= !points.iter().sum::<C>().is_identity();
|
||||
// A point was the point at identity
|
||||
for point in points {
|
||||
invalid_args |= point.is_identity();
|
||||
}
|
||||
if bool::from(invalid_args) {
|
||||
None?;
|
||||
}
|
||||
|
||||
let points_len = points.len();
|
||||
|
||||
// Create the initial set of divisors
|
||||
let mut divs = vec![];
|
||||
let mut iter = points.iter().copied();
|
||||
while let Some(a) = iter.next() {
|
||||
let b = iter.next();
|
||||
|
||||
// Draw the line between those points
|
||||
// These unwraps are branching on the length of the iterator, not violating the constant-time
|
||||
// priorites desired
|
||||
divs.push((2, a + b.unwrap_or(C::identity()), line::<C>(a, b.unwrap_or(-a))));
|
||||
}
|
||||
|
||||
let modulus = C::divisor_modulus();
|
||||
|
||||
// Our Poly algorithm is leaky and will create an excessive amount of y x**j and x**j
|
||||
// coefficients which are zero, yet as our implementation is constant time, still come with
|
||||
// an immense performance cost. This code truncates the coefficients we know are zero.
|
||||
let trim = |divisor: &mut Poly<_>, points_len: usize| {
|
||||
// We should only be trimming divisors reduced by the modulus
|
||||
debug_assert!(divisor.yx_coefficients.len() <= 1);
|
||||
if divisor.yx_coefficients.len() == 1 {
|
||||
let truncate_to = ((points_len + 1) / 2).saturating_sub(2);
|
||||
#[cfg(debug_assertions)]
|
||||
for p in truncate_to .. divisor.yx_coefficients[0].len() {
|
||||
debug_assert_eq!(divisor.yx_coefficients[0][p], <C::FieldElement as Field>::ZERO);
|
||||
}
|
||||
divisor.yx_coefficients[0].truncate(truncate_to);
|
||||
}
|
||||
{
|
||||
let truncate_to = points_len / 2;
|
||||
#[cfg(debug_assertions)]
|
||||
for p in truncate_to .. divisor.x_coefficients.len() {
|
||||
debug_assert_eq!(divisor.x_coefficients[p], <C::FieldElement as Field>::ZERO);
|
||||
}
|
||||
divisor.x_coefficients.truncate(truncate_to);
|
||||
}
|
||||
};
|
||||
|
||||
// Pair them off until only one remains
|
||||
while divs.len() > 1 {
|
||||
let mut next_divs = vec![];
|
||||
// If there's an odd amount of divisors, carry the odd one out to the next iteration
|
||||
if (divs.len() % 2) == 1 {
|
||||
next_divs.push(divs.pop().unwrap());
|
||||
}
|
||||
|
||||
while let Some((a_points, a, a_div)) = divs.pop() {
|
||||
let (b_points, b, b_div) = divs.pop().unwrap();
|
||||
let points = a_points + b_points;
|
||||
|
||||
// Merge the two divisors
|
||||
let numerator = a_div.mul_mod(&b_div, &modulus).mul_mod(&line::<C>(a, b), &modulus);
|
||||
let denominator = line::<C>(a, -a).mul_mod(&line::<C>(b, -b), &modulus);
|
||||
let (mut q, r) = numerator.div_rem(&denominator);
|
||||
debug_assert_eq!(r, Poly::zero());
|
||||
|
||||
trim(&mut q, 1 + points);
|
||||
|
||||
next_divs.push((points, a + b, q));
|
||||
}
|
||||
|
||||
divs = next_divs;
|
||||
}
|
||||
|
||||
// Return the unified divisor
|
||||
let mut divisor = divs.remove(0).2;
|
||||
trim(&mut divisor, points_len);
|
||||
Some(divisor)
|
||||
}
|
||||
|
||||
/// The decomposition of a scalar.
|
||||
///
|
||||
/// The decomposition ($d$) of a scalar ($s$) has the following two properties:
|
||||
///
|
||||
/// - $\sum^{\mathsf{NUM_BITS} - 1}_{i=0} d_i * 2^i = s$
|
||||
/// - $\sum^{\mathsf{NUM_BITS} - 1}_{i=0} d_i = \mathsf{NUM_BITS}$
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct ScalarDecomposition<F: Zeroize + PrimeFieldBits> {
|
||||
scalar: F,
|
||||
decomposition: Vec<u64>,
|
||||
}
|
||||
|
||||
impl<F: Zeroize + PrimeFieldBits> ScalarDecomposition<F> {
|
||||
/// Decompose a non-zero scalar.
|
||||
///
|
||||
/// Returns `None` if the scalar is zero.
|
||||
///
|
||||
/// This function is constant time if the scalar is non-zero.
|
||||
pub fn new(scalar: F) -> Option<Self> {
|
||||
if bool::from(scalar.is_zero()) {
|
||||
None?;
|
||||
}
|
||||
|
||||
/*
|
||||
We need the sum of the coefficients to equal F::NUM_BITS. The scalar's bits will be less than
|
||||
F::NUM_BITS. Accordingly, we need to increment the sum of the coefficients without
|
||||
incrementing the scalar represented. We do this by finding the highest non-0 coefficient,
|
||||
decrementing it, and increasing the immediately less significant coefficient by 2. This
|
||||
increases the sum of the coefficients by 1 (-1+2=1).
|
||||
*/
|
||||
|
||||
let num_bits = u64::from(F::NUM_BITS);
|
||||
|
||||
// Obtain the bits of the scalar
|
||||
let num_bits_usize = usize::try_from(num_bits).unwrap();
|
||||
let mut decomposition = vec![0; num_bits_usize];
|
||||
for (i, bit) in scalar.to_le_bits().into_iter().take(num_bits_usize).enumerate() {
|
||||
let bit = u64::from(u8::from(bit));
|
||||
decomposition[i] = bit;
|
||||
}
|
||||
|
||||
// The following algorithm only works if the value of the scalar exceeds num_bits
|
||||
// If it isn't, we increase it by the modulus such that it does exceed num_bits
|
||||
{
|
||||
let mut less_than_num_bits = Choice::from(0);
|
||||
for i in 0 .. num_bits {
|
||||
less_than_num_bits |= scalar.ct_eq(&F::from(i));
|
||||
}
|
||||
let mut decomposition_of_modulus = vec![0; num_bits_usize];
|
||||
// Decompose negative one
|
||||
for (i, bit) in (-F::ONE).to_le_bits().into_iter().take(num_bits_usize).enumerate() {
|
||||
let bit = u64::from(u8::from(bit));
|
||||
decomposition_of_modulus[i] = bit;
|
||||
}
|
||||
// Increment it by one
|
||||
decomposition_of_modulus[0] += 1;
|
||||
|
||||
// Add the decomposition onto the decomposition of the modulus
|
||||
for i in 0 .. num_bits_usize {
|
||||
let new_decomposition = <_>::conditional_select(
|
||||
&decomposition[i],
|
||||
&(decomposition[i] + decomposition_of_modulus[i]),
|
||||
less_than_num_bits,
|
||||
);
|
||||
decomposition[i] = new_decomposition;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculcate the sum of the coefficients
|
||||
let mut sum_of_coefficients: u64 = 0;
|
||||
for decomposition in &decomposition {
|
||||
sum_of_coefficients += *decomposition;
|
||||
}
|
||||
|
||||
/*
|
||||
Now, because we added a log2(k)-bit number to a k-bit number, we may have our sum of
|
||||
coefficients be *too high*. We attempt to reduce the sum of the coefficients accordingly.
|
||||
|
||||
This algorithm is guaranteed to complete as expected. Take the sequence `222`. `222` becomes
|
||||
`032` becomes `013`. Even if the next coefficient in the sequence is `2`, the third
|
||||
coefficient will be reduced once and the next coefficient (`2`, increased to `3`) will only
|
||||
be eligible for reduction once. This demonstrates, even for a worst case of log2(k) `2`s
|
||||
followed by `1`s (as possible if the modulus is a Mersenne prime), the log2(k) `2`s can be
|
||||
reduced as necessary so long as there is a single coefficient after (requiring the entire
|
||||
sequence be at least of length log2(k) + 1). For a 2-bit number, log2(k) + 1 == 2, so this
|
||||
holds for any odd prime field.
|
||||
|
||||
To fully type out the demonstration for the Mersenne prime 3, with scalar to encode 1 (the
|
||||
highest value less than the number of bits):
|
||||
|
||||
10 - Little-endian bits of 1
|
||||
21 - Little-endian bits of 1, plus the modulus
|
||||
02 - After one reduction, where the sum of the coefficients does in fact equal 2 (the target)
|
||||
*/
|
||||
{
|
||||
let mut log2_num_bits = 0;
|
||||
while (1 << log2_num_bits) < num_bits {
|
||||
log2_num_bits += 1;
|
||||
}
|
||||
|
||||
for _ in 0 .. log2_num_bits {
|
||||
// If the sum of coefficients is the amount of bits, we're done
|
||||
let mut done = sum_of_coefficients.ct_eq(&num_bits);
|
||||
|
||||
for i in 0 .. (num_bits_usize - 1) {
|
||||
let should_act = (!done) & decomposition[i].ct_gt(&1);
|
||||
// Subtract 2 from this coefficient
|
||||
let amount_to_sub = <_>::conditional_select(&0, &2, should_act);
|
||||
decomposition[i] -= amount_to_sub;
|
||||
// Add 1 to the next coefficient
|
||||
let amount_to_add = <_>::conditional_select(&0, &1, should_act);
|
||||
decomposition[i + 1] += amount_to_add;
|
||||
|
||||
// Also update the sum of coefficients
|
||||
sum_of_coefficients -= <_>::conditional_select(&0, &1, should_act);
|
||||
|
||||
// If we updated the coefficients this loop iter, we're done for this loop iter
|
||||
done |= should_act;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0 .. num_bits {
|
||||
// If the sum of coefficients is the amount of bits, we're done
|
||||
let mut done = sum_of_coefficients.ct_eq(&num_bits);
|
||||
|
||||
// Find the highest coefficient currently non-zero
|
||||
for i in (1 .. decomposition.len()).rev() {
|
||||
// If this is non-zero, we should decrement this coefficient if we haven't already
|
||||
// decremented a coefficient this round
|
||||
let is_non_zero = !(0.ct_eq(&decomposition[i]));
|
||||
let should_act = (!done) & is_non_zero;
|
||||
|
||||
// Update this coefficient and the prior coefficient
|
||||
let amount_to_sub = <_>::conditional_select(&0, &1, should_act);
|
||||
decomposition[i] -= amount_to_sub;
|
||||
|
||||
let amount_to_add = <_>::conditional_select(&0, &2, should_act);
|
||||
// i must be at least 1, so i - 1 will be at least 0 (meaning it's safe to index with)
|
||||
decomposition[i - 1] += amount_to_add;
|
||||
|
||||
// Also update the sum of coefficients
|
||||
sum_of_coefficients += <_>::conditional_select(&0, &1, should_act);
|
||||
|
||||
// If we updated the coefficients this loop iter, we're done for this loop iter
|
||||
done |= should_act;
|
||||
}
|
||||
}
|
||||
debug_assert!(bool::from(decomposition.iter().sum::<u64>().ct_eq(&num_bits)));
|
||||
|
||||
Some(ScalarDecomposition { scalar, decomposition })
|
||||
}
|
||||
|
||||
/// The scalar.
|
||||
pub fn scalar(&self) -> &F {
|
||||
&self.scalar
|
||||
}
|
||||
|
||||
/// The decomposition of the scalar.
|
||||
pub fn decomposition(&self) -> &[u64] {
|
||||
&self.decomposition
|
||||
}
|
||||
|
||||
/// A divisor to prove a scalar multiplication.
|
||||
///
|
||||
/// The divisor will interpolate $-(s \cdot G)$ with $d_i$ instances of $2^i \cdot G$.
|
||||
///
|
||||
/// This function executes in constant time with regards to the scalar.
|
||||
///
|
||||
/// This function MAY panic if the generator is the point at infinity.
|
||||
pub fn scalar_mul_divisor<C: Zeroize + DivisorCurve<Scalar = F>>(
|
||||
&self,
|
||||
mut generator: C,
|
||||
) -> Poly<C::FieldElement> {
|
||||
// 1 is used for the resulting point, NUM_BITS is used for the decomposition, and then we store
|
||||
// one additional index in a usize for the points we shouldn't write at all (hence the +2)
|
||||
let _ = usize::try_from(<C::Scalar as PrimeField>::NUM_BITS + 2)
|
||||
.expect("NUM_BITS + 2 didn't fit in usize");
|
||||
let mut divisor_points =
|
||||
vec![C::identity(); (<C::Scalar as PrimeField>::NUM_BITS + 1) as usize];
|
||||
|
||||
// Write the inverse of the resulting point
|
||||
divisor_points[0] = -generator * self.scalar;
|
||||
|
||||
// Write the decomposition
|
||||
let mut write_above: u64 = 0;
|
||||
for coefficient in &self.decomposition {
|
||||
// Write the generator to every slot except the slots we have already written to.
|
||||
for i in 1 ..= (<C::Scalar as PrimeField>::NUM_BITS as u64) {
|
||||
divisor_points[i as usize].conditional_assign(&generator, i.ct_gt(&write_above));
|
||||
}
|
||||
|
||||
// Increase the next write start by the coefficient.
|
||||
write_above += coefficient;
|
||||
generator = generator.double();
|
||||
}
|
||||
|
||||
// Create a divisor out of the points
|
||||
let res = new_divisor(&divisor_points).unwrap();
|
||||
divisor_points.zeroize();
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "pasta"))]
|
||||
mod pasta {
|
||||
use group::{ff::Field, Curve};
|
||||
use pasta_curves::{
|
||||
arithmetic::{Coordinates, CurveAffine},
|
||||
Ep, Fp, Eq, Fq,
|
||||
};
|
||||
use crate::DivisorCurve;
|
||||
|
||||
impl DivisorCurve for Ep {
|
||||
type FieldElement = Fp;
|
||||
|
||||
fn a() -> Self::FieldElement {
|
||||
Self::FieldElement::ZERO
|
||||
}
|
||||
fn b() -> Self::FieldElement {
|
||||
Self::FieldElement::from(5u64)
|
||||
}
|
||||
|
||||
fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> {
|
||||
Option::<Coordinates<_>>::from(point.to_affine().coordinates())
|
||||
.map(|coords| (*coords.x(), *coords.y()))
|
||||
}
|
||||
}
|
||||
|
||||
impl DivisorCurve for Eq {
|
||||
type FieldElement = Fq;
|
||||
|
||||
fn a() -> Self::FieldElement {
|
||||
Self::FieldElement::ZERO
|
||||
}
|
||||
fn b() -> Self::FieldElement {
|
||||
Self::FieldElement::from(5u64)
|
||||
}
|
||||
|
||||
fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> {
|
||||
Option::<Coordinates<_>>::from(point.to_affine().coordinates())
|
||||
.map(|coords| (*coords.x(), *coords.y()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "ed25519"))]
|
||||
mod ed25519 {
|
||||
use subtle::{Choice, ConditionallySelectable};
|
||||
use group::{
|
||||
ff::{Field, PrimeField},
|
||||
Group, GroupEncoding,
|
||||
};
|
||||
use dalek_ff_group::{FieldElement, EdwardsPoint};
|
||||
|
||||
impl crate::DivisorCurve for EdwardsPoint {
|
||||
type FieldElement = FieldElement;
|
||||
|
||||
// Wei25519 a/b
|
||||
// https://www.ietf.org/archive/id/draft-ietf-lwig-curve-representations-02.pdf E.3
|
||||
fn a() -> Self::FieldElement {
|
||||
let mut be_bytes =
|
||||
hex::decode("2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144").unwrap();
|
||||
be_bytes.reverse();
|
||||
let le_bytes = be_bytes;
|
||||
Self::FieldElement::from_repr(le_bytes.try_into().unwrap()).unwrap()
|
||||
}
|
||||
fn b() -> Self::FieldElement {
|
||||
let mut be_bytes =
|
||||
hex::decode("7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864").unwrap();
|
||||
be_bytes.reverse();
|
||||
let le_bytes = be_bytes;
|
||||
|
||||
Self::FieldElement::from_repr(le_bytes.try_into().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
// https://www.ietf.org/archive/id/draft-ietf-lwig-curve-representations-02.pdf E.2
|
||||
fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> {
|
||||
if bool::from(point.is_identity()) {
|
||||
None?;
|
||||
}
|
||||
|
||||
// Extract the y coordinate from the compressed point
|
||||
let mut edwards_y = point.to_bytes();
|
||||
let x_is_odd = edwards_y[31] >> 7;
|
||||
edwards_y[31] &= (1 << 7) - 1;
|
||||
let edwards_y = Self::FieldElement::from_repr(edwards_y).unwrap();
|
||||
|
||||
// Recover the x coordinate
|
||||
let edwards_y_sq = edwards_y * edwards_y;
|
||||
let D = -Self::FieldElement::from(121665u64) *
|
||||
Self::FieldElement::from(121666u64).invert().unwrap();
|
||||
let mut edwards_x = ((edwards_y_sq - Self::FieldElement::ONE) *
|
||||
((D * edwards_y_sq) + Self::FieldElement::ONE).invert().unwrap())
|
||||
.sqrt()
|
||||
.unwrap();
|
||||
|
||||
// Negate the x coordinate if the sign doesn't match
|
||||
edwards_x = <_>::conditional_select(
|
||||
&edwards_x,
|
||||
&-edwards_x,
|
||||
edwards_x.is_odd() ^ Choice::from(x_is_odd),
|
||||
);
|
||||
|
||||
// Calculate the x and y coordinates for Wei25519
|
||||
let edwards_y_plus_one = Self::FieldElement::ONE + edwards_y;
|
||||
let one_minus_edwards_y = Self::FieldElement::ONE - edwards_y;
|
||||
let wei_x = (edwards_y_plus_one * one_minus_edwards_y.invert().unwrap()) +
|
||||
(Self::FieldElement::from(486662u64) * Self::FieldElement::from(3u64).invert().unwrap());
|
||||
let c =
|
||||
(-(Self::FieldElement::from(486662u64) + Self::FieldElement::from(2u64))).sqrt().unwrap();
|
||||
let wei_y = c * edwards_y_plus_one * (one_minus_edwards_y * edwards_x).invert().unwrap();
|
||||
Some((wei_x, wei_y))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,744 +0,0 @@
|
||||
use core::ops::{Add, Neg, Sub, Mul, Rem};
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use subtle::{Choice, ConstantTimeEq, ConstantTimeGreater, ConditionallySelectable};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use group::ff::PrimeField;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
struct CoefficientIndex {
|
||||
y_pow: u64,
|
||||
x_pow: u64,
|
||||
}
|
||||
impl ConditionallySelectable for CoefficientIndex {
|
||||
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
|
||||
Self {
|
||||
y_pow: <_>::conditional_select(&a.y_pow, &b.y_pow, choice),
|
||||
x_pow: <_>::conditional_select(&a.x_pow, &b.x_pow, choice),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ConstantTimeEq for CoefficientIndex {
|
||||
fn ct_eq(&self, other: &Self) -> Choice {
|
||||
self.y_pow.ct_eq(&other.y_pow) & self.x_pow.ct_eq(&other.x_pow)
|
||||
}
|
||||
}
|
||||
impl ConstantTimeGreater for CoefficientIndex {
|
||||
fn ct_gt(&self, other: &Self) -> Choice {
|
||||
self.y_pow.ct_gt(&other.y_pow) |
|
||||
(self.y_pow.ct_eq(&other.y_pow) & self.x_pow.ct_gt(&other.x_pow))
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure representing a Polynomial with x^i, y^i, and y^i * x^j terms.
|
||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Poly<F: From<u64> + Zeroize + PrimeField> {
|
||||
/// c\[i] * y^(i + 1)
|
||||
pub y_coefficients: Vec<F>,
|
||||
/// c\[i]\[j] * y^(i + 1) x^(j + 1)
|
||||
pub yx_coefficients: Vec<Vec<F>>,
|
||||
/// c\[i] * x^(i + 1)
|
||||
pub x_coefficients: Vec<F>,
|
||||
/// Coefficient for x^0, y^0, and x^0 y^0 (the coefficient for 1)
|
||||
pub zero_coefficient: F,
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> PartialEq for Poly<F> {
|
||||
// This is not constant time and is not meant to be
|
||||
fn eq(&self, b: &Poly<F>) -> bool {
|
||||
{
|
||||
let mutual_y_coefficients = self.y_coefficients.len().min(b.y_coefficients.len());
|
||||
if self.y_coefficients[.. mutual_y_coefficients] != b.y_coefficients[.. mutual_y_coefficients]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for coeff in &self.y_coefficients[mutual_y_coefficients ..] {
|
||||
if *coeff != F::ZERO {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for coeff in &b.y_coefficients[mutual_y_coefficients ..] {
|
||||
if *coeff != F::ZERO {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
for (i, yx_coeffs) in self.yx_coefficients.iter().enumerate() {
|
||||
for (j, coeff) in yx_coeffs.iter().enumerate() {
|
||||
if coeff != b.yx_coefficients.get(i).unwrap_or(&vec![]).get(j).unwrap_or(&F::ZERO) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Run from the other perspective in case other is longer than self
|
||||
for (i, yx_coeffs) in b.yx_coefficients.iter().enumerate() {
|
||||
for (j, coeff) in yx_coeffs.iter().enumerate() {
|
||||
if coeff != self.yx_coefficients.get(i).unwrap_or(&vec![]).get(j).unwrap_or(&F::ZERO) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mutual_x_coefficients = self.x_coefficients.len().min(b.x_coefficients.len());
|
||||
if self.x_coefficients[.. mutual_x_coefficients] != b.x_coefficients[.. mutual_x_coefficients]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for coeff in &self.x_coefficients[mutual_x_coefficients ..] {
|
||||
if *coeff != F::ZERO {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for coeff in &b.x_coefficients[mutual_x_coefficients ..] {
|
||||
if *coeff != F::ZERO {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.zero_coefficient == b.zero_coefficient
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Poly<F> {
|
||||
/// A polynomial for zero.
|
||||
pub(crate) fn zero() -> Self {
|
||||
Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![],
|
||||
zero_coefficient: F::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Add<&Self> for Poly<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(mut self, other: &Self) -> Self {
|
||||
// Expand to be the neeeded size
|
||||
while self.y_coefficients.len() < other.y_coefficients.len() {
|
||||
self.y_coefficients.push(F::ZERO);
|
||||
}
|
||||
while self.yx_coefficients.len() < other.yx_coefficients.len() {
|
||||
self.yx_coefficients.push(vec![]);
|
||||
}
|
||||
for i in 0 .. other.yx_coefficients.len() {
|
||||
while self.yx_coefficients[i].len() < other.yx_coefficients[i].len() {
|
||||
self.yx_coefficients[i].push(F::ZERO);
|
||||
}
|
||||
}
|
||||
while self.x_coefficients.len() < other.x_coefficients.len() {
|
||||
self.x_coefficients.push(F::ZERO);
|
||||
}
|
||||
|
||||
// Perform the addition
|
||||
for (i, coeff) in other.y_coefficients.iter().enumerate() {
|
||||
self.y_coefficients[i] += coeff;
|
||||
}
|
||||
for (i, coeffs) in other.yx_coefficients.iter().enumerate() {
|
||||
for (j, coeff) in coeffs.iter().enumerate() {
|
||||
self.yx_coefficients[i][j] += coeff;
|
||||
}
|
||||
}
|
||||
for (i, coeff) in other.x_coefficients.iter().enumerate() {
|
||||
self.x_coefficients[i] += coeff;
|
||||
}
|
||||
self.zero_coefficient += other.zero_coefficient;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Neg for Poly<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(mut self) -> Self {
|
||||
for y_coeff in self.y_coefficients.iter_mut() {
|
||||
*y_coeff = -*y_coeff;
|
||||
}
|
||||
for yx_coeffs in self.yx_coefficients.iter_mut() {
|
||||
for yx_coeff in yx_coeffs.iter_mut() {
|
||||
*yx_coeff = -*yx_coeff;
|
||||
}
|
||||
}
|
||||
for x_coeff in self.x_coefficients.iter_mut() {
|
||||
*x_coeff = -*x_coeff;
|
||||
}
|
||||
self.zero_coefficient = -self.zero_coefficient;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Sub for Poly<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: Self) -> Self {
|
||||
self + &-other
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Mul<F> for Poly<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(mut self, scalar: F) -> Self {
|
||||
for y_coeff in self.y_coefficients.iter_mut() {
|
||||
*y_coeff *= scalar;
|
||||
}
|
||||
for coeffs in self.yx_coefficients.iter_mut() {
|
||||
for coeff in coeffs.iter_mut() {
|
||||
*coeff *= scalar;
|
||||
}
|
||||
}
|
||||
for x_coeff in self.x_coefficients.iter_mut() {
|
||||
*x_coeff *= scalar;
|
||||
}
|
||||
self.zero_coefficient *= scalar;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Poly<F> {
|
||||
#[must_use]
|
||||
fn shift_by_x(mut self, power_of_x: usize) -> Self {
|
||||
if power_of_x == 0 {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Shift up every x coefficient
|
||||
for _ in 0 .. power_of_x {
|
||||
self.x_coefficients.insert(0, F::ZERO);
|
||||
for yx_coeffs in &mut self.yx_coefficients {
|
||||
yx_coeffs.insert(0, F::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
// Move the zero coefficient
|
||||
self.x_coefficients[power_of_x - 1] = self.zero_coefficient;
|
||||
self.zero_coefficient = F::ZERO;
|
||||
|
||||
// Move the y coefficients
|
||||
// Start by creating yx coefficients with the necessary powers of x
|
||||
let mut yx_coefficients_to_push = vec![];
|
||||
while yx_coefficients_to_push.len() < power_of_x {
|
||||
yx_coefficients_to_push.push(F::ZERO);
|
||||
}
|
||||
// Now, ensure the yx coefficients has the slots for the y coefficients we're moving
|
||||
while self.yx_coefficients.len() < self.y_coefficients.len() {
|
||||
self.yx_coefficients.push(yx_coefficients_to_push.clone());
|
||||
}
|
||||
// Perform the move
|
||||
for (i, y_coeff) in self.y_coefficients.drain(..).enumerate() {
|
||||
self.yx_coefficients[i][power_of_x - 1] = y_coeff;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn shift_by_y(mut self, power_of_y: usize) -> Self {
|
||||
if power_of_y == 0 {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Shift up every y coefficient
|
||||
for _ in 0 .. power_of_y {
|
||||
self.y_coefficients.insert(0, F::ZERO);
|
||||
self.yx_coefficients.insert(0, vec![]);
|
||||
}
|
||||
|
||||
// Move the zero coefficient
|
||||
self.y_coefficients[power_of_y - 1] = self.zero_coefficient;
|
||||
self.zero_coefficient = F::ZERO;
|
||||
|
||||
// Move the x coefficients
|
||||
core::mem::swap(&mut self.yx_coefficients[power_of_y - 1], &mut self.x_coefficients);
|
||||
self.x_coefficients = vec![];
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Mul<&Poly<F>> for Poly<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, other: &Self) -> Self {
|
||||
let mut res = self.clone() * other.zero_coefficient;
|
||||
|
||||
for (i, y_coeff) in other.y_coefficients.iter().enumerate() {
|
||||
let scaled = self.clone() * *y_coeff;
|
||||
res = res + &scaled.shift_by_y(i + 1);
|
||||
}
|
||||
|
||||
for (y_i, yx_coeffs) in other.yx_coefficients.iter().enumerate() {
|
||||
for (x_i, yx_coeff) in yx_coeffs.iter().enumerate() {
|
||||
let scaled = self.clone() * *yx_coeff;
|
||||
res = res + &scaled.shift_by_y(y_i + 1).shift_by_x(x_i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (i, x_coeff) in other.x_coefficients.iter().enumerate() {
|
||||
let scaled = self.clone() * *x_coeff;
|
||||
res = res + &scaled.shift_by_x(i + 1);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Poly<F> {
|
||||
// The leading y coefficient and associated x coefficient.
|
||||
fn leading_coefficient(&self) -> (usize, usize) {
|
||||
if self.y_coefficients.len() > self.yx_coefficients.len() {
|
||||
(self.y_coefficients.len(), 0)
|
||||
} else if !self.yx_coefficients.is_empty() {
|
||||
(self.yx_coefficients.len(), self.yx_coefficients.last().unwrap().len())
|
||||
} else {
|
||||
(0, self.x_coefficients.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the highest non-zero coefficient greater than the specified coefficient.
|
||||
///
|
||||
/// If no non-zero coefficient is greater than the specified coefficient, this will return
|
||||
/// (0, 0).
|
||||
fn greater_than_or_equal_coefficient(
|
||||
&self,
|
||||
greater_than_or_equal: &CoefficientIndex,
|
||||
) -> CoefficientIndex {
|
||||
let mut leading_coefficient = CoefficientIndex { y_pow: 0, x_pow: 0 };
|
||||
for (y_pow_sub_one, coeff) in self.y_coefficients.iter().enumerate() {
|
||||
let y_pow = u64::try_from(y_pow_sub_one + 1).unwrap();
|
||||
let coeff_is_non_zero = !coeff.is_zero();
|
||||
let potential = CoefficientIndex { y_pow, x_pow: 0 };
|
||||
leading_coefficient = <_>::conditional_select(
|
||||
&leading_coefficient,
|
||||
&potential,
|
||||
coeff_is_non_zero &
|
||||
potential.ct_gt(&leading_coefficient) &
|
||||
(potential.ct_gt(greater_than_or_equal) | potential.ct_eq(greater_than_or_equal)),
|
||||
);
|
||||
}
|
||||
for (y_pow_sub_one, yx_coefficients) in self.yx_coefficients.iter().enumerate() {
|
||||
let y_pow = u64::try_from(y_pow_sub_one + 1).unwrap();
|
||||
for (x_pow_sub_one, coeff) in yx_coefficients.iter().enumerate() {
|
||||
let x_pow = u64::try_from(x_pow_sub_one + 1).unwrap();
|
||||
let coeff_is_non_zero = !coeff.is_zero();
|
||||
let potential = CoefficientIndex { y_pow, x_pow };
|
||||
leading_coefficient = <_>::conditional_select(
|
||||
&leading_coefficient,
|
||||
&potential,
|
||||
coeff_is_non_zero &
|
||||
potential.ct_gt(&leading_coefficient) &
|
||||
(potential.ct_gt(greater_than_or_equal) | potential.ct_eq(greater_than_or_equal)),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (x_pow_sub_one, coeff) in self.x_coefficients.iter().enumerate() {
|
||||
let x_pow = u64::try_from(x_pow_sub_one + 1).unwrap();
|
||||
let coeff_is_non_zero = !coeff.is_zero();
|
||||
let potential = CoefficientIndex { y_pow: 0, x_pow };
|
||||
leading_coefficient = <_>::conditional_select(
|
||||
&leading_coefficient,
|
||||
&potential,
|
||||
coeff_is_non_zero &
|
||||
potential.ct_gt(&leading_coefficient) &
|
||||
(potential.ct_gt(greater_than_or_equal) | potential.ct_eq(greater_than_or_equal)),
|
||||
);
|
||||
}
|
||||
leading_coefficient
|
||||
}
|
||||
|
||||
/// Perform multiplication mod `modulus`.
|
||||
#[must_use]
|
||||
pub(crate) fn mul_mod(self, other: &Self, modulus: &Self) -> Self {
|
||||
(self * other) % modulus
|
||||
}
|
||||
|
||||
/// Perform division, returning the result and remainder.
|
||||
///
|
||||
/// This function is constant time to the structure of the numerator and denominator. The actual
|
||||
/// value of the coefficients will not introduce timing differences.
|
||||
///
|
||||
/// Panics upon division by a polynomial where all coefficients are zero.
|
||||
#[must_use]
|
||||
pub(crate) fn div_rem(self, denominator: &Self) -> (Self, Self) {
|
||||
// These functions have undefined behavior if this isn't a valid index for this poly
|
||||
fn ct_get<F: From<u64> + Zeroize + PrimeField>(poly: &Poly<F>, index: CoefficientIndex) -> F {
|
||||
let mut res = poly.zero_coefficient;
|
||||
for (y_pow_sub_one, coeff) in poly.y_coefficients.iter().enumerate() {
|
||||
res = <_>::conditional_select(
|
||||
&res,
|
||||
coeff,
|
||||
index
|
||||
.ct_eq(&CoefficientIndex { y_pow: (y_pow_sub_one + 1).try_into().unwrap(), x_pow: 0 }),
|
||||
);
|
||||
}
|
||||
for (y_pow_sub_one, coeffs) in poly.yx_coefficients.iter().enumerate() {
|
||||
for (x_pow_sub_one, coeff) in coeffs.iter().enumerate() {
|
||||
res = <_>::conditional_select(
|
||||
&res,
|
||||
coeff,
|
||||
index.ct_eq(&CoefficientIndex {
|
||||
y_pow: (y_pow_sub_one + 1).try_into().unwrap(),
|
||||
x_pow: (x_pow_sub_one + 1).try_into().unwrap(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (x_pow_sub_one, coeff) in poly.x_coefficients.iter().enumerate() {
|
||||
res = <_>::conditional_select(
|
||||
&res,
|
||||
coeff,
|
||||
index
|
||||
.ct_eq(&CoefficientIndex { y_pow: 0, x_pow: (x_pow_sub_one + 1).try_into().unwrap() }),
|
||||
);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn ct_set<F: From<u64> + Zeroize + PrimeField>(
|
||||
poly: &mut Poly<F>,
|
||||
index: CoefficientIndex,
|
||||
value: F,
|
||||
) {
|
||||
for (y_pow_sub_one, coeff) in poly.y_coefficients.iter_mut().enumerate() {
|
||||
*coeff = <_>::conditional_select(
|
||||
coeff,
|
||||
&value,
|
||||
index
|
||||
.ct_eq(&CoefficientIndex { y_pow: (y_pow_sub_one + 1).try_into().unwrap(), x_pow: 0 }),
|
||||
);
|
||||
}
|
||||
for (y_pow_sub_one, coeffs) in poly.yx_coefficients.iter_mut().enumerate() {
|
||||
for (x_pow_sub_one, coeff) in coeffs.iter_mut().enumerate() {
|
||||
*coeff = <_>::conditional_select(
|
||||
coeff,
|
||||
&value,
|
||||
index.ct_eq(&CoefficientIndex {
|
||||
y_pow: (y_pow_sub_one + 1).try_into().unwrap(),
|
||||
x_pow: (x_pow_sub_one + 1).try_into().unwrap(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (x_pow_sub_one, coeff) in poly.x_coefficients.iter_mut().enumerate() {
|
||||
*coeff = <_>::conditional_select(
|
||||
coeff,
|
||||
&value,
|
||||
index
|
||||
.ct_eq(&CoefficientIndex { y_pow: 0, x_pow: (x_pow_sub_one + 1).try_into().unwrap() }),
|
||||
);
|
||||
}
|
||||
poly.zero_coefficient = <_>::conditional_select(
|
||||
&poly.zero_coefficient,
|
||||
&value,
|
||||
index.ct_eq(&CoefficientIndex { y_pow: 0, x_pow: 0 }),
|
||||
);
|
||||
}
|
||||
|
||||
fn conditional_select_poly<F: From<u64> + Zeroize + PrimeField>(
|
||||
mut a: Poly<F>,
|
||||
mut b: Poly<F>,
|
||||
choice: Choice,
|
||||
) -> Poly<F> {
|
||||
let pad_to = |a: &mut Poly<F>, b: &Poly<F>| {
|
||||
while a.x_coefficients.len() < b.x_coefficients.len() {
|
||||
a.x_coefficients.push(F::ZERO);
|
||||
}
|
||||
while a.yx_coefficients.len() < b.yx_coefficients.len() {
|
||||
a.yx_coefficients.push(vec![]);
|
||||
}
|
||||
for (a, b) in a.yx_coefficients.iter_mut().zip(&b.yx_coefficients) {
|
||||
while a.len() < b.len() {
|
||||
a.push(F::ZERO);
|
||||
}
|
||||
}
|
||||
while a.y_coefficients.len() < b.y_coefficients.len() {
|
||||
a.y_coefficients.push(F::ZERO);
|
||||
}
|
||||
};
|
||||
// Pad these to be the same size/layout as each other
|
||||
pad_to(&mut a, &b);
|
||||
pad_to(&mut b, &a);
|
||||
|
||||
let mut res = Poly::zero();
|
||||
for (a, b) in a.y_coefficients.iter().zip(&b.y_coefficients) {
|
||||
res.y_coefficients.push(<_>::conditional_select(a, b, choice));
|
||||
}
|
||||
for (a, b) in a.yx_coefficients.iter().zip(&b.yx_coefficients) {
|
||||
let mut yx_coefficients = Vec::with_capacity(a.len());
|
||||
for (a, b) in a.iter().zip(b) {
|
||||
yx_coefficients.push(<_>::conditional_select(a, b, choice))
|
||||
}
|
||||
res.yx_coefficients.push(yx_coefficients);
|
||||
}
|
||||
for (a, b) in a.x_coefficients.iter().zip(&b.x_coefficients) {
|
||||
res.x_coefficients.push(<_>::conditional_select(a, b, choice));
|
||||
}
|
||||
res.zero_coefficient =
|
||||
<_>::conditional_select(&a.zero_coefficient, &b.zero_coefficient, choice);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
// The following long division algorithm only works if the denominator actually has a variable
|
||||
// If the denominator isn't variable to anything, short-circuit to scalar 'division'
|
||||
// This is safe as `leading_coefficient` is based on the structure, not the values, of the poly
|
||||
let denominator_leading_coefficient = denominator.leading_coefficient();
|
||||
if denominator_leading_coefficient == (0, 0) {
|
||||
return (self * denominator.zero_coefficient.invert().unwrap(), Poly::zero());
|
||||
}
|
||||
|
||||
// The structure of the quotient, which is the the numerator with all coefficients set to 0
|
||||
let mut quotient_structure = Poly {
|
||||
y_coefficients: vec![F::ZERO; self.y_coefficients.len()],
|
||||
yx_coefficients: self.yx_coefficients.clone(),
|
||||
x_coefficients: vec![F::ZERO; self.x_coefficients.len()],
|
||||
zero_coefficient: F::ZERO,
|
||||
};
|
||||
for coeff in quotient_structure
|
||||
.yx_coefficients
|
||||
.iter_mut()
|
||||
.flat_map(|yx_coefficients| yx_coefficients.iter_mut())
|
||||
{
|
||||
*coeff = F::ZERO;
|
||||
}
|
||||
|
||||
// Calculate the amount of iterations we need to perform
|
||||
let iterations = self.y_coefficients.len() +
|
||||
self.yx_coefficients.iter().map(|yx_coefficients| yx_coefficients.len()).sum::<usize>() +
|
||||
self.x_coefficients.len();
|
||||
|
||||
// Find the highest non-zero coefficient in the denominator
|
||||
// This is the coefficient which we actually perform division with
|
||||
let denominator_dividing_coefficient =
|
||||
denominator.greater_than_or_equal_coefficient(&CoefficientIndex { y_pow: 0, x_pow: 0 });
|
||||
let denominator_dividing_coefficient_inv =
|
||||
ct_get(denominator, denominator_dividing_coefficient).invert().unwrap();
|
||||
|
||||
let mut quotient = quotient_structure.clone();
|
||||
let mut remainder = self.clone();
|
||||
for _ in 0 .. iterations {
|
||||
// Find the numerator coefficient we're clearing
|
||||
// This will be (0, 0) if we aren't clearing a coefficient
|
||||
let numerator_coefficient =
|
||||
remainder.greater_than_or_equal_coefficient(&denominator_dividing_coefficient);
|
||||
|
||||
// We only apply the effects of this iteration if the numerator's coefficient is actually >=
|
||||
let meaningful_iteration = numerator_coefficient.ct_gt(&denominator_dividing_coefficient) |
|
||||
numerator_coefficient.ct_eq(&denominator_dividing_coefficient);
|
||||
|
||||
// 1) Find the scalar `q` such that the leading coefficient of `q * denominator` is equal to
|
||||
// the leading coefficient of self.
|
||||
let numerator_coefficient_value = ct_get(&remainder, numerator_coefficient);
|
||||
let q = numerator_coefficient_value * denominator_dividing_coefficient_inv;
|
||||
|
||||
// 2) Calculate the full term of the quotient by scaling with the necessary powers of y/x
|
||||
let proper_powers_of_yx = CoefficientIndex {
|
||||
y_pow: numerator_coefficient.y_pow.wrapping_sub(denominator_dividing_coefficient.y_pow),
|
||||
x_pow: numerator_coefficient.x_pow.wrapping_sub(denominator_dividing_coefficient.x_pow),
|
||||
};
|
||||
let fallabck_powers_of_yx = CoefficientIndex { y_pow: 0, x_pow: 0 };
|
||||
let mut quotient_term = quotient_structure.clone();
|
||||
ct_set(
|
||||
&mut quotient_term,
|
||||
// If the numerator coefficient isn't >=, proper_powers_of_yx will have garbage in them
|
||||
<_>::conditional_select(&fallabck_powers_of_yx, &proper_powers_of_yx, meaningful_iteration),
|
||||
q,
|
||||
);
|
||||
|
||||
let quotient_if_meaningful = quotient.clone() + "ient_term;
|
||||
quotient = conditional_select_poly(quotient, quotient_if_meaningful, meaningful_iteration);
|
||||
|
||||
// 3) Remove what we've divided out from self
|
||||
let remainder_if_meaningful = remainder.clone() - (quotient_term * denominator);
|
||||
remainder = conditional_select_poly(remainder, remainder_if_meaningful, meaningful_iteration);
|
||||
}
|
||||
|
||||
quotient = conditional_select_poly(
|
||||
quotient,
|
||||
// If the dividing coefficient was for y**0 x**0, we return the poly scaled by its inverse
|
||||
self * denominator_dividing_coefficient_inv,
|
||||
denominator_dividing_coefficient.ct_eq(&CoefficientIndex { y_pow: 0, x_pow: 0 }),
|
||||
);
|
||||
remainder = conditional_select_poly(
|
||||
remainder,
|
||||
// If the dividing coefficient was for y**0 x**0, we're able to perfectly divide and there's
|
||||
// no remainder
|
||||
Poly::zero(),
|
||||
denominator_dividing_coefficient.ct_eq(&CoefficientIndex { y_pow: 0, x_pow: 0 }),
|
||||
);
|
||||
|
||||
// Clear any junk terms out of the remainder which are less than the denominator
|
||||
let denominator_leading_coefficient = CoefficientIndex {
|
||||
y_pow: denominator_leading_coefficient.0.try_into().unwrap(),
|
||||
x_pow: denominator_leading_coefficient.1.try_into().unwrap(),
|
||||
};
|
||||
if denominator_leading_coefficient != (CoefficientIndex { y_pow: 0, x_pow: 0 }) {
|
||||
while {
|
||||
let index =
|
||||
CoefficientIndex { y_pow: remainder.y_coefficients.len().try_into().unwrap(), x_pow: 0 };
|
||||
bool::from(
|
||||
index.ct_gt(&denominator_leading_coefficient) |
|
||||
index.ct_eq(&denominator_leading_coefficient),
|
||||
)
|
||||
} {
|
||||
let popped = remainder.y_coefficients.pop();
|
||||
debug_assert_eq!(popped, Some(F::ZERO));
|
||||
}
|
||||
while {
|
||||
let index = CoefficientIndex {
|
||||
y_pow: remainder.yx_coefficients.len().try_into().unwrap(),
|
||||
x_pow: remainder
|
||||
.yx_coefficients
|
||||
.last()
|
||||
.map(|yx_coefficients| yx_coefficients.len())
|
||||
.unwrap_or(0)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
};
|
||||
bool::from(
|
||||
index.ct_gt(&denominator_leading_coefficient) |
|
||||
index.ct_eq(&denominator_leading_coefficient),
|
||||
)
|
||||
} {
|
||||
let popped = remainder.yx_coefficients.last_mut().unwrap().pop();
|
||||
// This may have been `vec![]`
|
||||
if let Some(popped) = popped {
|
||||
debug_assert_eq!(popped, F::ZERO);
|
||||
}
|
||||
if remainder.yx_coefficients.last().unwrap().is_empty() {
|
||||
let popped = remainder.yx_coefficients.pop();
|
||||
debug_assert_eq!(popped, Some(vec![]));
|
||||
}
|
||||
}
|
||||
while {
|
||||
let index =
|
||||
CoefficientIndex { y_pow: 0, x_pow: remainder.x_coefficients.len().try_into().unwrap() };
|
||||
bool::from(
|
||||
index.ct_gt(&denominator_leading_coefficient) |
|
||||
index.ct_eq(&denominator_leading_coefficient),
|
||||
)
|
||||
} {
|
||||
let popped = remainder.x_coefficients.pop();
|
||||
debug_assert_eq!(popped, Some(F::ZERO));
|
||||
}
|
||||
}
|
||||
|
||||
(quotient, remainder)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Rem<&Self> for Poly<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn rem(self, modulus: &Self) -> Self {
|
||||
self.div_rem(modulus).1
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: From<u64> + Zeroize + PrimeField> Poly<F> {
|
||||
/// Evaluate this polynomial with the specified x/y values.
|
||||
///
|
||||
/// Panics on polynomials with terms whose powers exceed 2^64.
|
||||
#[must_use]
|
||||
pub fn eval(&self, x: F, y: F) -> F {
|
||||
let mut res = self.zero_coefficient;
|
||||
for (pow, coeff) in
|
||||
self.y_coefficients.iter().enumerate().map(|(i, v)| (u64::try_from(i + 1).unwrap(), v))
|
||||
{
|
||||
res += y.pow([pow]) * coeff;
|
||||
}
|
||||
for (y_pow, coeffs) in
|
||||
self.yx_coefficients.iter().enumerate().map(|(i, v)| (u64::try_from(i + 1).unwrap(), v))
|
||||
{
|
||||
let y_pow = y.pow([y_pow]);
|
||||
for (x_pow, coeff) in
|
||||
coeffs.iter().enumerate().map(|(i, v)| (u64::try_from(i + 1).unwrap(), v))
|
||||
{
|
||||
res += y_pow * x.pow([x_pow]) * coeff;
|
||||
}
|
||||
}
|
||||
for (pow, coeff) in
|
||||
self.x_coefficients.iter().enumerate().map(|(i, v)| (u64::try_from(i + 1).unwrap(), v))
|
||||
{
|
||||
res += x.pow([pow]) * coeff;
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Differentiate a polynomial, reduced by a modulus with a leading y term y^2 x^0, by x and y.
|
||||
///
|
||||
/// This function has undefined behavior if unreduced.
|
||||
#[must_use]
|
||||
pub fn differentiate(&self) -> (Poly<F>, Poly<F>) {
|
||||
// Differentation by x practically involves:
|
||||
// - Dropping everything without an x component
|
||||
// - Shifting everything down a power of x
|
||||
// - Multiplying the new coefficient by the power it prior was used with
|
||||
let diff_x = {
|
||||
let mut diff_x = Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![],
|
||||
zero_coefficient: F::ZERO,
|
||||
};
|
||||
if !self.x_coefficients.is_empty() {
|
||||
let mut x_coeffs = self.x_coefficients.clone();
|
||||
diff_x.zero_coefficient = x_coeffs.remove(0);
|
||||
diff_x.x_coefficients = x_coeffs;
|
||||
|
||||
let mut prior_x_power = F::from(2);
|
||||
for x_coeff in &mut diff_x.x_coefficients {
|
||||
*x_coeff *= prior_x_power;
|
||||
prior_x_power += F::ONE;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.yx_coefficients.is_empty() {
|
||||
let mut yx_coeffs = self.yx_coefficients[0].clone();
|
||||
if !yx_coeffs.is_empty() {
|
||||
diff_x.y_coefficients = vec![yx_coeffs.remove(0)];
|
||||
diff_x.yx_coefficients = vec![yx_coeffs];
|
||||
|
||||
let mut prior_x_power = F::from(2);
|
||||
for yx_coeff in &mut diff_x.yx_coefficients[0] {
|
||||
*yx_coeff *= prior_x_power;
|
||||
prior_x_power += F::ONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
diff_x
|
||||
};
|
||||
|
||||
// Differentation by y is trivial
|
||||
// It's the y coefficient as the zero coefficient, and the yx coefficients as the x
|
||||
// coefficients
|
||||
// This is thanks to any y term over y^2 being reduced out
|
||||
let diff_y = Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: self.yx_coefficients.first().cloned().unwrap_or(vec![]),
|
||||
zero_coefficient: self.y_coefficients.first().cloned().unwrap_or(F::ZERO),
|
||||
};
|
||||
|
||||
(diff_x, diff_y)
|
||||
}
|
||||
|
||||
/// Normalize the x coefficient to 1.
|
||||
///
|
||||
/// Panics if there is no x coefficient to normalize or if it cannot be normalized to 1.
|
||||
#[must_use]
|
||||
pub fn normalize_x_coefficient(self) -> Self {
|
||||
let scalar = self.x_coefficients[0].invert().unwrap();
|
||||
self * scalar
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use group::{ff::Field, Group};
|
||||
use dalek_ff_group::EdwardsPoint;
|
||||
use pasta_curves::{Ep, Eq};
|
||||
|
||||
use crate::{DivisorCurve, Poly, new_divisor};
|
||||
|
||||
mod poly;
|
||||
|
||||
// Equation 4 in the security proofs
|
||||
fn check_divisor<C: DivisorCurve>(points: Vec<C>) {
|
||||
// Create the divisor
|
||||
let divisor = new_divisor::<C>(&points).unwrap();
|
||||
let eval = |c| {
|
||||
let (x, y) = C::to_xy(c).unwrap();
|
||||
divisor.eval(x, y)
|
||||
};
|
||||
|
||||
// Decide challgenges
|
||||
let c0 = C::random(&mut OsRng);
|
||||
let c1 = C::random(&mut OsRng);
|
||||
let c2 = -(c0 + c1);
|
||||
let (slope, intercept) = crate::slope_intercept::<C>(c0, c1);
|
||||
|
||||
let mut rhs = <C as DivisorCurve>::FieldElement::ONE;
|
||||
for point in points {
|
||||
let (x, y) = C::to_xy(point).unwrap();
|
||||
rhs *= intercept - (y - (slope * x));
|
||||
}
|
||||
assert_eq!(eval(c0) * eval(c1) * eval(c2), rhs);
|
||||
}
|
||||
|
||||
fn test_divisor<C: DivisorCurve>() {
|
||||
for i in 1 ..= 255 {
|
||||
println!("Test iteration {i}");
|
||||
|
||||
// Select points
|
||||
let mut points = vec![];
|
||||
for _ in 0 .. i {
|
||||
points.push(C::random(&mut OsRng));
|
||||
}
|
||||
points.push(-points.iter().sum::<C>());
|
||||
println!("Points {}", points.len());
|
||||
|
||||
// Perform the original check
|
||||
check_divisor(points.clone());
|
||||
|
||||
// Create the divisor
|
||||
let divisor = new_divisor::<C>(&points).unwrap();
|
||||
|
||||
// For a divisor interpolating 256 points, as one does when interpreting a 255-bit discrete log
|
||||
// with the result of its scalar multiplication against a fixed generator, the lengths of the
|
||||
// yx/x coefficients shouldn't supersede the following bounds
|
||||
assert!((divisor.yx_coefficients.first().unwrap_or(&vec![]).len()) <= 126);
|
||||
assert!((divisor.x_coefficients.len() - 1) <= 127);
|
||||
assert!(
|
||||
(1 + divisor.yx_coefficients.first().unwrap_or(&vec![]).len() +
|
||||
(divisor.x_coefficients.len() - 1) +
|
||||
1) <=
|
||||
255
|
||||
);
|
||||
|
||||
// Decide challgenges
|
||||
let c0 = C::random(&mut OsRng);
|
||||
let c1 = C::random(&mut OsRng);
|
||||
let c2 = -(c0 + c1);
|
||||
let (slope, intercept) = crate::slope_intercept::<C>(c0, c1);
|
||||
|
||||
// Perform the Logarithmic derivative check
|
||||
{
|
||||
let dx_over_dz = {
|
||||
let dx = Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![C::FieldElement::ZERO, C::FieldElement::from(3)],
|
||||
zero_coefficient: C::a(),
|
||||
};
|
||||
|
||||
let dy = Poly {
|
||||
y_coefficients: vec![C::FieldElement::from(2)],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![],
|
||||
zero_coefficient: C::FieldElement::ZERO,
|
||||
};
|
||||
|
||||
let dz = (dy.clone() * -slope) + &dx;
|
||||
|
||||
// We want dx/dz, and dz/dx is equal to dy/dx - slope
|
||||
// Sagemath claims this, dy / dz, is the proper inverse
|
||||
(dy, dz)
|
||||
};
|
||||
|
||||
{
|
||||
let sanity_eval = |c| {
|
||||
let (x, y) = C::to_xy(c).unwrap();
|
||||
dx_over_dz.0.eval(x, y) * dx_over_dz.1.eval(x, y).invert().unwrap()
|
||||
};
|
||||
let sanity = sanity_eval(c0) + sanity_eval(c1) + sanity_eval(c2);
|
||||
// This verifies the dx/dz polynomial is correct
|
||||
assert_eq!(sanity, C::FieldElement::ZERO);
|
||||
}
|
||||
|
||||
// Logarithmic derivative check
|
||||
let test = |divisor: Poly<_>| {
|
||||
let (dx, dy) = divisor.differentiate();
|
||||
|
||||
let lhs = |c| {
|
||||
let (x, y) = C::to_xy(c).unwrap();
|
||||
|
||||
let n_0 = (C::FieldElement::from(3) * (x * x)) + C::a();
|
||||
let d_0 = (C::FieldElement::from(2) * y).invert().unwrap();
|
||||
let p_0_n_0 = n_0 * d_0;
|
||||
|
||||
let n_1 = dy.eval(x, y);
|
||||
let first = p_0_n_0 * n_1;
|
||||
|
||||
let second = dx.eval(x, y);
|
||||
|
||||
let d_1 = divisor.eval(x, y);
|
||||
|
||||
let fraction_1_n = first + second;
|
||||
let fraction_1_d = d_1;
|
||||
|
||||
let fraction_2_n = dx_over_dz.0.eval(x, y);
|
||||
let fraction_2_d = dx_over_dz.1.eval(x, y);
|
||||
|
||||
fraction_1_n * fraction_2_n * (fraction_1_d * fraction_2_d).invert().unwrap()
|
||||
};
|
||||
let lhs = lhs(c0) + lhs(c1) + lhs(c2);
|
||||
|
||||
let mut rhs = C::FieldElement::ZERO;
|
||||
for point in &points {
|
||||
let (x, y) = <C as DivisorCurve>::to_xy(*point).unwrap();
|
||||
rhs += (intercept - (y - (slope * x))).invert().unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(lhs, rhs);
|
||||
};
|
||||
// Test the divisor and the divisor with a normalized x coefficient
|
||||
test(divisor.clone());
|
||||
test(divisor.normalize_x_coefficient());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_same_point<C: DivisorCurve>() {
|
||||
let mut points = vec![C::random(&mut OsRng)];
|
||||
points.push(points[0]);
|
||||
points.push(-points.iter().sum::<C>());
|
||||
check_divisor(points);
|
||||
}
|
||||
|
||||
fn test_subset_sum_to_infinity<C: DivisorCurve>() {
|
||||
// Internally, a binary tree algorithm is used
|
||||
// This executes the first pass to end up with [0, 0] for further reductions
|
||||
{
|
||||
let mut points = vec![C::random(&mut OsRng)];
|
||||
points.push(-points[0]);
|
||||
|
||||
let next = C::random(&mut OsRng);
|
||||
points.push(next);
|
||||
points.push(-next);
|
||||
check_divisor(points);
|
||||
}
|
||||
|
||||
// This executes the first pass to end up with [0, X, -X, 0]
|
||||
{
|
||||
let mut points = vec![C::random(&mut OsRng)];
|
||||
points.push(-points[0]);
|
||||
|
||||
let x_1 = C::random(&mut OsRng);
|
||||
let x_2 = C::random(&mut OsRng);
|
||||
points.push(x_1);
|
||||
points.push(x_2);
|
||||
|
||||
points.push(-x_1);
|
||||
points.push(-x_2);
|
||||
|
||||
let next = C::random(&mut OsRng);
|
||||
points.push(next);
|
||||
points.push(-next);
|
||||
check_divisor(points);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_divisor_pallas() {
|
||||
test_same_point::<Ep>();
|
||||
test_subset_sum_to_infinity::<Ep>();
|
||||
test_divisor::<Ep>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_divisor_vesta() {
|
||||
test_same_point::<Eq>();
|
||||
test_subset_sum_to_infinity::<Eq>();
|
||||
test_divisor::<Eq>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_divisor_ed25519() {
|
||||
// Since we're implementing Wei25519 ourselves, check the isomorphism works as expected
|
||||
{
|
||||
let incomplete_add = |p1, p2| {
|
||||
let (x1, y1) = EdwardsPoint::to_xy(p1).unwrap();
|
||||
let (x2, y2) = EdwardsPoint::to_xy(p2).unwrap();
|
||||
|
||||
// mmadd-1998-cmo
|
||||
let u = y2 - y1;
|
||||
let uu = u * u;
|
||||
let v = x2 - x1;
|
||||
let vv = v * v;
|
||||
let vvv = v * vv;
|
||||
let R = vv * x1;
|
||||
let A = uu - vvv - R.double();
|
||||
let x3 = v * A;
|
||||
let y3 = (u * (R - A)) - (vvv * y1);
|
||||
let z3 = vvv;
|
||||
|
||||
// Normalize from XYZ to XY
|
||||
let x3 = x3 * z3.invert().unwrap();
|
||||
let y3 = y3 * z3.invert().unwrap();
|
||||
|
||||
// Edwards addition -> Wei25519 coordinates should be equivalent to Wei25519 addition
|
||||
assert_eq!(EdwardsPoint::to_xy(p1 + p2).unwrap(), (x3, y3));
|
||||
};
|
||||
|
||||
for _ in 0 .. 256 {
|
||||
incomplete_add(EdwardsPoint::random(&mut OsRng), EdwardsPoint::random(&mut OsRng));
|
||||
}
|
||||
}
|
||||
|
||||
test_same_point::<EdwardsPoint>();
|
||||
test_subset_sum_to_infinity::<EdwardsPoint>();
|
||||
test_divisor::<EdwardsPoint>();
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use group::ff::Field;
|
||||
use pasta_curves::Ep;
|
||||
|
||||
use crate::{DivisorCurve, Poly};
|
||||
|
||||
type F = <Ep as DivisorCurve>::FieldElement;
|
||||
|
||||
#[test]
|
||||
fn test_poly() {
|
||||
let zero = F::ZERO;
|
||||
let one = F::ONE;
|
||||
|
||||
{
|
||||
let mut poly = Poly::zero();
|
||||
poly.y_coefficients = vec![zero, one];
|
||||
|
||||
let mut modulus = Poly::zero();
|
||||
modulus.y_coefficients = vec![one];
|
||||
assert_eq!(
|
||||
poly.clone().div_rem(&modulus).0,
|
||||
Poly {
|
||||
y_coefficients: vec![one],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![],
|
||||
zero_coefficient: zero
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
poly % &modulus,
|
||||
Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![],
|
||||
zero_coefficient: zero
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let mut poly = Poly::zero();
|
||||
poly.y_coefficients = vec![zero, one];
|
||||
|
||||
let mut squared = Poly::zero();
|
||||
squared.y_coefficients = vec![zero, zero, zero, one];
|
||||
assert_eq!(poly.clone() * &poly, squared);
|
||||
}
|
||||
|
||||
{
|
||||
let mut a = Poly::zero();
|
||||
a.zero_coefficient = F::from(2u64);
|
||||
|
||||
let mut b = Poly::zero();
|
||||
b.zero_coefficient = F::from(3u64);
|
||||
|
||||
let mut res = Poly::zero();
|
||||
res.zero_coefficient = F::from(6u64);
|
||||
assert_eq!(a.clone() * &b, res);
|
||||
|
||||
b.y_coefficients = vec![F::from(4u64)];
|
||||
res.y_coefficients = vec![F::from(8u64)];
|
||||
assert_eq!(a.clone() * &b, res);
|
||||
assert_eq!(b.clone() * &a, res);
|
||||
|
||||
a.x_coefficients = vec![F::from(5u64)];
|
||||
res.x_coefficients = vec![F::from(15u64)];
|
||||
res.yx_coefficients = vec![vec![F::from(20u64)]];
|
||||
assert_eq!(a.clone() * &b, res);
|
||||
assert_eq!(b * &a, res);
|
||||
|
||||
// res is now 20xy + 8*y + 15*x + 6
|
||||
// res ** 2 =
|
||||
// 400*x^2*y^2 + 320*x*y^2 + 64*y^2 + 600*x^2*y + 480*x*y + 96*y + 225*x^2 + 180*x + 36
|
||||
|
||||
let mut squared = Poly::zero();
|
||||
squared.y_coefficients = vec![F::from(96u64), F::from(64u64)];
|
||||
squared.yx_coefficients =
|
||||
vec![vec![F::from(480u64), F::from(600u64)], vec![F::from(320u64), F::from(400u64)]];
|
||||
squared.x_coefficients = vec![F::from(180u64), F::from(225u64)];
|
||||
squared.zero_coefficient = F::from(36u64);
|
||||
assert_eq!(res.clone() * &res, squared);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_differentation() {
|
||||
let random = || F::random(&mut OsRng);
|
||||
|
||||
let input = Poly {
|
||||
y_coefficients: vec![random()],
|
||||
yx_coefficients: vec![vec![random()]],
|
||||
x_coefficients: vec![random(), random(), random()],
|
||||
zero_coefficient: random(),
|
||||
};
|
||||
let (diff_x, diff_y) = input.differentiate();
|
||||
assert_eq!(
|
||||
diff_x,
|
||||
Poly {
|
||||
y_coefficients: vec![input.yx_coefficients[0][0]],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![
|
||||
F::from(2) * input.x_coefficients[1],
|
||||
F::from(3) * input.x_coefficients[2]
|
||||
],
|
||||
zero_coefficient: input.x_coefficients[0],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
diff_y,
|
||||
Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![input.yx_coefficients[0][0]],
|
||||
zero_coefficient: input.y_coefficients[0],
|
||||
}
|
||||
);
|
||||
|
||||
let input = Poly {
|
||||
y_coefficients: vec![random()],
|
||||
yx_coefficients: vec![vec![random(), random()]],
|
||||
x_coefficients: vec![random(), random(), random(), random()],
|
||||
zero_coefficient: random(),
|
||||
};
|
||||
let (diff_x, diff_y) = input.differentiate();
|
||||
assert_eq!(
|
||||
diff_x,
|
||||
Poly {
|
||||
y_coefficients: vec![input.yx_coefficients[0][0]],
|
||||
yx_coefficients: vec![vec![F::from(2) * input.yx_coefficients[0][1]]],
|
||||
x_coefficients: vec![
|
||||
F::from(2) * input.x_coefficients[1],
|
||||
F::from(3) * input.x_coefficients[2],
|
||||
F::from(4) * input.x_coefficients[3],
|
||||
],
|
||||
zero_coefficient: input.x_coefficients[0],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
diff_y,
|
||||
Poly {
|
||||
y_coefficients: vec![],
|
||||
yx_coefficients: vec![],
|
||||
x_coefficients: vec![input.yx_coefficients[0][0], input.yx_coefficients[0][1]],
|
||||
zero_coefficient: input.y_coefficients[0],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "generalized-bulletproofs-ec-gadgets"
|
||||
version = "0.1.0"
|
||||
description = "Gadgets for working with an embedded Elliptic Curve in a Generalized Bulletproofs circuit"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/fcmps/ec-gadgets"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["bulletproofs", "circuit", "divisors"]
|
||||
edition = "2021"
|
||||
rust-version = "1.69"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||
|
||||
generic-array = { version = "1", default-features = false, features = ["alloc"] }
|
||||
|
||||
ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false }
|
||||
|
||||
generalized-bulletproofs-circuit-abstraction = { path = "../circuit-abstraction", default-features = false }
|
||||
|
||||
[features]
|
||||
std = ["std-shims/std", "ciphersuite/std", "generalized-bulletproofs-circuit-abstraction/std"]
|
||||
default = ["std"]
|
||||
@@ -1,3 +0,0 @@
|
||||
# Generalized Bulletproofs Circuit Abstraction
|
||||
|
||||
A circuit abstraction around `generalized-bulletproofs`.
|
||||
@@ -1,524 +0,0 @@
|
||||
use core::fmt;
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use ciphersuite::{
|
||||
group::ff::{Field, PrimeField, BatchInverter},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
use generalized_bulletproofs_circuit_abstraction::*;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Parameters for a discrete logarithm proof.
|
||||
pub trait DiscreteLogParameters {
|
||||
/// The amount of bits used to represent a scalar.
|
||||
type ScalarBits: ArrayLength;
|
||||
|
||||
/// The amount of x**i coefficients in a divisor.
|
||||
///
|
||||
/// This is the amount of points in a divisor (the amount of bits in a scalar, plus one) divided
|
||||
/// by two.
|
||||
type XCoefficients: ArrayLength;
|
||||
|
||||
/// The amount of x**i coefficients in a divisor, minus one.
|
||||
type XCoefficientsMinusOne: ArrayLength;
|
||||
|
||||
/// The amount of y x**i coefficients in a divisor.
|
||||
///
|
||||
/// This is the amount of points in a divisor (the amount of bits in a scalar, plus one) divided
|
||||
/// by two, minus two.
|
||||
type YxCoefficients: ArrayLength;
|
||||
}
|
||||
|
||||
/// A tabled generator for proving/verifying discrete logarithm claims.
|
||||
#[derive(Clone)]
|
||||
pub struct GeneratorTable<F: PrimeField, Parameters: DiscreteLogParameters>(
|
||||
GenericArray<(F, F), Parameters::ScalarBits>,
|
||||
);
|
||||
|
||||
impl<F: PrimeField, Parameters: DiscreteLogParameters> fmt::Debug
|
||||
for GeneratorTable<F, Parameters>
|
||||
{
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt
|
||||
.debug_struct("GeneratorTable")
|
||||
.field("x", &self.0[0].0)
|
||||
.field("y", &self.0[0].1)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField, Parameters: DiscreteLogParameters> GeneratorTable<F, Parameters> {
|
||||
/// Create a new table for this generator.
|
||||
///
|
||||
/// The generator is assumed to be well-formed and on-curve. This function may panic if it's not.
|
||||
pub fn new(curve: &CurveSpec<F>, generator_x: F, generator_y: F) -> Self {
|
||||
// mdbl-2007-bl
|
||||
fn dbl<F: PrimeField>(a: F, x1: F, y1: F) -> (F, F) {
|
||||
let xx = x1 * x1;
|
||||
let w = a + (xx + xx.double());
|
||||
let y1y1 = y1 * y1;
|
||||
let r = y1y1 + y1y1;
|
||||
let sss = (y1 * r).double().double();
|
||||
let rr = r * r;
|
||||
|
||||
let b = x1 + r;
|
||||
let b = (b * b) - xx - rr;
|
||||
|
||||
let h = (w * w) - b.double();
|
||||
let x3 = h.double() * y1;
|
||||
let y3 = (w * (b - h)) - rr.double();
|
||||
let z3 = sss;
|
||||
|
||||
// Normalize from XYZ to XY
|
||||
let z3_inv = z3.invert().unwrap();
|
||||
let x3 = x3 * z3_inv;
|
||||
let y3 = y3 * z3_inv;
|
||||
|
||||
(x3, y3)
|
||||
}
|
||||
|
||||
let mut res = Self(GenericArray::default());
|
||||
res.0[0] = (generator_x, generator_y);
|
||||
for i in 1 .. Parameters::ScalarBits::USIZE {
|
||||
let last = res.0[i - 1];
|
||||
res.0[i] = dbl(curve.a, last.0, last.1);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// A representation of the divisor.
|
||||
///
|
||||
/// The coefficient for x**1 is explicitly excluded as it's expected to be normalized to 1.
|
||||
#[derive(Clone)]
|
||||
pub struct Divisor<Parameters: DiscreteLogParameters> {
|
||||
/// The coefficient for the `y` term of the divisor.
|
||||
///
|
||||
/// There is never more than one `y**i x**0` coefficient as the leading term of the modulus is
|
||||
/// `y**2`. It's assumed the coefficient is non-zero (and present) as it will be for any divisor
|
||||
/// exceeding trivial complexity.
|
||||
pub y: Variable,
|
||||
/// The coefficients for the `y**1 x**i` terms of the polynomial.
|
||||
pub yx: GenericArray<Variable, Parameters::YxCoefficients>,
|
||||
/// The coefficients for the `x**i` terms of the polynomial, skipping x**1.
|
||||
///
|
||||
/// x**1 is skipped as it's expected to be normalized to 1, and therefore constant, in order to
|
||||
/// ensure the divisor is non-zero (as necessary for the proof to be complete).
|
||||
// Subtract 1 from the length due to skipping the coefficient for x**1
|
||||
pub x_from_power_of_2: GenericArray<Variable, Parameters::XCoefficientsMinusOne>,
|
||||
/// The constant term in the polynomial (alternatively, the coefficient for y**0 x**0).
|
||||
pub zero: Variable,
|
||||
}
|
||||
|
||||
/// A point, its discrete logarithm, and the divisor to prove it.
|
||||
#[derive(Clone)]
|
||||
pub struct PointWithDlog<Parameters: DiscreteLogParameters> {
|
||||
/// The point which is supposedly the result of scaling the generator by the discrete logarithm.
|
||||
pub point: (Variable, Variable),
|
||||
/// The discrete logarithm, represented as coefficients of a polynomial of 2**i.
|
||||
pub dlog: GenericArray<Variable, Parameters::ScalarBits>,
|
||||
/// The divisor interpolating the relevant doublings of generator with the inverse of the point.
|
||||
pub divisor: Divisor<Parameters>,
|
||||
}
|
||||
|
||||
/// A struct containing a point used for the evaluation of a divisor.
|
||||
///
|
||||
/// Preprocesses and caches as much of the calculation as possible to minimize work upon reuse of
|
||||
/// challenge points.
|
||||
struct ChallengePoint<F: PrimeField, Parameters: DiscreteLogParameters> {
|
||||
y: F,
|
||||
yx: GenericArray<F, Parameters::YxCoefficients>,
|
||||
x: GenericArray<F, Parameters::XCoefficients>,
|
||||
p_0_n_0: F,
|
||||
x_p_0_n_0: GenericArray<F, Parameters::YxCoefficients>,
|
||||
p_1_n: F,
|
||||
p_1_d: F,
|
||||
}
|
||||
|
||||
impl<F: PrimeField, Parameters: DiscreteLogParameters> ChallengePoint<F, Parameters> {
|
||||
fn new(
|
||||
curve: &CurveSpec<F>,
|
||||
// The slope between all of the challenge points
|
||||
slope: F,
|
||||
// The x and y coordinates
|
||||
x: F,
|
||||
y: F,
|
||||
// The inversion of twice the y coordinate
|
||||
// We accept this as an argument so that the caller can calculcate these with a batch inversion
|
||||
inv_two_y: F,
|
||||
) -> Self {
|
||||
// Powers of x, skipping x**0
|
||||
let divisor_x_len = Parameters::XCoefficients::USIZE;
|
||||
let mut x_pows = GenericArray::default();
|
||||
x_pows[0] = x;
|
||||
for i in 1 .. divisor_x_len {
|
||||
let last = x_pows[i - 1];
|
||||
x_pows[i] = last * x;
|
||||
}
|
||||
|
||||
// Powers of x multiplied by y
|
||||
let divisor_yx_len = Parameters::YxCoefficients::USIZE;
|
||||
let mut yx = GenericArray::default();
|
||||
// Skips x**0
|
||||
yx[0] = y * x;
|
||||
for i in 1 .. divisor_yx_len {
|
||||
let last = yx[i - 1];
|
||||
yx[i] = last * x;
|
||||
}
|
||||
|
||||
let x_sq = x.square();
|
||||
let three_x_sq = x_sq.double() + x_sq;
|
||||
let three_x_sq_plus_a = three_x_sq + curve.a;
|
||||
let two_y = y.double();
|
||||
|
||||
// p_0_n_0 from `DivisorChallenge`
|
||||
let p_0_n_0 = three_x_sq_plus_a * inv_two_y;
|
||||
let mut x_p_0_n_0 = GenericArray::default();
|
||||
// Since this iterates over x, which skips x**0, this also skips p_0_n_0 x**0
|
||||
for (i, x) in x_pows.iter().take(divisor_yx_len).enumerate() {
|
||||
x_p_0_n_0[i] = p_0_n_0 * x;
|
||||
}
|
||||
|
||||
// p_1_n from `DivisorChallenge`
|
||||
let p_1_n = two_y;
|
||||
// p_1_d from `DivisorChallenge`
|
||||
let p_1_d = (-slope * p_1_n) + three_x_sq_plus_a;
|
||||
|
||||
ChallengePoint { x: x_pows, y, yx, p_0_n_0, x_p_0_n_0, p_1_n, p_1_d }
|
||||
}
|
||||
}
|
||||
|
||||
// `DivisorChallenge` from the section `Discrete Log Proof`
|
||||
fn divisor_challenge_eval<C: Ciphersuite, Parameters: DiscreteLogParameters>(
|
||||
circuit: &mut Circuit<C>,
|
||||
divisor: &Divisor<Parameters>,
|
||||
challenge: &ChallengePoint<C::F, Parameters>,
|
||||
) -> Variable {
|
||||
// The evaluation of the divisor differentiated by y, further multiplied by p_0_n_0
|
||||
// Differentation drops everything without a y coefficient, and drops what remains by a power
|
||||
// of y
|
||||
// (y**1 -> y**0, yx**i -> x**i)
|
||||
// This aligns with p_0_n_1 from `DivisorChallenge`
|
||||
let p_0_n_1 = {
|
||||
let mut p_0_n_1 = LinComb::empty().term(challenge.p_0_n_0, divisor.y);
|
||||
for (j, var) in divisor.yx.iter().enumerate() {
|
||||
// This does not raise by `j + 1` as x_p_0_n_0 omits x**0
|
||||
p_0_n_1 = p_0_n_1.term(challenge.x_p_0_n_0[j], *var);
|
||||
}
|
||||
p_0_n_1
|
||||
};
|
||||
|
||||
// The evaluation of the divisor differentiated by x
|
||||
// This aligns with p_0_n_2 from `DivisorChallenge`
|
||||
let p_0_n_2 = {
|
||||
// The coefficient for x**1 is 1, so 1 becomes the new zero coefficient
|
||||
let mut p_0_n_2 = LinComb::empty().constant(C::F::ONE);
|
||||
|
||||
// Handle the new y coefficient
|
||||
p_0_n_2 = p_0_n_2.term(challenge.y, divisor.yx[0]);
|
||||
|
||||
// Handle the new yx coefficients
|
||||
for (j, yx) in divisor.yx.iter().enumerate().skip(1) {
|
||||
// For the power which was shifted down, we multiply this coefficient
|
||||
// 3 x**2 -> 2 * 3 x**1
|
||||
let original_power_of_x = C::F::from(u64::try_from(j + 1).unwrap());
|
||||
// `j - 1` so `j = 1` indexes yx[0] as yx[0] is the y x**1
|
||||
// (yx omits y x**0)
|
||||
let this_weight = original_power_of_x * challenge.yx[j - 1];
|
||||
p_0_n_2 = p_0_n_2.term(this_weight, *yx);
|
||||
}
|
||||
|
||||
// Handle the x coefficients
|
||||
// We don't skip the first one as `x_from_power_of_2` already omits x**1
|
||||
for (i, x) in divisor.x_from_power_of_2.iter().enumerate() {
|
||||
// i + 2 as the paper expects i to start from 1 and be + 1, yet we start from 0
|
||||
let original_power_of_x = C::F::from(u64::try_from(i + 2).unwrap());
|
||||
// Still x[i] as x[0] is x**1
|
||||
let this_weight = original_power_of_x * challenge.x[i];
|
||||
|
||||
p_0_n_2 = p_0_n_2.term(this_weight, *x);
|
||||
}
|
||||
|
||||
p_0_n_2
|
||||
};
|
||||
|
||||
// p_0_n from `DivisorChallenge`
|
||||
let p_0_n = p_0_n_1 + &p_0_n_2;
|
||||
|
||||
// Evaluation of the divisor
|
||||
// p_0_d from `DivisorChallenge`
|
||||
let p_0_d = {
|
||||
let mut p_0_d = LinComb::empty().term(challenge.y, divisor.y);
|
||||
|
||||
for (var, c_yx) in divisor.yx.iter().zip(&challenge.yx) {
|
||||
p_0_d = p_0_d.term(*c_yx, *var);
|
||||
}
|
||||
|
||||
for (i, var) in divisor.x_from_power_of_2.iter().enumerate() {
|
||||
// This `i+1` is preserved, despite most not being as x omits x**0, as this assumes we
|
||||
// start with `i=1`
|
||||
p_0_d = p_0_d.term(challenge.x[i + 1], *var);
|
||||
}
|
||||
|
||||
// Adding x effectively adds a `1 x` term, ensuring the divisor isn't 0
|
||||
p_0_d.term(C::F::ONE, divisor.zero).constant(challenge.x[0])
|
||||
};
|
||||
|
||||
// Calculate the joint numerator
|
||||
// p_n from `DivisorChallenge`
|
||||
let p_n = p_0_n * challenge.p_1_n;
|
||||
// Calculate the joint denominator
|
||||
// p_d from `DivisorChallenge`
|
||||
let p_d = p_0_d * challenge.p_1_d;
|
||||
|
||||
// We want `n / d = o`
|
||||
// `n / d = o` == `n = d * o`
|
||||
// These are safe unwraps as they're solely done by the prover and should always be non-zero
|
||||
let witness =
|
||||
circuit.eval(&p_d).map(|p_d| (p_d, circuit.eval(&p_n).unwrap() * p_d.invert().unwrap()));
|
||||
let (_l, o, n_claim) = circuit.mul(Some(p_d), None, witness);
|
||||
circuit.equality(p_n, &n_claim.into());
|
||||
o
|
||||
}
|
||||
|
||||
/// A challenge to evaluate divisors with.
|
||||
///
|
||||
/// This challenge must be sampled after writing the commitments to the transcript. This challenge
|
||||
/// is reusable across various divisors.
|
||||
pub struct DiscreteLogChallenge<F: PrimeField, Parameters: DiscreteLogParameters> {
|
||||
c0: ChallengePoint<F, Parameters>,
|
||||
c1: ChallengePoint<F, Parameters>,
|
||||
c2: ChallengePoint<F, Parameters>,
|
||||
slope: F,
|
||||
intercept: F,
|
||||
}
|
||||
|
||||
/// A generator which has been challenged and is ready for use in evaluating discrete logarithm
|
||||
/// claims.
|
||||
pub struct ChallengedGenerator<F: PrimeField, Parameters: DiscreteLogParameters>(
|
||||
GenericArray<F, Parameters::ScalarBits>,
|
||||
);
|
||||
|
||||
/// Gadgets for proving the discrete logarithm of points on an elliptic curve defined over the
|
||||
/// scalar field of the curve of the Bulletproof.
|
||||
pub trait EcDlogGadgets<C: Ciphersuite> {
|
||||
/// Sample a challenge for a series of discrete logarithm claims.
|
||||
///
|
||||
/// This must be called after writing the commitments to the transcript.
|
||||
///
|
||||
/// The generators are assumed to be non-empty. They are not transcripted. If your generators are
|
||||
/// dynamic, they must be properly transcripted into the context.
|
||||
///
|
||||
/// May panic/have undefined behavior if an assumption is broken.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn discrete_log_challenge<T: Transcript, Parameters: DiscreteLogParameters>(
|
||||
&self,
|
||||
transcript: &mut T,
|
||||
curve: &CurveSpec<C::F>,
|
||||
generators: &[&GeneratorTable<C::F, Parameters>],
|
||||
) -> (DiscreteLogChallenge<C::F, Parameters>, Vec<ChallengedGenerator<C::F, Parameters>>);
|
||||
|
||||
/// Prove this point has the specified discrete logarithm over the specified generator.
|
||||
///
|
||||
/// The discrete logarithm is not validated to be in a canonical form. The only guarantee made on
|
||||
/// it is that it's a consistent representation of _a_ discrete logarithm (reuse won't enable
|
||||
/// re-interpretation as a distinct discrete logarithm).
|
||||
///
|
||||
/// This does ensure the point is on-curve.
|
||||
///
|
||||
/// This MUST only be called with `Variable`s present within commitments.
|
||||
///
|
||||
/// May panic/have undefined behavior if an assumption is broken, or if passed an invalid
|
||||
/// witness.
|
||||
fn discrete_log<Parameters: DiscreteLogParameters>(
|
||||
&mut self,
|
||||
curve: &CurveSpec<C::F>,
|
||||
point: PointWithDlog<Parameters>,
|
||||
challenge: &DiscreteLogChallenge<C::F, Parameters>,
|
||||
challenged_generator: &ChallengedGenerator<C::F, Parameters>,
|
||||
) -> OnCurve;
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> EcDlogGadgets<C> for Circuit<C> {
|
||||
// This is part of `DiscreteLog` from `Discrete Log Proof`, specifically, the challenges and
|
||||
// the calculations dependent solely on them
|
||||
fn discrete_log_challenge<T: Transcript, Parameters: DiscreteLogParameters>(
|
||||
&self,
|
||||
transcript: &mut T,
|
||||
curve: &CurveSpec<C::F>,
|
||||
generators: &[&GeneratorTable<C::F, Parameters>],
|
||||
) -> (DiscreteLogChallenge<C::F, Parameters>, Vec<ChallengedGenerator<C::F, Parameters>>) {
|
||||
// Get the challenge points
|
||||
let sign_of_points = transcript.challenge_bytes();
|
||||
let sign_of_point_0 = (sign_of_points[0] & 1) == 1;
|
||||
let sign_of_point_1 = ((sign_of_points[0] >> 1) & 1) == 1;
|
||||
let (c0_x, c0_y) = loop {
|
||||
let c0_x = transcript.challenge::<C>();
|
||||
let Some(c0_y) =
|
||||
Option::<C::F>::from(((c0_x.square() * c0_x) + (curve.a * c0_x) + curve.b).sqrt())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// Takes the even y coordinate as to not be dependent on whatever root the above sqrt
|
||||
// happens to returns
|
||||
break (c0_x, if bool::from(c0_y.is_odd()) != sign_of_point_0 { -c0_y } else { c0_y });
|
||||
};
|
||||
let (c1_x, c1_y) = loop {
|
||||
let c1_x = transcript.challenge::<C>();
|
||||
let Some(c1_y) =
|
||||
Option::<C::F>::from(((c1_x.square() * c1_x) + (curve.a * c1_x) + curve.b).sqrt())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
break (c1_x, if bool::from(c1_y.is_odd()) != sign_of_point_1 { -c1_y } else { c1_y });
|
||||
};
|
||||
|
||||
// mmadd-1998-cmo
|
||||
fn incomplete_add<F: PrimeField>(x1: F, y1: F, x2: F, y2: F) -> Option<(F, F)> {
|
||||
if x1 == x2 {
|
||||
None?
|
||||
}
|
||||
|
||||
let u = y2 - y1;
|
||||
let uu = u * u;
|
||||
let v = x2 - x1;
|
||||
let vv = v * v;
|
||||
let vvv = v * vv;
|
||||
let r = vv * x1;
|
||||
let a = uu - vvv - r.double();
|
||||
let x3 = v * a;
|
||||
let y3 = (u * (r - a)) - (vvv * y1);
|
||||
let z3 = vvv;
|
||||
|
||||
// Normalize from XYZ to XY
|
||||
let z3_inv = Option::<F>::from(z3.invert())?;
|
||||
let x3 = x3 * z3_inv;
|
||||
let y3 = y3 * z3_inv;
|
||||
|
||||
Some((x3, y3))
|
||||
}
|
||||
|
||||
let (c2_x, c2_y) = incomplete_add::<C::F>(c0_x, c0_y, c1_x, c1_y)
|
||||
.expect("randomly selected points shared an x coordinate");
|
||||
// We want C0, C1, C2 = -(C0 + C1)
|
||||
let c2_y = -c2_y;
|
||||
|
||||
// Calculate the slope and intercept
|
||||
// Safe invert as these x coordinates must be distinct due to passing the above incomplete_add
|
||||
let slope = (c1_y - c0_y) * (c1_x - c0_x).invert().unwrap();
|
||||
let intercept = c0_y - (slope * c0_x);
|
||||
|
||||
// Calculate the inversions for 2 c_y (for each c) and all of the challenged generators
|
||||
let mut inversions = vec![C::F::ZERO; 3 + (generators.len() * Parameters::ScalarBits::USIZE)];
|
||||
|
||||
// Needed for the left-hand side eval
|
||||
{
|
||||
inversions[0] = c0_y.double();
|
||||
inversions[1] = c1_y.double();
|
||||
inversions[2] = c2_y.double();
|
||||
}
|
||||
|
||||
// Perform the inversions for the generators
|
||||
for (i, generator) in generators.iter().enumerate() {
|
||||
// Needed for the right-hand side eval
|
||||
for (j, generator) in generator.0.iter().enumerate() {
|
||||
// `DiscreteLog` has weights of `(mu - (G_i.y + (slope * G_i.x)))**-1` in its last line
|
||||
inversions[3 + (i * Parameters::ScalarBits::USIZE) + j] =
|
||||
intercept - (generator.1 - (slope * generator.0));
|
||||
}
|
||||
}
|
||||
for challenge_inversion in &inversions {
|
||||
// This should be unreachable barring negligible probability
|
||||
if challenge_inversion.is_zero().into() {
|
||||
panic!("trying to invert 0");
|
||||
}
|
||||
}
|
||||
let mut scratch = vec![C::F::ZERO; inversions.len()];
|
||||
let _ = BatchInverter::invert_with_external_scratch(&mut inversions, &mut scratch);
|
||||
|
||||
let mut inversions = inversions.into_iter();
|
||||
let inv_c0_two_y = inversions.next().unwrap();
|
||||
let inv_c1_two_y = inversions.next().unwrap();
|
||||
let inv_c2_two_y = inversions.next().unwrap();
|
||||
|
||||
let c0 = ChallengePoint::new(curve, slope, c0_x, c0_y, inv_c0_two_y);
|
||||
let c1 = ChallengePoint::new(curve, slope, c1_x, c1_y, inv_c1_two_y);
|
||||
let c2 = ChallengePoint::new(curve, slope, c2_x, c2_y, inv_c2_two_y);
|
||||
|
||||
// Fill in the inverted values
|
||||
let mut challenged_generators = Vec::with_capacity(generators.len());
|
||||
for _ in 0 .. generators.len() {
|
||||
let mut challenged_generator = GenericArray::default();
|
||||
for i in 0 .. Parameters::ScalarBits::USIZE {
|
||||
challenged_generator[i] = inversions.next().unwrap();
|
||||
}
|
||||
challenged_generators.push(ChallengedGenerator(challenged_generator));
|
||||
}
|
||||
|
||||
(DiscreteLogChallenge { c0, c1, c2, slope, intercept }, challenged_generators)
|
||||
}
|
||||
|
||||
// `DiscreteLog` from `Discrete Log Proof`
|
||||
fn discrete_log<Parameters: DiscreteLogParameters>(
|
||||
&mut self,
|
||||
curve: &CurveSpec<C::F>,
|
||||
point: PointWithDlog<Parameters>,
|
||||
challenge: &DiscreteLogChallenge<C::F, Parameters>,
|
||||
challenged_generator: &ChallengedGenerator<C::F, Parameters>,
|
||||
) -> OnCurve {
|
||||
let PointWithDlog { divisor, dlog, point } = point;
|
||||
|
||||
// Ensure this is being safely called
|
||||
let arg_iter = [point.0, point.1, divisor.y, divisor.zero];
|
||||
let arg_iter = arg_iter.iter().chain(divisor.yx.iter());
|
||||
let arg_iter = arg_iter.chain(divisor.x_from_power_of_2.iter());
|
||||
let arg_iter = arg_iter.chain(dlog.iter());
|
||||
for variable in arg_iter {
|
||||
debug_assert!(
|
||||
matches!(variable, Variable::CG { .. } | Variable::V(_)),
|
||||
"discrete log proofs requires all arguments belong to commitments",
|
||||
);
|
||||
}
|
||||
|
||||
// Check the point is on curve
|
||||
let point = self.on_curve(curve, point);
|
||||
|
||||
// The challenge has already been sampled so those lines aren't necessary
|
||||
|
||||
// lhs from the paper, evaluating the divisor
|
||||
let lhs_eval = LinComb::from(divisor_challenge_eval(self, &divisor, &challenge.c0)) +
|
||||
&LinComb::from(divisor_challenge_eval(self, &divisor, &challenge.c1)) +
|
||||
&LinComb::from(divisor_challenge_eval(self, &divisor, &challenge.c2));
|
||||
|
||||
// Interpolate the doublings of the generator
|
||||
let mut rhs_eval = LinComb::empty();
|
||||
// We call this `bit` yet it's not constrained to being a bit
|
||||
// It's presumed to be yet may be malleated
|
||||
for (bit, weight) in dlog.into_iter().zip(&challenged_generator.0) {
|
||||
rhs_eval = rhs_eval.term(*weight, bit);
|
||||
}
|
||||
|
||||
// Interpolate the output point
|
||||
// intercept - (y - (slope * x))
|
||||
// intercept - y + (slope * x)
|
||||
// -y + (slope * x) + intercept
|
||||
// EXCEPT the output point we're proving the discrete log for isn't the one interpolated
|
||||
// Its negative is, so -y becomes y
|
||||
// y + (slope * x) + intercept
|
||||
let output_interpolation = LinComb::empty()
|
||||
.constant(challenge.intercept)
|
||||
.term(C::F::ONE, point.y)
|
||||
.term(challenge.slope, point.x);
|
||||
let output_interpolation_eval = self.eval(&output_interpolation);
|
||||
let (_output_interpolation, inverse) =
|
||||
self.inverse(Some(output_interpolation), output_interpolation_eval);
|
||||
rhs_eval = rhs_eval.term(C::F::ONE, inverse);
|
||||
|
||||
self.equality(lhs_eval, &rhs_eval);
|
||||
|
||||
point
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use generic_array::{typenum::Unsigned, ArrayLength, GenericArray};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use generalized_bulletproofs_circuit_abstraction::*;
|
||||
|
||||
mod dlog;
|
||||
pub use dlog::*;
|
||||
|
||||
/// The specification of a short Weierstrass curve over the field `F`.
|
||||
///
|
||||
/// The short Weierstrass curve is defined via the formula `y**2 = x**3 + a*x + b`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct CurveSpec<F> {
|
||||
/// The `a` constant in the curve formula.
|
||||
pub a: F,
|
||||
/// The `b` constant in the curve formula.
|
||||
pub b: F,
|
||||
}
|
||||
|
||||
/// A struct for a point on a towered curve which has been confirmed to be on-curve.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct OnCurve {
|
||||
pub(crate) x: Variable,
|
||||
pub(crate) y: Variable,
|
||||
}
|
||||
|
||||
impl OnCurve {
|
||||
/// The variable for the x-coordinate.
|
||||
pub fn x(&self) -> Variable {
|
||||
self.x
|
||||
}
|
||||
/// The variable for the y-coordinate.
|
||||
pub fn y(&self) -> Variable {
|
||||
self.y
|
||||
}
|
||||
}
|
||||
|
||||
/// Gadgets for working with points on an elliptic curve defined over the scalar field of the curve
|
||||
/// of the Bulletproof.
|
||||
pub trait EcGadgets<C: Ciphersuite> {
|
||||
/// Constrain an x and y coordinate as being on the specified curve.
|
||||
///
|
||||
/// The specified curve is defined over the scalar field of the curve this proof is performed
|
||||
/// over, offering efficient arithmetic.
|
||||
///
|
||||
/// May panic if the prover and the point is not actually on-curve.
|
||||
fn on_curve(&mut self, curve: &CurveSpec<C::F>, point: (Variable, Variable)) -> OnCurve;
|
||||
|
||||
/// Perform incomplete addition for a fixed point and an on-curve point.
|
||||
///
|
||||
/// `a` is the x and y coordinates of the fixed point, assumed to be on-curve.
|
||||
///
|
||||
/// `b` is a point prior checked to be on-curve.
|
||||
///
|
||||
/// `c` is a point prior checked to be on-curve, constrained to be the sum of `a` and `b`.
|
||||
///
|
||||
/// `a` and `b` are checked to have distinct x coordinates.
|
||||
///
|
||||
/// This function may panic if `a` is malformed or if the prover and `c` is not actually the sum
|
||||
/// of `a` and `b`.
|
||||
fn incomplete_add_fixed(&mut self, a: (C::F, C::F), b: OnCurve, c: OnCurve) -> OnCurve;
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> EcGadgets<C> for Circuit<C> {
|
||||
fn on_curve(&mut self, curve: &CurveSpec<C::F>, (x, y): (Variable, Variable)) -> OnCurve {
|
||||
let x_eval = self.eval(&LinComb::from(x));
|
||||
let (_x, _x_2, x2) =
|
||||
self.mul(Some(LinComb::from(x)), Some(LinComb::from(x)), x_eval.map(|x| (x, x)));
|
||||
let (_x, _x_2, x3) =
|
||||
self.mul(Some(LinComb::from(x2)), Some(LinComb::from(x)), x_eval.map(|x| (x * x, x)));
|
||||
let expected_y2 = LinComb::from(x3).term(curve.a, x).constant(curve.b);
|
||||
|
||||
let y_eval = self.eval(&LinComb::from(y));
|
||||
let (_y, _y_2, y2) =
|
||||
self.mul(Some(LinComb::from(y)), Some(LinComb::from(y)), y_eval.map(|y| (y, y)));
|
||||
|
||||
self.equality(y2.into(), &expected_y2);
|
||||
|
||||
OnCurve { x, y }
|
||||
}
|
||||
|
||||
fn incomplete_add_fixed(&mut self, a: (C::F, C::F), b: OnCurve, c: OnCurve) -> OnCurve {
|
||||
// Check b.x != a.0
|
||||
{
|
||||
let bx_lincomb = LinComb::from(b.x);
|
||||
let bx_eval = self.eval(&bx_lincomb);
|
||||
self.inequality(bx_lincomb, &LinComb::empty().constant(a.0), bx_eval.map(|bx| (bx, a.0)));
|
||||
}
|
||||
|
||||
let (x0, y0) = (a.0, a.1);
|
||||
let (x1, y1) = (b.x, b.y);
|
||||
let (x2, y2) = (c.x, c.y);
|
||||
|
||||
let slope_eval = self.eval(&LinComb::from(x1)).map(|x1| {
|
||||
let y1 = self.eval(&LinComb::from(b.y)).unwrap();
|
||||
|
||||
(y1 - y0) * (x1 - x0).invert().unwrap()
|
||||
});
|
||||
|
||||
// slope * (x1 - x0) = y1 - y0
|
||||
let x1_minus_x0 = LinComb::from(x1).constant(-x0);
|
||||
let x1_minus_x0_eval = self.eval(&x1_minus_x0);
|
||||
let (slope, _r, o) =
|
||||
self.mul(None, Some(x1_minus_x0), slope_eval.map(|slope| (slope, x1_minus_x0_eval.unwrap())));
|
||||
self.equality(LinComb::from(o), &LinComb::from(y1).constant(-y0));
|
||||
|
||||
// slope * (x2 - x0) = -y2 - y0
|
||||
let x2_minus_x0 = LinComb::from(x2).constant(-x0);
|
||||
let x2_minus_x0_eval = self.eval(&x2_minus_x0);
|
||||
let (_slope, _x2_minus_x0, o) = self.mul(
|
||||
Some(slope.into()),
|
||||
Some(x2_minus_x0),
|
||||
slope_eval.map(|slope| (slope, x2_minus_x0_eval.unwrap())),
|
||||
);
|
||||
self.equality(o.into(), &LinComb::empty().term(-C::F::ONE, y2).constant(-y0));
|
||||
|
||||
// slope * slope = x0 + x1 + x2
|
||||
let (_slope, _slope_2, o) =
|
||||
self.mul(Some(slope.into()), Some(slope.into()), slope_eval.map(|slope| (slope, slope)));
|
||||
self.equality(o.into(), &LinComb::from(x1).term(C::F::ONE, x2).constant(x0));
|
||||
|
||||
OnCurve { x: x2, y: y2 }
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,8 @@ dalek-ff-group = { path = "../../dalek-ff-group", version = "0.4", default-featu
|
||||
|
||||
blake2 = { version = "0.10", default-features = false }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false }
|
||||
ec-divisors = { path = "../divisors", default-features = false }
|
||||
generalized-bulletproofs-ec-gadgets = { path = "../ec-gadgets", default-features = false }
|
||||
ec-divisors = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
|
||||
generalized-bulletproofs-ec-gadgets = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use std_shims::prelude::*;
|
||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||
use std_shims::io::{self, Read};
|
||||
|
||||
@@ -37,10 +39,6 @@ impl ciphersuite::Ciphersuite for Embedwards25519 {
|
||||
Point::generator()
|
||||
}
|
||||
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
Scalar::wide_reduce(scalar)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
use blake2::Digest;
|
||||
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_slice().try_into().unwrap())
|
||||
@@ -57,7 +55,7 @@ impl ciphersuite::Ciphersuite for Embedwards25519 {
|
||||
reader.read_exact(encoding.as_mut())?;
|
||||
|
||||
let point = Option::<Self::G>::from(Self::G::from_bytes(&encoding))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))?;
|
||||
.ok_or_else(|| io::Error::other("invalid point"))?;
|
||||
Ok(point)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +377,15 @@ impl PrimeGroup for Point {}
|
||||
|
||||
impl ec_divisors::DivisorCurve for Point {
|
||||
type FieldElement = FieldElement;
|
||||
type XyPoint = ec_divisors::Projective<Self>;
|
||||
|
||||
fn interpolator_for_scalar_mul() -> &'static ec_divisors::Interpolator<Self::FieldElement> {
|
||||
static PRECOMPUTE: std_shims::sync::LazyLock<ec_divisors::Interpolator<FieldElement>> =
|
||||
std_shims::sync::LazyLock::new(|| {
|
||||
ec_divisors::Interpolator::new(usize::try_from(130).unwrap())
|
||||
});
|
||||
&PRECOMPUTE
|
||||
}
|
||||
|
||||
fn a() -> Self::FieldElement {
|
||||
-FieldElement::from(3u64)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
[package]
|
||||
name = "generalized-bulletproofs"
|
||||
version = "0.1.0"
|
||||
description = "Generalized Bulletproofs"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/generalized-bulletproofs"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["ciphersuite", "ff", "group"]
|
||||
edition = "2021"
|
||||
rust-version = "1.69"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
||||
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
blake2 = { version = "0.10", default-features = false }
|
||||
|
||||
multiexp = { path = "../../multiexp", version = "0.4", default-features = false, features = ["batch"] }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../../transcript", features = ["recommended"] }
|
||||
|
||||
ciphersuite = { path = "../../ciphersuite", features = ["ristretto"] }
|
||||
|
||||
[features]
|
||||
std = ["std-shims/std", "rand_core/std", "zeroize/std", "blake2/std", "multiexp/std", "ciphersuite/std"]
|
||||
tests = ["std"]
|
||||
default = ["std"]
|
||||
@@ -1,6 +0,0 @@
|
||||
# Generalized Bulletproofs
|
||||
|
||||
An implementation of
|
||||
[Generalized Bulletproofs](https://repo.getmonero.org/monero-project/ccs-proposals/uploads/a9baa50c38c6312efc0fea5c6a188bb9/gbp.pdf),
|
||||
a variant of the Bulletproofs arithmetic circuit statement to support Pedersen
|
||||
vector commitments.
|
||||
@@ -1,663 +0,0 @@
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use multiexp::{multiexp, multiexp_vartime};
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
use crate::{
|
||||
ScalarVector, PointVector, ProofGenerators, PedersenCommitment, PedersenVectorCommitment,
|
||||
BatchVerifier,
|
||||
transcript::*,
|
||||
lincomb::accumulate_vector,
|
||||
inner_product::{IpError, IpStatement, IpWitness, P},
|
||||
};
|
||||
pub use crate::lincomb::{Variable, LinComb};
|
||||
|
||||
/// An Arithmetic Circuit Statement.
|
||||
///
|
||||
/// Bulletproofs' constraints are of the form
|
||||
/// `aL * aR = aO, WL * aL + WR * aR + WO * aO = WV * V + c`.
|
||||
///
|
||||
/// Generalized Bulletproofs modifies this to
|
||||
/// `aL * aR = aO, WL * aL + WR * aR + WO * aO + WCG * C_G = WV * V + c`.
|
||||
///
|
||||
/// We implement the latter, yet represented (for simplicity) as
|
||||
/// `aL * aR = aO, WL * aL + WR * aR + WO * aO + WCG * C_G + WV * V + c = 0`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArithmeticCircuitStatement<'a, C: Ciphersuite> {
|
||||
generators: ProofGenerators<'a, C>,
|
||||
|
||||
constraints: Vec<LinComb<C::F>>,
|
||||
C: PointVector<C>,
|
||||
V: PointVector<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Zeroize for ArithmeticCircuitStatement<'_, C> {
|
||||
fn zeroize(&mut self) {
|
||||
self.constraints.zeroize();
|
||||
self.C.zeroize();
|
||||
self.V.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
/// The witness for an arithmetic circuit statement.
|
||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct ArithmeticCircuitWitness<C: Ciphersuite> {
|
||||
aL: ScalarVector<C::F>,
|
||||
aR: ScalarVector<C::F>,
|
||||
aO: ScalarVector<C::F>,
|
||||
|
||||
c: Vec<PedersenVectorCommitment<C>>,
|
||||
v: Vec<PedersenCommitment<C>>,
|
||||
}
|
||||
|
||||
/// An error incurred during arithmetic circuit proof operations.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum AcError {
|
||||
/// The vectors of scalars which are multiplied against each other were of different lengths.
|
||||
DifferingLrLengths,
|
||||
/// The matrices of constraints are of different lengths.
|
||||
InconsistentAmountOfConstraints,
|
||||
/// A constraint referred to a non-existent term.
|
||||
ConstrainedNonExistentTerm,
|
||||
/// A constraint referred to a non-existent commitment.
|
||||
ConstrainedNonExistentCommitment,
|
||||
/// There weren't enough generators to prove for this statement.
|
||||
NotEnoughGenerators,
|
||||
/// The witness was inconsistent to the statement.
|
||||
///
|
||||
/// Sanity checks on the witness are always performed. If the library is compiled with debug
|
||||
/// assertions on, the satisfaction of all constraints and validity of the commitmentsd is
|
||||
/// additionally checked.
|
||||
InconsistentWitness,
|
||||
/// There was an error from the inner-product proof.
|
||||
Ip(IpError),
|
||||
/// The proof wasn't complete and the necessary values could not be read from the transcript.
|
||||
IncompleteProof,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> ArithmeticCircuitWitness<C> {
|
||||
/// Constructs a new witness instance.
|
||||
pub fn new(
|
||||
aL: ScalarVector<C::F>,
|
||||
aR: ScalarVector<C::F>,
|
||||
c: Vec<PedersenVectorCommitment<C>>,
|
||||
v: Vec<PedersenCommitment<C>>,
|
||||
) -> Result<Self, AcError> {
|
||||
if aL.len() != aR.len() {
|
||||
Err(AcError::DifferingLrLengths)?;
|
||||
}
|
||||
|
||||
// The Pedersen Vector Commitments don't have their variables' lengths checked as they aren't
|
||||
// paired off with each other as aL, aR are
|
||||
|
||||
// The PVC commit function ensures there's enough generators for their amount of terms
|
||||
// If there aren't enough/the same generators when this is proven for, it'll trigger
|
||||
// InconsistentWitness
|
||||
|
||||
let aO = aL.clone() * &aR;
|
||||
Ok(ArithmeticCircuitWitness { aL, aR, aO, c, v })
|
||||
}
|
||||
}
|
||||
|
||||
struct YzChallenges<C: Ciphersuite> {
|
||||
y_inv: ScalarVector<C::F>,
|
||||
z: ScalarVector<C::F>,
|
||||
}
|
||||
|
||||
impl<'a, C: Ciphersuite> ArithmeticCircuitStatement<'a, C> {
|
||||
// The amount of multiplications performed.
|
||||
fn n(&self) -> usize {
|
||||
self.generators.len()
|
||||
}
|
||||
|
||||
// The amount of constraints.
|
||||
fn q(&self) -> usize {
|
||||
self.constraints.len()
|
||||
}
|
||||
|
||||
// The amount of Pedersen vector commitments.
|
||||
fn c(&self) -> usize {
|
||||
self.C.len()
|
||||
}
|
||||
|
||||
// The amount of Pedersen commitments.
|
||||
fn m(&self) -> usize {
|
||||
self.V.len()
|
||||
}
|
||||
|
||||
/// Create a new ArithmeticCircuitStatement for the specified relationship.
|
||||
///
|
||||
/// The `LinComb`s passed as `constraints` will be bound to evaluate to 0.
|
||||
///
|
||||
/// The constraints are not transcripted. They're expected to be deterministic from the context
|
||||
/// and higher-level statement. If your constraints are variable, you MUST transcript them before
|
||||
/// calling prove/verify.
|
||||
///
|
||||
/// The commitments are expected to have been transcripted extenally to this statement's
|
||||
/// invocation. That's practically ensured by taking a `Commitments` struct here, which is only
|
||||
/// obtainable via a transcript.
|
||||
pub fn new(
|
||||
generators: ProofGenerators<'a, C>,
|
||||
constraints: Vec<LinComb<C::F>>,
|
||||
commitments: Commitments<C>,
|
||||
) -> Result<Self, AcError> {
|
||||
let Commitments { C, V } = commitments;
|
||||
|
||||
for constraint in &constraints {
|
||||
if Some(generators.len()) <= constraint.highest_a_index {
|
||||
Err(AcError::ConstrainedNonExistentTerm)?;
|
||||
}
|
||||
if Some(C.len()) <= constraint.highest_c_index {
|
||||
Err(AcError::ConstrainedNonExistentCommitment)?;
|
||||
}
|
||||
if Some(V.len()) <= constraint.highest_v_index {
|
||||
Err(AcError::ConstrainedNonExistentCommitment)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { generators, constraints, C, V })
|
||||
}
|
||||
|
||||
fn yz_challenges(&self, y: C::F, z_1: C::F) -> YzChallenges<C> {
|
||||
let y_inv = y.invert().unwrap();
|
||||
let y_inv = ScalarVector::powers(y_inv, self.n());
|
||||
|
||||
// Powers of z *starting with z**1*
|
||||
// We could reuse powers and remove the first element, yet this is cheaper than the shift that
|
||||
// would require
|
||||
let q = self.q();
|
||||
let mut z = ScalarVector(Vec::with_capacity(q));
|
||||
z.0.push(z_1);
|
||||
for _ in 1 .. q {
|
||||
z.0.push(*z.0.last().unwrap() * z_1);
|
||||
}
|
||||
z.0.truncate(q);
|
||||
|
||||
YzChallenges { y_inv, z }
|
||||
}
|
||||
|
||||
/// Prove for this statement/witness.
|
||||
pub fn prove<R: RngCore + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
transcript: &mut Transcript,
|
||||
mut witness: ArithmeticCircuitWitness<C>,
|
||||
) -> Result<(), AcError> {
|
||||
let n = self.n();
|
||||
let c = self.c();
|
||||
let m = self.m();
|
||||
|
||||
// Check the witness length and pad it to the necessary power of two
|
||||
if witness.aL.len() > n {
|
||||
Err(AcError::NotEnoughGenerators)?;
|
||||
}
|
||||
while witness.aL.len() < n {
|
||||
witness.aL.0.push(C::F::ZERO);
|
||||
witness.aR.0.push(C::F::ZERO);
|
||||
witness.aO.0.push(C::F::ZERO);
|
||||
}
|
||||
for c in &mut witness.c {
|
||||
if c.g_values.len() > n {
|
||||
Err(AcError::NotEnoughGenerators)?;
|
||||
}
|
||||
// The Pedersen vector commitments internally have n terms
|
||||
while c.g_values.len() < n {
|
||||
c.g_values.0.push(C::F::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the witness's consistency with the statement
|
||||
if (c != witness.c.len()) || (m != witness.v.len()) {
|
||||
Err(AcError::InconsistentWitness)?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
for (commitment, opening) in self.V.0.iter().zip(witness.v.iter()) {
|
||||
if *commitment != opening.commit(self.generators.g(), self.generators.h()) {
|
||||
Err(AcError::InconsistentWitness)?;
|
||||
}
|
||||
}
|
||||
for (commitment, opening) in self.C.0.iter().zip(witness.c.iter()) {
|
||||
if Some(*commitment) != opening.commit(self.generators.g_bold_slice(), self.generators.h())
|
||||
{
|
||||
Err(AcError::InconsistentWitness)?;
|
||||
}
|
||||
}
|
||||
for constraint in &self.constraints {
|
||||
let eval =
|
||||
constraint
|
||||
.WL
|
||||
.iter()
|
||||
.map(|(i, weight)| *weight * witness.aL[*i])
|
||||
.chain(constraint.WR.iter().map(|(i, weight)| *weight * witness.aR[*i]))
|
||||
.chain(constraint.WO.iter().map(|(i, weight)| *weight * witness.aO[*i]))
|
||||
.chain(
|
||||
constraint.WCG.iter().zip(&witness.c).flat_map(|(weights, c)| {
|
||||
weights.iter().map(|(j, weight)| *weight * c.g_values[*j])
|
||||
}),
|
||||
)
|
||||
.chain(constraint.WV.iter().map(|(i, weight)| *weight * witness.v[*i].value))
|
||||
.chain(core::iter::once(constraint.c))
|
||||
.sum::<C::F>();
|
||||
|
||||
if eval != C::F::ZERO {
|
||||
Err(AcError::InconsistentWitness)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alpha = C::F::random(&mut *rng);
|
||||
let beta = C::F::random(&mut *rng);
|
||||
let rho = C::F::random(&mut *rng);
|
||||
|
||||
let AI = {
|
||||
let alg = witness.aL.0.iter().enumerate().map(|(i, aL)| (*aL, self.generators.g_bold(i)));
|
||||
let arh = witness.aR.0.iter().enumerate().map(|(i, aR)| (*aR, self.generators.h_bold(i)));
|
||||
let ah = core::iter::once((alpha, self.generators.h()));
|
||||
let mut AI_terms = alg.chain(arh).chain(ah).collect::<Vec<_>>();
|
||||
let AI = multiexp(&AI_terms);
|
||||
AI_terms.zeroize();
|
||||
AI
|
||||
};
|
||||
let AO = {
|
||||
let aog = witness.aO.0.iter().enumerate().map(|(i, aO)| (*aO, self.generators.g_bold(i)));
|
||||
let bh = core::iter::once((beta, self.generators.h()));
|
||||
let mut AO_terms = aog.chain(bh).collect::<Vec<_>>();
|
||||
let AO = multiexp(&AO_terms);
|
||||
AO_terms.zeroize();
|
||||
AO
|
||||
};
|
||||
|
||||
let mut sL = ScalarVector(Vec::with_capacity(n));
|
||||
let mut sR = ScalarVector(Vec::with_capacity(n));
|
||||
for _ in 0 .. n {
|
||||
sL.0.push(C::F::random(&mut *rng));
|
||||
sR.0.push(C::F::random(&mut *rng));
|
||||
}
|
||||
let S = {
|
||||
let slg = sL.0.iter().enumerate().map(|(i, sL)| (*sL, self.generators.g_bold(i)));
|
||||
let srh = sR.0.iter().enumerate().map(|(i, sR)| (*sR, self.generators.h_bold(i)));
|
||||
let rh = core::iter::once((rho, self.generators.h()));
|
||||
let mut S_terms = slg.chain(srh).chain(rh).collect::<Vec<_>>();
|
||||
let S = multiexp(&S_terms);
|
||||
S_terms.zeroize();
|
||||
S
|
||||
};
|
||||
|
||||
transcript.push_point(AI);
|
||||
transcript.push_point(AO);
|
||||
transcript.push_point(S);
|
||||
let y = transcript.challenge::<C>();
|
||||
let z = transcript.challenge::<C>();
|
||||
let YzChallenges { y_inv, z } = self.yz_challenges(y, z);
|
||||
let y = ScalarVector::powers(y, n);
|
||||
|
||||
// t is a n'-term polynomial
|
||||
// While Bulletproofs discuss it as a 6-term polynomial, Generalized Bulletproofs re-defines it
|
||||
// as `2(n' + 1)`-term, where `n'` is `2 (c + 1)`.
|
||||
// When `c = 0`, `n' = 2`, and t is `6` (which lines up with Bulletproofs having a 6-term
|
||||
// polynomial).
|
||||
|
||||
// ni = n'
|
||||
let ni = 2 + (2 * (c / 2));
|
||||
// These indexes are from the Generalized Bulletproofs paper
|
||||
#[rustfmt::skip]
|
||||
let ilr = ni / 2; // 1 if c = 0
|
||||
#[rustfmt::skip]
|
||||
let io = ni; // 2 if c = 0
|
||||
#[rustfmt::skip]
|
||||
let is = ni + 1; // 3 if c = 0
|
||||
#[rustfmt::skip]
|
||||
let jlr = ni / 2; // 1 if c = 0
|
||||
#[rustfmt::skip]
|
||||
let jo = 0; // 0 if c = 0
|
||||
#[rustfmt::skip]
|
||||
let js = ni + 1; // 3 if c = 0
|
||||
|
||||
// If c = 0, these indexes perfectly align with the stated powers of X from the Bulletproofs
|
||||
// paper for the following coefficients
|
||||
|
||||
// Declare the l and r polynomials, assigning the traditional coefficients to their positions
|
||||
let mut l = vec![];
|
||||
let mut r = vec![];
|
||||
for _ in 0 .. (is + 1) {
|
||||
l.push(ScalarVector::new(0));
|
||||
r.push(ScalarVector::new(0));
|
||||
}
|
||||
|
||||
let mut l_weights = ScalarVector::new(n);
|
||||
let mut r_weights = ScalarVector::new(n);
|
||||
let mut o_weights = ScalarVector::new(n);
|
||||
for (constraint, z) in self.constraints.iter().zip(&z.0) {
|
||||
accumulate_vector(&mut l_weights, &constraint.WL, *z);
|
||||
accumulate_vector(&mut r_weights, &constraint.WR, *z);
|
||||
accumulate_vector(&mut o_weights, &constraint.WO, *z);
|
||||
}
|
||||
|
||||
l[ilr] = (r_weights * &y_inv) + &witness.aL;
|
||||
l[io] = witness.aO.clone();
|
||||
l[is] = sL;
|
||||
r[jlr] = l_weights + &(witness.aR.clone() * &y);
|
||||
r[jo] = o_weights - &y;
|
||||
r[js] = sR * &y;
|
||||
|
||||
// Pad as expected
|
||||
for l in &mut l {
|
||||
debug_assert!((l.len() == 0) || (l.len() == n));
|
||||
if l.len() == 0 {
|
||||
*l = ScalarVector::new(n);
|
||||
}
|
||||
}
|
||||
for r in &mut r {
|
||||
debug_assert!((r.len() == 0) || (r.len() == n));
|
||||
if r.len() == 0 {
|
||||
*r = ScalarVector::new(n);
|
||||
}
|
||||
}
|
||||
|
||||
// We now fill in the vector commitments
|
||||
// We use unused coefficients of l increasing from 0 (skipping ilr), and unused coefficients of
|
||||
// r decreasing from n' (skipping jlr)
|
||||
|
||||
let mut cg_weights = Vec::with_capacity(witness.c.len());
|
||||
for i in 0 .. witness.c.len() {
|
||||
let mut cg = ScalarVector::new(n);
|
||||
for (constraint, z) in self.constraints.iter().zip(&z.0) {
|
||||
if let Some(WCG) = constraint.WCG.get(i) {
|
||||
accumulate_vector(&mut cg, WCG, *z);
|
||||
}
|
||||
}
|
||||
cg_weights.push(cg);
|
||||
}
|
||||
|
||||
for (mut i, (c, cg_weights)) in witness.c.iter().zip(cg_weights).enumerate() {
|
||||
if i >= ilr {
|
||||
i += 1;
|
||||
}
|
||||
// Because i has skipped ilr, j will skip jlr
|
||||
let j = ni - i;
|
||||
|
||||
l[i] = c.g_values.clone();
|
||||
r[j] = cg_weights;
|
||||
}
|
||||
|
||||
// Multiply them to obtain t
|
||||
let mut t = ScalarVector::new(1 + (2 * (l.len() - 1)));
|
||||
for (i, l) in l.iter().enumerate() {
|
||||
for (j, r) in r.iter().enumerate() {
|
||||
let new_coeff = i + j;
|
||||
t[new_coeff] += l.inner_product(r.0.iter());
|
||||
}
|
||||
}
|
||||
|
||||
// Per Bulletproofs, calculate masks tau for each t where (i > 0) && (i != 2)
|
||||
// Per Generalized Bulletproofs, calculate masks tau for each t where i != n'
|
||||
// With Bulletproofs, t[0] is zero, hence its omission, yet Generalized Bulletproofs uses it
|
||||
let mut tau_before_ni = vec![];
|
||||
for _ in 0 .. ni {
|
||||
tau_before_ni.push(C::F::random(&mut *rng));
|
||||
}
|
||||
let mut tau_after_ni = vec![];
|
||||
for _ in 0 .. t.0[(ni + 1) ..].len() {
|
||||
tau_after_ni.push(C::F::random(&mut *rng));
|
||||
}
|
||||
// Calculate commitments to the coefficients of t, blinded by tau
|
||||
debug_assert_eq!(t.0[0 .. ni].len(), tau_before_ni.len());
|
||||
for (t, tau) in t.0[0 .. ni].iter().zip(tau_before_ni.iter()) {
|
||||
transcript.push_point(multiexp(&[(*t, self.generators.g()), (*tau, self.generators.h())]));
|
||||
}
|
||||
debug_assert_eq!(t.0[(ni + 1) ..].len(), tau_after_ni.len());
|
||||
for (t, tau) in t.0[(ni + 1) ..].iter().zip(tau_after_ni.iter()) {
|
||||
transcript.push_point(multiexp(&[(*t, self.generators.g()), (*tau, self.generators.h())]));
|
||||
}
|
||||
|
||||
let x: ScalarVector<C::F> = ScalarVector::powers(transcript.challenge::<C>(), t.len());
|
||||
|
||||
let poly_eval = |poly: &[ScalarVector<C::F>], x: &ScalarVector<_>| -> ScalarVector<_> {
|
||||
let mut res = ScalarVector::<C::F>::new(poly[0].0.len());
|
||||
for (i, coeff) in poly.iter().enumerate() {
|
||||
res = res + &(coeff.clone() * x[i]);
|
||||
}
|
||||
res
|
||||
};
|
||||
let l = poly_eval(&l, &x);
|
||||
let r = poly_eval(&r, &x);
|
||||
|
||||
let t_caret = l.inner_product(r.0.iter());
|
||||
|
||||
let mut V_weights = ScalarVector::new(self.V.len());
|
||||
for (constraint, z) in self.constraints.iter().zip(&z.0) {
|
||||
// We use `-z`, not `z`, as we write our constraint as `... + WV V = 0` not `= WV V + ..`
|
||||
// This means we need to subtract `WV V` from both sides, which we accomplish here
|
||||
accumulate_vector(&mut V_weights, &constraint.WV, -*z);
|
||||
}
|
||||
|
||||
let tau_x = {
|
||||
let mut tau_x_poly = vec![];
|
||||
tau_x_poly.extend(tau_before_ni);
|
||||
tau_x_poly.push(V_weights.inner_product(witness.v.iter().map(|v| &v.mask)));
|
||||
tau_x_poly.extend(tau_after_ni);
|
||||
|
||||
let mut tau_x = C::F::ZERO;
|
||||
for (i, coeff) in tau_x_poly.into_iter().enumerate() {
|
||||
tau_x += coeff * x[i];
|
||||
}
|
||||
tau_x
|
||||
};
|
||||
|
||||
// Calculate u for the powers of x variable to ilr/io/is
|
||||
let u = {
|
||||
// Calculate the first part of u
|
||||
let mut u = (alpha * x[ilr]) + (beta * x[io]) + (rho * x[is]);
|
||||
|
||||
// Incorporate the commitment masks multiplied by the associated power of x
|
||||
for (mut i, commitment) in witness.c.iter().enumerate() {
|
||||
// If this index is ni / 2, skip it
|
||||
if i >= (ni / 2) {
|
||||
i += 1;
|
||||
}
|
||||
u += x[i] * commitment.mask;
|
||||
}
|
||||
u
|
||||
};
|
||||
|
||||
// Use the Inner-Product argument to prove for this
|
||||
// P = t_caret * g + l * g_bold + r * (y_inv * h_bold)
|
||||
|
||||
let mut P_terms = Vec::with_capacity(1 + (2 * self.generators.len()));
|
||||
debug_assert_eq!(l.len(), r.len());
|
||||
for (i, (l, r)) in l.0.iter().zip(r.0.iter()).enumerate() {
|
||||
P_terms.push((*l, self.generators.g_bold(i)));
|
||||
P_terms.push((y_inv[i] * r, self.generators.h_bold(i)));
|
||||
}
|
||||
|
||||
// Protocol 1, inlined, since our IpStatement is for Protocol 2
|
||||
transcript.push_scalar(tau_x);
|
||||
transcript.push_scalar(u);
|
||||
transcript.push_scalar(t_caret);
|
||||
let ip_x = transcript.challenge::<C>();
|
||||
P_terms.push((ip_x * t_caret, self.generators.g()));
|
||||
IpStatement::new(
|
||||
self.generators,
|
||||
y_inv,
|
||||
ip_x,
|
||||
// Safe since IpStatement isn't a ZK proof
|
||||
P::Prover(multiexp_vartime(&P_terms)),
|
||||
)
|
||||
.unwrap()
|
||||
.prove(transcript, IpWitness::new(l, r).unwrap())
|
||||
.map_err(AcError::Ip)
|
||||
}
|
||||
|
||||
/// Verify a proof for this statement.
|
||||
///
|
||||
/// This solely queues the statement for batch verification. The resulting BatchVerifier MUST
|
||||
/// still be verified.
|
||||
///
|
||||
/// If this proof returns an error, the BatchVerifier MUST be assumed corrupted and discarded.
|
||||
pub fn verify<R: RngCore + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
verifier: &mut BatchVerifier<C>,
|
||||
transcript: &mut VerifierTranscript,
|
||||
) -> Result<(), AcError> {
|
||||
if verifier.g_bold.len() < self.generators.len() {
|
||||
verifier.g_bold.resize(self.generators.len(), C::F::ZERO);
|
||||
verifier.h_bold.resize(self.generators.len(), C::F::ZERO);
|
||||
verifier.h_sum.resize(self.generators.len(), C::F::ZERO);
|
||||
}
|
||||
|
||||
let n = self.n();
|
||||
let c = self.c();
|
||||
|
||||
let ni = 2 + (2 * (c / 2));
|
||||
|
||||
let ilr = ni / 2;
|
||||
let io = ni;
|
||||
let is = ni + 1;
|
||||
let jlr = ni / 2;
|
||||
|
||||
let l_r_poly_len = 1 + ni + 1;
|
||||
let t_poly_len = (2 * l_r_poly_len) - 1;
|
||||
|
||||
let AI = transcript.read_point::<C>().map_err(|_| AcError::IncompleteProof)?;
|
||||
let AO = transcript.read_point::<C>().map_err(|_| AcError::IncompleteProof)?;
|
||||
let S = transcript.read_point::<C>().map_err(|_| AcError::IncompleteProof)?;
|
||||
let y = transcript.challenge::<C>();
|
||||
let z = transcript.challenge::<C>();
|
||||
let YzChallenges { y_inv, z } = self.yz_challenges(y, z);
|
||||
|
||||
let mut l_weights = ScalarVector::new(n);
|
||||
let mut r_weights = ScalarVector::new(n);
|
||||
let mut o_weights = ScalarVector::new(n);
|
||||
for (constraint, z) in self.constraints.iter().zip(&z.0) {
|
||||
accumulate_vector(&mut l_weights, &constraint.WL, *z);
|
||||
accumulate_vector(&mut r_weights, &constraint.WR, *z);
|
||||
accumulate_vector(&mut o_weights, &constraint.WO, *z);
|
||||
}
|
||||
let r_weights = r_weights * &y_inv;
|
||||
|
||||
let delta = r_weights.inner_product(l_weights.0.iter());
|
||||
|
||||
let mut T_before_ni = Vec::with_capacity(ni);
|
||||
let mut T_after_ni = Vec::with_capacity(t_poly_len - ni - 1);
|
||||
for _ in 0 .. ni {
|
||||
T_before_ni.push(transcript.read_point::<C>().map_err(|_| AcError::IncompleteProof)?);
|
||||
}
|
||||
for _ in 0 .. (t_poly_len - ni - 1) {
|
||||
T_after_ni.push(transcript.read_point::<C>().map_err(|_| AcError::IncompleteProof)?);
|
||||
}
|
||||
let x: ScalarVector<C::F> = ScalarVector::powers(transcript.challenge::<C>(), t_poly_len);
|
||||
|
||||
let tau_x = transcript.read_scalar::<C>().map_err(|_| AcError::IncompleteProof)?;
|
||||
let u = transcript.read_scalar::<C>().map_err(|_| AcError::IncompleteProof)?;
|
||||
let t_caret = transcript.read_scalar::<C>().map_err(|_| AcError::IncompleteProof)?;
|
||||
|
||||
// Lines 88-90, modified per Generalized Bulletproofs as needed w.r.t. t
|
||||
{
|
||||
let verifier_weight = C::F::random(&mut *rng);
|
||||
// lhs of the equation, weighted to enable batch verification
|
||||
verifier.g += t_caret * verifier_weight;
|
||||
verifier.h += tau_x * verifier_weight;
|
||||
|
||||
let mut V_weights = ScalarVector::new(self.V.len());
|
||||
for (constraint, z) in self.constraints.iter().zip(&z.0) {
|
||||
// We use `-z`, not `z`, as we write our constraint as `... + WV V = 0` not `= WV V + ..`
|
||||
// This means we need to subtract `WV V` from both sides, which we accomplish here
|
||||
accumulate_vector(&mut V_weights, &constraint.WV, -*z);
|
||||
}
|
||||
V_weights = V_weights * x[ni];
|
||||
|
||||
// rhs of the equation, negated to cause a sum to zero
|
||||
// `delta - z...`, instead of `delta + z...`, is done for the same reason as in the above WV
|
||||
// matrix transform
|
||||
verifier.g -= verifier_weight *
|
||||
x[ni] *
|
||||
(delta - z.inner_product(self.constraints.iter().map(|constraint| &constraint.c)));
|
||||
for pair in V_weights.0.into_iter().zip(self.V.0) {
|
||||
verifier.additional.push((-verifier_weight * pair.0, pair.1));
|
||||
}
|
||||
for (i, T) in T_before_ni.into_iter().enumerate() {
|
||||
verifier.additional.push((-verifier_weight * x[i], T));
|
||||
}
|
||||
for (i, T) in T_after_ni.into_iter().enumerate() {
|
||||
verifier.additional.push((-verifier_weight * x[ni + 1 + i], T));
|
||||
}
|
||||
}
|
||||
|
||||
let verifier_weight = C::F::random(&mut *rng);
|
||||
// Multiply `x` by `verifier_weight` as this effects `verifier_weight` onto most scalars and
|
||||
// saves a notable amount of operations
|
||||
let x = x * verifier_weight;
|
||||
|
||||
// This following block effectively calculates P, within the multiexp
|
||||
{
|
||||
verifier.additional.push((x[ilr], AI));
|
||||
verifier.additional.push((x[io], AO));
|
||||
// h' ** y is equivalent to h as h' is h ** y_inv
|
||||
let mut log2_n = 0;
|
||||
while (1 << log2_n) != n {
|
||||
log2_n += 1;
|
||||
}
|
||||
verifier.h_sum[log2_n] -= verifier_weight;
|
||||
verifier.additional.push((x[is], S));
|
||||
|
||||
// Lines 85-87 calculate WL, WR, WO
|
||||
// We preserve them in terms of g_bold and h_bold for a more efficient multiexp
|
||||
let mut h_bold_scalars = l_weights * x[jlr];
|
||||
for (i, wr) in (r_weights * x[jlr]).0.into_iter().enumerate() {
|
||||
verifier.g_bold[i] += wr;
|
||||
}
|
||||
// WO is weighted by x**jo where jo == 0, hence why we can ignore the x term
|
||||
h_bold_scalars = h_bold_scalars + &(o_weights * verifier_weight);
|
||||
|
||||
let mut cg_weights = Vec::with_capacity(self.C.len());
|
||||
for i in 0 .. self.C.len() {
|
||||
let mut cg = ScalarVector::new(n);
|
||||
for (constraint, z) in self.constraints.iter().zip(&z.0) {
|
||||
if let Some(WCG) = constraint.WCG.get(i) {
|
||||
accumulate_vector(&mut cg, WCG, *z);
|
||||
}
|
||||
}
|
||||
cg_weights.push(cg);
|
||||
}
|
||||
|
||||
// Push the terms for C, which increment from 0, and the terms for WC, which decrement from
|
||||
// n'
|
||||
for (mut i, (C, WCG)) in self.C.0.into_iter().zip(cg_weights).enumerate() {
|
||||
if i >= (ni / 2) {
|
||||
i += 1;
|
||||
}
|
||||
let j = ni - i;
|
||||
verifier.additional.push((x[i], C));
|
||||
h_bold_scalars = h_bold_scalars + &(WCG * x[j]);
|
||||
}
|
||||
|
||||
// All terms for h_bold here have actually been for h_bold', h_bold * y_inv
|
||||
h_bold_scalars = h_bold_scalars * &y_inv;
|
||||
for (i, scalar) in h_bold_scalars.0.into_iter().enumerate() {
|
||||
verifier.h_bold[i] += scalar;
|
||||
}
|
||||
|
||||
// Remove u * h from P
|
||||
verifier.h -= verifier_weight * u;
|
||||
}
|
||||
|
||||
// Prove for lines 88, 92 with an Inner-Product statement
|
||||
// This inlines Protocol 1, as our IpStatement implements Protocol 2
|
||||
let ip_x = transcript.challenge::<C>();
|
||||
// P is amended with this additional term
|
||||
verifier.g += verifier_weight * ip_x * t_caret;
|
||||
IpStatement::new(self.generators, y_inv, ip_x, P::Verifier { verifier_weight })
|
||||
.unwrap()
|
||||
.verify(verifier, transcript)
|
||||
.map_err(AcError::Ip)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use multiexp::multiexp_vartime;
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
||||
|
||||
#[rustfmt::skip]
|
||||
use crate::{ScalarVector, PointVector, ProofGenerators, BatchVerifier, transcript::*, padded_pow_of_2};
|
||||
|
||||
/// An error from proving/verifying Inner-Product statements.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum IpError {
|
||||
/// An incorrect amount of generators was provided.
|
||||
IncorrectAmountOfGenerators,
|
||||
/// The witness was inconsistent to the statement.
|
||||
///
|
||||
/// Sanity checks on the witness are always performed. If the library is compiled with debug
|
||||
/// assertions on, whether or not this witness actually opens `P` is checked.
|
||||
InconsistentWitness,
|
||||
/// The proof wasn't complete and the necessary values could not be read from the transcript.
|
||||
IncompleteProof,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum P<C: Ciphersuite> {
|
||||
Verifier { verifier_weight: C::F },
|
||||
Prover(C::G),
|
||||
}
|
||||
|
||||
/// The Bulletproofs Inner-Product statement.
|
||||
///
|
||||
/// This is for usage with Protocol 2 from the Bulletproofs paper.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct IpStatement<'a, C: Ciphersuite> {
|
||||
generators: ProofGenerators<'a, C>,
|
||||
// Weights for h_bold
|
||||
h_bold_weights: ScalarVector<C::F>,
|
||||
// u as the discrete logarithm of G
|
||||
u: C::F,
|
||||
// P
|
||||
P: P<C>,
|
||||
}
|
||||
|
||||
/// The witness for the Bulletproofs Inner-Product statement.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct IpWitness<C: Ciphersuite> {
|
||||
// a
|
||||
a: ScalarVector<C::F>,
|
||||
// b
|
||||
b: ScalarVector<C::F>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> IpWitness<C> {
|
||||
/// Construct a new witness for an Inner-Product statement.
|
||||
///
|
||||
/// If the witness is less than a power of two, it is padded to the nearest power of two.
|
||||
///
|
||||
/// This functions return None if the lengths of a, b are mismatched or either are empty.
|
||||
pub(crate) fn new(mut a: ScalarVector<C::F>, mut b: ScalarVector<C::F>) -> Option<Self> {
|
||||
if a.0.is_empty() || (a.len() != b.len()) {
|
||||
None?;
|
||||
}
|
||||
|
||||
// Pad to the nearest power of 2
|
||||
let missing = padded_pow_of_2(a.len()) - a.len();
|
||||
a.0.reserve(missing);
|
||||
b.0.reserve(missing);
|
||||
for _ in 0 .. missing {
|
||||
a.0.push(C::F::ZERO);
|
||||
b.0.push(C::F::ZERO);
|
||||
}
|
||||
|
||||
Some(Self { a, b })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C: Ciphersuite> IpStatement<'a, C> {
|
||||
/// Create a new Inner-Product statement.
|
||||
///
|
||||
/// This does not perform any transcripting of any variables within this statement. They must be
|
||||
/// deterministic to the existing transcript.
|
||||
pub(crate) fn new(
|
||||
generators: ProofGenerators<'a, C>,
|
||||
h_bold_weights: ScalarVector<C::F>,
|
||||
u: C::F,
|
||||
P: P<C>,
|
||||
) -> Result<Self, IpError> {
|
||||
if generators.h_bold_slice().len() != h_bold_weights.len() {
|
||||
Err(IpError::IncorrectAmountOfGenerators)?
|
||||
}
|
||||
Ok(Self { generators, h_bold_weights, u, P })
|
||||
}
|
||||
|
||||
/// Prove for this Inner-Product statement.
|
||||
///
|
||||
/// Returns an error if this statement couldn't be proven for (such as if the witness isn't
|
||||
/// consistent).
|
||||
pub(crate) fn prove(
|
||||
self,
|
||||
transcript: &mut Transcript,
|
||||
witness: IpWitness<C>,
|
||||
) -> Result<(), IpError> {
|
||||
let (mut g_bold, mut h_bold, u, mut P, mut a, mut b) = {
|
||||
let IpStatement { generators, h_bold_weights, u, P } = self;
|
||||
let u = generators.g() * u;
|
||||
|
||||
// Ensure we have the exact amount of generators
|
||||
if generators.g_bold_slice().len() != witness.a.len() {
|
||||
Err(IpError::IncorrectAmountOfGenerators)?;
|
||||
}
|
||||
// Acquire a local copy of the generators
|
||||
let g_bold = PointVector::<C>(generators.g_bold_slice().to_vec());
|
||||
let h_bold = PointVector::<C>(generators.h_bold_slice().to_vec()).mul_vec(&h_bold_weights);
|
||||
|
||||
let IpWitness { a, b } = witness;
|
||||
|
||||
let P = match P {
|
||||
P::Prover(point) => point,
|
||||
P::Verifier { .. } => {
|
||||
panic!("prove called with a P specification which was for the verifier")
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure this witness actually opens this statement
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let ag = a.0.iter().cloned().zip(g_bold.0.iter().cloned());
|
||||
let bh = b.0.iter().cloned().zip(h_bold.0.iter().cloned());
|
||||
let cu = core::iter::once((a.inner_product(b.0.iter()), u));
|
||||
if P != multiexp_vartime(&ag.chain(bh).chain(cu).collect::<Vec<_>>()) {
|
||||
Err(IpError::InconsistentWitness)?;
|
||||
}
|
||||
}
|
||||
|
||||
(g_bold, h_bold, u, P, a, b)
|
||||
};
|
||||
|
||||
// `else: (n > 1)` case, lines 18-35 of the Bulletproofs paper
|
||||
// This interprets `g_bold.len()` as `n`
|
||||
while g_bold.len() > 1 {
|
||||
// Split a, b, g_bold, h_bold as needed for lines 20-24
|
||||
let (a1, a2) = a.clone().split();
|
||||
let (b1, b2) = b.clone().split();
|
||||
|
||||
let (g_bold1, g_bold2) = g_bold.split();
|
||||
let (h_bold1, h_bold2) = h_bold.split();
|
||||
|
||||
let n_hat = g_bold1.len();
|
||||
|
||||
// Sanity
|
||||
debug_assert_eq!(a1.len(), n_hat);
|
||||
debug_assert_eq!(a2.len(), n_hat);
|
||||
debug_assert_eq!(b1.len(), n_hat);
|
||||
debug_assert_eq!(b2.len(), n_hat);
|
||||
debug_assert_eq!(g_bold1.len(), n_hat);
|
||||
debug_assert_eq!(g_bold2.len(), n_hat);
|
||||
debug_assert_eq!(h_bold1.len(), n_hat);
|
||||
debug_assert_eq!(h_bold2.len(), n_hat);
|
||||
|
||||
// cl, cr, lines 21-22
|
||||
let cl = a1.inner_product(b2.0.iter());
|
||||
let cr = a2.inner_product(b1.0.iter());
|
||||
|
||||
let L = {
|
||||
let mut L_terms = Vec::with_capacity(1 + (2 * g_bold1.len()));
|
||||
for (a, g) in a1.0.iter().zip(g_bold2.0.iter()) {
|
||||
L_terms.push((*a, *g));
|
||||
}
|
||||
for (b, h) in b2.0.iter().zip(h_bold1.0.iter()) {
|
||||
L_terms.push((*b, *h));
|
||||
}
|
||||
L_terms.push((cl, u));
|
||||
// Uses vartime since this isn't a ZK proof
|
||||
multiexp_vartime(&L_terms)
|
||||
};
|
||||
|
||||
let R = {
|
||||
let mut R_terms = Vec::with_capacity(1 + (2 * g_bold1.len()));
|
||||
for (a, g) in a2.0.iter().zip(g_bold1.0.iter()) {
|
||||
R_terms.push((*a, *g));
|
||||
}
|
||||
for (b, h) in b1.0.iter().zip(h_bold2.0.iter()) {
|
||||
R_terms.push((*b, *h));
|
||||
}
|
||||
R_terms.push((cr, u));
|
||||
multiexp_vartime(&R_terms)
|
||||
};
|
||||
|
||||
// Now that we've calculate L, R, transcript them to receive x (26-27)
|
||||
transcript.push_point(L);
|
||||
transcript.push_point(R);
|
||||
let x: C::F = transcript.challenge::<C>();
|
||||
let x_inv = x.invert().unwrap();
|
||||
|
||||
// The prover and verifier now calculate the following (28-31)
|
||||
g_bold = PointVector(Vec::with_capacity(g_bold1.len()));
|
||||
for (a, b) in g_bold1.0.into_iter().zip(g_bold2.0.into_iter()) {
|
||||
g_bold.0.push(multiexp_vartime(&[(x_inv, a), (x, b)]));
|
||||
}
|
||||
h_bold = PointVector(Vec::with_capacity(h_bold1.len()));
|
||||
for (a, b) in h_bold1.0.into_iter().zip(h_bold2.0.into_iter()) {
|
||||
h_bold.0.push(multiexp_vartime(&[(x, a), (x_inv, b)]));
|
||||
}
|
||||
P = (L * (x * x)) + P + (R * (x_inv * x_inv));
|
||||
|
||||
// 32-34
|
||||
a = (a1 * x) + &(a2 * x_inv);
|
||||
b = (b1 * x_inv) + &(b2 * x);
|
||||
}
|
||||
|
||||
// `if n = 1` case from line 14-17
|
||||
|
||||
// Sanity
|
||||
debug_assert_eq!(g_bold.len(), 1);
|
||||
debug_assert_eq!(h_bold.len(), 1);
|
||||
debug_assert_eq!(a.len(), 1);
|
||||
debug_assert_eq!(b.len(), 1);
|
||||
|
||||
// We simply send a/b
|
||||
transcript.push_scalar(a[0]);
|
||||
transcript.push_scalar(b[0]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
This has room for optimization worth investigating further. It currently takes
|
||||
an iterative approach. It can be optimized further via divide and conquer.
|
||||
|
||||
Assume there are 4 challenges.
|
||||
|
||||
Iterative approach (current):
|
||||
1. Do the optimal multiplications across challenge column 0 and 1.
|
||||
2. Do the optimal multiplications across that result and column 2.
|
||||
3. Do the optimal multiplications across that result and column 3.
|
||||
|
||||
Divide and conquer (worth investigating further):
|
||||
1. Do the optimal multiplications across challenge column 0 and 1.
|
||||
2. Do the optimal multiplications across challenge column 2 and 3.
|
||||
3. Multiply both results together.
|
||||
|
||||
When there are 4 challenges (n=16), the iterative approach does 28 multiplications
|
||||
versus divide and conquer's 24.
|
||||
*/
|
||||
fn challenge_products(challenges: &[(C::F, C::F)]) -> Vec<C::F> {
|
||||
let mut products = vec![C::F::ONE; 1 << challenges.len()];
|
||||
|
||||
if !challenges.is_empty() {
|
||||
products[0] = challenges[0].1;
|
||||
products[1] = challenges[0].0;
|
||||
|
||||
for (j, challenge) in challenges.iter().enumerate().skip(1) {
|
||||
let mut slots = (1 << (j + 1)) - 1;
|
||||
while slots > 0 {
|
||||
products[slots] = products[slots / 2] * challenge.0;
|
||||
products[slots - 1] = products[slots / 2] * challenge.1;
|
||||
|
||||
slots = slots.saturating_sub(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check since if the above failed to populate, it'd be critical
|
||||
for product in &products {
|
||||
debug_assert!(!bool::from(product.is_zero()));
|
||||
}
|
||||
}
|
||||
|
||||
products
|
||||
}
|
||||
|
||||
/// Queue an Inner-Product proof for batch verification.
|
||||
///
|
||||
/// This will return Err if there is an error. This will return Ok if the proof was successfully
|
||||
/// queued for batch verification. The caller is required to verify the batch in order to ensure
|
||||
/// the proof is actually correct.
|
||||
///
|
||||
/// If this proof returns an error, the BatchVerifier MUST be assumed corrupted and discarded.
|
||||
pub(crate) fn verify(
|
||||
self,
|
||||
verifier: &mut BatchVerifier<C>,
|
||||
transcript: &mut VerifierTranscript,
|
||||
) -> Result<(), IpError> {
|
||||
if verifier.g_bold.len() < self.generators.len() {
|
||||
verifier.g_bold.resize(self.generators.len(), C::F::ZERO);
|
||||
verifier.h_bold.resize(self.generators.len(), C::F::ZERO);
|
||||
verifier.h_sum.resize(self.generators.len(), C::F::ZERO);
|
||||
}
|
||||
|
||||
let IpStatement { generators, h_bold_weights, u, P } = self;
|
||||
|
||||
// Calculate the discrete log w.r.t. 2 for the amount of generators present
|
||||
let mut lr_len = 0;
|
||||
while (1 << lr_len) < generators.g_bold_slice().len() {
|
||||
lr_len += 1;
|
||||
}
|
||||
|
||||
let weight = match P {
|
||||
P::Prover(_) => panic!("prove called with a P specification which was for the prover"),
|
||||
P::Verifier { verifier_weight } => verifier_weight,
|
||||
};
|
||||
|
||||
// Again, we start with the `else: (n > 1)` case
|
||||
|
||||
// We need x, x_inv per lines 25-27 for lines 28-31
|
||||
let mut L = Vec::with_capacity(lr_len);
|
||||
let mut R = Vec::with_capacity(lr_len);
|
||||
let mut xs: Vec<C::F> = Vec::with_capacity(lr_len);
|
||||
for _ in 0 .. lr_len {
|
||||
L.push(transcript.read_point::<C>().map_err(|_| IpError::IncompleteProof)?);
|
||||
R.push(transcript.read_point::<C>().map_err(|_| IpError::IncompleteProof)?);
|
||||
xs.push(transcript.challenge::<C>());
|
||||
}
|
||||
|
||||
// We calculate their inverse in batch
|
||||
let mut x_invs = xs.clone();
|
||||
{
|
||||
let mut scratch = vec![C::F::ZERO; x_invs.len()];
|
||||
ciphersuite::group::ff::BatchInverter::invert_with_external_scratch(
|
||||
&mut x_invs,
|
||||
&mut scratch,
|
||||
);
|
||||
}
|
||||
|
||||
// Now, with x and x_inv, we need to calculate g_bold', h_bold', P'
|
||||
//
|
||||
// For the sake of performance, we solely want to calculate all of these in terms of scalings
|
||||
// for g_bold, h_bold, P, and don't want to actually perform intermediary scalings of the
|
||||
// points
|
||||
//
|
||||
// L and R are easy, as it's simply x**2, x**-2
|
||||
//
|
||||
// For the series of g_bold, h_bold, we use the `challenge_products` function
|
||||
// For how that works, please see its own documentation
|
||||
let product_cache = {
|
||||
let mut challenges = Vec::with_capacity(lr_len);
|
||||
|
||||
let x_iter = xs.into_iter().zip(x_invs);
|
||||
let lr_iter = L.into_iter().zip(R);
|
||||
for ((x, x_inv), (L, R)) in x_iter.zip(lr_iter) {
|
||||
challenges.push((x, x_inv));
|
||||
verifier.additional.push((weight * x.square(), L));
|
||||
verifier.additional.push((weight * x_inv.square(), R));
|
||||
}
|
||||
|
||||
Self::challenge_products(&challenges)
|
||||
};
|
||||
|
||||
// And now for the `if n = 1` case
|
||||
let a = transcript.read_scalar::<C>().map_err(|_| IpError::IncompleteProof)?;
|
||||
let b = transcript.read_scalar::<C>().map_err(|_| IpError::IncompleteProof)?;
|
||||
let c = a * b;
|
||||
|
||||
// The multiexp of these terms equate to the final permutation of P
|
||||
// We now add terms for a * g_bold' + b * h_bold' b + c * u, with the scalars negative such
|
||||
// that the terms sum to 0 for an honest prover
|
||||
|
||||
// The g_bold * a term case from line 16
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0 .. generators.g_bold_slice().len() {
|
||||
verifier.g_bold[i] -= weight * product_cache[i] * a;
|
||||
}
|
||||
// The h_bold * b term case from line 16
|
||||
for i in 0 .. generators.h_bold_slice().len() {
|
||||
verifier.h_bold[i] -=
|
||||
weight * product_cache[product_cache.len() - 1 - i] * b * h_bold_weights[i];
|
||||
}
|
||||
// The c * u term case from line 16
|
||||
verifier.g -= weight * c * u;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use core::fmt;
|
||||
use std_shims::{vec, vec::Vec, collections::HashSet};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use multiexp::{multiexp, multiexp_vartime};
|
||||
use ciphersuite::{
|
||||
group::{ff::Field, Group, GroupEncoding},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
mod scalar_vector;
|
||||
pub use scalar_vector::ScalarVector;
|
||||
mod point_vector;
|
||||
pub use point_vector::PointVector;
|
||||
|
||||
/// The transcript formats.
|
||||
pub mod transcript;
|
||||
|
||||
pub(crate) mod inner_product;
|
||||
|
||||
pub(crate) mod lincomb;
|
||||
|
||||
/// The arithmetic circuit proof.
|
||||
pub mod arithmetic_circuit_proof;
|
||||
|
||||
/// Functionlity useful when testing.
|
||||
#[cfg(any(test, feature = "tests"))]
|
||||
pub mod tests;
|
||||
|
||||
/// Calculate the nearest power of two greater than or equivalent to the argument.
|
||||
pub(crate) fn padded_pow_of_2(i: usize) -> usize {
|
||||
let mut next_pow_of_2 = 1;
|
||||
while next_pow_of_2 < i {
|
||||
next_pow_of_2 <<= 1;
|
||||
}
|
||||
next_pow_of_2
|
||||
}
|
||||
|
||||
/// An error from working with generators.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum GeneratorsError {
|
||||
/// The provided list of generators for `g` (bold) was empty.
|
||||
GBoldEmpty,
|
||||
/// The provided list of generators for `h` (bold) did not match `g` (bold) in length.
|
||||
DifferingGhBoldLengths,
|
||||
/// The amount of provided generators were not a power of two.
|
||||
NotPowerOfTwo,
|
||||
/// A generator was used multiple times.
|
||||
DuplicatedGenerator,
|
||||
}
|
||||
|
||||
/// A full set of generators.
|
||||
#[derive(Clone)]
|
||||
pub struct Generators<C: Ciphersuite> {
|
||||
g: C::G,
|
||||
h: C::G,
|
||||
|
||||
g_bold: Vec<C::G>,
|
||||
h_bold: Vec<C::G>,
|
||||
h_sum: Vec<C::G>,
|
||||
}
|
||||
|
||||
/// A batch verifier of proofs.
|
||||
#[must_use]
|
||||
#[derive(Clone)]
|
||||
pub struct BatchVerifier<C: Ciphersuite> {
|
||||
/// The summed scalar for the G generator.
|
||||
pub g: C::F,
|
||||
/// The summed scalar for the G generator.
|
||||
pub h: C::F,
|
||||
|
||||
/// The summed scalars for the G_bold generators.
|
||||
pub g_bold: Vec<C::F>,
|
||||
/// The summed scalars for the H_bold generators.
|
||||
pub h_bold: Vec<C::F>,
|
||||
/// The summed scalars for the sums of all H generators prior to the index.
|
||||
///
|
||||
/// This is not populated with the full set of summed H generators. This is only populated with
|
||||
/// the powers of 2. Accordingly, an index i specifies a scalar for the sum of all H generators
|
||||
/// from H**2**0 ..= H**2**i.
|
||||
pub h_sum: Vec<C::F>,
|
||||
|
||||
/// Additional (non-fixed) points to include in the multiexp.
|
||||
///
|
||||
/// This is used for proof-specific elements.
|
||||
pub additional: Vec<(C::F, C::G)>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for Generators<C> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let g = self.g.to_bytes();
|
||||
let g: &[u8] = g.as_ref();
|
||||
|
||||
let h = self.h.to_bytes();
|
||||
let h: &[u8] = h.as_ref();
|
||||
|
||||
fmt.debug_struct("Generators").field("g", &g).field("h", &h).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// The generators for a specific proof.
|
||||
///
|
||||
/// This potentially have been reduced in size from the original set of generators, as beneficial
|
||||
/// to performance.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ProofGenerators<'a, C: Ciphersuite> {
|
||||
g: &'a C::G,
|
||||
h: &'a C::G,
|
||||
|
||||
g_bold: &'a [C::G],
|
||||
h_bold: &'a [C::G],
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> fmt::Debug for ProofGenerators<'_, C> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let g = self.g.to_bytes();
|
||||
let g: &[u8] = g.as_ref();
|
||||
|
||||
let h = self.h.to_bytes();
|
||||
let h: &[u8] = h.as_ref();
|
||||
|
||||
fmt.debug_struct("ProofGenerators").field("g", &g).field("h", &h).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Generators<C> {
|
||||
/// Construct an instance of Generators for usage with Bulletproofs.
|
||||
pub fn new(
|
||||
g: C::G,
|
||||
h: C::G,
|
||||
g_bold: Vec<C::G>,
|
||||
h_bold: Vec<C::G>,
|
||||
) -> Result<Self, GeneratorsError> {
|
||||
if g_bold.is_empty() {
|
||||
Err(GeneratorsError::GBoldEmpty)?;
|
||||
}
|
||||
if g_bold.len() != h_bold.len() {
|
||||
Err(GeneratorsError::DifferingGhBoldLengths)?;
|
||||
}
|
||||
if padded_pow_of_2(g_bold.len()) != g_bold.len() {
|
||||
Err(GeneratorsError::NotPowerOfTwo)?;
|
||||
}
|
||||
|
||||
let mut set = HashSet::new();
|
||||
let mut add_generator = |generator: &C::G| {
|
||||
assert!(!bool::from(generator.is_identity()));
|
||||
let bytes = generator.to_bytes();
|
||||
!set.insert(bytes.as_ref().to_vec())
|
||||
};
|
||||
|
||||
assert!(!add_generator(&g), "g was prior present in empty set");
|
||||
if add_generator(&h) {
|
||||
Err(GeneratorsError::DuplicatedGenerator)?;
|
||||
}
|
||||
for g in &g_bold {
|
||||
if add_generator(g) {
|
||||
Err(GeneratorsError::DuplicatedGenerator)?;
|
||||
}
|
||||
}
|
||||
for h in &h_bold {
|
||||
if add_generator(h) {
|
||||
Err(GeneratorsError::DuplicatedGenerator)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut running_h_sum = C::G::identity();
|
||||
let mut h_sum = vec![];
|
||||
let mut next_pow_of_2 = 1;
|
||||
for (i, h) in h_bold.iter().enumerate() {
|
||||
running_h_sum += h;
|
||||
if (i + 1) == next_pow_of_2 {
|
||||
h_sum.push(running_h_sum);
|
||||
next_pow_of_2 *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Generators { g, h, g_bold, h_bold, h_sum })
|
||||
}
|
||||
|
||||
/// Create a BatchVerifier for proofs which use a consistent set of generators.
|
||||
pub fn batch_verifier() -> BatchVerifier<C> {
|
||||
BatchVerifier {
|
||||
g: C::F::ZERO,
|
||||
h: C::F::ZERO,
|
||||
|
||||
g_bold: vec![],
|
||||
h_bold: vec![],
|
||||
h_sum: vec![],
|
||||
|
||||
additional: Vec::with_capacity(128),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify all proofs queued for batch verification in this BatchVerifier.
|
||||
#[must_use]
|
||||
pub fn verify(&self, verifier: BatchVerifier<C>) -> bool {
|
||||
multiexp_vartime(
|
||||
&[(verifier.g, self.g), (verifier.h, self.h)]
|
||||
.into_iter()
|
||||
.chain(verifier.g_bold.into_iter().zip(self.g_bold.iter().cloned()))
|
||||
.chain(verifier.h_bold.into_iter().zip(self.h_bold.iter().cloned()))
|
||||
.chain(verifier.h_sum.into_iter().zip(self.h_sum.iter().cloned()))
|
||||
.chain(verifier.additional)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.is_identity()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The `g` generator.
|
||||
pub fn g(&self) -> C::G {
|
||||
self.g
|
||||
}
|
||||
|
||||
/// The `h` generator.
|
||||
pub fn h(&self) -> C::G {
|
||||
self.h
|
||||
}
|
||||
|
||||
/// A slice to view the `g` (bold) generators.
|
||||
pub fn g_bold_slice(&self) -> &[C::G] {
|
||||
&self.g_bold
|
||||
}
|
||||
|
||||
/// A slice to view the `h` (bold) generators.
|
||||
pub fn h_bold_slice(&self) -> &[C::G] {
|
||||
&self.h_bold
|
||||
}
|
||||
|
||||
/// Reduce a set of generators to the quantity necessary to support a certain amount of
|
||||
/// in-circuit multiplications/terms in a Pedersen vector commitment.
|
||||
///
|
||||
/// Returns None if reducing to 0 or if the generators reduced are insufficient to provide this
|
||||
/// many generators.
|
||||
pub fn reduce(&self, generators: usize) -> Option<ProofGenerators<'_, C>> {
|
||||
if generators == 0 {
|
||||
None?;
|
||||
}
|
||||
|
||||
// Round to the nearest power of 2
|
||||
let generators = padded_pow_of_2(generators);
|
||||
if generators > self.g_bold.len() {
|
||||
None?;
|
||||
}
|
||||
|
||||
Some(ProofGenerators {
|
||||
g: &self.g,
|
||||
h: &self.h,
|
||||
|
||||
g_bold: &self.g_bold[.. generators],
|
||||
h_bold: &self.h_bold[.. generators],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> ProofGenerators<'_, C> {
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.g_bold.len()
|
||||
}
|
||||
|
||||
pub(crate) fn g(&self) -> C::G {
|
||||
*self.g
|
||||
}
|
||||
|
||||
pub(crate) fn h(&self) -> C::G {
|
||||
*self.h
|
||||
}
|
||||
|
||||
pub(crate) fn g_bold(&self, i: usize) -> C::G {
|
||||
self.g_bold[i]
|
||||
}
|
||||
|
||||
pub(crate) fn h_bold(&self, i: usize) -> C::G {
|
||||
self.h_bold[i]
|
||||
}
|
||||
|
||||
pub(crate) fn g_bold_slice(&self) -> &[C::G] {
|
||||
self.g_bold
|
||||
}
|
||||
|
||||
pub(crate) fn h_bold_slice(&self) -> &[C::G] {
|
||||
self.h_bold
|
||||
}
|
||||
}
|
||||
|
||||
/// The opening of a Pedersen commitment.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct PedersenCommitment<C: Ciphersuite> {
|
||||
/// The value committed to.
|
||||
pub value: C::F,
|
||||
/// The mask blinding the value committed to.
|
||||
pub mask: C::F,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> PedersenCommitment<C> {
|
||||
/// Commit to this value, yielding the Pedersen commitment.
|
||||
pub fn commit(&self, g: C::G, h: C::G) -> C::G {
|
||||
multiexp(&[(self.value, g), (self.mask, h)])
|
||||
}
|
||||
}
|
||||
|
||||
/// The opening of a Pedersen vector commitment.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct PedersenVectorCommitment<C: Ciphersuite> {
|
||||
/// The values committed to across the `g` (bold) generators.
|
||||
pub g_values: ScalarVector<C::F>,
|
||||
/// The mask blinding the values committed to.
|
||||
pub mask: C::F,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> PedersenVectorCommitment<C> {
|
||||
/// Commit to the vectors of values.
|
||||
///
|
||||
/// This function returns None if the amount of generators is less than the amount of values
|
||||
/// within the relevant vector.
|
||||
pub fn commit(&self, g_bold: &[C::G], h: C::G) -> Option<C::G> {
|
||||
if g_bold.len() < self.g_values.len() {
|
||||
None?;
|
||||
};
|
||||
|
||||
let mut terms = vec![(self.mask, h)];
|
||||
for pair in self.g_values.0.iter().cloned().zip(g_bold.iter().cloned()) {
|
||||
terms.push(pair);
|
||||
}
|
||||
let res = multiexp(&terms);
|
||||
terms.zeroize();
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
use core::ops::{Add, Sub, Mul};
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use ciphersuite::group::ff::PrimeField;
|
||||
|
||||
use crate::ScalarVector;
|
||||
|
||||
/// A reference to a variable usable within linear combinations.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Variable {
|
||||
/// A variable within the left vector of vectors multiplied against each other.
|
||||
aL(usize),
|
||||
/// A variable within the right vector of vectors multiplied against each other.
|
||||
aR(usize),
|
||||
/// A variable within the output vector of the left vector multiplied by the right vector.
|
||||
aO(usize),
|
||||
/// A variable within a Pedersen vector commitment, committed to with a generator from `g` (bold).
|
||||
CG {
|
||||
/// The commitment being indexed.
|
||||
commitment: usize,
|
||||
/// The index of the variable.
|
||||
index: usize,
|
||||
},
|
||||
/// A variable within a Pedersen commitment.
|
||||
V(usize),
|
||||
}
|
||||
|
||||
// Does a NOP as there shouldn't be anything critical here
|
||||
impl Zeroize for Variable {
|
||||
fn zeroize(&mut self) {}
|
||||
}
|
||||
|
||||
/// A linear combination.
|
||||
///
|
||||
/// Specifically, `WL aL + WR aR + WO aO + WCG C_G + WV V + c`.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
#[must_use]
|
||||
pub struct LinComb<F: PrimeField> {
|
||||
pub(crate) highest_a_index: Option<usize>,
|
||||
pub(crate) highest_c_index: Option<usize>,
|
||||
pub(crate) highest_v_index: Option<usize>,
|
||||
|
||||
// Sparse representation of WL/WR/WO
|
||||
pub(crate) WL: Vec<(usize, F)>,
|
||||
pub(crate) WR: Vec<(usize, F)>,
|
||||
pub(crate) WO: Vec<(usize, F)>,
|
||||
// Sparse representation once within a commitment
|
||||
pub(crate) WCG: Vec<Vec<(usize, F)>>,
|
||||
// Sparse representation of WV
|
||||
pub(crate) WV: Vec<(usize, F)>,
|
||||
pub(crate) c: F,
|
||||
}
|
||||
|
||||
impl<F: PrimeField> From<Variable> for LinComb<F> {
|
||||
fn from(constrainable: Variable) -> LinComb<F> {
|
||||
LinComb::empty().term(F::ONE, constrainable)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> Add<&LinComb<F>> for LinComb<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(mut self, constraint: &Self) -> Self {
|
||||
self.highest_a_index = self.highest_a_index.max(constraint.highest_a_index);
|
||||
self.highest_c_index = self.highest_c_index.max(constraint.highest_c_index);
|
||||
self.highest_v_index = self.highest_v_index.max(constraint.highest_v_index);
|
||||
|
||||
self.WL.extend(&constraint.WL);
|
||||
self.WR.extend(&constraint.WR);
|
||||
self.WO.extend(&constraint.WO);
|
||||
while self.WCG.len() < constraint.WCG.len() {
|
||||
self.WCG.push(vec![]);
|
||||
}
|
||||
for (sWC, cWC) in self.WCG.iter_mut().zip(&constraint.WCG) {
|
||||
sWC.extend(cWC);
|
||||
}
|
||||
self.WV.extend(&constraint.WV);
|
||||
self.c += constraint.c;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> Sub<&LinComb<F>> for LinComb<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(mut self, constraint: &Self) -> Self {
|
||||
self.highest_a_index = self.highest_a_index.max(constraint.highest_a_index);
|
||||
self.highest_c_index = self.highest_c_index.max(constraint.highest_c_index);
|
||||
self.highest_v_index = self.highest_v_index.max(constraint.highest_v_index);
|
||||
|
||||
self.WL.extend(constraint.WL.iter().map(|(i, weight)| (*i, -*weight)));
|
||||
self.WR.extend(constraint.WR.iter().map(|(i, weight)| (*i, -*weight)));
|
||||
self.WO.extend(constraint.WO.iter().map(|(i, weight)| (*i, -*weight)));
|
||||
while self.WCG.len() < constraint.WCG.len() {
|
||||
self.WCG.push(vec![]);
|
||||
}
|
||||
for (sWC, cWC) in self.WCG.iter_mut().zip(&constraint.WCG) {
|
||||
sWC.extend(cWC.iter().map(|(i, weight)| (*i, -*weight)));
|
||||
}
|
||||
self.WV.extend(constraint.WV.iter().map(|(i, weight)| (*i, -*weight)));
|
||||
self.c -= constraint.c;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> Mul<F> for LinComb<F> {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(mut self, scalar: F) -> Self {
|
||||
for (_, weight) in self.WL.iter_mut() {
|
||||
*weight *= scalar;
|
||||
}
|
||||
for (_, weight) in self.WR.iter_mut() {
|
||||
*weight *= scalar;
|
||||
}
|
||||
for (_, weight) in self.WO.iter_mut() {
|
||||
*weight *= scalar;
|
||||
}
|
||||
for WC in self.WCG.iter_mut() {
|
||||
for (_, weight) in WC {
|
||||
*weight *= scalar;
|
||||
}
|
||||
}
|
||||
for (_, weight) in self.WV.iter_mut() {
|
||||
*weight *= scalar;
|
||||
}
|
||||
self.c *= scalar;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> LinComb<F> {
|
||||
/// Create an empty linear combination.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
highest_a_index: None,
|
||||
highest_c_index: None,
|
||||
highest_v_index: None,
|
||||
WL: vec![],
|
||||
WR: vec![],
|
||||
WO: vec![],
|
||||
WCG: vec![],
|
||||
WV: vec![],
|
||||
c: F::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new instance of a term to this linear combination.
|
||||
pub fn term(mut self, scalar: F, constrainable: Variable) -> Self {
|
||||
match constrainable {
|
||||
Variable::aL(i) => {
|
||||
self.highest_a_index = self.highest_a_index.max(Some(i));
|
||||
self.WL.push((i, scalar))
|
||||
}
|
||||
Variable::aR(i) => {
|
||||
self.highest_a_index = self.highest_a_index.max(Some(i));
|
||||
self.WR.push((i, scalar))
|
||||
}
|
||||
Variable::aO(i) => {
|
||||
self.highest_a_index = self.highest_a_index.max(Some(i));
|
||||
self.WO.push((i, scalar))
|
||||
}
|
||||
Variable::CG { commitment: i, index: j } => {
|
||||
self.highest_c_index = self.highest_c_index.max(Some(i));
|
||||
self.highest_a_index = self.highest_a_index.max(Some(j));
|
||||
while self.WCG.len() <= i {
|
||||
self.WCG.push(vec![]);
|
||||
}
|
||||
self.WCG[i].push((j, scalar))
|
||||
}
|
||||
Variable::V(i) => {
|
||||
self.highest_v_index = self.highest_v_index.max(Some(i));
|
||||
self.WV.push((i, scalar));
|
||||
}
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Add to the constant c.
|
||||
pub fn constant(mut self, scalar: F) -> Self {
|
||||
self.c += scalar;
|
||||
self
|
||||
}
|
||||
|
||||
/// View the current weights for aL.
|
||||
pub fn WL(&self) -> &[(usize, F)] {
|
||||
&self.WL
|
||||
}
|
||||
|
||||
/// View the current weights for aR.
|
||||
pub fn WR(&self) -> &[(usize, F)] {
|
||||
&self.WR
|
||||
}
|
||||
|
||||
/// View the current weights for aO.
|
||||
pub fn WO(&self) -> &[(usize, F)] {
|
||||
&self.WO
|
||||
}
|
||||
|
||||
/// View the current weights for CG.
|
||||
pub fn WCG(&self) -> &[Vec<(usize, F)>] {
|
||||
&self.WCG
|
||||
}
|
||||
|
||||
/// View the current weights for V.
|
||||
pub fn WV(&self) -> &[(usize, F)] {
|
||||
&self.WV
|
||||
}
|
||||
|
||||
/// View the current constant.
|
||||
pub fn c(&self) -> F {
|
||||
self.c
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn accumulate_vector<F: PrimeField>(
|
||||
accumulator: &mut ScalarVector<F>,
|
||||
values: &[(usize, F)],
|
||||
weight: F,
|
||||
) {
|
||||
for (i, coeff) in values {
|
||||
accumulator[*i] += *coeff * weight;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use core::ops::{Index, IndexMut};
|
||||
use std_shims::vec::Vec;
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use ciphersuite::Ciphersuite;
|
||||
|
||||
#[cfg(test)]
|
||||
use multiexp::multiexp;
|
||||
|
||||
use crate::ScalarVector;
|
||||
|
||||
/// A point vector struct with the functionality necessary for Bulletproofs.
|
||||
///
|
||||
/// The math operations for this panic upon any invalid operation, such as if vectors of different
|
||||
/// lengths are added. The full extent of invalidity is not fully defined. Only field access is
|
||||
/// guaranteed to have a safe, public API.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct PointVector<C: Ciphersuite>(pub(crate) Vec<C::G>);
|
||||
|
||||
impl<C: Ciphersuite> Index<usize> for PointVector<C> {
|
||||
type Output = C::G;
|
||||
fn index(&self, index: usize) -> &C::G {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> IndexMut<usize> for PointVector<C> {
|
||||
fn index_mut(&mut self, index: usize) -> &mut C::G {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> PointVector<C> {
|
||||
/*
|
||||
pub(crate) fn add(&self, point: impl AsRef<C::G>) -> Self {
|
||||
let mut res = self.clone();
|
||||
for val in res.0.iter_mut() {
|
||||
*val += point.as_ref();
|
||||
}
|
||||
res
|
||||
}
|
||||
pub(crate) fn sub(&self, point: impl AsRef<C::G>) -> Self {
|
||||
let mut res = self.clone();
|
||||
for val in res.0.iter_mut() {
|
||||
*val -= point.as_ref();
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn mul(&self, scalar: impl core::borrow::Borrow<C::F>) -> Self {
|
||||
let mut res = self.clone();
|
||||
for val in res.0.iter_mut() {
|
||||
*val *= scalar.borrow();
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn add_vec(&self, vector: &Self) -> Self {
|
||||
debug_assert_eq!(self.len(), vector.len());
|
||||
let mut res = self.clone();
|
||||
for (i, val) in res.0.iter_mut().enumerate() {
|
||||
*val += vector.0[i];
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn sub_vec(&self, vector: &Self) -> Self {
|
||||
debug_assert_eq!(self.len(), vector.len());
|
||||
let mut res = self.clone();
|
||||
for (i, val) in res.0.iter_mut().enumerate() {
|
||||
*val -= vector.0[i];
|
||||
}
|
||||
res
|
||||
}
|
||||
*/
|
||||
|
||||
pub(crate) fn mul_vec(&self, vector: &ScalarVector<C::F>) -> Self {
|
||||
debug_assert_eq!(self.len(), vector.len());
|
||||
let mut res = self.clone();
|
||||
for (i, val) in res.0.iter_mut().enumerate() {
|
||||
*val *= vector.0[i];
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn multiexp(&self, vector: &crate::ScalarVector<C::F>) -> C::G {
|
||||
debug_assert_eq!(self.len(), vector.len());
|
||||
let mut res = Vec::with_capacity(self.len());
|
||||
for (point, scalar) in self.0.iter().copied().zip(vector.0.iter().copied()) {
|
||||
res.push((scalar, point));
|
||||
}
|
||||
multiexp(&res)
|
||||
}
|
||||
|
||||
/*
|
||||
pub(crate) fn multiexp_vartime(&self, vector: &ScalarVector<C::F>) -> C::G {
|
||||
debug_assert_eq!(self.len(), vector.len());
|
||||
let mut res = Vec::with_capacity(self.len());
|
||||
for (point, scalar) in self.0.iter().copied().zip(vector.0.iter().copied()) {
|
||||
res.push((scalar, point));
|
||||
}
|
||||
multiexp_vartime(&res)
|
||||
}
|
||||
|
||||
pub(crate) fn sum(&self) -> C::G {
|
||||
self.0.iter().sum()
|
||||
}
|
||||
*/
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub(crate) fn split(mut self) -> (Self, Self) {
|
||||
assert!(self.len() > 1);
|
||||
let r = self.0.split_off(self.0.len() / 2);
|
||||
debug_assert_eq!(self.len(), r.len());
|
||||
(self, PointVector(r))
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
use core::ops::{Index, IndexMut, Add, Sub, Mul};
|
||||
use std_shims::{vec, vec::Vec};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use ciphersuite::group::ff::PrimeField;
|
||||
|
||||
/// A scalar vector struct with the functionality necessary for Bulletproofs.
|
||||
///
|
||||
/// The math operations for this panic upon any invalid operation, such as if vectors of different
|
||||
/// lengths are added. The full extent of invalidity is not fully defined. Only `new`, `len`,
|
||||
/// and field access is guaranteed to have a safe, public API.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ScalarVector<F: PrimeField>(pub(crate) Vec<F>);
|
||||
|
||||
impl<F: PrimeField + Zeroize> Zeroize for ScalarVector<F> {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.zeroize()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> Index<usize> for ScalarVector<F> {
|
||||
type Output = F;
|
||||
fn index(&self, index: usize) -> &F {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> IndexMut<usize> for ScalarVector<F> {
|
||||
fn index_mut(&mut self, index: usize) -> &mut F {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> Add<F> for ScalarVector<F> {
|
||||
type Output = ScalarVector<F>;
|
||||
fn add(mut self, scalar: F) -> Self {
|
||||
for s in &mut self.0 {
|
||||
*s += scalar;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> Sub<F> for ScalarVector<F> {
|
||||
type Output = ScalarVector<F>;
|
||||
fn sub(mut self, scalar: F) -> Self {
|
||||
for s in &mut self.0 {
|
||||
*s -= scalar;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> Mul<F> for ScalarVector<F> {
|
||||
type Output = ScalarVector<F>;
|
||||
fn mul(mut self, scalar: F) -> Self {
|
||||
for s in &mut self.0 {
|
||||
*s *= scalar;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> Add<&ScalarVector<F>> for ScalarVector<F> {
|
||||
type Output = ScalarVector<F>;
|
||||
fn add(mut self, other: &ScalarVector<F>) -> Self {
|
||||
assert_eq!(self.len(), other.len());
|
||||
for (s, o) in self.0.iter_mut().zip(other.0.iter()) {
|
||||
*s += o;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> Sub<&ScalarVector<F>> for ScalarVector<F> {
|
||||
type Output = ScalarVector<F>;
|
||||
fn sub(mut self, other: &ScalarVector<F>) -> Self {
|
||||
assert_eq!(self.len(), other.len());
|
||||
for (s, o) in self.0.iter_mut().zip(other.0.iter()) {
|
||||
*s -= o;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<F: PrimeField> Mul<&ScalarVector<F>> for ScalarVector<F> {
|
||||
type Output = ScalarVector<F>;
|
||||
fn mul(mut self, other: &ScalarVector<F>) -> Self {
|
||||
assert_eq!(self.len(), other.len());
|
||||
for (s, o) in self.0.iter_mut().zip(other.0.iter()) {
|
||||
*s *= o;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> ScalarVector<F> {
|
||||
/// Create a new scalar vector, initialized with `len` zero scalars.
|
||||
pub fn new(len: usize) -> Self {
|
||||
ScalarVector(vec![F::ZERO; len])
|
||||
}
|
||||
|
||||
pub(crate) fn powers(x: F, len: usize) -> Self {
|
||||
assert!(len != 0);
|
||||
|
||||
let mut res = Vec::with_capacity(len);
|
||||
res.push(F::ONE);
|
||||
res.push(x);
|
||||
for i in 2 .. len {
|
||||
res.push(res[i - 1] * x);
|
||||
}
|
||||
res.truncate(len);
|
||||
ScalarVector(res)
|
||||
}
|
||||
|
||||
/// The length of this scalar vector.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/*
|
||||
pub(crate) fn sum(mut self) -> F {
|
||||
self.0.drain(..).sum()
|
||||
}
|
||||
*/
|
||||
|
||||
pub(crate) fn inner_product<'a, V: Iterator<Item = &'a F>>(&self, vector: V) -> F {
|
||||
let mut count = 0;
|
||||
let mut res = F::ZERO;
|
||||
for (a, b) in self.0.iter().zip(vector) {
|
||||
res += *a * b;
|
||||
count += 1;
|
||||
}
|
||||
debug_assert_eq!(self.len(), count);
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn split(mut self) -> (Self, Self) {
|
||||
assert!(self.len() > 1);
|
||||
let r = self.0.split_off(self.0.len() / 2);
|
||||
debug_assert_eq!(self.len(), r.len());
|
||||
(self, ScalarVector(r))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: PrimeField> From<Vec<F>> for ScalarVector<F> {
|
||||
fn from(vec: Vec<F>) -> Self {
|
||||
Self(vec)
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use ciphersuite::{group::ff::Field, Ciphersuite, Ristretto};
|
||||
|
||||
use crate::{
|
||||
ScalarVector, PedersenCommitment, PedersenVectorCommitment, Generators,
|
||||
transcript::*,
|
||||
arithmetic_circuit_proof::{
|
||||
Variable, LinComb, ArithmeticCircuitStatement, ArithmeticCircuitWitness,
|
||||
},
|
||||
tests::generators,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_zero_arithmetic_circuit() {
|
||||
let generators = generators(1);
|
||||
|
||||
let value = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
let gamma = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
let commitment = (generators.g() * value) + (generators.h() * gamma);
|
||||
let V = vec![commitment];
|
||||
|
||||
let aL = ScalarVector::<<Ristretto as Ciphersuite>::F>(vec![<Ristretto as Ciphersuite>::F::ZERO]);
|
||||
let aR = aL.clone();
|
||||
|
||||
let mut transcript = Transcript::new([0; 32]);
|
||||
let commitments = transcript.write_commitments(vec![], V);
|
||||
let statement = ArithmeticCircuitStatement::<Ristretto>::new(
|
||||
generators.reduce(1).unwrap(),
|
||||
vec![],
|
||||
commitments.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let witness = ArithmeticCircuitWitness::<Ristretto>::new(
|
||||
aL,
|
||||
aR,
|
||||
vec![],
|
||||
vec![PedersenCommitment { value, mask: gamma }],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let proof = {
|
||||
statement.clone().prove(&mut OsRng, &mut transcript, witness).unwrap();
|
||||
transcript.complete()
|
||||
};
|
||||
let mut verifier = Generators::batch_verifier();
|
||||
|
||||
let mut transcript = VerifierTranscript::new([0; 32], &proof);
|
||||
let verifier_commmitments = transcript.read_commitments(0, 1);
|
||||
assert_eq!(commitments, verifier_commmitments.unwrap());
|
||||
statement.verify(&mut OsRng, &mut verifier, &mut transcript).unwrap();
|
||||
assert!(generators.verify(verifier));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vector_commitment_arithmetic_circuit() {
|
||||
let generators = generators(2);
|
||||
let reduced = generators.reduce(2).unwrap();
|
||||
|
||||
let v1 = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
let v2 = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
let gamma = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
let commitment = (reduced.g_bold(0) * v1) + (reduced.g_bold(1) * v2) + (generators.h() * gamma);
|
||||
let V = vec![];
|
||||
let C = vec![commitment];
|
||||
|
||||
let zero_vec =
|
||||
|| ScalarVector::<<Ristretto as Ciphersuite>::F>(vec![<Ristretto as Ciphersuite>::F::ZERO]);
|
||||
|
||||
let aL = zero_vec();
|
||||
let aR = zero_vec();
|
||||
|
||||
let mut transcript = Transcript::new([0; 32]);
|
||||
let commitments = transcript.write_commitments(C, V);
|
||||
let statement = ArithmeticCircuitStatement::<Ristretto>::new(
|
||||
reduced,
|
||||
vec![LinComb::empty()
|
||||
.term(<Ristretto as Ciphersuite>::F::ONE, Variable::CG { commitment: 0, index: 0 })
|
||||
.term(<Ristretto as Ciphersuite>::F::from(2u64), Variable::CG { commitment: 0, index: 1 })
|
||||
.constant(-(v1 + (v2 + v2)))],
|
||||
commitments.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let witness = ArithmeticCircuitWitness::<Ristretto>::new(
|
||||
aL,
|
||||
aR,
|
||||
vec![PedersenVectorCommitment { g_values: ScalarVector(vec![v1, v2]), mask: gamma }],
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let proof = {
|
||||
statement.clone().prove(&mut OsRng, &mut transcript, witness).unwrap();
|
||||
transcript.complete()
|
||||
};
|
||||
let mut verifier = Generators::batch_verifier();
|
||||
|
||||
let mut transcript = VerifierTranscript::new([0; 32], &proof);
|
||||
let verifier_commmitments = transcript.read_commitments(1, 0);
|
||||
assert_eq!(commitments, verifier_commmitments.unwrap());
|
||||
statement.verify(&mut OsRng, &mut verifier, &mut transcript).unwrap();
|
||||
assert!(generators.verify(verifier));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_test_arithmetic_circuit() {
|
||||
let generators = generators(32);
|
||||
|
||||
for i in 0 .. 100 {
|
||||
dbg!(i);
|
||||
|
||||
// Create aL, aR, aO
|
||||
let mut aL = ScalarVector(vec![]);
|
||||
let mut aR = ScalarVector(vec![]);
|
||||
while aL.len() < ((OsRng.next_u64() % 8) + 1).try_into().unwrap() {
|
||||
aL.0.push(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
}
|
||||
while aR.len() < aL.len() {
|
||||
aR.0.push(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
}
|
||||
let aO = aL.clone() * &aR;
|
||||
|
||||
// Create C
|
||||
let mut C = vec![];
|
||||
while C.len() < (OsRng.next_u64() % 16).try_into().unwrap() {
|
||||
let mut g_values = ScalarVector(vec![]);
|
||||
while g_values.0.len() < ((OsRng.next_u64() % 8) + 1).try_into().unwrap() {
|
||||
g_values.0.push(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||
}
|
||||
C.push(PedersenVectorCommitment {
|
||||
g_values,
|
||||
mask: <Ristretto as Ciphersuite>::F::random(&mut OsRng),
|
||||
});
|
||||
}
|
||||
|
||||
// Create V
|
||||
let mut V = vec![];
|
||||
while V.len() < (OsRng.next_u64() % 4).try_into().unwrap() {
|
||||
V.push(PedersenCommitment {
|
||||
value: <Ristretto as Ciphersuite>::F::random(&mut OsRng),
|
||||
mask: <Ristretto as Ciphersuite>::F::random(&mut OsRng),
|
||||
});
|
||||
}
|
||||
|
||||
// Generate random constraints
|
||||
let mut constraints = vec![];
|
||||
for _ in 0 .. (OsRng.next_u64() % 8).try_into().unwrap() {
|
||||
let mut eval = <Ristretto as Ciphersuite>::F::ZERO;
|
||||
let mut constraint = LinComb::empty();
|
||||
|
||||
for _ in 0 .. (OsRng.next_u64() % 4) {
|
||||
let index = usize::try_from(OsRng.next_u64()).unwrap() % aL.len();
|
||||
let weight = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
constraint = constraint.term(weight, Variable::aL(index));
|
||||
eval += weight * aL[index];
|
||||
}
|
||||
|
||||
for _ in 0 .. (OsRng.next_u64() % 4) {
|
||||
let index = usize::try_from(OsRng.next_u64()).unwrap() % aR.len();
|
||||
let weight = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
constraint = constraint.term(weight, Variable::aR(index));
|
||||
eval += weight * aR[index];
|
||||
}
|
||||
|
||||
for _ in 0 .. (OsRng.next_u64() % 4) {
|
||||
let index = usize::try_from(OsRng.next_u64()).unwrap() % aO.len();
|
||||
let weight = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
constraint = constraint.term(weight, Variable::aO(index));
|
||||
eval += weight * aO[index];
|
||||
}
|
||||
|
||||
for (commitment, C) in C.iter().enumerate() {
|
||||
for _ in 0 .. (OsRng.next_u64() % 4) {
|
||||
let index = usize::try_from(OsRng.next_u64()).unwrap() % C.g_values.len();
|
||||
let weight = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
constraint = constraint.term(weight, Variable::CG { commitment, index });
|
||||
eval += weight * C.g_values[index];
|
||||
}
|
||||
}
|
||||
|
||||
if !V.is_empty() {
|
||||
for _ in 0 .. (OsRng.next_u64() % 4) {
|
||||
let index = usize::try_from(OsRng.next_u64()).unwrap() % V.len();
|
||||
let weight = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
constraint = constraint.term(weight, Variable::V(index));
|
||||
eval += weight * V[index].value;
|
||||
}
|
||||
}
|
||||
|
||||
constraint = constraint.constant(-eval);
|
||||
|
||||
constraints.push(constraint);
|
||||
}
|
||||
|
||||
let mut transcript = Transcript::new([0; 32]);
|
||||
let commitments = transcript.write_commitments(
|
||||
C.iter().map(|C| C.commit(generators.g_bold_slice(), generators.h()).unwrap()).collect(),
|
||||
V.iter().map(|V| V.commit(generators.g(), generators.h())).collect(),
|
||||
);
|
||||
|
||||
let statement = ArithmeticCircuitStatement::<Ristretto>::new(
|
||||
generators.reduce(16).unwrap(),
|
||||
constraints,
|
||||
commitments.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness = ArithmeticCircuitWitness::<Ristretto>::new(aL, aR, C.clone(), V.clone()).unwrap();
|
||||
|
||||
let proof = {
|
||||
statement.clone().prove(&mut OsRng, &mut transcript, witness).unwrap();
|
||||
transcript.complete()
|
||||
};
|
||||
let mut verifier = Generators::batch_verifier();
|
||||
|
||||
let mut transcript = VerifierTranscript::new([0; 32], &proof);
|
||||
let verifier_commmitments = transcript.read_commitments(C.len(), V.len());
|
||||
assert_eq!(commitments, verifier_commmitments.unwrap());
|
||||
statement.verify(&mut OsRng, &mut verifier, &mut transcript).unwrap();
|
||||
assert!(generators.verify(verifier));
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// The inner product relation is P = sum(g_bold * a, h_bold * b, g * (a * b))
|
||||
|
||||
use rand_core::OsRng;
|
||||
|
||||
use ciphersuite::{
|
||||
group::{ff::Field, Group},
|
||||
Ciphersuite, Ristretto,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ScalarVector, PointVector, Generators,
|
||||
transcript::*,
|
||||
inner_product::{P, IpStatement, IpWitness},
|
||||
tests::generators,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_zero_inner_product() {
|
||||
let P = <Ristretto as Ciphersuite>::G::identity();
|
||||
|
||||
let generators = generators::<Ristretto>(1);
|
||||
let reduced = generators.reduce(1).unwrap();
|
||||
let witness = IpWitness::<Ristretto>::new(
|
||||
ScalarVector::<<Ristretto as Ciphersuite>::F>::new(1),
|
||||
ScalarVector::<<Ristretto as Ciphersuite>::F>::new(1),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let proof = {
|
||||
let mut transcript = Transcript::new([0; 32]);
|
||||
IpStatement::<Ristretto>::new(
|
||||
reduced,
|
||||
ScalarVector(vec![<Ristretto as Ciphersuite>::F::ONE; 1]),
|
||||
<Ristretto as Ciphersuite>::F::ONE,
|
||||
P::Prover(P),
|
||||
)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.prove(&mut transcript, witness)
|
||||
.unwrap();
|
||||
transcript.complete()
|
||||
};
|
||||
|
||||
let mut verifier = Generators::batch_verifier();
|
||||
IpStatement::<Ristretto>::new(
|
||||
reduced,
|
||||
ScalarVector(vec![<Ristretto as Ciphersuite>::F::ONE; 1]),
|
||||
<Ristretto as Ciphersuite>::F::ONE,
|
||||
P::Verifier { verifier_weight: <Ristretto as Ciphersuite>::F::ONE },
|
||||
)
|
||||
.unwrap()
|
||||
.verify(&mut verifier, &mut VerifierTranscript::new([0; 32], &proof))
|
||||
.unwrap();
|
||||
assert!(generators.verify(verifier));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_product() {
|
||||
// P = sum(g_bold * a, h_bold * b)
|
||||
let generators = generators::<Ristretto>(32);
|
||||
let mut verifier = Generators::batch_verifier();
|
||||
for i in [1, 2, 4, 8, 16, 32] {
|
||||
let generators = generators.reduce(i).unwrap();
|
||||
let g = generators.g();
|
||||
assert_eq!(generators.len(), i);
|
||||
let mut g_bold = vec![];
|
||||
let mut h_bold = vec![];
|
||||
for i in 0 .. i {
|
||||
g_bold.push(generators.g_bold(i));
|
||||
h_bold.push(generators.h_bold(i));
|
||||
}
|
||||
let g_bold = PointVector::<Ristretto>(g_bold);
|
||||
let h_bold = PointVector::<Ristretto>(h_bold);
|
||||
|
||||
let mut a = ScalarVector::<<Ristretto as Ciphersuite>::F>::new(i);
|
||||
let mut b = ScalarVector::<<Ristretto as Ciphersuite>::F>::new(i);
|
||||
|
||||
for i in 0 .. i {
|
||||
a[i] = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
b[i] = <Ristretto as Ciphersuite>::F::random(&mut OsRng);
|
||||
}
|
||||
|
||||
let P = g_bold.multiexp(&a) + h_bold.multiexp(&b) + (g * a.inner_product(b.0.iter()));
|
||||
|
||||
let witness = IpWitness::<Ristretto>::new(a, b).unwrap();
|
||||
|
||||
let proof = {
|
||||
let mut transcript = Transcript::new([0; 32]);
|
||||
IpStatement::<Ristretto>::new(
|
||||
generators,
|
||||
ScalarVector(vec![<Ristretto as Ciphersuite>::F::ONE; i]),
|
||||
<Ristretto as Ciphersuite>::F::ONE,
|
||||
P::Prover(P),
|
||||
)
|
||||
.unwrap()
|
||||
.prove(&mut transcript, witness)
|
||||
.unwrap();
|
||||
transcript.complete()
|
||||
};
|
||||
|
||||
verifier.additional.push((<Ristretto as Ciphersuite>::F::ONE, P));
|
||||
IpStatement::<Ristretto>::new(
|
||||
generators,
|
||||
ScalarVector(vec![<Ristretto as Ciphersuite>::F::ONE; i]),
|
||||
<Ristretto as Ciphersuite>::F::ONE,
|
||||
P::Verifier { verifier_weight: <Ristretto as Ciphersuite>::F::ONE },
|
||||
)
|
||||
.unwrap()
|
||||
.verify(&mut verifier, &mut VerifierTranscript::new([0; 32], &proof))
|
||||
.unwrap();
|
||||
}
|
||||
assert!(generators.verify(verifier));
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use rand_core::OsRng;
|
||||
|
||||
use ciphersuite::{group::Group, Ciphersuite};
|
||||
|
||||
use crate::{Generators, padded_pow_of_2};
|
||||
|
||||
#[cfg(test)]
|
||||
mod inner_product;
|
||||
|
||||
#[cfg(test)]
|
||||
mod arithmetic_circuit_proof;
|
||||
|
||||
/// Generate a set of generators for testing purposes.
|
||||
///
|
||||
/// This should not be considered secure.
|
||||
pub fn generators<C: Ciphersuite>(n: usize) -> Generators<C> {
|
||||
assert_eq!(padded_pow_of_2(n), n, "amount of generators wasn't a power of 2");
|
||||
|
||||
let gens = || {
|
||||
let mut res = Vec::with_capacity(n);
|
||||
for _ in 0 .. n {
|
||||
res.push(C::G::random(&mut OsRng));
|
||||
}
|
||||
res
|
||||
};
|
||||
Generators::new(C::G::random(&mut OsRng), C::G::random(&mut OsRng), gens(), gens()).unwrap()
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
use std_shims::{vec::Vec, io};
|
||||
|
||||
use blake2::{Digest, Blake2b512};
|
||||
|
||||
use ciphersuite::{
|
||||
group::{
|
||||
ff::{Field, PrimeField},
|
||||
GroupEncoding,
|
||||
},
|
||||
Ciphersuite,
|
||||
};
|
||||
|
||||
use crate::PointVector;
|
||||
|
||||
const SCALAR: u8 = 0;
|
||||
const POINT: u8 = 1;
|
||||
const CHALLENGE: u8 = 2;
|
||||
|
||||
fn challenge<C: Ciphersuite>(digest: &mut Blake2b512) -> C::F {
|
||||
digest.update([CHALLENGE]);
|
||||
let chl = digest.clone().finalize().into();
|
||||
|
||||
let res = C::reduce_512(chl);
|
||||
|
||||
// Negligible probability
|
||||
if bool::from(res.is_zero()) {
|
||||
panic!("zero challenge");
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Commitments written to/read from a transcript.
|
||||
// We use a dedicated type for this to coerce the caller into transcripting the commitments as
|
||||
// expected.
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Debug))]
|
||||
pub struct Commitments<C: Ciphersuite> {
|
||||
pub(crate) C: PointVector<C>,
|
||||
pub(crate) V: PointVector<C>,
|
||||
}
|
||||
|
||||
impl<C: Ciphersuite> Commitments<C> {
|
||||
/// The vector commitments.
|
||||
pub fn C(&self) -> &[C::G] {
|
||||
&self.C.0
|
||||
}
|
||||
/// The non-vector commitments.
|
||||
pub fn V(&self) -> &[C::G] {
|
||||
&self.V.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A transcript for proving proofs.
|
||||
pub struct Transcript {
|
||||
digest: Blake2b512,
|
||||
transcript: Vec<u8>,
|
||||
}
|
||||
|
||||
/*
|
||||
We define our proofs as Vec<u8> and derive our transcripts from the values we deserialize from
|
||||
them. This format assumes the order of the values read, their size, and their quantity are
|
||||
constant to the context.
|
||||
*/
|
||||
impl Transcript {
|
||||
/// Create a new transcript off some context.
|
||||
pub fn new(context: [u8; 32]) -> Self {
|
||||
let mut digest = Blake2b512::new();
|
||||
digest.update(context);
|
||||
Self { digest, transcript: Vec::with_capacity(1024) }
|
||||
}
|
||||
|
||||
/// Push a scalar onto the transcript.
|
||||
///
|
||||
/// The order and layout of this must be constant to the context.
|
||||
pub fn push_scalar(&mut self, scalar: impl PrimeField) {
|
||||
self.digest.update([SCALAR]);
|
||||
let bytes = scalar.to_repr();
|
||||
self.digest.update(bytes);
|
||||
self.transcript.extend(bytes.as_ref());
|
||||
}
|
||||
|
||||
/// Push a point onto the transcript.
|
||||
///
|
||||
/// The order and layout of this must be constant to the context.
|
||||
pub fn push_point(&mut self, point: impl GroupEncoding) {
|
||||
self.digest.update([POINT]);
|
||||
let bytes = point.to_bytes();
|
||||
self.digest.update(bytes);
|
||||
self.transcript.extend(bytes.as_ref());
|
||||
}
|
||||
|
||||
/// Write the Pedersen (vector) commitments to this transcript.
|
||||
pub fn write_commitments<C: Ciphersuite>(
|
||||
&mut self,
|
||||
C: Vec<C::G>,
|
||||
V: Vec<C::G>,
|
||||
) -> Commitments<C> {
|
||||
self.digest.update(u32::try_from(C.len()).unwrap().to_le_bytes());
|
||||
for C in &C {
|
||||
self.push_point(*C);
|
||||
}
|
||||
self.digest.update(u32::try_from(V.len()).unwrap().to_le_bytes());
|
||||
for V in &V {
|
||||
self.push_point(*V);
|
||||
}
|
||||
Commitments { C: PointVector(C), V: PointVector(V) }
|
||||
}
|
||||
|
||||
/// Sample a challenge.
|
||||
pub fn challenge<C: Ciphersuite>(&mut self) -> C::F {
|
||||
challenge::<C>(&mut self.digest)
|
||||
}
|
||||
|
||||
/// Sample a challenge as a byte array.
|
||||
pub fn challenge_bytes(&mut self) -> [u8; 64] {
|
||||
self.digest.update([CHALLENGE]);
|
||||
self.digest.clone().finalize().into()
|
||||
}
|
||||
|
||||
/// Complete a transcript, yielding the fully serialized proof.
|
||||
pub fn complete(self) -> Vec<u8> {
|
||||
self.transcript
|
||||
}
|
||||
}
|
||||
|
||||
/// A transcript for verifying proofs.
|
||||
pub struct VerifierTranscript<'a> {
|
||||
digest: Blake2b512,
|
||||
transcript: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> VerifierTranscript<'a> {
|
||||
/// Create a new transcript to verify a proof with.
|
||||
pub fn new(context: [u8; 32], proof: &'a [u8]) -> Self {
|
||||
let mut digest = Blake2b512::new();
|
||||
digest.update(context);
|
||||
Self { digest, transcript: proof }
|
||||
}
|
||||
|
||||
/// Read a scalar from the transcript.
|
||||
///
|
||||
/// The order and layout of this must be constant to the context.
|
||||
pub fn read_scalar<C: Ciphersuite>(&mut self) -> io::Result<C::F> {
|
||||
// Read the scalar onto the transcript using the serialization present in the transcript
|
||||
self.digest.update([SCALAR]);
|
||||
let scalar_len = <C::F as PrimeField>::Repr::default().as_ref().len();
|
||||
if self.transcript.len() < scalar_len {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "not enough bytes to read_scalar"))?;
|
||||
}
|
||||
self.digest.update(&self.transcript[.. scalar_len]);
|
||||
|
||||
// Read the actual scalar, where `read_F` ensures its canonically serialized
|
||||
let scalar = C::read_F(&mut self.transcript)?;
|
||||
Ok(scalar)
|
||||
}
|
||||
|
||||
/// Read a point from the transcript.
|
||||
///
|
||||
/// The order and layout of this must be constant to the context.
|
||||
pub fn read_point<C: Ciphersuite>(&mut self) -> io::Result<C::G> {
|
||||
// Read the point onto the transcript using the serialization present in the transcript
|
||||
self.digest.update([POINT]);
|
||||
let point_len = <C::G as GroupEncoding>::Repr::default().as_ref().len();
|
||||
if self.transcript.len() < point_len {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "not enough bytes to read_point"))?;
|
||||
}
|
||||
self.digest.update(&self.transcript[.. point_len]);
|
||||
|
||||
// Read the actual point, where `read_G` ensures its canonically serialized
|
||||
let point = C::read_G(&mut self.transcript)?;
|
||||
Ok(point)
|
||||
}
|
||||
|
||||
/// Read the Pedersen (Vector) Commitments from the transcript.
|
||||
///
|
||||
/// The lengths of the vectors are not transcripted.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn read_commitments<C: Ciphersuite>(
|
||||
&mut self,
|
||||
C: usize,
|
||||
V: usize,
|
||||
) -> io::Result<Commitments<C>> {
|
||||
self.digest.update(u32::try_from(C).unwrap().to_le_bytes());
|
||||
let mut C_vec = Vec::with_capacity(C);
|
||||
for _ in 0 .. C {
|
||||
C_vec.push(self.read_point::<C>()?);
|
||||
}
|
||||
self.digest.update(u32::try_from(V).unwrap().to_le_bytes());
|
||||
let mut V_vec = Vec::with_capacity(V);
|
||||
for _ in 0 .. V {
|
||||
V_vec.push(self.read_point::<C>()?);
|
||||
}
|
||||
Ok(Commitments { C: PointVector(C_vec), V: PointVector(V_vec) })
|
||||
}
|
||||
|
||||
/// Sample a challenge.
|
||||
pub fn challenge<C: Ciphersuite>(&mut self) -> C::F {
|
||||
challenge::<C>(&mut self.digest)
|
||||
}
|
||||
|
||||
/// Sample a challenge as a byte array.
|
||||
pub fn challenge_bytes(&mut self) -> [u8; 64] {
|
||||
self.digest.update([CHALLENGE]);
|
||||
self.digest.clone().finalize().into()
|
||||
}
|
||||
|
||||
/// Complete the transcript transcript, yielding what remains.
|
||||
pub fn complete(self) -> &'a [u8] {
|
||||
self.transcript
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,8 @@ k256 = { version = "0.13", default-features = false, features = ["arithmetic"] }
|
||||
|
||||
blake2 = { version = "0.10", default-features = false }
|
||||
ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false }
|
||||
ec-divisors = { path = "../divisors", default-features = false }
|
||||
generalized-bulletproofs-ec-gadgets = { path = "../ec-gadgets", default-features = false }
|
||||
ec-divisors = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
|
||||
generalized-bulletproofs-ec-gadgets = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use std_shims::prelude::*;
|
||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||
use std_shims::io::{self, Read};
|
||||
|
||||
@@ -37,10 +39,6 @@ impl ciphersuite::Ciphersuite for Secq256k1 {
|
||||
Point::generator()
|
||||
}
|
||||
|
||||
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||
Scalar::wide_reduce(scalar)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
use blake2::Digest;
|
||||
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_slice().try_into().unwrap())
|
||||
@@ -57,7 +55,7 @@ impl ciphersuite::Ciphersuite for Secq256k1 {
|
||||
reader.read_exact(encoding.as_mut())?;
|
||||
|
||||
let point = Option::<Self::G>::from(Self::G::from_bytes(&encoding))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))?;
|
||||
.ok_or_else(|| io::Error::other("invalid point"))?;
|
||||
Ok(point)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,15 @@ impl PrimeGroup for Point {}
|
||||
|
||||
impl ec_divisors::DivisorCurve for Point {
|
||||
type FieldElement = FieldElement;
|
||||
type XyPoint = ec_divisors::Projective<Self>;
|
||||
|
||||
fn interpolator_for_scalar_mul() -> &'static ec_divisors::Interpolator<Self::FieldElement> {
|
||||
static PRECOMPUTE: std_shims::sync::LazyLock<ec_divisors::Interpolator<FieldElement>> =
|
||||
std_shims::sync::LazyLock::new(|| {
|
||||
ec_divisors::Interpolator::new(usize::try_from(130).unwrap())
|
||||
});
|
||||
&PRECOMPUTE
|
||||
}
|
||||
|
||||
fn a() -> Self::FieldElement {
|
||||
FieldElement::from(0u64)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ff-group-tests"
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
description = "A collection of sanity tests for implementors of ff/group APIs"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ff-group-tests"
|
||||
@@ -30,4 +30,4 @@ p256 = { version = "^0.13.1", default-features = false, features = ["std", "arit
|
||||
|
||||
bls12_381 = "0.8"
|
||||
|
||||
pasta_curves = { git = "https://github.com/kayabaNerve/pasta_curves", rev = "a46b5be95cacbff54d06aad8d3bbcba42e05d616" }
|
||||
pasta_curves = "0.5"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "modular-frost"
|
||||
version = "0.8.1"
|
||||
version = "0.10.1"
|
||||
description = "Modular implementation of FROST over ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost"
|
||||
@@ -34,26 +34,31 @@ dalek-ff-group = { path = "../dalek-ff-group", version = "0.4", default-features
|
||||
minimal-ed448 = { path = "../ed448", version = "0.4", default-features = false, features = ["std"], optional = true }
|
||||
|
||||
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
||||
ciphersuite-kp256 = { path = "../ciphersuite/kp256", version = "0.4", default-features = false, features = ["std"], optional = true }
|
||||
|
||||
multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["std", "batch"] }
|
||||
|
||||
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1", default-features = false, features = ["std"] }
|
||||
|
||||
dkg = { path = "../dkg", version = "^0.5.1", default-features = false, features = ["std"] }
|
||||
dkg = { path = "../dkg", version = "0.6.1", default-features = false, features = ["std"] }
|
||||
dkg-recovery = { path = "../dkg/recovery", version = "0.6", default-features = false, features = ["std"], optional = true }
|
||||
dkg-dealer = { path = "../dkg/dealer", version = "0.6", default-features = false, features = ["std"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||
|
||||
dkg = { path = "../dkg", features = ["tests"] }
|
||||
dkg = { path = "../dkg", default-features = false, features = ["std"] }
|
||||
dkg-recovery = { path = "../dkg/recovery", default-features = false, features = ["std"] }
|
||||
dkg-dealer = { path = "../dkg/dealer", default-features = false, features = ["std"] }
|
||||
|
||||
[features]
|
||||
ed25519 = ["dalek-ff-group", "ciphersuite/ed25519"]
|
||||
ristretto = ["dalek-ff-group", "ciphersuite/ristretto"]
|
||||
ed25519 = ["dalek-ff-group"]
|
||||
ristretto = ["dalek-ff-group"]
|
||||
|
||||
secp256k1 = ["ciphersuite/secp256k1"]
|
||||
p256 = ["ciphersuite/p256"]
|
||||
secp256k1 = ["ciphersuite-kp256"]
|
||||
p256 = ["ciphersuite-kp256"]
|
||||
|
||||
ed448 = ["minimal-ed448", "ciphersuite/ed448"]
|
||||
ed448 = ["minimal-ed448"]
|
||||
|
||||
tests = ["hex", "rand_core/getrandom", "dkg/tests"]
|
||||
tests = ["hex", "rand_core/getrandom", "dkg-dealer", "dkg-recovery"]
|
||||
|
||||
@@ -12,6 +12,10 @@ This library offers ciphersuites compatible with the
|
||||
[IETF draft](https://github.com/cfrg/draft-irtf-cfrg-frost). Currently, version
|
||||
15 is supported.
|
||||
|
||||
A variety of testing utilities are provided under the `tests` feature. These
|
||||
are provided with no guarantees and may have completely arbitrary behavior,
|
||||
including panicking for completely well-reasoned input.
|
||||
|
||||
This library was
|
||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
||||
culminating in commit
|
||||
|
||||
@@ -25,7 +25,7 @@ pub trait Addendum: Send + Sync + Clone + PartialEq + Debug + WriteAddendum {}
|
||||
impl<A: Send + Sync + Clone + PartialEq + Debug + WriteAddendum> Addendum for A {}
|
||||
|
||||
/// Algorithm trait usable by the FROST signing machine to produce signatures..
|
||||
pub trait Algorithm<C: Curve>: Send + Sync + Clone {
|
||||
pub trait Algorithm<C: Curve>: Send + Sync {
|
||||
/// The transcript format this algorithm uses. This likely should NOT be the IETF-compatible
|
||||
/// transcript included in this crate.
|
||||
type Transcript: Sync + Clone + Debug + Transcript;
|
||||
@@ -135,6 +135,8 @@ pub trait Hram<C: Curve>: Send + Sync + Clone {
|
||||
}
|
||||
|
||||
/// Schnorr signature algorithm ((R, s) where s = r + cx).
|
||||
///
|
||||
/// `verify`, `verify_share` must be called after `sign_share` is called.
|
||||
#[derive(Clone)]
|
||||
pub struct Schnorr<C: Curve, T: Sync + Clone + Debug + Transcript, H: Hram<C>> {
|
||||
transcript: T,
|
||||
|
||||
@@ -16,7 +16,7 @@ macro_rules! dalek_curve {
|
||||
$CONTEXT: literal,
|
||||
$chal: literal
|
||||
) => {
|
||||
pub use ciphersuite::$Curve;
|
||||
pub use dalek_ff_group::$Curve;
|
||||
|
||||
impl Curve for $Curve {
|
||||
const CONTEXT: &'static [u8] = $CONTEXT;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use digest::Digest;
|
||||
|
||||
use minimal_ed448::{Scalar, Point};
|
||||
pub use ciphersuite::{group::GroupEncoding, Shake256_114, Ed448};
|
||||
pub use minimal_ed448::Ed448;
|
||||
pub use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||
|
||||
use crate::{curve::Curve, algorithm::Hram};
|
||||
|
||||
@@ -18,7 +19,7 @@ impl Ietf8032Ed448Hram {
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn hram(context: &[u8], R: &Point, A: &Point, m: &[u8]) -> Scalar {
|
||||
Scalar::wide_reduce(
|
||||
Shake256_114::digest(
|
||||
<Ed448 as Ciphersuite>::H::digest(
|
||||
[
|
||||
&[b"SigEd448".as_ref(), &[0, u8::try_from(context.len()).unwrap()]].concat(),
|
||||
context,
|
||||
|
||||
@@ -11,7 +11,7 @@ macro_rules! kp_curve {
|
||||
|
||||
$CONTEXT: literal
|
||||
) => {
|
||||
pub use ciphersuite::$Curve;
|
||||
pub use ciphersuite_kp256::$Curve;
|
||||
|
||||
impl Curve for $Curve {
|
||||
const CONTEXT: &'static [u8] = $CONTEXT;
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Distributed key generation protocol.
|
||||
pub use dkg::{self, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, ThresholdView};
|
||||
pub use dkg::{self, Participant, ThresholdParams, ThresholdKeys, ThresholdView};
|
||||
|
||||
/// Curve trait and provided curves/HRAMs, forming various ciphersuites.
|
||||
pub mod curve;
|
||||
|
||||
@@ -47,7 +47,7 @@ impl<T: Writable> Writable for Vec<T> {
|
||||
}
|
||||
|
||||
// Pairing of an Algorithm with a ThresholdKeys instance.
|
||||
#[derive(Clone, Zeroize)]
|
||||
#[derive(Zeroize)]
|
||||
struct Params<C: Curve, A: Algorithm<C>> {
|
||||
// Skips the algorithm due to being too large a bound to feasibly enforce on users
|
||||
#[zeroize(skip)]
|
||||
@@ -125,8 +125,11 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
||||
let mut params = self.params;
|
||||
|
||||
let mut rng = ChaCha20Rng::from_seed(*seed.0);
|
||||
let (nonces, commitments) =
|
||||
Commitments::new::<_>(&mut rng, params.keys.secret_share(), ¶ms.algorithm.nonces());
|
||||
let (nonces, commitments) = Commitments::new::<_>(
|
||||
&mut rng,
|
||||
params.keys.original_secret_share(),
|
||||
¶ms.algorithm.nonces(),
|
||||
);
|
||||
let addendum = params.algorithm.preprocess_addendum(&mut rng, ¶ms.keys);
|
||||
|
||||
let preprocess = Preprocess { commitments, addendum };
|
||||
@@ -193,7 +196,7 @@ impl<C: Curve> SignatureShare<C> {
|
||||
/// Trait for the second machine of a two-round signing protocol.
|
||||
pub trait SignMachine<S>: Send + Sync + Sized {
|
||||
/// Params used to instantiate this machine which can be used to rebuild from a cache.
|
||||
type Params: Clone;
|
||||
type Params;
|
||||
/// Keys used for signing operations.
|
||||
type Keys;
|
||||
/// Preprocess message for this machine.
|
||||
@@ -357,12 +360,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
||||
|
||||
// Re-format into the FROST-expected rho transcript
|
||||
let mut rho_transcript = A::Transcript::new(b"FROST_rho");
|
||||
rho_transcript.append_message(
|
||||
b"group_key",
|
||||
(self.params.keys.group_key() +
|
||||
(C::generator() * self.params.keys.current_offset().unwrap_or(C::F::ZERO)))
|
||||
.to_bytes(),
|
||||
);
|
||||
rho_transcript.append_message(b"group_key", self.params.keys.group_key().to_bytes());
|
||||
rho_transcript.append_message(b"message", C::hash_msg(msg));
|
||||
rho_transcript.append_message(
|
||||
b"preprocesses",
|
||||
@@ -401,7 +399,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
||||
|
||||
Ok((
|
||||
AlgorithmSignatureMachine {
|
||||
params: self.params.clone(),
|
||||
params: self.params,
|
||||
view,
|
||||
B,
|
||||
Rs,
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::collections::HashMap;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
pub use dkg::tests::{key_gen, musig_key_gen, recover_key};
|
||||
use ciphersuite::Ciphersuite;
|
||||
pub use dkg_recovery::recover_key;
|
||||
|
||||
use crate::{
|
||||
Curve, Participant, ThresholdKeys, FrostError,
|
||||
@@ -26,6 +27,18 @@ pub const PARTICIPANTS: u16 = 5;
|
||||
/// Constant threshold of participants to use when signing.
|
||||
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
||||
|
||||
/// Create a key, for testing purposes.
|
||||
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||
rng: &mut R,
|
||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||
let res = dkg_dealer::key_gen::<R, C>(rng, THRESHOLD, PARTICIPANTS).unwrap();
|
||||
assert_eq!(
|
||||
C::generator() * *recover_key(&res.values().cloned().collect::<Vec<_>>()).unwrap(),
|
||||
res.values().next().unwrap().group_key()
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
/// Clone a map without a specific value.
|
||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
map: &HashMap<K, V>,
|
||||
@@ -37,10 +50,10 @@ pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||
}
|
||||
|
||||
/// Spawn algorithm machines for a random selection of signers, each executing the given algorithm.
|
||||
pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
||||
pub fn algorithm_machines_without_clone<R: RngCore, C: Curve, A: Algorithm<C>>(
|
||||
rng: &mut R,
|
||||
algorithm: &A,
|
||||
keys: &HashMap<Participant, ThresholdKeys<C>>,
|
||||
machines: HashMap<Participant, AlgorithmMachine<C, A>>,
|
||||
) -> HashMap<Participant, AlgorithmMachine<C, A>> {
|
||||
let mut included = vec![];
|
||||
while included.len() < usize::from(keys[&Participant::new(1).unwrap()].params().t()) {
|
||||
@@ -54,18 +67,28 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
||||
included.push(n);
|
||||
}
|
||||
|
||||
keys
|
||||
.iter()
|
||||
.filter_map(|(i, keys)| {
|
||||
if included.contains(i) {
|
||||
Some((*i, AlgorithmMachine::new(algorithm.clone(), keys.clone())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
machines
|
||||
.into_iter()
|
||||
.filter_map(|(i, machine)| if included.contains(&i) { Some((i, machine)) } else { None })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Spawn algorithm machines for a random selection of signers, each executing the given algorithm.
|
||||
pub fn algorithm_machines<R: RngCore, C: Curve, A: Clone + Algorithm<C>>(
|
||||
rng: &mut R,
|
||||
algorithm: &A,
|
||||
keys: &HashMap<Participant, ThresholdKeys<C>>,
|
||||
) -> HashMap<Participant, AlgorithmMachine<C, A>> {
|
||||
algorithm_machines_without_clone(
|
||||
rng,
|
||||
keys,
|
||||
keys
|
||||
.values()
|
||||
.map(|keys| (keys.params().i(), AlgorithmMachine::new(algorithm.clone(), keys.clone())))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
// Run the preprocess step
|
||||
pub(crate) fn preprocess<
|
||||
R: RngCore + CryptoRng,
|
||||
@@ -165,10 +188,10 @@ pub fn sign_without_caching<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||
|
||||
/// Execute the signing protocol, randomly caching various machines to ensure they can cache
|
||||
/// successfully.
|
||||
pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||
pub fn sign_without_clone<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||
rng: &mut R,
|
||||
params: &<M::SignMachine as SignMachine<M::Signature>>::Params,
|
||||
mut keys: HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::Keys>,
|
||||
mut params: HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::Params>,
|
||||
machines: HashMap<Participant, M>,
|
||||
msg: &[u8],
|
||||
) -> M::Signature {
|
||||
@@ -183,7 +206,8 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||
let cache = machines.remove(&i).unwrap().cache();
|
||||
machines.insert(
|
||||
i,
|
||||
M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).0,
|
||||
M::SignMachine::from_cache(params.remove(&i).unwrap(), keys.remove(&i).unwrap(), cache)
|
||||
.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -192,6 +216,22 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||
)
|
||||
}
|
||||
|
||||
/// Execute the signing protocol, randomly caching various machines to ensure they can cache
|
||||
/// successfully.
|
||||
pub fn sign<
|
||||
R: RngCore + CryptoRng,
|
||||
M: PreprocessMachine<SignMachine: SignMachine<M::Signature, Params: Clone>>,
|
||||
>(
|
||||
rng: &mut R,
|
||||
params: &<M::SignMachine as SignMachine<M::Signature>>::Params,
|
||||
keys: HashMap<Participant, <M::SignMachine as SignMachine<M::Signature>>::Keys>,
|
||||
machines: HashMap<Participant, M>,
|
||||
msg: &[u8],
|
||||
) -> M::Signature {
|
||||
let params = keys.keys().map(|i| (*i, params.clone())).collect();
|
||||
sign_without_clone(rng, keys, params, machines, msg)
|
||||
}
|
||||
|
||||
/// Test a basic Schnorr signature with the provided keys.
|
||||
pub fn test_schnorr_with_keys<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
||||
rng: &mut R,
|
||||
@@ -211,12 +251,6 @@ pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
test_schnorr_with_keys::<_, _, H>(&mut *rng, &keys)
|
||||
}
|
||||
|
||||
/// Test a basic Schnorr signature, yet with MuSig.
|
||||
pub fn test_musig_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
let keys = musig_key_gen(&mut *rng);
|
||||
test_schnorr_with_keys::<_, _, H>(&mut *rng, &keys)
|
||||
}
|
||||
|
||||
/// Test an offset Schnorr signature.
|
||||
pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
const MSG: &[u8] = b"Hello, World!";
|
||||
@@ -224,10 +258,11 @@ pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &m
|
||||
let mut keys = key_gen(&mut *rng);
|
||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||
|
||||
let scalar = C::F::from(3);
|
||||
let offset = C::F::from(5);
|
||||
let offset_key = group_key + (C::generator() * offset);
|
||||
let offset_key = (group_key * scalar) + (C::generator() * offset);
|
||||
for keys in keys.values_mut() {
|
||||
*keys = keys.offset(offset);
|
||||
*keys = keys.clone().scale(scalar).unwrap().offset(offset);
|
||||
assert_eq!(keys.group_key(), offset_key);
|
||||
}
|
||||
|
||||
@@ -262,7 +297,6 @@ pub fn test_schnorr_blame<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mu
|
||||
/// Run a variety of tests against a ciphersuite.
|
||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||
test_schnorr::<R, C, H>(rng);
|
||||
test_musig_schnorr::<R, C, H>(rng);
|
||||
test_offset_schnorr::<R, C, H>(rng);
|
||||
test_schnorr_blame::<R, C, H>(rng);
|
||||
|
||||
|
||||
@@ -9,12 +9,10 @@ use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use ciphersuite::group::{ff::Field, Group, GroupEncoding};
|
||||
|
||||
pub use dkg::tests::{key_gen, recover_key};
|
||||
|
||||
use crate::{
|
||||
Curve, Participant, ThresholdView, ThresholdKeys, FrostError,
|
||||
algorithm::Algorithm,
|
||||
tests::{algorithm_machines, sign},
|
||||
tests::{key_gen, algorithm_machines, sign},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -13,7 +13,7 @@ use ciphersuite::group::{ff::PrimeField, GroupEncoding};
|
||||
|
||||
use crate::{
|
||||
curve::Curve,
|
||||
Participant, ThresholdCore, ThresholdKeys,
|
||||
Participant, ThresholdKeys,
|
||||
algorithm::{Hram, IetfSchnorr},
|
||||
sign::{
|
||||
Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess,
|
||||
@@ -115,7 +115,7 @@ fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<Participant,
|
||||
|
||||
let mut keys = HashMap::new();
|
||||
for i in 1 ..= u16::try_from(shares.len()).unwrap() {
|
||||
// Manually re-implement the serialization for ThresholdCore to import this data
|
||||
// Manually re-implement the serialization for ThresholdKeys to import this data
|
||||
let mut serialized = vec![];
|
||||
serialized.extend(u32::try_from(C::ID.len()).unwrap().to_le_bytes());
|
||||
serialized.extend(C::ID);
|
||||
@@ -128,14 +128,14 @@ fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<Participant,
|
||||
serialized.extend(share.to_bytes().as_ref());
|
||||
}
|
||||
|
||||
let these_keys = ThresholdCore::<C>::read::<&[u8]>(&mut serialized.as_ref()).unwrap();
|
||||
let these_keys = ThresholdKeys::<C>::read::<&[u8]>(&mut serialized.as_ref()).unwrap();
|
||||
assert_eq!(these_keys.params().t(), vectors.threshold);
|
||||
assert_eq!(usize::from(these_keys.params().n()), shares.len());
|
||||
let participant = Participant::new(i).unwrap();
|
||||
assert_eq!(these_keys.params().i(), participant);
|
||||
assert_eq!(these_keys.secret_share().deref(), &shares[usize::from(i - 1)]);
|
||||
assert_eq!(these_keys.original_secret_share().deref(), &shares[usize::from(i - 1)]);
|
||||
assert_eq!(hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key);
|
||||
keys.insert(participant, ThresholdKeys::new(these_keys));
|
||||
keys.insert(participant, these_keys);
|
||||
}
|
||||
|
||||
keys
|
||||
@@ -157,7 +157,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
||||
let secret =
|
||||
C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
|
||||
assert_eq!(C::generator() * secret, group_key);
|
||||
assert_eq!(recover_key(&keys), secret);
|
||||
assert_eq!(*recover_key(&keys.values().cloned().collect::<Vec<_>>()).unwrap(), secret);
|
||||
|
||||
let mut machines = vec![];
|
||||
for i in &vectors.included {
|
||||
@@ -346,14 +346,21 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
||||
|
||||
// Calculate the expected nonces
|
||||
let mut expected = (C::generator() *
|
||||
C::random_nonce(keys[i].secret_share(), &mut TransparentRng(vec![randomness.0])).deref())
|
||||
C::random_nonce(
|
||||
keys[i].original_secret_share(),
|
||||
&mut TransparentRng(vec![randomness.0]),
|
||||
)
|
||||
.deref())
|
||||
.to_bytes()
|
||||
.as_ref()
|
||||
.to_vec();
|
||||
expected.extend(
|
||||
(C::generator() *
|
||||
C::random_nonce(keys[i].secret_share(), &mut TransparentRng(vec![randomness.1]))
|
||||
.deref())
|
||||
C::random_nonce(
|
||||
keys[i].original_secret_share(),
|
||||
&mut TransparentRng(vec![randomness.1]),
|
||||
)
|
||||
.deref())
|
||||
.to_bytes()
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "multiexp"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
description = "Multiexponentiation algorithms for ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/multiexp"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user