One Round DKG (#589)

* Upstream GBP, divisor, circuit abstraction, and EC gadgets from FCMP++

* Initial eVRF implementation

Not quite done yet. It needs to communicate the resulting points and proofs to
extract them from the Pedersen Commitments in order to return those, and then
be tested.

* Add the openings of the PCs to the eVRF as necessary

* Add implementation of secq256k1

* Make DKG Encryption a bit more flexible

No longer requires the use of an EncryptionKeyMessage, and allows pre-defined
keys for encryption.

* Make NUM_BITS an argument for the field macro

* Have the eVRF take a Zeroizing private key

* Initial eVRF-based DKG

* Add embedwards25519 curve

* Inline the eVRF into the DKG library

Due to how we're handling share encryption, we'd either need two circuits or to
dedicate this circuit to the DKG. The latter makes sense at this time.

* Add documentation to the eVRF-based DKG

* Add paragraph claiming robustness

* Update to the new eVRF proof

* Finish routing the eVRF functionality

Still needs errors and serialization, along with a few other TODOs.

* Add initial eVRF DKG test

* Improve eVRF DKG

Updates how we calculcate verification shares, improves performance when
extracting multiple sets of keys, and adds more to the test for it.

* Start using a proper error for the eVRF DKG

* Resolve various TODOs

Supports recovering multiple key shares from the eVRF DKG.

Inlines two loops to save 2**16 iterations.

Adds support for creating a constant time representation of scalars < NUM_BITS.

* Ban zero ECDH keys, document non-zero requirements

* Implement eVRF traits, all the way up to the DKG, for secp256k1/ed25519

* Add Ristretto eVRF trait impls

* Support participating multiple times in the eVRF DKG

* Only participate once per key, not once per key share

* Rewrite processor key-gen around the eVRF DKG

Still a WIP.

* Finish routing the new key gen in the processor

Doesn't touch the tests, coordinator, nor Substrate yet.
`cargo +nightly fmt && cargo +nightly-2024-07-01 clippy --all-features -p serai-processor`
does pass.

* Deduplicate and better document in processor key_gen

* Update serai-processor tests to the new key gen

* Correct amount of yx coefficients, get processor key gen test to pass

* Add embedded elliptic curve keys to Substrate

* Update processor key gen tests to the eVRF DKG

* Have set_keys take signature_participants, not removed_participants

Now no one is removed from the DKG. Only `t` people publish the key however.

Uses a BitVec for an efficient encoding of the participants.

* Update the coordinator binary for the new DKG

This does not yet update any tests.

* Add sensible Debug to key_gen::[Processor, Coordinator]Message

* Have the DKG explicitly declare how to interpolate its shares

Removes the hack for MuSig where we multiply keys by the inverse of their
lagrange interpolation factor.

* Replace Interpolation::None with Interpolation::Constant

Allows the MuSig DKG to keep the secret share as the original private key,
enabling deriving FROST nonces consistently regardless of the MuSig context.

* Get coordinator tests to pass

* Update spec to the new DKG

* Get clippy to pass across the repo

* cargo machete

* Add an extra sleep to ensure expected ordering of `Participation`s

* Update orchestration

* Remove bad panic in coordinator

It expected ConfirmationShare to be n-of-n, not t-of-n.

* Improve documentation on  functions

* Update TX size limit

We now no longer have to support the ridiculous case of having 49 DKG
participations within a 101-of-150 DKG. It does remain quite high due to
needing to _sign_ so many times. It'd may be optimal for parties with multiple
key shares to independently send their preprocesses/shares (despite the
overhead that'll cause with signatures and the transaction structure).

* Correct error in the Processor spec document

* Update a few comments in the validator-sets pallet

* Send/Recv Participation one at a time

Sending all, then attempting to receive all in an expected order, wasn't working
even with notable delays between sending messages. This points to the mempool
not working as expected...

* Correct ThresholdKeys serialization in modular-frost test

* Updating existing TX size limit test for the new DKG parameters

* Increase time allowed for the DKG on the GH CI

* Correct construction of signature_participants in serai-client tests

Fault identified by akil.

* Further contextualize DkgConfirmer by ValidatorSet

Caught by a safety check we wouldn't reuse preprocesses across messages. That
raises the question of we were prior reusing preprocesses (reusing keys)?
Except that'd have caused a variety of signing failures (suggesting we had some
staggered timing avoiding it in practice but yes, this was possible in theory).

* Add necessary calls to set_embedded_elliptic_curve_key in coordinator set rotation tests

* Correct shimmed setting of a secq256k1 key

* cargo fmt

* Don't use `[0; 32]` for the embedded keys in the coordinator rotation test

The key_gen function expects the random values already decided.

* Big-endian secq256k1 scalars

Also restores the prior, safer, Encryption::register function.
This commit is contained in:
Luke Parker
2024-08-16 11:26:07 -07:00
parent 669b2fef72
commit e4e4245ee3
121 changed files with 10388 additions and 2480 deletions

View File

@@ -0,0 +1,235 @@
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};
// 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_divisor::<Ep>();
test_same_point::<Ep>();
test_subset_sum_to_infinity::<Ep>();
}
#[test]
fn test_divisor_vesta() {
test_divisor::<Eq>();
test_same_point::<Eq>();
test_subset_sum_to_infinity::<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_divisor::<EdwardsPoint>();
test_same_point::<EdwardsPoint>();
test_subset_sum_to_infinity::<EdwardsPoint>();
}

View File

@@ -0,0 +1,129 @@
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 % &modulus, Poly::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.clone(), 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.clone(), res);
b.y_coefficients = vec![F::from(4u64)];
res.y_coefficients = vec![F::from(8u64)];
assert_eq!(a.clone() * b.clone(), res);
assert_eq!(b.clone() * a.clone(), 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.clone(), res);
assert_eq!(b * a.clone(), 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],
}
);
}