Generate Bulletproofs(+) generators at compile time

Creates a new monero-generators crate so the monero crate can run the 
code in question at build time.

Saves several seconds from running the tests.

Closes https://github.com/serai-dex/serai/issues/101.
This commit is contained in:
Luke Parker 2022-08-21 06:36:53 -04:00
parent 577fe99a08
commit 603a3f8c9f
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
19 changed files with 274 additions and 133 deletions

14
Cargo.lock generated
View file

@ -4583,6 +4583,18 @@ dependencies = [
"serde",
]
[[package]]
name = "monero-generators"
version = "0.1.0"
dependencies = [
"curve25519-dalek 3.2.0",
"dalek-ff-group",
"group",
"lazy_static",
"subtle",
"tiny-keccak",
]
[[package]]
name = "monero-serai"
version = "0.1.0"
@ -4600,6 +4612,7 @@ dependencies = [
"modular-frost",
"monero",
"monero-epee-bin-serde",
"monero-generators",
"multiexp",
"rand 0.8.5",
"rand_chacha 0.3.1",
@ -4608,7 +4621,6 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"sha2 0.10.2",
"subtle",
"thiserror",
"tiny-keccak",

View file

@ -9,6 +9,7 @@ members = [
"crypto/frost",
"coins/ethereum",
"coins/monero/generators",
"coins/monero",
"processor",

1
coins/monero/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.generators

View file

@ -32,6 +32,8 @@ transcript = { package = "flexible-transcript", path = "../../crypto/transcript"
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
dleq = { path = "../../crypto/dleq", features = ["serialize"], optional = true }
monero-generators = { path = "generators" }
hex = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -45,6 +47,9 @@ reqwest = { version = "0.11", features = ["json"] }
[features]
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
[build-dependencies]
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
monero-generators = { path = "generators" }
[dev-dependencies]
sha2 = "0.10"
tokio = { version = "1", features = ["full"] }

67
coins/monero/build.rs Normal file
View file

@ -0,0 +1,67 @@
use std::{
io::Write,
path::Path,
fs::{File, remove_file},
};
use dalek_ff_group::EdwardsPoint;
use monero_generators::bulletproofs_generators;
fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) {
for generator in points {
generators_string.extend(
format!(
"
dalek_ff_group::EdwardsPoint(
curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap()
),
",
generator.compress().to_bytes()
)
.chars(),
);
}
}
fn generators(prefix: &'static str, path: &str) {
let generators = bulletproofs_generators(prefix.as_bytes());
#[allow(non_snake_case)]
let mut G_str = "".to_string();
serialize(&mut G_str, &generators.G);
#[allow(non_snake_case)]
let mut H_str = "".to_string();
serialize(&mut H_str, &generators.H);
let path = Path::new(".generators").join(path);
let _ = remove_file(&path);
File::create(&path)
.unwrap()
.write_all(
format!(
"
lazy_static! {{
static ref GENERATORS: Generators = Generators {{
G: [
{}
],
H: [
{}
],
}};
}}
",
G_str, H_str,
)
.as_bytes(),
)
.unwrap();
}
fn main() {
// For some reason, filtering off .generators does not work. This prevents re-building overall
println!("cargo:rerun-if-changed=build.rs");
generators("bulletproof", "generators.rs");
generators("bulletproof_plus", "generators_plus.rs");
}

View file

@ -0,0 +1,19 @@
[package]
name = "monero-generators"
version = "0.1.0"
description = "Monero's hash_to_point and generators"
license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[dependencies]
lazy_static = "1"
subtle = "2.4"
tiny-keccak = { version = "2", features = ["keccak"] }
curve25519-dalek = { version = "3", features = ["std"] }
group = { version = "0.12" }
dalek-ff-group = { path = "../../../crypto/dalek-ff-group" }

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 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.

View file

@ -0,0 +1,51 @@
use subtle::ConditionallySelectable;
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
use group::ff::{Field, PrimeField};
use dalek_ff_group::field::FieldElement;
use crate::hash;
#[allow(dead_code)]
pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
#[allow(non_snake_case)]
let A = FieldElement::from(486662u64);
let v = FieldElement::from_square(hash(&bytes)).double();
let w = v + FieldElement::one();
let x = w.square() + (-A.square() * v);
// This isn't the complete X, yet its initial value
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
// While inefficient, it solves API boundaries and reduces the amount of work done here
#[allow(non_snake_case)]
let X = {
let u = w;
let v = x;
let v3 = v * v * v;
let uv3 = u * v3;
let v7 = v3 * v3 * v;
let uv7 = u * v7;
uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap())
};
let x = X.square() * x;
let y = w - x;
let non_zero_0 = !y.is_zero();
let y_if_non_zero_0 = w + x;
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
let mut z = -A;
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
#[allow(non_snake_case)]
let Z = z + w;
#[allow(non_snake_case)]
let mut Y = z - w;
Y *= Z.invert().unwrap();
let mut bytes = Y.to_repr();
bytes[31] |= sign.unwrap_u8() << 7;
CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor()
}

View file

@ -0,0 +1,61 @@
use lazy_static::lazy_static;
use tiny_keccak::{Hasher, Keccak};
use curve25519_dalek::{
constants::ED25519_BASEPOINT_POINT,
edwards::{EdwardsPoint as DalekPoint, CompressedEdwardsY},
};
use group::Group;
use dalek_ff_group::EdwardsPoint;
mod varint;
use varint::write_varint;
mod hash_to_point;
pub use hash_to_point::hash_to_point;
fn hash(data: &[u8]) -> [u8; 32] {
let mut keccak = Keccak::v256();
keccak.update(data);
let mut res = [0; 32];
keccak.finalize(&mut res);
res
}
lazy_static! {
pub static ref H: DalekPoint =
CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes()))
.decompress()
.unwrap()
.mul_by_cofactor();
}
const MAX_M: usize = 16;
const N: usize = 64;
const MAX_MN: usize = MAX_M * N;
#[allow(non_snake_case)]
pub struct Generators {
pub G: [EdwardsPoint; MAX_MN],
pub H: [EdwardsPoint; MAX_MN],
}
pub fn bulletproofs_generators(prefix: &'static [u8]) -> Generators {
let mut res =
Generators { G: [EdwardsPoint::identity(); MAX_MN], H: [EdwardsPoint::identity(); MAX_MN] };
for i in 0 .. MAX_MN {
let i = 2 * i;
let mut even = H.compress().to_bytes().to_vec();
even.extend(prefix);
let mut odd = even.clone();
write_varint(&i.try_into().unwrap(), &mut even).unwrap();
write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap();
res.H[i / 2] = EdwardsPoint(hash_to_point(hash(&even)));
res.G[i / 2] = EdwardsPoint(hash_to_point(hash(&odd)));
}
res
}

View file

@ -0,0 +1,16 @@
use std::io;
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
pub(crate) fn write_varint<W: io::Write>(varint: &u64, w: &mut W) -> io::Result<()> {
let mut varint = *varint;
while {
let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap();
varint >>= 7;
if varint != 0 {
b |= VARINT_CONTINUATION_MASK;
}
w.write_all(&[b])?;
varint != 0
} {}
Ok(())
}

View file

@ -6,11 +6,13 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
use tiny_keccak::{Hasher, Keccak};
use curve25519_dalek::{
constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE},
constants::ED25519_BASEPOINT_TABLE,
scalar::Scalar,
edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY},
edwards::{EdwardsPoint, EdwardsBasepointTable},
};
pub use monero_generators::H;
#[cfg(feature = "multisig")]
pub mod frost;
@ -54,11 +56,6 @@ impl Protocol {
}
lazy_static! {
static ref H: EdwardsPoint =
CompressedEdwardsY(hash(&ED25519_BASEPOINT_POINT.compress().to_bytes()))
.decompress()
.unwrap()
.mul_by_cofactor();
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H);
}

View file

@ -13,10 +13,9 @@ use dalek_ff_group::{Scalar, EdwardsPoint};
use multiexp::multiexp as multiexp_const;
use crate::{
H as DALEK_H, Commitment, hash, hash_to_scalar as dalek_hash,
ringct::hash_to_point::raw_hash_to_point, serialize::write_varint,
};
pub(crate) use monero_generators::Generators;
use crate::{H as DALEK_H, Commitment, hash_to_scalar as dalek_hash};
pub(crate) use crate::ringct::bulletproofs::scalar_vector::*;
// Bring things into ff/group
@ -33,29 +32,6 @@ pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
pub(crate) const MAX_M: usize = 16;
pub(crate) const LOG_N: usize = 6; // 2 << 6 == N
pub(crate) const N: usize = 64;
pub(crate) const MAX_MN: usize = MAX_M * N;
pub(crate) struct Generators {
pub(crate) G: Vec<EdwardsPoint>,
pub(crate) H: Vec<EdwardsPoint>,
}
pub(crate) fn generators_core(prefix: &'static [u8]) -> Generators {
let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) };
for i in 0 .. MAX_MN {
let i = 2 * i;
let mut even = (*H).compress().to_bytes().to_vec();
even.extend(prefix);
let mut odd = even.clone();
write_varint(&i.try_into().unwrap(), &mut even).unwrap();
write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap();
res.H.push(EdwardsPoint(raw_hash_to_point(hash(&even))));
res.G.push(EdwardsPoint(raw_hash_to_point(hash(&odd))));
}
res
}
pub(crate) fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
multiexp_const(pairs) * *INV_EIGHT
@ -153,12 +129,6 @@ lazy_static! {
pub(crate) static ref TWO_N: ScalarVector = ScalarVector::powers(Scalar::from(2u8), N);
}
pub(crate) fn init() {
let _ = &*INV_EIGHT;
let _ = &*H;
let _ = &*TWO_N;
}
pub(crate) fn challenge_products(w: &[Scalar], winv: &[Scalar]) -> Vec<Scalar> {
let mut products = vec![Scalar::zero(); 1 << w.len()];
products[0] = winv[0];

View file

@ -43,14 +43,6 @@ impl Bulletproofs {
len + clawback
}
pub fn init(plus: bool) {
if !plus {
OriginalStruct::init();
} else {
PlusStruct::init();
}
}
pub fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
outputs: &[Commitment],

View file

@ -12,8 +12,9 @@ use multiexp::BatchVerifier;
use crate::{Commitment, ringct::bulletproofs::core::*};
include!("../../../.generators/generators.rs");
lazy_static! {
static ref GENERATORS: Generators = generators_core(b"bulletproof");
static ref ONE_N: ScalarVector = ScalarVector(vec![Scalar::one(); N]);
static ref IP12: Scalar = inner_product(&ONE_N, &TWO_N);
}
@ -34,13 +35,6 @@ pub struct OriginalStruct {
}
impl OriginalStruct {
pub(crate) fn init() {
init();
let _ = &*GENERATORS;
let _ = &*ONE_N;
let _ = &*IP12;
}
pub(crate) fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
commitments: &[Commitment],

View file

@ -15,8 +15,9 @@ use crate::{
ringct::{hash_to_point::raw_hash_to_point, bulletproofs::core::*},
};
include!("../../../.generators/generators_plus.rs");
lazy_static! {
static ref GENERATORS: Generators = generators_core(b"bulletproof_plus");
static ref TRANSCRIPT: [u8; 32] =
EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))).compress().to_bytes();
}
@ -52,12 +53,6 @@ pub struct PlusStruct {
}
impl PlusStruct {
pub(crate) fn init() {
init();
let _ = &*GENERATORS;
let _ = &*TRANSCRIPT;
}
pub(crate) fn prove<R: RngCore + CryptoRng>(
rng: &mut R,
commitments: &[Commitment],

View file

@ -1,54 +1,6 @@
use subtle::ConditionallySelectable;
use curve25519_dalek::edwards::EdwardsPoint;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use group::ff::{Field, PrimeField};
use dalek_ff_group::field::FieldElement;
use crate::hash;
#[allow(dead_code)]
pub(crate) fn raw_hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
#[allow(non_snake_case)]
let A = FieldElement::from(486662u64);
let v = FieldElement::from_square(hash(&bytes)).double();
let w = v + FieldElement::one();
let x = w.square() + (-A.square() * v);
// This isn't the complete X, yet its initial value
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
// While inefficient, it solves API boundaries and reduces the amount of work done here
#[allow(non_snake_case)]
let X = {
let u = w;
let v = x;
let v3 = v * v * v;
let uv3 = u * v3;
let v7 = v3 * v3 * v;
let uv7 = u * v7;
uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap())
};
let x = X.square() * x;
let y = w - x;
let non_zero_0 = !y.is_zero();
let y_if_non_zero_0 = w + x;
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
let mut z = -A;
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
#[allow(non_snake_case)]
let Z = z + w;
#[allow(non_snake_case)]
let mut Y = z - w;
Y *= Z.invert().unwrap();
let mut bytes = Y.to_repr();
bytes[31] |= sign.unwrap_u8() << 7;
CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor()
}
pub(crate) use monero_generators::{hash_to_point as raw_hash_to_point};
pub fn hash_to_point(key: EdwardsPoint) -> EdwardsPoint {
raw_hash_to_point(key.compress().to_bytes())

View file

@ -57,12 +57,7 @@ fn bulletproofs_vector() {
}
macro_rules! bulletproofs_tests {
($init: ident, $name: ident, $max: ident, $plus: literal) => {
#[test]
fn $init() {
Bulletproofs::init($plus);
}
($name: ident, $max: ident, $plus: literal) => {
#[test]
fn $name() {
// Create Bulletproofs for all possible output quantities
@ -93,5 +88,5 @@ macro_rules! bulletproofs_tests {
};
}
bulletproofs_tests!(bulletproofs_init, bulletproofs, bulletproofs_max, false);
bulletproofs_tests!(bulletproofs_plus_init, bulletproofs_plus, bulletproofs_plus_max, true);
bulletproofs_tests!(bulletproofs, bulletproofs_max, false);
bulletproofs_tests!(bulletproofs_plus, bulletproofs_plus_max, true);

View file

@ -7,7 +7,6 @@ use transcript::RecommendedTranscript;
use frost::{curve::Ed25519, FrostKeys};
use monero_serai::{
ringct::bulletproofs::Bulletproofs,
transaction::Transaction,
rpc::Rpc,
wallet::{
@ -69,14 +68,7 @@ pub struct Monero {
impl Monero {
pub async fn new(url: String) -> Monero {
let view = view_key::<Monero>(0).0;
let res = Monero { rpc: Rpc::new(url), view };
// Initialize Bulletproofs now to prevent the first call from taking several seconds
// TODO: Do this for both, unless we're sure we're only working on a single protocol
Bulletproofs::init(res.rpc.get_protocol().await.unwrap().bp_plus());
res
Monero { rpc: Rpc::new(url), view: view_key::<Monero>(0).0 }
}
fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair {

View file

@ -86,7 +86,7 @@ impl CoinDb for MemCoinDb {
fn add_output<O: Output>(&mut self, output: &O) -> bool {
// This would be insecure as we're indexing by ID and this will replace the output as a whole
// Multiple outputs may have the same ID in edge cases such as Monero, where outputs are ID'd
// by key image, not by hash + index
// by output key, not by hash + index
// self.outputs.insert(output.id(), output).is_some()
let id = output.id().as_ref().to_vec();
if self.outputs.contains_key(&id) {