Skip to content

Commit

Permalink
Validate discriminants by default and add fixed discriminant (#786)
Browse files Browse the repository at this point in the history
* Validate discriminants by default and add from_trusted_* functions

* Use explicit from_trusted_* function

* cleanup

* Get rid of from_trusted methods

* Use fixed discriminant

* doc
  • Loading branch information
jonas-lj authored May 17, 2024
1 parent 33f4df4 commit ba175be
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 109 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 8 additions & 60 deletions fastcrypto-cli/src/vdf.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions fastcrypto-vdf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rand_chacha = "0.3.1"
serde.workspace = true
bcs.workspace = true
serde_with = "2.1.0"
lazy_static = "1.4.0"

[features]
experimental = []
Expand Down
73 changes: 47 additions & 26 deletions fastcrypto-vdf/src/class_group/discriminant.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::class_group::bigint_serde;
use crate::math::hash_prime;
use crate::math::hash_prime::is_probable_prime;
use crate::math::parameterized_group::Parameter;
use fastcrypto::error::FastCryptoError::InvalidInput;
use fastcrypto::error::{FastCryptoError, FastCryptoResult};
use lazy_static::lazy_static;
use num_bigint::{BigInt, ToBigInt};
use num_integer::Integer;
use num_traits::Signed;
use num_traits::{One, Signed};
use serde::{Deserialize, Serialize};
use std::ops::Neg;
use std::str::FromStr;

/// A discriminant for an imaginary class group. The discriminant is a negative integer congruent to
/// 1 mod 8.
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Debug, Clone, Serialize)]
pub struct Discriminant(#[serde(with = "crate::class_group::bigint_serde")] BigInt);

lazy_static! {
/// Fixed 3072 bit discriminant. Generated from the seed [1,2,3] using [Discriminant::from_seed].
// TODO: Generate this using a seed that we provably cannot influence.
pub static ref DISCRIMINANT_3072: Discriminant = Discriminant(BigInt::from_str("-4080390101490206102067801750685552291425412528983716161454985565795560716833845004659207152503580931176637478422335625954692628868126419714053340412299850300602673802493259771830686596468801304317015718872352674945215883546019961626928140286675493693757393881479657605888983279619347902770789061953207866325747708864327315769009839190765716943013935708854055658243676903245686125751909996824976354309908771869043784640567352757672203749399825983258156684652782580603170228640173640869773628592618889352385821753919281706169861276929330689892675986265846043432389737049521845230769417696140636288030698887830215613149485135897148487896368642774768920061430225392365148291796645740474628778185683682893521776342856643134668770656709308404166182149870849376649591338267281149794078240401323227967073641261327798339424740171219484355109588337730742391198073121589465833677609362668436116144203312494461735357918360857667357985711").unwrap());
}

impl<'de> Deserialize<'de> for Discriminant {
fn deserialize<D>(deserializer: D) -> Result<Discriminant, D::Error>
where
D: serde::Deserializer<'de>,
{
Discriminant::try_from(bigint_serde::deserialize(deserializer)?)
.map_err(serde::de::Error::custom)
}
}

impl TryFrom<BigInt> for Discriminant {
type Error = FastCryptoError;

/// A valid discriminant should be a negative prime congruent to 1 mod 8. The sign and
/// congruency are checked here but the primality is _not_. See also [Discriminant::check_primality].
/// A valid discriminant should be a negative prime congruent to 1 mod 8. For large discriminants,
/// this is very slow to check.
fn try_from(value: BigInt) -> FastCryptoResult<Self> {
if !value.is_negative() || value.mod_floor(&BigInt::from(8)) != BigInt::from(1) {
if !value.is_negative()
|| value.mod_floor(&BigInt::from(8)) != BigInt::one()
|| !is_probable_prime(value.abs().magnitude())
{
return Err(InvalidInput);
}
Ok(Self(value))
Expand All @@ -41,18 +63,12 @@ impl Discriminant {
&self.0
}

/// Check the primality of this discriminant and return an error if it is not prime.
pub fn check_primality(&self) -> FastCryptoResult<()> {
match is_probable_prime(
&self
.0
.abs()
.to_biguint()
.expect("Absolute value is non-negative"),
) {
true => Ok(()),
false => Err(InvalidInput),
}
/// Create a discriminant from the given value. It is assumed that the discriminant is a negative
/// prime which is 1 mod 8. If this is not the case, some functions may panic, so this should only
/// be used in tests.
#[cfg(test)]
pub(crate) fn from_trusted_bigint(value: BigInt) -> Self {
Self(value)
}
}

Expand All @@ -64,12 +80,12 @@ impl Parameter for Discriminant {
return Err(InvalidInput);
}
// Set the lower three bits to ensure that the prime is 7 mod 8 which makes the discriminant 1 mod 8.
Self::try_from(
Ok(Self(
hash_prime::hash_prime(seed, size_in_bits / 8, &[0, 1, 2, size_in_bits - 1])
.to_bigint()
.expect("Never fails")
.neg(),
)
))
}
}

Expand All @@ -79,7 +95,7 @@ mod tests {

#[test]
fn test_discriminant() {
let discriminant = Discriminant::try_from(-BigInt::from(223)).unwrap();
let discriminant = Discriminant(-BigInt::from(223));
assert_eq!(discriminant.bits(), 8);
assert_eq!(discriminant.as_bigint(), &-BigInt::from(223));

Expand All @@ -95,8 +111,7 @@ mod tests {

// Not prime
let candidate = BigInt::from(-231);
let discriminant = Discriminant::try_from(candidate).unwrap();
assert!(discriminant.check_primality().is_err());
assert!(Discriminant::try_from(candidate).is_err());
}

#[test]
Expand All @@ -105,24 +120,30 @@ mod tests {
let target_size = 1024;
let discriminant = Discriminant::from_seed(&seed, target_size).unwrap();
assert_eq!(discriminant.bits() as usize, target_size);
assert!(discriminant.check_primality().is_ok());

// Test vector from chiavdf computed using https://github.com/Chia-Network/chiavdf/blob/2844974ff81274060778a56dfefd2515bc567b90/tests/test_verifier.py.
assert_eq!(discriminant.as_bigint().to_str_radix(16), "-95a0b0523b6c516e813d745e7e58b3c7223d511f6008a0ff2757c9a0f15cba8841293cc903af3a40654670c9dee17ec14da1457360aafe40a93831d90c3dd59738d8a24e415b6e33780224fa24171de1d4a1ca5fe4c877bf44361e7ba869126ac12367714eb4246a5e310515508ad35e170aee19cae371069d6d92e94c21d63f");
}

#[test]
fn test_discriminant_to_from_bytes() {
let discriminant = Discriminant::try_from(BigInt::from(-223)).unwrap();
let discriminant = Discriminant(BigInt::from(-223));
let bytes = bcs::to_bytes(&discriminant).unwrap();
let discriminant2 = bcs::from_bytes(&bytes).unwrap();
assert_eq!(discriminant, discriminant2);
assert!(discriminant.check_primality().is_ok());

let discriminant = Discriminant::from_seed(&[0x01, 0x02, 0x03], 512).unwrap();
let bytes = bcs::to_bytes(&discriminant).unwrap();
let discriminant2 = bcs::from_bytes(&bytes).unwrap();
assert_eq!(discriminant, discriminant2);
assert!(discriminant.check_primality().is_ok());

// Test serde on invalid discriminants
let invalid_discriminant_bytes =
bcs::to_bytes(&BigInt::from(-221).to_signed_bytes_be()).unwrap();
assert!(bcs::from_bytes::<Discriminant>(&invalid_discriminant_bytes).is_err());

let invalid_discriminant_bytes =
bcs::to_bytes(&BigInt::from(17).to_signed_bytes_be()).unwrap();
assert!(bcs::from_bytes::<Discriminant>(&invalid_discriminant_bytes).is_err());
}
}
4 changes: 2 additions & 2 deletions fastcrypto-vdf/src/class_group/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ mod tests {

#[test]
fn qf_from_seed_regression_tests() {
let discriminant = Discriminant::try_from(-BigInt::from_str_radix("c3811f4ad2f4a7bdf2ed89385866ad526c6dd3aa942e04c141d0562a8e7b014f08804f47b3c2ecbba0a5a0ad8f4d8e869a10cff13dbc522aea141f6d1c42913f2d3bff8d3e7656c72523a2e9d47f838234bd65f05ef3ca86c2f640bca6630ed8d1da21e30a67f83e25b89c32c2d0dc0bacb81bd971b0932a82d131b4a74bff36b60b66543105da2c3ecb1a4e8c2cb6d47c1e85942cce8f3fc50c27856e6dfbd15c0bd5017fea15ae0eb43dfb32b2d947c3131d1951f00bcc40352eeb65e364551e40d13768f443406760ee6b37a5b5819d3f630c034c7f42212ad49c803772aaafd4cd1f87697c68d5a6b0855f475b370b20058558993e76759caa38edbc82407b4e3559bade5f7479a860ebef62fed82d657765ebb8f7f375c2b78f73669760e4bd4932177087a49a0b68d7", 16).unwrap()).unwrap();
let discriminant = Discriminant::from_trusted_bigint(-BigInt::from_str_radix("c3811f4ad2f4a7bdf2ed89385866ad526c6dd3aa942e04c141d0562a8e7b014f08804f47b3c2ecbba0a5a0ad8f4d8e869a10cff13dbc522aea141f6d1c42913f2d3bff8d3e7656c72523a2e9d47f838234bd65f05ef3ca86c2f640bca6630ed8d1da21e30a67f83e25b89c32c2d0dc0bacb81bd971b0932a82d131b4a74bff36b60b66543105da2c3ecb1a4e8c2cb6d47c1e85942cce8f3fc50c27856e6dfbd15c0bd5017fea15ae0eb43dfb32b2d947c3131d1951f00bcc40352eeb65e364551e40d13768f443406760ee6b37a5b5819d3f630c034c7f42212ad49c803772aaafd4cd1f87697c68d5a6b0855f475b370b20058558993e76759caa38edbc82407b4e3559bade5f7479a860ebef62fed82d657765ebb8f7f375c2b78f73669760e4bd4932177087a49a0b68d7", 16).unwrap());

let qf = QuadraticForm::hash_to_group(b"seed", &discriminant, 64).unwrap();
assert_eq!(bcs::to_bytes(&qf).unwrap(), hex::decode("8b0104397d87d59220ec1bbbf79f471689ad0a48f67625abed478749b2f110d79990684b782160a7e00288c240160d10da0198298fd68f7e3fa0964975f1816d5bb38c978ea1bc9fb5aaefa62971435de9565801c80e545b497d00783c0d518722311c6fa7e924bff4f4765f6f3c6b3de8dcf1c314e7ea8df998de524af394e5cec7dfc867cf07f7eb501dfc279102ff304620732b3d44d3ceeadbd054e20eb953eed85ac684044b1192c1ccaeb9ba79695b28204e7148e8560e4b782c6b20ea20123bda9061eed1920d7ff1fd9741220ee1fac09f596524a12aa993734f2fa4ccf792d46c3bf8320073def0c938e6fb608b8866f70fc380f1a37f3fd9c52935837f5ff06ef6ab882599460e7b950ab17a75602a0b29523ab99c4d030923244a5a9e0431759c59a33a471641c013dadaebdc711baf3a05320330959f13b88c6619c64201bc10517c0bbc69524e6d3345eaeade453ea1ebe8b4ce41068e321399c41e8a90831f9713aa2df564423dfa2fe36e65ccf8157c9ebd24f4ac545482b1a609b7bce94316af8e53cbe191ba073b312a60831ea1f657a92ded17350710ed960309f9853536dd6c8dc45ed0069b1feb7e4acbc7adcf15252e96d5c35e37afbd5b6c413c8511e8225b0f938ae03225b5f2a856aed3551f424795b08807fdc0e38566acdebd699e4db85cf216e3467e9ca3d6c82cfaf77d8446e612de64b1fd3a5df8c5a982df1470daf56f3a15787b35439769c6003693c9b88bf14962b40995931baad12c0e00ac9456556deee599db40209dffe5ebf04f193776ba1dbf3c6fa1a81daafb80ec83e9ce0f8365b8f8fffedf13fe1c2d34d12c2bc5c3c223dd1ea6158645d229e65ccf774c04e4aa4715a67d20f2a20752553c30410651990bbb27d1be95c8b690c7a4edd6450ecbeef312e862944c3eeac7ad25541c14aa6c3da7c14bb4ab6119be67dda0ff244b18711c3da20570ecc1536c3c5cfe297bce254df07fe52d05f54d59272d1fe8c6b30c0eb60a6154b86c71e48379a109986a3632dea79fa0e77fe49931abae8a9188163dc388e772342c0f66d90791a3dd8e337211884aea2b7c2f7920862ade85b6e93d4b1e6afe44a0bf62ed509917cde6ef93a8f6dcd763831652e5c193b7b7ed7d9bb937d1ca1870").unwrap());
Expand All @@ -251,7 +251,7 @@ mod tests {

#[test]
fn qf_default_hash_test() {
let discriminant = Discriminant::try_from(-BigInt::from_str_radix("c3811f4ad2f4a7bdf2ed89385866ad526c6dd3aa942e04c141d0562a8e7b014f08804f47b3c2ecbba0a5a0ad8f4d8e869a10cff13dbc522aea141f6d1c42913f2d3bff8d3e7656c72523a2e9d47f838234bd65f05ef3ca86c2f640bca6630ed8d1da21e30a67f83e25b89c32c2d0dc0bacb81bd971b0932a82d131b4a74bff36b60b66543105da2c3ecb1a4e8c2cb6d47c1e85942cce8f3fc50c27856e6dfbd15c0bd5017fea15ae0eb43dfb32b2d947c3131d1951f00bcc40352eeb65e364551e40d13768f443406760ee6b37a5b5819d3f630c034c7f42212ad49c803772aaafd4cd1f87697c68d5a6b0855f475b370b20058558993e76759caa38edbc82407b4e3559bade5f7479a860ebef62fed82d657765ebb8f7f375c2b78f73669760e4bd4932177087a49a0b68d7", 16).unwrap()).unwrap();
let discriminant = Discriminant::from_trusted_bigint(-BigInt::from_str_radix("c3811f4ad2f4a7bdf2ed89385866ad526c6dd3aa942e04c141d0562a8e7b014f08804f47b3c2ecbba0a5a0ad8f4d8e869a10cff13dbc522aea141f6d1c42913f2d3bff8d3e7656c72523a2e9d47f838234bd65f05ef3ca86c2f640bca6630ed8d1da21e30a67f83e25b89c32c2d0dc0bacb81bd971b0932a82d131b4a74bff36b60b66543105da2c3ecb1a4e8c2cb6d47c1e85942cce8f3fc50c27856e6dfbd15c0bd5017fea15ae0eb43dfb32b2d947c3131d1951f00bcc40352eeb65e364551e40d13768f443406760ee6b37a5b5819d3f630c034c7f42212ad49c803772aaafd4cd1f87697c68d5a6b0855f475b370b20058558993e76759caa38edbc82407b4e3559bade5f7479a860ebef62fed82d657765ebb8f7f375c2b78f73669760e4bd4932177087a49a0b68d7", 16).unwrap());

let qf =
QuadraticForm::hash_to_group_with_default_parameters(b"seed", &discriminant).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion fastcrypto-vdf/src/class_group/reduction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ mod tests {

#[test]
fn test_normalization_and_reduction() {
let discriminant = Discriminant::try_from(BigInt::from(-223)).unwrap();
let discriminant = Discriminant::from_trusted_bigint(BigInt::from(-223));
let mut quadratic_form = QuadraticForm::from_a_b_and_discriminant(
BigInt::from(41),
BigInt::from(49),
Expand Down
14 changes: 6 additions & 8 deletions fastcrypto-vdf/src/class_group/tests.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::str::FromStr;

use num_bigint::BigInt;

use crate::class_group::discriminant::Discriminant;
use crate::class_group::discriminant::{Discriminant, DISCRIMINANT_3072};
use crate::class_group::QuadraticForm;
use crate::math::parameterized_group::ParameterizedGroupElement;

#[test]
fn test_composition() {
// The order of the class group (the class number) for -223 is 7 (see https://mathworld.wolfram.com/ClassNumber.html).
let discriminant = Discriminant::try_from(BigInt::from(-223)).unwrap();
let discriminant = Discriminant::from_trusted_bigint(BigInt::from(-223));
let mut g = QuadraticForm::generator(&discriminant);

for _ in 1..=6 {
Expand All @@ -24,7 +22,7 @@ fn test_composition() {

#[test]
fn test_qf_to_from_bytes() {
let discriminant = Discriminant::try_from(BigInt::from(-223)).unwrap();
let discriminant = Discriminant::from_trusted_bigint(BigInt::from(-223));
let expected = QuadraticForm::generator(&discriminant);
let bytes = bcs::to_bytes(&expected).unwrap();
let actual = bcs::from_bytes(&bytes).unwrap();
Expand All @@ -33,11 +31,11 @@ fn test_qf_to_from_bytes() {

#[test]
fn test_large_qf_to_from_bytes() {
let discriminant = Discriminant::try_from(BigInt::from_str("-4080390101490206102067801750685552291425412528983716161454985565795560716833845004659207152503580931176637478422335625954692628868126419714053340412299850300602673802493259771830686596468801304317015718872352674945215883546019961626928140286675493693757393881479657605888983279619347902770789061953207866325747708864327315769009839190765716943013935708854055658243676903245686125751909996824976354309908771869043784640567352757672203749399825983258156684652782580603170228640173640869773628592618889352385821753919281706169861276929330689892675986265846043432389737049521845230769417696140636288030698887830215613149485135897148487896368642774768920061430225392365148291796645740474628778185683682893521776342856643134668770656709308404166182149870849376649591338267281149794078240401323227967073641261327798339424740171219484355109588337730742391198073121589465833677609362668436116144203312494461735357918360857667357985711").unwrap()).unwrap();
assert_eq!(discriminant.bits(), 3072);
assert_eq!(DISCRIMINANT_3072.bits(), 3072);

let expected =
QuadraticForm::hash_to_group_with_default_parameters(&[1, 2, 3], &discriminant).unwrap();
QuadraticForm::hash_to_group_with_default_parameters(&[1, 2, 3], &DISCRIMINANT_3072)
.unwrap();
let bytes = bcs::to_bytes(&expected).unwrap();
let actual = bcs::from_bytes(&bytes).unwrap();
assert_eq!(expected, actual);
Expand Down
17 changes: 5 additions & 12 deletions fastcrypto-vdf/src/vdf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,19 @@ pub trait VDF {

#[cfg(test)]
mod tests {
use crate::class_group::discriminant::Discriminant;
use fastcrypto::hash::{HashFunction, Sha256};

use crate::class_group::discriminant::DISCRIMINANT_3072;
use crate::class_group::QuadraticForm;
use crate::vdf::wesolowski::DefaultVDF;
use crate::vdf::VDF;
use fastcrypto::hash::{HashFunction, Sha256};
use num_bigint::BigInt;
use std::str::FromStr;

#[test]
fn vdf_e2e_test() {
// This test runs an e2e test of the VDF-based randomness protocol with a 3072 bit discriminant.

// Fixed 3072 bit discriminant. Generated from the seed [1,2,3] using [Discriminant::from_seed].
let discriminant =
Discriminant::try_from(BigInt::from_str("-4080390101490206102067801750685552291425412528983716161454985565795560716833845004659207152503580931176637478422335625954692628868126419714053340412299850300602673802493259771830686596468801304317015718872352674945215883546019961626928140286675493693757393881479657605888983279619347902770789061953207866325747708864327315769009839190765716943013935708854055658243676903245686125751909996824976354309908771869043784640567352757672203749399825983258156684652782580603170228640173640869773628592618889352385821753919281706169861276929330689892675986265846043432389737049521845230769417696140636288030698887830215613149485135897148487896368642774768920061430225392365148291796645740474628778185683682893521776342856643134668770656709308404166182149870849376649591338267281149794078240401323227967073641261327798339424740171219484355109588337730742391198073121589465833677609362668436116144203312494461735357918360857667357985711").unwrap())
.unwrap();

// Number of iterations for the VDF
let iterations = 100;
let vdf = DefaultVDF::new(discriminant.clone(), iterations);
let vdf = DefaultVDF::new(DISCRIMINANT_3072.clone(), iterations);

// Add some randomness
let mut combined_randomness = Vec::new();
Expand All @@ -74,7 +67,7 @@ mod tests {
// Compute the VDF input from the combined randomness
let input = QuadraticForm::hash_to_group_with_default_parameters(
&combined_randomness,
&discriminant,
&DISCRIMINANT_3072,
)
.unwrap();
assert_eq!(bcs::to_bytes(&input).unwrap(), hex::decode("bc01009ed67199c633da0faa75a8ed0ff5c3ae7c278c79aad85953cdf86d57101b1ee941e5239b7d614e5b16eac269c460f16d57a12c75b52c48fac643a1c4918fab86805fe08fcd6b38093a17143cca7550fd018b8bc6871fb441b406bec7a7f3a61c3b2a5aa9daca5f9a6fa474b270688de643323de1acc8073a3418bc1681a614c1abb5fa56b61a7d6df4260547c9f613c5f0dbd7cb91a478ac94b1cce6b1f4784dc161ec3c85bf02cf99fd460b0b25a44d1990dacd1fe7a43b797611ea0210645fef3905f7e1accf97bd3b868a8a99d4a1a546e5a55e20f343fc2724829f1770591b50a73c93ec9b8c01ce1fa6b84eddd5a7ddd077110e21b8e058bf2fed2592a5449db177ec0e32595b20bda5779c2f346b01df8c0d277d9d3a7fe0a04e67b210be60334efdadb7abc5ac001b71509c2d487d9d26443527c1b8b02dfcffc50ef98020f569cdf6fffca5870b0e502493fceee35b79eed99e2c758a0aff4c86b2af0dd223e270ecf84eb7405fe35a9e37d6b080efa3c59806c2ceffa82f38502f9d37b6c298cf07534347cd9ee436406784bd7e0a57d380dd3923ddca13d86f3b2c83a135f125f9429a6802247a0e926b54144d74e4e8f66f0303cdc91843ce7e1fb9c6276c000512c0709c7fbfde2b80e66db77222447ef6b4da4a698e011c6de95ad88738aea465c158288a54223c7f7152577cc48691af57e2631e3224b7c94e2a4c5034db35bbf9e807753fa51da8798bf63b7e6ebd857ca4cf01fcab7a33e63fa89eb386e2ef98046c44491bdf8d62ede2af4ab79ccac88e404abb649b92f49c9f9abcf2216bb628e96400a75a66c12b6ff1c6dae498dd4183ad989921ebc6a1be73127741333671eb72cd25eabc69fecc3c50da06b4a3af155264d4e39e8c681b8c5555d4cab748ed15d119527820e01854fa203c2deba3a67620d47733919e8c71d659e60e86db69905ebdc4dbeda67f77291c2202b2116a05f227f963a97eb8c87104b2df349f01f251aa22bbd41541998ce755309b98d9597d7ee26b6acaef1869885c775e6ceb710c36c07e401e17a8ccb838e33f64e43e4db3491b5cef6e800c4e494610ab81a8b489263b86976160d7d0106cab79bf2a2fce5b01e8f9d1fb069a98e814c94f10d9917b7ea27209bc822b35741f56a9aeadb75a7eae6a8cbd7df08e079db64fd48655f42c24c14bb6c72e744206a3e15deee45cab74d589deb1055e0e69fe508a2ef356dc4e2caaaf89f44a520722490374eade8573429d0d6d16e3c681853f96759cc6e3ea3aaad55284282abd40686281ff944c6a507086143cf76d0f7f93b486d552fa4698656cff8a325fea84943333645b29ee11c99555b2076a09466f6e602db663e1bd45c523a12a7fcd2328d5139d14b25561b94f62f69d436c5d4c92b01ae3a91baa1b5781bd0bf2156e1d0042ab2cbc6e10f4389868fc41d05b19bfe3dfcaacb0478b3dce887da8435c9d49f457fd54e129133e5ce87c39acb9206213daec867fca35e6b612c523fb9fba959542a777ea74").unwrap());
Expand Down

0 comments on commit ba175be

Please sign in to comment.