Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Outline of a PRSS framework #40

Merged
merged 5 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ self-signed-certs = ["hyper-tls"]
[dependencies]
axum = { version = "0.5.7", optional = true, features = ["http2"] }
axum-server = { version = "0.4.0", optional = true, features = ["rustls", "rustls-pemfile", "tls-rustls"] }
aes = "0.8"
byteorder = "1"
# rust-elgamal (via curve25519-dalek-ng) only works with digest 0.9, not 0.10
digest = "0.9"
hex = { version = "0.4", optional = true }
Expand All @@ -23,6 +25,9 @@ hkdf = "0.11"
hyper = { version = "0.14.19", optional = true, features = ["client", "h2"] }
hyper-tls = { version = "0.5.0", optional = true }
log = "0.4"
# This is stupid, but we have packages that want this old interface
# those have to use the same RNG as packages that want the new interface.
old_rand_core = { package = "rand_core", version = "0.5", default-features = false }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be easier than what I did, yeah.

rand = "0.8"
rand_core = "0.6"
redis = "0.21.5"
Expand All @@ -37,6 +42,7 @@ tokio = { version = "1.19.2", optional = true, features = ["rt", "rt-multi-threa
tower-http = { version = "0.3.4", optional = true, features = ["trace"] }
tracing = "0.1.35"
tracing-subscriber = { version = "0.3.14", optional = true }
x25519-dalek = "1"

[dev-dependencies]
hex = "0.4"
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use thiserror::Error;
pub enum Error {
#[error("already exists")]
AlreadyExists,
#[error("already setup")]
AlreadySetup,
#[error("internal")]
Internal,
#[error("invalid id")]
Expand Down
146 changes: 146 additions & 0 deletions src/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use std::{
fmt::Debug,
ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
};

pub trait Field:
Add<Output = Self>
+ AddAssign
+ Neg<Output = Self>
+ Sub<Output = Self>
+ SubAssign
+ Mul<Output = Self>
+ MulAssign
+ From<u128>
+ Clone
+ Copy
+ PartialEq
+ Debug
+ Sized
{
type Integer;
const PRIME: Self::Integer;
}

// TODO(mt) - this code defining fields can be turned into a macro if we ever
// need lots of fields with different primes.

#[derive(Clone, Copy, PartialEq)]
pub struct Fp31(<Self as Field>::Integer);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow i haven't seen this syntax before, at least not as a field of a struct. im not even sure how this works

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic. I was a little surprised that this sort of recursiveness was tolerated by the compiler too. The syntax isn't new though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what this means =). In human language what is going on here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically Self is Fp31 when you are defining anything related to Fp31. But that is a nebulous type defined by multiple implementations. <Fp31 as Field> is specifically the implementation of Field for Fp31. That has an associated type of Integer, which turns out to be usable here.

Honestly, this was even more DRY than I would have thought possible, but given that rust accepted it, I left it there for us all to wonder at.


impl Field for Fp31 {
type Integer = u8;
const PRIME: Self::Integer = 31;
}

impl Add for Fp31 {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
// TODO(mt) - constant time?
Self((self.0 + rhs.0) % Self::PRIME)
}
}

impl AddAssign for Fp31 {
#[allow(clippy::assign_op_pattern)]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
Comment on lines +45 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code seems like it would be the same for all prime fields. Is there some way to put this into "Field" itself, and not Fp31?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is step 1. As you can see, it's likely to be very repetitive for new primes. The next step is to define a macro that includes all these implementations. There are a few choices we need to make (prime, basic type, upconversion type for addition and multiplication), but it should be a fairly easy thing to do.


impl Neg for Fp31 {
type Output = Self;

fn neg(self) -> Self::Output {
Self((Self::PRIME - self.0) % Self::PRIME)
}
}

impl Sub for Fp31 {
type Output = Self;

fn sub(self, rhs: Self) -> Self::Output {
// TODO(mt) - constant time?
// Note: no upcast needed here because `2*p < u8::MAX`.
Self((Self::PRIME + self.0 - rhs.0) % Self::PRIME)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be more complex for other primes =).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the generic implementation will need to upconvert, which - for types that don't need that upconversion, that is most of them - will need some nice warning suppression annotations.

}
}

impl SubAssign for Fp31 {
#[allow(clippy::assign_op_pattern)]
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}
Comment on lines +70 to +75
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the one on AddAssign. I'd love to only have this defined once - and not repeat it on Fp2147483647 and friends =).


impl Mul for Fp31 {
type Output = Self;

fn mul(self, rhs: Self) -> Self::Output {
// TODO(mt) - constant time?
let c = u16::from;
#[allow(clippy::cast_possible_truncation)]
Self(((c(self.0) * c(rhs.0)) % c(Self::PRIME)) as <Self as Field>::Integer)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as <Self as Field>::Integer

That part kind of blows my mind =). Is there a way to simplify that? Why is the cast necessary? Can we not just do:

as Self::Integer

I'm sure there's a good reason why not - I'd just love to learn what it is =).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that multiplying two values from [0..31) produces a value that is > 8 bits. So I need to upconvert to u16. The cast takes it back down from u16 to u8.

I could use u8::try_from(...).unwrap() but that comes with a runtime cost.

}
}

impl MulAssign for Fp31 {
#[allow(clippy::assign_op_pattern)]
fn mul_assign(&mut self, rhs: Self) {
*self = *self * rhs;
}
}

/// An infallible conversion from `u128` to this type. This can be used to draw
/// a random value in the field. This introduces bias into the final value
/// but for our purposes that bias is small provided that `2^128 >> PRIME`, which
/// is true provided that `PRIME` is kept to at most 64 bits in value.
///
/// This method is simpler than rejection sampling for these small prime fields.
impl<T: Into<u128>> From<T> for Fp31 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so T: Into<u128> is sufficient for trait requirement From<u128>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also magic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, when you define something as generic, what it really does is create what rust calls "monomorphisms" of the type for each of the concrete types that use the code. So as u128 implements Into<u128> (there is a default no-op implementation for that), the trait requirement is naturally fulfilled.

fn from(v: T) -> Self {
#[allow(clippy::cast_possible_truncation)]
Self((v.into() % u128::from(Self::PRIME)) as <Self as Field>::Integer)
}
}

impl Debug for Fp31 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}_mod{}", self.0, Self::PRIME)
}
}

#[cfg(test)]
mod test {
use crate::field::Field;

use super::Fp31;

#[test]
fn fp31() {
let x = Fp31(24);
let y = Fp31(23);

assert_eq!(Fp31(16), x + y);
assert_eq!(Fp31(25), x * y);
assert_eq!(Fp31(1), x - y);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding a few more test cases like 0 + 0 and 0 - 0 and 1 + 0, 0 - 1, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the zero tests are fun. Easy to get that wrong.


let mut x = Fp31(1);
x += Fp31(2);
assert_eq!(Fp31(3), x);
}

#[test]
fn zero() {
assert_eq!(
Fp31(0),
Fp31::from(<Fp31 as Field>::PRIME),
"from takes a modulus"
);
assert_eq!(Fp31(0), Fp31(0) + Fp31(0));
assert_eq!(Fp31(0), Fp31(0) + Fp31(0));
assert_eq!(Fp31(<Fp31 as Field>::PRIME - 1), Fp31(0) - Fp31(1));
assert_eq!(Fp31(0), Fp31(0) * Fp31(1));
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#[cfg(feature = "cli")]
pub mod cli;
pub mod error;
pub mod field;
pub mod helpers;
pub mod net;
pub mod prss;
pub mod report;
pub mod threshold;
pub mod user;
Loading