diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 48a33db74..1a05eecb1 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -70,7 +70,7 @@ jobs: run: | pytest tests - benchmarks: + generator-benchmarks: name: Generator performance runs-on: benchmark strategy: @@ -121,3 +121,23 @@ jobs: cd tests ./generate-programs.py ./run-programs.py + + benchmarks: + name: rust benchmarks + runs-on: benchmark + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + + - name: Set up rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: cargo bench + run: | + cargo bench --all diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6c23d9d82..73ac3204c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -220,7 +220,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: packages - path: ./target/wheels/ + path: ./wheel/target/wheels/ check-typestubs: name: Check chia_rs.pyi @@ -280,7 +280,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: packages - path: ./target/wheels/ + path: ./wheel/target/wheels/ fmt: runs-on: ubuntu-latest @@ -310,7 +310,7 @@ jobs: - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --all + args: --all-features --workspace fuzz_targets: runs-on: ubuntu-latest @@ -338,7 +338,7 @@ jobs: with: toolchain: stable - name: cargo test - run: cargo test + run: cargo test --workspace --all-features upload: name: Upload to PyPI - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} diff --git a/Cargo.toml b/Cargo.toml index c7b9aef93..bb702d0ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ +# the "wheel" crate is excluded from the workspace because pyo3 has problems with +# "cargo test" and "cargo bench" [workspace] -members = ["wasm", "wheel", "chia_streamable_macro"] +members = ["wasm", "chia_streamable_macro", "chia-bls"] +exclude = ["wheel"] [package] name = "chia" diff --git a/chia-bls/Cargo.toml b/chia-bls/Cargo.toml new file mode 100644 index 000000000..fbd6a85b0 --- /dev/null +++ b/chia-bls/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "chia-bls" +version = "0.1.0" +edition = "2021" + +[dependencies] +tiny-bip39 = "=1.0.0" +anyhow = "=1.0.65" +# the newer sha2 crate doesn't implement the digest traits required by hkdf +sha2 = "=0.9.9" +bls12_381_plus = "=0.7.0" +num-bigint = "=0.4.3" +hkdf = "=0.11.0" +group = "=0.12.0" + +[dev-dependencies] +hex = "^0.4.3" +rand = "^0.8.5" +criterion = "^0.4" + +[[bench]] +name = "derive_key" +harness = false + +[[bench]] +name = "sign" +harness = false + +[[bench]] +name = "verify" +harness = false diff --git a/chia-bls/README.md b/chia-bls/README.md new file mode 100644 index 000000000..96e1e2d8a --- /dev/null +++ b/chia-bls/README.md @@ -0,0 +1,76 @@ +Library providing building blocks for a Chia wallet. + +BIP39 mnemonic handling: + +``` +fn entropy_to_mnemonic(entropy: &[u8; 32]) -> String +fn mnemonic_to_entropy(mnemonic: &str) -> Result<[u8; 32], Error> +fn entropy_to_seed(entropy: &[u8; 32]) -> [u8; 64] +``` + +SecretKey + +``` +impl SecretKey { + pub fn from_seed(seed: &[u8; 64]) -> SecretKey + pub fn from_bytes(bytes: &[u8; 32]) -> Option + pub fn to_bytes(&self) -> [u8; 32] + + pub fn public_key(&self) -> PublicKey + + pub fn derive_unhardened(&self, idx: u32) -> SecretKey + pub fn derive_hardened(&self, idx: u32) -> SecretKey +} +``` + +PublicKey + +``` +impl PublicKey { + pub fn from_bytes(bytes: &[u8; 48]) -> Option + pub fn to_bytes(&self) -> [u8; 48] + pub fn derive_unhardened(&self, idx: u32) -> PublicKey +} +``` + +Unhardened Key derivation (`Key` can be both a secret- or public key) + +``` +fn master_to_wallet_unhardened_intermediate(key: &Key) -> Key +fn master_to_wallet_unhardened(key: &Key, idx: u32) -> Key + +``` + +Hardened key derivation (only SecretKey) + +``` +fn master_to_wallet_hardened_intermediate(key: &SecretKey) -> SecretKey +fn master_to_wallet_hardened(key: &SecretKey, idx: u32) -> SecretKey +fn master_to_pool_singleton(key: &SecretKey, pool_wallet_idx: u32) -> SecretKey +fn master_to_pool_authentication(key: &SecretKey, pool_wallet_idx: u32, idx: u32) -> SecretKey +``` + +Signature + +``` +impl Signature { + pub fn from_bytes(buf: &[u8; 96]) -> Option + pub fn to_bytes(&self) -> [u8; 96] + pub fn aggregate(&mut self, sig: &Signature) +} + +impl Default for Signature { + fn default() -> Self +} +``` + +sign and verify (using the Augmented scheme) + +``` +pub fn sign>(sk: &SecretKey, msg: Msg) -> Signature +pub fn aggregate, I>(sigs: I) -> Signature + where I: IntoIterator +pub fn verify>(sig: &Signature, key: &PublicKey, msg: Msg) -> bool +pub fn aggregate_verify, Msg: Borrow<[u8]>, I>(sig: &Signature, data: I) -> bool + where I: IntoIterator +``` diff --git a/chia-bls/benches/derive_key.rs b/chia-bls/benches/derive_key.rs new file mode 100644 index 000000000..16749ccb0 --- /dev/null +++ b/chia-bls/benches/derive_key.rs @@ -0,0 +1,46 @@ +use chia_bls::derivable_key::DerivableKey; +use chia_bls::secret_key::SecretKey; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use std::time::Instant; + +fn key_derivation_benchmark(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + rng.fill(data.as_mut_slice()); + + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + + c.bench_function("secret key, unhardened", |b| { + b.iter_custom(|iters| { + let start = Instant::now(); + for i in 0..iters { + black_box(sk.derive_unhardened(i as u32)); + } + start.elapsed() + }) + }); + c.bench_function("secret key, hardened", |b| { + b.iter_custom(|iters| { + let start = Instant::now(); + for i in 0..iters { + black_box(sk.derive_hardened(i as u32)); + } + start.elapsed() + }) + }); + c.bench_function("public key, unhardened", |b| { + b.iter_custom(|iters| { + let start = Instant::now(); + for i in 0..iters { + black_box(pk.derive_unhardened(i as u32)); + } + start.elapsed() + }) + }); +} + +criterion_group!(key_derivation, key_derivation_benchmark); +criterion_main!(key_derivation); diff --git a/chia-bls/benches/sign.rs b/chia-bls/benches/sign.rs new file mode 100644 index 000000000..0b0ca381f --- /dev/null +++ b/chia-bls/benches/sign.rs @@ -0,0 +1,29 @@ +use chia_bls::secret_key::SecretKey; +use chia_bls::signature::sign; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +fn sign_benchmark(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + rng.fill(data.as_mut_slice()); + + let sk = SecretKey::from_seed(&data); + let small_msg = b"The quick brown fox jumps over the lazy dog"; + let large_msg = [42_u8; 4096]; + + c.bench_function("sign, small msg", |b| { + b.iter(|| { + sign(&sk, black_box(&small_msg)); + }); + }); + c.bench_function("sign, 4kiB msg", |b| { + b.iter(|| { + sign(&sk, black_box(&large_msg)); + }); + }); +} + +criterion_group!(signing, sign_benchmark); +criterion_main!(signing); diff --git a/chia-bls/benches/verify.rs b/chia-bls/benches/verify.rs new file mode 100644 index 000000000..bd27706bf --- /dev/null +++ b/chia-bls/benches/verify.rs @@ -0,0 +1,33 @@ +use chia_bls::secret_key::SecretKey; +use chia_bls::signature; +use chia_bls::signature::sign; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +fn verify_benchmark(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + rng.fill(data.as_mut_slice()); + + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + let msg_small = b"The quick brown fox jumps over the lazy dog"; + let msg_large = [42_u8; 4096]; + let sig_small = sign(&sk, &msg_small); + let sig_large = sign(&sk, &msg_large); + + c.bench_function("verify, small msg", |b| { + b.iter(|| { + signature::verify(&sig_small, &pk, black_box(&msg_small)); + }); + }); + c.bench_function("verify, 4kiB msg", |b| { + b.iter(|| { + signature::verify(&sig_large, &pk, black_box(&msg_large)); + }); + }); +} + +criterion_group!(verify, verify_benchmark); +criterion_main!(verify); diff --git a/chia-bls/fuzz/.gitignore b/chia-bls/fuzz/.gitignore new file mode 100644 index 000000000..a0925114d --- /dev/null +++ b/chia-bls/fuzz/.gitignore @@ -0,0 +1,3 @@ +target +corpus +artifacts diff --git a/chia-bls/fuzz/Cargo.toml b/chia-bls/fuzz/Cargo.toml new file mode 100644 index 000000000..f6fa826e9 --- /dev/null +++ b/chia-bls/fuzz/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "chia-bls-fuzz" +version = "0.0.0" +authors = ["Automatically generated"] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +pyo3 = { version = ">=0.17.2", features = ["auto-initialize"]} + +[dependencies.chia-bls] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "derive" +path = "fuzz_targets/derive.rs" +test = false +doc = false + +[[bin]] +name = "blspy-fidelity" +path = "fuzz_targets/blspy-fidelity.rs" +test = false +doc = false diff --git a/chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs b/chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs new file mode 100644 index 000000000..0f7c08307 --- /dev/null +++ b/chia-bls/fuzz/fuzz_targets/blspy-fidelity.rs @@ -0,0 +1,88 @@ +// PyO3 might use an unexpected version of python. So make sure blspy is +// installed in the one that's actually being used. +// On M1 MacOS, for example, pyO3 doesn't appear to be using the active venv, +// but the system Python3.9 executable (even though the default system python is +// 3.8.9). To install blspy: + +// python3.9 -m pip install blspy + +#![no_main] +use libfuzzer_sys::fuzz_target; +use pyo3::prelude::*; +use std::convert::TryFrom; + +use chia_bls::secret_key::SecretKey; +use chia_bls::signature::{sign, aggregate}; +use chia_bls::derivable_key::DerivableKey; +use pyo3::types::{PyTuple, PyBytes, PyList}; + +fn to_bytes<'a>(obj: &'a PyAny) -> &'a [u8] { + obj.call_method0("__bytes__").unwrap().downcast::().unwrap().as_bytes() +} + +fuzz_target!(|data: &[u8]| { + if data.len() < 32 { + return; + } + + Python::with_gil(|py| { + + let blspy = py.import("blspy").unwrap(); + let aug = blspy.getattr("AugSchemeMPL").unwrap(); + + // Generate key pair from seed + let rust_sk = SecretKey::from_seed(data); + let py_sk = aug.call_method1("key_gen", PyTuple::new(py, &[PyBytes::new(py, data)])).unwrap(); + + // convert to bytes + let rust_sk_bytes = rust_sk.to_bytes(); + let py_sk_bytes = to_bytes(py_sk); + assert_eq!(py_sk_bytes, rust_sk_bytes); + + // get the public key + let rust_pk = rust_sk.public_key(); + let py_pk = py_sk.call_method0("get_g1").unwrap(); + + // convert to bytes + let rust_pk_bytes = rust_pk.to_bytes(); + let py_pk_bytes = to_bytes(py_pk); + assert_eq!(py_pk_bytes, rust_pk_bytes); + + let idx = u32::from_be_bytes(<[u8; 4]>::try_from(&data[0..4]).unwrap()); + let rust_sk1 = rust_sk.derive_unhardened(idx); + let py_sk1 = aug.call_method1("derive_child_sk_unhardened", + PyTuple::new(py, &[py_sk, idx.to_object(py).as_ref(py)])).unwrap(); + assert_eq!(to_bytes(py_sk1), rust_sk1.to_bytes()); + + let rust_pk1 = rust_pk.derive_unhardened(idx); + let py_pk1 = aug.call_method1("derive_child_pk_unhardened", + PyTuple::new(py, &[py_pk, idx.to_object(py).as_ref(py)])).unwrap(); + assert_eq!(to_bytes(py_pk1), rust_pk1.to_bytes()); + + // sign with the derived keys + let rust_sig1 = sign(&rust_sk1, data); + let py_sig1 = aug.call_method1("sign", + PyTuple::new(py, &[py_sk1, PyBytes::new(py, data)])).unwrap(); + assert_eq!(to_bytes(py_sig1), rust_sig1.to_bytes()); + + // derive hardened + let idx = u32::from_be_bytes(<[u8; 4]>::try_from(&data[4..8]).unwrap()); + let rust_sk2 = rust_sk.derive_hardened(idx); + let py_sk2 = aug.call_method1("derive_child_sk", + PyTuple::new(py, &[py_sk, idx.to_object(py).as_ref(py)])).unwrap(); + assert_eq!(to_bytes(py_sk2), rust_sk2.to_bytes()); + + // sign with the derived keys + let rust_sig2 = sign(&rust_sk2, data); + let py_sig2 = aug.call_method1("sign", + PyTuple::new(py, &[py_sk2, PyBytes::new(py, data)])).unwrap(); + assert_eq!(to_bytes(py_sig2), rust_sig2.to_bytes()); + + // aggregate + let rust_agg = aggregate(&[rust_sig1, rust_sig2]); + let py_agg = aug.call_method1("aggregate", PyTuple::new(py, + &[PyList::new(py, &[py_sig1, py_sig2])])).unwrap(); + assert_eq!(to_bytes(py_agg), rust_agg.to_bytes()); + }); + +}); diff --git a/chia-bls/fuzz/fuzz_targets/derive.rs b/chia-bls/fuzz/fuzz_targets/derive.rs new file mode 100644 index 000000000..8912f021e --- /dev/null +++ b/chia-bls/fuzz/fuzz_targets/derive.rs @@ -0,0 +1,32 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use chia_bls::secret_key::SecretKey; +use chia_bls::public_key::PublicKey; +use chia_bls::signature::{sign, verify}; +use chia_bls::derivable_key::DerivableKey; + +fuzz_target!(|data: &[u8]| { + if data.len() < 32 { + return; + } + + let sk = SecretKey::from_seed(data); + let pk = sk.public_key(); + + // round-trip SecretKey + let bytes = sk.to_bytes(); + assert_eq!(sk, SecretKey::from_bytes(&bytes).unwrap()); + + // round-trip PublicKey + let bytes = pk.to_bytes(); + assert_eq!(pk, PublicKey::from_bytes(&bytes).unwrap()); + + // unhardened derivation + let sk1 = sk.derive_unhardened(1337); + let pk1 = pk.derive_unhardened(1337); + + let sig = sign(&sk1, b"foobar"); + assert!(verify(&sig, &pk1, b"foobar")); + +}); diff --git a/chia-bls/src/derivable_key.rs b/chia-bls/src/derivable_key.rs new file mode 100644 index 000000000..0d4d5645b --- /dev/null +++ b/chia-bls/src/derivable_key.rs @@ -0,0 +1,3 @@ +pub trait DerivableKey { + fn derive_unhardened(&self, idx: u32) -> Self; +} diff --git a/chia-bls/src/derive_keys.rs b/chia-bls/src/derive_keys.rs new file mode 100644 index 000000000..ea789975d --- /dev/null +++ b/chia-bls/src/derive_keys.rs @@ -0,0 +1,44 @@ +use crate::derivable_key::DerivableKey; +use crate::secret_key::SecretKey; + +fn derive_path_unhardened(key: &Key, path: &[u32]) -> Key { + let mut derived = key.derive_unhardened(path[0]); + for idx in &path[1..] { + derived = derived.derive_unhardened(*idx); + } + derived +} + +fn derive_path_hardened(key: &SecretKey, path: &[u32]) -> SecretKey { + let mut derived = key.derive_hardened(path[0]); + for idx in &path[1..] { + derived = derived.derive_hardened(*idx); + } + derived +} + +pub fn master_to_wallet_unhardened_intermediate(key: &Key) -> Key { + derive_path_unhardened(key, &[12381_u32, 8444, 2]) +} + +pub fn master_to_wallet_unhardened(key: &Key, idx: u32) -> Key { + derive_path_unhardened(key, &[12381_u32, 8444, 2, idx]) +} + +pub fn master_to_wallet_hardened_intermediate(key: &SecretKey) -> SecretKey { + derive_path_hardened(key, &[12381_u32, 8444, 2]) +} + +pub fn master_to_wallet_hardened(key: &SecretKey, idx: u32) -> SecretKey { + derive_path_hardened(key, &[12381_u32, 8444, 2, idx]) +} + +pub fn master_to_pool_singleton(key: &SecretKey, pool_wallet_idx: u32) -> SecretKey { + derive_path_hardened(key, &[12381_u32, 8444, 5, pool_wallet_idx]) +} + +pub fn master_to_pool_authentication(key: &SecretKey, pool_wallet_idx: u32, idx: u32) -> SecretKey { + assert!(pool_wallet_idx < 10000); + assert!(idx < 10000); + derive_path_hardened(key, &[12381_u32, 8444, 6, pool_wallet_idx * 10000 + idx]) +} diff --git a/chia-bls/src/lib.rs b/chia-bls/src/lib.rs new file mode 100644 index 000000000..e60d4db5f --- /dev/null +++ b/chia-bls/src/lib.rs @@ -0,0 +1,6 @@ +pub mod derivable_key; +pub mod derive_keys; +pub mod mnemonic; +pub mod public_key; +pub mod secret_key; +pub mod signature; diff --git a/chia-bls/src/mnemonic.rs b/chia-bls/src/mnemonic.rs new file mode 100644 index 000000000..a83ebd96d --- /dev/null +++ b/chia-bls/src/mnemonic.rs @@ -0,0 +1,97 @@ +use anyhow::Error; +use bip39::{Language, Mnemonic, Seed}; +use std::array::TryFromSliceError; +use std::result::Result; + +pub fn entropy_to_mnemonic(entropy: &[u8; 32]) -> String { + Mnemonic::from_entropy(entropy, Language::English) + .unwrap() + .into_phrase() +} + +pub fn mnemonic_to_entropy(mnemonic: &str) -> Result<[u8; 32], Error> { + let m = Mnemonic::from_phrase(mnemonic, Language::English)?; + let ent = m.entropy(); + ent.try_into().map_err(|e: TryFromSliceError| { + Error::from(e).context("incorrect number of words in mnemonic") + }) +} + +pub fn entropy_to_seed(entropy: &[u8; 32]) -> [u8; 64] { + let m = Mnemonic::from_entropy(entropy, Language::English).unwrap(); + Seed::new(&m, "").as_bytes().try_into().unwrap() +} + +#[cfg(test)] +use hex::encode; +#[cfg(test)] +use hex::FromHex; + +#[test] +fn test_parse_mnemonic() { + // test vectors from BIP39 + // https://github.com/trezor/python-mnemonic/blob/master/vectors.json + // The seeds are changed to account for chia using an empty passphrase + // (whereas the trezor test vectors use "TREZOR") + + // phrase, entropy, seed + let test_cases = &[ + ("all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", + "fc795be0c3f18c50dddb34e72179dc597d64055497ecc1e69e2e56a5409651bc139aae8070d4df0ea14d8d2a518a9a00bb1cc6e92e053fe34051f6821df9164c" + ), + ("void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", + "b873212f885ccffbf4692afcb84bc2e55886de2dfa07d90f5c3c239abc31c0a6ce047e30fd8bf6a281e71389aa82d73df74c7bbfb3b06b4639a5cee775cccd3c" + ), + ("panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", + "3e066d7dee2dbf8fcd3fe240a3975658ca118a8f6f4ca81cf99104944604b05a5090a79d99e545704b914ca0397fedb82fd00fd6a72098703709c891a065ee49") + ]; + + for (phrase, entropy, seed) in test_cases { + println!("{}", phrase); + assert_eq!( + encode(mnemonic_to_entropy(phrase).unwrap()), + entropy.to_string() + ); + assert_eq!( + entropy_to_mnemonic(&<[u8; 32]>::from_hex(entropy).unwrap()).as_str(), + *phrase + ); + assert_eq!( + encode(entropy_to_seed(&<[u8; 32]>::from_hex(entropy).unwrap())), + seed.to_string() + ) + } +} + +#[test] +fn test_invalid_mnemonic() { + assert_eq!( + format!( + "{}", + mnemonic_to_entropy("camp survey warrior").unwrap_err() + ), + "invalid number of words in phrase: 3" + ); + assert_eq!( + format!( + "{}", + mnemonic_to_entropy( + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich" + ) + .unwrap_err() + ), + "invalid checksum" + ); + assert_eq!(format!("{}", mnemonic_to_entropy("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap_err()), "incorrect number of words in mnemonic"); + assert_eq!(mnemonic_to_entropy("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art").unwrap(), <[u8; 32]>::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap()); + + assert_eq!(mnemonic_to_entropy("letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless").unwrap(), + <[u8; 32]>::from_hex("8080808080808080808080808080808080808080808080808080808080808080").unwrap()); + + // make sure all whitespace is ignored + assert_eq!(mnemonic_to_entropy("letter advice \t cage\t absurd \tamount doctor acoustic \n avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless").unwrap(), + <[u8; 32]>::from_hex("8080808080808080808080808080808080808080808080808080808080808080").unwrap()); +} diff --git a/chia-bls/src/public_key.rs b/chia-bls/src/public_key.rs new file mode 100644 index 000000000..8029a81f1 --- /dev/null +++ b/chia-bls/src/public_key.rs @@ -0,0 +1,112 @@ +use crate::derivable_key::DerivableKey; +use bls12_381_plus::{G1Affine, G1Projective, Scalar}; +use group::Curve; +use num_bigint::BigUint; +use sha2::{Digest, Sha256}; + +#[derive(PartialEq, Eq, Debug)] +pub struct PublicKey(pub G1Projective); + +impl PublicKey { + pub fn from_bytes(bytes: &[u8; 48]) -> Option { + G1Affine::from_compressed(bytes) + .map(|p| Self(G1Projective::from(&p))) + .into() + } + + pub fn to_bytes(&self) -> [u8; 48] { + self.0.to_affine().to_compressed() + } + + pub fn is_valid(&self) -> bool { + self.0.is_identity().unwrap_u8() == 0 && self.0.is_on_curve().unwrap_u8() == 1 + } +} + +impl DerivableKey for PublicKey { + fn derive_unhardened(&self, idx: u32) -> Self { + let mut hasher = Sha256::new(); + hasher.update(self.to_bytes()); + hasher.update(idx.to_be_bytes()); + let digest = hasher.finalize(); + + // in an ideal world, we would not need to reach for the sledge-hammer of + // num-bigint here. This would most likely be faster if implemented in + // Scalar directly. + + // interpret the hash as an unsigned big-endian number + let mut nounce = BigUint::from_bytes_be(digest.as_slice()); + + let q = BigUint::from_bytes_be(&[ + 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, + 0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, + ]); + + // mod by G1 Order + nounce %= q; + + let raw_bytes = nounce.to_bytes_be(); + let mut bytes = [0_u8; 32]; + bytes[32 - raw_bytes.len()..].copy_from_slice(&raw_bytes); + bytes.reverse(); + + let nounce = Scalar::from_bytes(&bytes).unwrap(); + + PublicKey(self.0 + G1Projective::generator() * nounce) + } +} + +#[cfg(test)] +use hex::FromHex; + +#[cfg(test)] +use crate::secret_key::SecretKey; + +#[test] +fn test_derive_unhardened() { + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + let pk = sk.public_key(); + + // make sure deriving the secret keys produce the same public keys as + // deriving the public key + for idx in 0..4_usize { + let derived_sk = sk.derive_unhardened(idx as u32); + let derived_pk = pk.derive_unhardened(idx as u32); + assert_eq!(derived_pk.to_bytes(), derived_sk.public_key().to_bytes()); + } +} + +#[cfg(test)] +use rand::{Rng, SeedableRng}; + +#[cfg(test)] +use rand::rngs::StdRng; + +#[test] +fn test_from_bytes() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 48]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + // just any random bytes are not a valid key and should fail + assert_eq!(PublicKey::from_bytes(&data), None); + } +} + +#[test] +fn test_roundtrip() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + let bytes = pk.to_bytes(); + let pk2 = PublicKey::from_bytes(&bytes).unwrap(); + assert_eq!(pk, pk2); + } +} + +// ERROR test is_valid diff --git a/chia-bls/src/secret_key.rs b/chia-bls/src/secret_key.rs new file mode 100644 index 000000000..0674036d5 --- /dev/null +++ b/chia-bls/src/secret_key.rs @@ -0,0 +1,305 @@ +use crate::derivable_key::DerivableKey; +use crate::public_key::PublicKey; +use bls12_381_plus::{G1Projective, Scalar}; +use hkdf::HkdfExtract; +use num_bigint::BigUint; +use sha2::{Digest, Sha256}; + +#[derive(PartialEq, Eq, Debug)] +pub struct SecretKey(pub(crate) Scalar); + +fn flip_bits(input: [u8; 32]) -> [u8; 32] { + let mut ret = [0; 32]; + for i in 0..32 { + ret[i] = input[i] ^ 0xff; + } + ret +} + +fn ikm_to_lamport_sk(ikm: &[u8; 32], salt: &[u8; 4]) -> [u8; 255 * 32] { + let mut extracter = HkdfExtract::::new(Some(salt)); + extracter.input_ikm(ikm); + let (_, h) = extracter.finalize(); + + let mut output = [0_u8; 255 * 32]; + h.expand(&[], &mut output).unwrap(); + output +} + +fn sha256(bytes: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hasher.finalize().try_into().unwrap() +} + +impl SecretKey { + pub fn from_seed(seed: &[u8]) -> SecretKey { + // described here: + // https://eips.ethereum.org/EIPS/eip-2333#derive_master_sk + assert!(seed.len() >= 32); + + const SALT: &[u8] = b"BLS-SIG-KEYGEN-SALT-"; + let mut extracter = HkdfExtract::::new(Some(SALT)); + extracter.input_ikm(seed); + extracter.input_ikm(&[0_u8]); + let h = extracter.finalize().1; + + // TODO: = uninitialized + let mut sk = [0_u8; 48]; + h.expand(&[0_u8, 48], &mut sk) + .expect("failed to generate secret key"); + SecretKey(Scalar::from_okm(&sk)) + } + + pub fn from_bytes(b: &[u8; 32]) -> Option { + let t = [ + b[31], b[30], b[29], b[28], b[27], b[26], b[25], b[24], b[23], b[22], b[21], b[20], + b[19], b[18], b[17], b[16], b[15], b[14], b[13], b[12], b[11], b[10], b[9], b[8], b[7], + b[6], b[5], b[4], b[3], b[2], b[1], b[0], + ]; + Scalar::from_bytes(&t).map(SecretKey).into() + } + + pub fn to_bytes(&self) -> [u8; 32] { + let mut bytes = self.0.to_bytes(); + bytes.reverse(); + bytes + } + + pub fn public_key(&self) -> PublicKey { + PublicKey(G1Projective::generator() * self.0) + } + + fn to_lamport_pk(&self, idx: u32) -> [u8; 32] { + let ikm = self.to_bytes(); + let not_ikm = flip_bits(ikm); + let salt = idx.to_be_bytes(); + + let mut lamport0 = ikm_to_lamport_sk(&ikm, &salt); + let mut lamport1 = ikm_to_lamport_sk(¬_ikm, &salt); + + for i in (0..32 * 255).step_by(32) { + let hash = sha256(&lamport0[i..i + 32]); + lamport0[i..i + 32].copy_from_slice(&hash); + } + for i in (0..32 * 255).step_by(32) { + let hash = sha256(&lamport1[i..i + 32]); + lamport1[i..i + 32].copy_from_slice(&hash); + } + + let mut hasher = Sha256::new(); + hasher.update(lamport0); + hasher.update(lamport1); + hasher.finalize().try_into().unwrap() + } + + pub fn derive_hardened(&self, idx: u32) -> SecretKey { + // described here: + // https://eips.ethereum.org/EIPS/eip-2333#derive_child_sk + SecretKey::from_seed(&self.to_lamport_pk(idx)) + } +} + +impl DerivableKey for SecretKey { + fn derive_unhardened(&self, idx: u32) -> Self { + let pk = self.public_key(); + + let mut hasher = Sha256::new(); + hasher.update(pk.to_bytes()); + hasher.update(idx.to_be_bytes()); + let digest = hasher.finalize(); + + // in an ideal world, we would not need to reach for the sledge-hammer of + // num-bigint here. This would most likely be faster if implemented in + // Scalar directly. + + // interpret the hash as an unsigned big-endian number + let mut scalar = BigUint::from_bytes_be(digest.as_slice()); + + let q = BigUint::from_bytes_be(&[ + 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1, + 0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x01, + ]); + + // mod by G1 Order + scalar %= q; + + // Now, convert BigUint -> Scalar, that we can use to create the new secret key with + let mut raw_limbs = [0_u64; 4]; + for (it, limb) in raw_limbs.iter_mut().zip(scalar.to_u64_digits()) { + *it = limb; + } + + let mut new_sk = Scalar::from_raw(raw_limbs); + + // aggregate the new secret with the existing secret + // The Scalar type uses modulus arithmetic in the Q order, so the modulus Q + // is implied in these addition operations + new_sk += self.0; + SecretKey(new_sk) + } +} + +#[cfg(test)] +use hex::FromHex; + +#[test] +fn test_make_key() { + // test vectors from: + // from chia.util.keychain import KeyDataSecrets + // print(KeyDataSecrets.from_mnemonic(phrase)["privatekey"]) + + // (seed, secret-key) + let test_cases = &[ + ("fc795be0c3f18c50dddb34e72179dc597d64055497ecc1e69e2e56a5409651bc139aae8070d4df0ea14d8d2a518a9a00bb1cc6e92e053fe34051f6821df9164c", + "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"), + ("b873212f885ccffbf4692afcb84bc2e55886de2dfa07d90f5c3c239abc31c0a6ce047e30fd8bf6a281e71389aa82d73df74c7bbfb3b06b4639a5cee775cccd3c", + "35d65c35d926f62ba2dd128754ddb556edb4e2c926237ab9e02a23e7b3533613"), + ("3e066d7dee2dbf8fcd3fe240a3975658ca118a8f6f4ca81cf99104944604b05a5090a79d99e545704b914ca0397fedb82fd00fd6a72098703709c891a065ee49", + "59095c391107936599b7ee6f09067979b321932bd62e23c7f53ed5fb19f851f6") + ]; + + for (seed, sk) in test_cases { + assert_eq!( + SecretKey::from_seed(&<[u8; 64]>::from_hex(seed).unwrap()) + .to_bytes() + .to_vec(), + Vec::::from_hex(sk).unwrap() + ); + } +} + +#[test] +fn test_derive_unhardened() { + // test vectors from: + // from blspy import AugSchemeMPL + // from blspy import PrivateKey + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 0) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 1) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 2) + // AugSchemeMPL.derive_child_sk_unhardened(sk, 3) + // + // + // + // + + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let derived_hex = [ + "399638f99d446500f3c3a363f24c2b0634ad7caf646f503455093f35f29290bd", + "3dcb4098ad925d8940e2f516d2d5a4dbab393db928a8c6cb06b93066a09a843a", + "13115c8fb68a3d667938dac2ffc6b867a4a0f216bbb228aa43d6bdde14245575", + "52e7e9f2fb51f2c5705aea8e11ac82737b95e664ae578f015af22031d956f92b", + ]; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + + for idx in 0..4_usize { + let derived = sk.derive_unhardened(idx as u32); + assert_eq!( + derived.to_bytes(), + <[u8; 32]>::from_hex(derived_hex[idx]).unwrap() + ) + } +} + +#[test] +fn test_public_key() { + // test vectors from: + // from blspy import PrivateKey + // from blspy import AugSchemeMPL + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // for i in [100, 52312, 352350, 316]: + // sk0 = AugSchemeMPL.derive_child_sk_unhardened(sk, i) + // print(bytes(sk0).hex()) + // print(bytes(sk0.get_g1()).hex()) + + // secret key, public key + let test_cases = [ + ("5aac8405befe4cb3748a67177c56df26355f1f98d979afdb0b2f97858d2f71c3", + "b9de000821a610ef644d160c810e35113742ff498002c2deccd8f1a349e423047e9b3fc17ebfc733dbee8fd902ba2961"), + ("23f1fb291d3bd7434282578b842d5ea4785994bb89bd2c94896d1b4be6c70ba2", + "96f304a5885e67abdeab5e1ed0576780a1368777ea7760124834529e8694a1837a20ffea107b9769c4f92a1f6c167e69"), + ("2bc1d6d6efe58d365c29ccb7ad12c8457c0eec70a29003073692ac4cb1cd7ba2", + "b10568446def64b17fc9b6d614ae036deaac3f2d654e12e45ea04b19208246e0d760e8826426e97f9f0666b7ce340d75"), + ("2bfc8672d859700e30aa6c8edc24a8ce9e6dc53bb1ef936f82de722847d05b9e", + "9641472acbd6af7e5313d2500791b87117612af43eef929cf7975aaaa5a203a32698a8ef53763a84d90ad3f00b86ad66"), + ("3311f883dad1e39c52bf82d5870d05371c0b1200576287b5160808f55568151b", + "928ea102b5a3e3efe4f4c240d3458a568dfeb505e02901a85ed70a384944b0c08c703a35245322709921b8f2b7f5e54a"), + ]; + + for (sk_hex, pk_hex) in test_cases { + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + let pk = sk.public_key(); + assert_eq!( + pk, + PublicKey::from_bytes(&<[u8; 48]>::from_hex(pk_hex).unwrap()).unwrap() + ); + } +} + +#[test] +fn test_derive_hardened() { + // test vectors from: + // from blspy import AugSchemeMPL + // from blspy import PrivateKey + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // AugSchemeMPL.derive_child_sk(sk, 0) + // AugSchemeMPL.derive_child_sk(sk, 1) + // AugSchemeMPL.derive_child_sk(sk, 2) + // AugSchemeMPL.derive_child_sk(sk, 3) + // + // + // + // + + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let derived_hex = [ + "05eccb2d70e814f51a30d8b9965505605c677afa97228fa2419db583a8121db9", + "612ae96bdce2e9bc01693ac579918fbb559e04ec365cce9b66bb80e328f62c46", + "5df14a0a34fd6c30a80136d4103f0a93422ce82d5c537bebbecbc56e19fee5b9", + "3ea55db88d9a6bf5f1d9c9de072e3c9a56b13f4156d72fca7880cd39b4bd4fdc", + ]; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + + for idx in 0..derived_hex.len() { + let derived = sk.derive_hardened(idx as u32); + assert_eq!( + derived.to_bytes(), + <[u8; 32]>::from_hex(derived_hex[idx]).unwrap() + ) + } +} + +#[cfg(test)] +use rand::{Rng, SeedableRng}; + +#[cfg(test)] +use rand::rngs::StdRng; + +#[test] +fn test_from_bytes() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + // make the bytes exceed q + data[0] |= 0x80; + // just any random bytes are not a valid key and should fail + assert_eq!(SecretKey::from_bytes(&data), None); + } +} + +#[test] +fn test_roundtrip() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let bytes = sk.to_bytes(); + let sk2 = SecretKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk, sk2); + assert_eq!(sk.public_key(), sk2.public_key()); + } +} diff --git a/chia-bls/src/signature.rs b/chia-bls/src/signature.rs new file mode 100644 index 000000000..380bb793c --- /dev/null +++ b/chia-bls/src/signature.rs @@ -0,0 +1,505 @@ +use crate::public_key::PublicKey; +use crate::secret_key::SecretKey; +use bls12_381_plus::{ + multi_miller_loop, ExpandMsgXmd, G1Affine, G2Affine, G2Prepared, G2Projective, +}; +use group::{Curve, Group}; +use std::borrow::Borrow; +use std::convert::AsRef; +use std::ops::Neg; + +#[derive(PartialEq, Eq, Debug)] +pub struct Signature(pub(crate) G2Projective); + +impl Signature { + pub fn from_bytes(buf: &[u8; 96]) -> Option { + G2Affine::from_compressed(buf) + .map(|p| Self(G2Projective::from(&p))) + .into() + } + + pub fn to_bytes(&self) -> [u8; 96] { + self.0.to_affine().to_compressed() + } + + pub fn aggregate(&mut self, sig: &Signature) { + self.0 += sig.0; + } + + pub fn is_valid(&self) -> bool { + self.0.is_on_curve().unwrap_u8() == 1 + } +} + +impl Default for Signature { + fn default() -> Self { + Signature(G2Projective::identity()) + } +} + +fn hash_msg>(pk: &PublicKey, msg: Msg) -> G2Projective { + let mut prepended_msg = pk.to_bytes().to_vec(); + prepended_msg.extend_from_slice(msg.as_ref()); + // domain separation tag + const CIPHER_SUITE: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_"; + G2Projective::hash::>(&prepended_msg, CIPHER_SUITE) +} + +pub fn aggregate, I>(sigs: I) -> Signature +where + I: IntoIterator, +{ + let mut ret = Signature::default(); + + for s in sigs.into_iter() { + ret.aggregate(s.borrow()); + } + ret +} + +pub fn verify>(sig: &Signature, key: &PublicKey, msg: Msg) -> bool { + if !key.is_valid() || !sig.is_valid() { + return false; + } + let a = hash_msg(key, msg); + let g1 = G1Affine::generator().neg(); + + multi_miller_loop(&[ + (&key.0.to_affine(), &G2Prepared::from(a.to_affine())), + (&g1, &G2Prepared::from(sig.0.to_affine())), + ]) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn aggregate_verify, Msg: Borrow<[u8]>, I>( + sig: &Signature, + data: I, +) -> bool +where + I: IntoIterator, +{ + if !sig.is_valid() { + return false; + } + let mut store = Vec::<(G1Affine, G2Prepared)>::new(); + + for (key, msg) in data.into_iter() { + let key = key.borrow(); + if !key.is_valid() { + return false; + } + store.push(( + key.0.to_affine(), + G2Prepared::from(hash_msg(key, msg.borrow()).to_affine()), + )); + } + + if store.is_empty() { + // if we have exactly zero messages to verify, the only correct + // signature is the identity + // This is an optimization for the edge case of having 0 messages + return sig == &Signature::default(); + } + + store.push(( + G1Affine::generator().neg(), + G2Prepared::from(sig.0.to_affine()), + )); + + let mut terms = Vec::<(&G1Affine, &G2Prepared)>::new(); + for (g1, g2) in &store { + terms.push((g1, g2)); + } + + // multi_miller_loop takes a slice of *references*, which means we need to build + // both a vector owning the elements (G1Affine and G2Prepared) in addition to a + // vector holding references into it. + multi_miller_loop(terms.as_slice()) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn sign>(sk: &SecretKey, msg: Msg) -> Signature { + let g2 = hash_msg(&sk.public_key(), msg); + Signature(g2 * sk.0) +} + +#[cfg(test)] +use rand::{Rng, SeedableRng}; + +#[cfg(test)] +use rand::rngs::StdRng; + +#[test] +fn test_from_bytes() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 96]; + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + // just any random bytes are not a valid signature and should fail + assert_eq!(Signature::from_bytes(&data), None); + } +} + +#[test] +fn test_roundtrip() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + let mut msg = [0u8; 32]; + rng.fill(msg.as_mut_slice()); + for _i in 0..50 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let sig = sign(&sk, msg); + let bytes = sig.to_bytes(); + let sig2 = Signature::from_bytes(&bytes).unwrap(); + assert_eq!(sig, sig2); + } +} + +#[test] +fn test_random_verify() { + let mut rng = StdRng::seed_from_u64(1337); + let mut data = [0u8; 32]; + let mut msg = [0u8; 32]; + rng.fill(msg.as_mut_slice()); + for _i in 0..20 { + rng.fill(data.as_mut_slice()); + let sk = SecretKey::from_seed(&data); + let pk = sk.public_key(); + let sig = sign(&sk, &msg); + assert!(verify(&sig, &pk, msg)); + + let bytes = sig.to_bytes(); + let sig2 = Signature::from_bytes(&bytes).unwrap(); + assert!(verify(&sig2, &pk, msg)); + } +} + +#[cfg(test)] +use hex::FromHex; + +#[test] +fn test_verify() { + // test case from: + // from blspy import PrivateKey + // from blspy import AugSchemeMPL + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // data = b"foobar" + // print(AugSchemeMPL.sign(sk, data)) + let msg = b"foobar"; + let sk = SecretKey::from_bytes( + &<[u8; 32]>::from_hex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb") + .unwrap(), + ) + .unwrap(); + + let sig = sign(&sk, &msg); + assert!(verify(&sig, &sk.public_key(), msg)); + + assert_eq!(sig.to_bytes(), <[u8; 96]>::from_hex("b45825c0ee7759945c0189b4c38b7e54231ebadc83a851bec3bb7cf954a124ae0cc8e8e5146558332ea152f63bf8846e04826185ef60e817f271f8d500126561319203f9acb95809ed20c193757233454be1562a5870570941a84605bd2c9c9a").unwrap()); +} + +#[test] +fn test_aggregate_signature() { + // from blspy import PrivateKey + // from blspy import AugSchemeMPL + // sk = PrivateKey.from_bytes(bytes.fromhex("52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb")) + // data = b"foobar" + // sk0 = AugSchemeMPL.derive_child_sk(sk, 0) + // sk1 = AugSchemeMPL.derive_child_sk(sk, 1) + // sk2 = AugSchemeMPL.derive_child_sk(sk, 2) + // sk3 = AugSchemeMPL.derive_child_sk(sk, 3) + + // sig0 = AugSchemeMPL.sign(sk0, data) + // sig1 = AugSchemeMPL.sign(sk1, data) + // sig2 = AugSchemeMPL.sign(sk2, data) + // sig3 = AugSchemeMPL.sign(sk3, data) + + // agg = AugSchemeMPL.aggregate([sig0, sig1, sig2, sig3]) + + // 87bce2c588f4257e2792d929834548c7d3af679272cb4f8e1d24cf4bf584dd287aa1d9f5e53a86f288190db45e1d100d0a5e936079a66a709b5f35394cf7d52f49dd963284cb5241055d54f8cf48f61bc1037d21cae6c025a7ea5e9f4d289a18 + + let sk_hex = "52d75c4707e39595b27314547f9723e5530c01198af3fc5849d9a7af65631efb"; + let sk = SecretKey::from_bytes(&<[u8; 32]>::from_hex(sk_hex).unwrap()).unwrap(); + let msg = b"foobar"; + let mut agg = Signature::default(); + let mut data = Vec::<(PublicKey, &[u8])>::new(); + for idx in 0..4 { + let derived = sk.derive_hardened(idx as u32); + data.push((derived.public_key(), msg)); + agg.aggregate(&sign(&derived, msg)); + } + assert_eq!(agg.to_bytes(), <[u8; 96]>::from_hex("87bce2c588f4257e2792d929834548c7d3af679272cb4f8e1d24cf4bf584dd287aa1d9f5e53a86f288190db45e1d100d0a5e936079a66a709b5f35394cf7d52f49dd963284cb5241055d54f8cf48f61bc1037d21cae6c025a7ea5e9f4d289a18").unwrap()); + + // ensure the aggregate signature verifies OK + assert!(aggregate_verify(&agg, data)); +} + +#[cfg(test)] +fn random_sk(rng: &mut R) -> SecretKey { + let mut data = [0u8; 64]; + rng.fill(data.as_mut_slice()); + SecretKey::from_seed(&data) +} + +#[test] +fn test_aggregate_signature_separate_msg() { + let mut rng = StdRng::seed_from_u64(1337); + let sk = [random_sk(&mut rng), random_sk(&mut rng)]; + let pk = [sk[0].public_key(), sk[1].public_key()]; + let msg: [&'static [u8]; 2] = [b"foo", b"foobar"]; + let sig = [sign(&sk[0], msg[0]), sign(&sk[1], msg[1])]; + let mut agg = Signature::default(); + agg.aggregate(&sig[0]); + agg.aggregate(&sig[1]); + + assert!(aggregate_verify(&agg, pk.iter().zip(msg))); + // order does not matter + assert!(aggregate_verify(&agg, pk.iter().zip(msg).rev())); +} + +#[test] +fn test_aggregate_signature_identity() { + // when verifying 0 messages, an identity signature is considered valid + let empty = Vec::<(PublicKey, &[u8])>::new(); + assert!(aggregate_verify(&Signature::default(), empty)); +} + +#[test] +fn test_invalid_aggregate_signature() { + let mut rng = StdRng::seed_from_u64(1337); + let sk = [random_sk(&mut rng), random_sk(&mut rng)]; + let pk = [sk[0].public_key(), sk[1].public_key()]; + let msg: [&'static [u8]; 2] = [b"foo", b"foobar"]; + let sig = [sign(&sk[0], msg[0]), sign(&sk[1], msg[1])]; + let mut agg = Signature::default(); + agg.aggregate(&sig[0]); + agg.aggregate(&sig[1]); + + assert!(aggregate_verify(&agg, [(&pk[0], msg[0])]) == false); + assert!(aggregate_verify(&agg, [(&pk[1], msg[1])]) == false); + // public keys mixed with the wrong message + assert!(aggregate_verify(&agg, [(&pk[0], msg[1]), (&pk[1], msg[0])]) == false); + assert!(aggregate_verify(&agg, [(&pk[1], msg[0]), (&pk[0], msg[1])]) == false); +} + +#[test] +fn test_vector_2_aggregate_of_aggregates() { + // test case from: bls-signatures/src/test.cpp + // "Chia test vector 2 (Augmented, aggregate of aggregates)" + let message1 = [1_u8, 2, 3, 40]; + let message2 = [5_u8, 6, 70, 201]; + let message3 = [9_u8, 10, 11, 12, 13]; + let message4 = [15_u8, 63, 244, 92, 0, 1]; + + let sk1 = SecretKey::from_seed(&[2_u8; 32]); + let sk2 = SecretKey::from_seed(&[3_u8; 32]); + + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + + let sig1 = sign(&sk1, &message1); + let sig2 = sign(&sk2, &message2); + let sig3 = sign(&sk2, &message1); + let sig4 = sign(&sk1, &message3); + let sig5 = sign(&sk1, &message1); + let sig6 = sign(&sk1, &message4); + + let agg_sig_l = aggregate(&[sig1, sig2]); + let agg_sig_r = aggregate(&[sig3, sig4, sig5]); + let aggsig = aggregate(&[agg_sig_l, agg_sig_r, sig6]); + + assert!(aggregate_verify( + &aggsig, + [ + (&pk1, &message1 as &[u8]), + (&pk2, &message2), + (&pk2, &message1), + (&pk1, &message3), + (&pk1, &message1), + (&pk1, &message4) + ] + )); + + assert_eq!( + aggsig.to_bytes(), + <[u8; 96]>::from_hex( + "a1d5360dcb418d33b29b90b912b4accde535cf0e52caf467a005dc632d9f7af44b6c4e9acd4\ + 6eac218b28cdb07a3e3bc087df1cd1e3213aa4e11322a3ff3847bbba0b2fd19ddc25ca964871\ + 997b9bceeab37a4c2565876da19382ea32a962200" + ) + .unwrap() + ); +} + +#[test] +fn test_signature_zero_key() { + // test case from: bls-signatures/src/test.cpp + // "Should sign with the zero key" + let sk = SecretKey::from_bytes(&[0; 32]).unwrap(); + assert_eq!(sign(&sk, &[1_u8, 2, 3]), Signature::default()); +} + +#[test] +fn test_aggregate_many_g2_elements_diff_message() { + // test case from: bls-signatures/src/test.cpp + // "Should Aug aggregate many G2Elements, diff message" + + let mut rng = StdRng::seed_from_u64(1337); + + let mut pairs = Vec::<(PublicKey, Vec)>::new(); + let mut sigs = Vec::::new(); + + for i in 0..80 { + let message = vec![0_u8, 100, 2, 45, 64, 12, 12, 63, i]; + let sk = random_sk(&mut rng); + let sig = sign(&sk, &message); + pairs.push((sk.public_key(), message)); + sigs.push(sig); + } + + let aggsig = aggregate(sigs); + + assert!(aggregate_verify(&aggsig, pairs)); +} + +#[test] +fn test_aggregate_identity() { + // test case from: bls-signatures/src/test.cpp + // "Aggregate Verification of zero items with infinity should pass" + let sig = Signature::default(); + let aggsig = aggregate([&sig]); + assert_eq!(aggsig, sig); + assert_eq!(aggsig, Signature::default()); + + assert!(aggregate_verify(&aggsig, [] as [(&PublicKey, &[u8]); 0])); +} + +#[test] +fn test_aggregate_multiple_levels_degenerate() { + // test case from: bls-signatures/src/test.cpp + // "Should aggregate with multiple levels, degenerate" + + let mut rng = StdRng::seed_from_u64(1337); + + let message1 = [100_u8, 2, 254, 88, 90, 45, 23]; + let sk1 = random_sk(&mut rng); + let pk1 = sk1.public_key(); + let mut agg_sig = sign(&sk1, &message1); + let mut pairs: Vec<(PublicKey, &[u8])> = vec![(pk1, &message1)]; + + for _i in 0..10 { + let sk = random_sk(&mut rng); + let pk = sk.public_key(); + pairs.push((pk, &message1)); + let sig = sign(&sk, &message1); + agg_sig.aggregate(&sig); + } + assert!(aggregate_verify(&agg_sig, pairs)); +} + +#[test] +fn test_aggregate_multiple_levels_different_messages() { + // test case from: bls-signatures/src/test.cpp + // "Should aggregate with multiple levels, different messages" + + let mut rng = StdRng::seed_from_u64(1337); + + let message1 = [100_u8, 2, 254, 88, 90, 45, 23]; + let message2 = [192_u8, 29, 2, 0, 0, 45, 23]; + let message3 = [52_u8, 29, 2, 0, 0, 45, 102]; + let message4 = [99_u8, 29, 2, 0, 0, 45, 222]; + + let sk1 = random_sk(&mut rng); + let sk2 = random_sk(&mut rng); + + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + + let sig1 = sign(&sk1, &message1); + let sig2 = sign(&sk2, &message2); + let sig3 = sign(&sk2, &message3); + let sig4 = sign(&sk1, &message4); + + let agg_sig_l = aggregate([sig1, sig2]); + let agg_sig_r = aggregate([sig3, sig4]); + let agg_sig = aggregate([agg_sig_l, agg_sig_r]); + + let all_pairs: [(&PublicKey, &[u8]); 4] = [ + (&pk1, &message1), + (&pk2, &message2), + (&pk2, &message3), + (&pk1, &message4), + ]; + assert!(aggregate_verify(&agg_sig, all_pairs)); +} + +#[test] +fn test_aug_scheme() { + // test case from: bls-signatures/src/test.cpp + // "Aug Scheme" + + let msg1 = [7_u8, 8, 9]; + let msg2 = [10_u8, 11, 12]; + + let sk1 = SecretKey::from_seed(&[4_u8; 32]); + let pk1 = sk1.public_key(); + let pk1v = pk1.to_bytes(); + let sig1 = sign(&sk1, &msg1); + let sig1v = sig1.to_bytes(); + + assert!(verify(&sig1, &pk1, &msg1)); + assert!(verify( + &Signature::from_bytes(&sig1v).unwrap(), + &PublicKey::from_bytes(&pk1v).unwrap(), + &msg1 + )); + + let sk2 = SecretKey::from_seed(&[5_u8; 32]); + let pk2 = sk2.public_key(); + let pk2v = pk2.to_bytes(); + let sig2 = sign(&sk2, &msg2); + let sig2v = sig2.to_bytes(); + + assert!(verify(&sig2, &pk2, &msg2)); + assert!(verify( + &Signature::from_bytes(&sig2v).unwrap(), + &PublicKey::from_bytes(&pk2v).unwrap(), + &msg2 + )); + + // Wrong G2Element + assert!(!verify(&sig2, &pk1, &msg1)); + assert!(!verify( + &Signature::from_bytes(&sig2v).unwrap(), + &PublicKey::from_bytes(&pk1v).unwrap(), + &msg1 + )); + // Wrong msg + assert!(!verify(&sig1, &pk1, &msg2)); + assert!(!verify( + &Signature::from_bytes(&sig1v).unwrap(), + &PublicKey::from_bytes(&pk1v).unwrap(), + &msg2 + )); + // Wrong pk + assert!(!verify(&sig1, &pk2, &msg1)); + assert!(!verify( + &Signature::from_bytes(&sig1v).unwrap(), + &PublicKey::from_bytes(&pk2v).unwrap(), + &msg1 + )); + + let aggsig = aggregate([sig1, sig2]); + let aggsigv = aggsig.to_bytes(); + let pairs: [(&PublicKey, &[u8]); 2] = [(&pk1, &msg1), (&pk2, &msg2)]; + assert!(aggregate_verify(&aggsig, pairs)); + assert!(aggregate_verify( + &Signature::from_bytes(&aggsigv).unwrap(), + pairs + )); +}