diff --git a/.gitignore b/.gitignore index a230892..2df413c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__ src/__pycache__ src/merlin/extension -target/* \ No newline at end of file +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..12b991a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,488 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bitcoin" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +dependencies = [ + "base58ck", + "base64", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cashu-kvac" +version = "0.1.0" +dependencies = [ + "bitcoin", + "hex", + "merlin", + "once_cell", + "rug", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0205cd82059bc63b63cf516d714352a30c44f2c74da9961dfda2617ae6b5918" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rug" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ae2c1089ec0575193eb9222881310cc1ed8bce3646ef8b81b44b518595b79d" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643caef17e3128658ff44d85923ef2d28af81bb71e0d67bbfe1d76f19a73e053" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995d0bbc9995d1f19d28b7215a9352b0fc3cd3a2d2ec95c2cadc485cdedbcdde" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7e8801a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cashu-kvac" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitcoin = { version= "0.32.2", features = ["base64", "serde", "rand", "rand-std"] } +hex = "0.4.3" +merlin = "3.0.0" +once_cell = "1.20.2" +rug = "1.26.1" +thiserror = "2.0.5" diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..da25676 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,17 @@ +use bitcoin::secp256k1; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("No script in this was found")] + NoScriptProvided, + #[error("Cannot create proofs of empty")] + EmptyList, + #[error("Cannot instantiate SchnorrProver")] + InvalidConfiguration, + #[error("Cannot map hash to a valid point on the curve")] + InvalidPoint, + /// Secp256k1 error + #[error(transparent)] + Secp256k1(#[from] secp256k1::Error), +} diff --git a/src/generators.py b/src/generators.py index 22acb77..13f4206 100644 --- a/src/generators.py +++ b/src/generators.py @@ -17,7 +17,7 @@ def hash_to_curve(message: bytes) -> GroupElement: raise ValueError("No valid point found") # Generators drawn with NUMS -W, W_, X0, X1, Gz_mac, Gz_attribute, Gz_script, G_amount, G_script, G_blind, G_serial = ( +W, W_, X0, X1, Gz_mac, Gz_attribute, Gz_script, G_amount, G_script, G_blind = ( hash_to_curve(b"W"), hash_to_curve(b"W_"), hash_to_curve(b"X0"), @@ -28,8 +28,19 @@ def hash_to_curve(message: bytes) -> GroupElement: hash_to_curve(b"G_amount"), hash_to_curve(b"G_script"), hash_to_curve(b"G_blind"), - hash_to_curve(b"G_serial"), ) # Point at infinity O = GroupElement(ELEMENT_ZERO) + +if __name__ == '__main__': + print(f"{W.serialize(True).hex() = }\n") + print(f"{W_.serialize(True).hex() = }\n") + print(f"{X0.serialize(True).hex() = }\n") + print(f"{X1.serialize(True).hex() = }\n") + print(f"{Gz_mac.serialize(True).hex() = }\n") + print(f"{Gz_attribute.serialize(True).hex() = }\n") + print(f"{Gz_script.serialize(True).hex() = }\n") + print(f"{G_amount.serialize(True).hex() = }\n") + print(f"{G_script.serialize(True).hex() = }\n") + print(f"{G_blind.serialize(True).hex() = }\n") diff --git a/src/generators.rs b/src/generators.rs new file mode 100644 index 0000000..f14f208 --- /dev/null +++ b/src/generators.rs @@ -0,0 +1,109 @@ +use crate::errors::Error; +use crate::secp::{GroupElement, GROUP_ELEMENT_ZERO}; +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +use once_cell::sync::Lazy; + +const DOMAIN_SEPARATOR: &[u8; 28] = b"Secp256k1_HashToCurve_Cashu_"; + +pub fn hash_to_curve(message: &[u8]) -> Result { + let msg_to_hash: Vec = [DOMAIN_SEPARATOR, message].concat(); + + let msg_hash: [u8; 32] = Sha256Hash::hash(&msg_to_hash).to_byte_array(); + + let mut counter: u32 = 0; + while counter < 2_u32.pow(16) { + let mut bytes_to_hash: Vec = Vec::with_capacity(36); + bytes_to_hash.extend_from_slice(&msg_hash); + bytes_to_hash.extend_from_slice(&counter.to_le_bytes()); + let mut hash: [u8; 33] = [0; 33]; + hash[0] = 0x02; + hash[1..33].copy_from_slice(&Sha256Hash::hash(&bytes_to_hash).to_byte_array()[0..32]); + + // Try to parse public key + match PublicKey::from_slice(&hash) { + Ok(_) => return Ok(GroupElement::new(&hash)), + Err(_) => { + counter += 1; + } + } + } + + Err(Error::InvalidPoint) +} + +#[allow(non_snake_case)] +pub struct Generators { + // Point at infinity (ZERO) + pub O: GroupElement, + + pub W: GroupElement, + pub W_: GroupElement, + pub X0: GroupElement, + pub X1: GroupElement, + pub Gz_mac: GroupElement, + pub Gz_attribute: GroupElement, + pub Gz_script: GroupElement, + pub G_amount: GroupElement, + pub G_script: GroupElement, + pub G_blind: GroupElement, +} + +impl Generators { + fn new() -> Self { + let w = hash_to_curve(b"W").expect("Failed to hash to curve"); + let w_ = hash_to_curve(b"W_").expect("Failed to hash to curve"); + let x0 = hash_to_curve(b"X0").expect("Failed to hash to curve"); + let x1 = hash_to_curve(b"X1").expect("Failed to hash to curve"); + let gz_mac = hash_to_curve(b"Gz_mac").expect("Failed to hash to curve"); + let gz_attribute = hash_to_curve(b"Gz_attribute").expect("Failed to hash to curve"); + let gz_script = hash_to_curve(b"Gz_script").expect("Failed to hash to curve"); + let g_amount = hash_to_curve(b"G_amount").expect("Failed to hash to curve"); + let g_script = hash_to_curve(b"G_script").expect("Failed to hash to curve"); + let g_blind = hash_to_curve(b"G_blind").expect("Failed to hash to curve"); + + Generators { + O: GroupElement::new(&GROUP_ELEMENT_ZERO), + W: w, + W_: w_, + X0: x0, + X1: x1, + Gz_mac: gz_mac, + Gz_attribute: gz_attribute, + Gz_script: gz_script, + G_amount: g_amount, + G_script: g_script, + G_blind: g_blind, + } + } +} + +pub static GENERATORS: Lazy = Lazy::new(|| Generators::new()); + +#[cfg(test)] +mod tests { + use crate::secp::GroupElement; + + use super::hash_to_curve; + + #[test] + fn test_hash_to_curve() { + let msg = b"G_amount"; + let g_amount = GroupElement::from( + "024e76426e405fa7f7d3403ea8671fe11b8bec2da6dcda5583ce1ac37ed0de9b04", + ); + let g_amount_ = hash_to_curve(msg).expect("Couldn't map hash to groupelement"); + assert!(g_amount == g_amount_) + } + + #[test] + fn test_hash_to_curve_2() { + let msg = b"G_blind"; + let g_blind = GroupElement::from( + "0264f39fbee428ab6165e907b5d463a17e315b9f06f6200ed7e9c4bcbe0df73383", + ); + let g_blind_ = hash_to_curve(msg).expect("Couldn't map hash to groupelement"); + assert!(g_blind == g_blind_) + } +} diff --git a/src/kvac.rs b/src/kvac.rs new file mode 100644 index 0000000..6260a58 --- /dev/null +++ b/src/kvac.rs @@ -0,0 +1,723 @@ +use crate::errors::Error; +use crate::generators::{hash_to_curve, GENERATORS}; +use crate::models::{AmountAttribute, Coin, Equation, MintPrivateKey, RandomizedCoin, ScriptAttribute, Statement, ZKP}; +use crate::transcript::CashuTranscript; +use crate::secp::{GroupElement, Scalar, GROUP_ELEMENT_ZERO, SCALAR_ZERO}; +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; + +pub struct SchnorrProver<'a> { + random_terms: Vec, + secrets: Vec, + transcript: &'a mut CashuTranscript, +} + +impl<'a> SchnorrProver<'a> { + pub fn new( + transcript: &'a mut CashuTranscript, + secrets: Vec, + ) -> Self { + SchnorrProver { + random_terms: vec![Scalar::random(); secrets.len()], + secrets, + transcript, + } + } + + #[allow(non_snake_case)] + pub fn add_statement(self, statement: Statement) -> Self { + // Append proof-specific domain separator to the transcript + self.transcript.domain_sep(&statement.domain_separator); + + for equation in statement.equations.into_iter() { + + let mut R = GroupElement::new(&GROUP_ELEMENT_ZERO); + let V = equation.lhs; + + for row in equation.rhs.into_iter() { + for (k, P) in self.random_terms.iter().zip(row.into_iter()) { + R = R + (P * k).as_ref(); + } + } + + self.transcript.append_element(b"R_", &R); + self.transcript.append_element(b"V_", &V); + } + self + } + + #[allow(non_snake_case)] + pub fn prove(self) -> ZKP { + // Draw a challenge from the running transcript + let c = self.transcript.get_challenge(b"chall_"); + + // Create responses to the challenge + let mut responses: Vec = vec![]; + for (k, s) in self.random_terms.into_iter().zip(self.secrets.into_iter()) { + responses.push( + k + (s * c.as_ref()).as_ref() + ); + } + + ZKP { s: responses, c } + } +} + +pub struct SchnorrVerifier<'a> { + responses: Vec, + challenge: Scalar, + transcript: &'a mut CashuTranscript, +} + +impl<'a> SchnorrVerifier<'a> { + pub fn new( + transcript: &'a mut CashuTranscript, + proof: ZKP, + ) -> Self { + SchnorrVerifier { + responses: proof.s, + challenge: proof.c, + transcript, + } + } + + #[allow(non_snake_case)] + pub fn add_statement( + self, + statement: Statement + ) -> Self { + // Append proof-specific domain separator to the transcript + self.transcript.domain_sep(&statement.domain_separator); + + for equation in statement.equations.into_iter() { + + let mut R = GroupElement::new(&GROUP_ELEMENT_ZERO); + let V = equation.lhs; + + for row in equation.rhs.into_iter() { + for (s, P) in self.responses.iter().zip(row.into_iter()) { + R = R + (P * s).as_ref(); + } + } + R = R - (V.clone() * self.challenge.as_ref()).as_ref(); + + self.transcript.append_element(b"R_", &R); + self.transcript.append_element(b"V_", &V); + } + self + } + + pub fn verify(&mut self) -> bool { + let challenge_ = self.transcript.get_challenge(b"chall_"); + challenge_ == self.challenge + } +} + +pub struct BootstrapProof; + +impl BootstrapProof { + + pub fn statement(amount_commitment: &GroupElement) -> Statement { + Statement { + domain_separator: b"Bootstrap_Statement_", + equations: vec![ + Equation { // Ma = r*G_blind + lhs: amount_commitment.clone(), + rhs: vec![vec![GENERATORS.G_blind.clone()]] + } + ] + } + } + + pub fn create(amount_attribute: &mut AmountAttribute, transcript: &mut CashuTranscript) -> ZKP { + let statement = BootstrapProof::statement(amount_attribute.commitment().as_ref()); + SchnorrProver::new(transcript, vec![amount_attribute.r.clone()]) + .add_statement(statement) + .prove() + } + + pub fn verify(amount_commitment: &GroupElement, proof: ZKP, transcript: &mut CashuTranscript) -> bool { + let statement = BootstrapProof::statement(amount_commitment); + SchnorrVerifier::new(transcript, proof) + .add_statement(statement) + .verify() + } + +} + +pub struct MacProof; + +#[allow(non_snake_case)] +impl MacProof { + + pub fn statement(Z: GroupElement, I: GroupElement, randomized_coin: &RandomizedCoin) -> Statement { + let Cx0 = randomized_coin.Cx0.clone(); + let Cx1 = randomized_coin.Cx1.clone(); + let Ca = randomized_coin.Ca.clone(); + let O = GroupElement::new(&GROUP_ELEMENT_ZERO); + Statement { + domain_separator: b"MAC_Statement_", + equations: vec![ // Z = r*I + Equation { + lhs: Z, + rhs: vec![vec![I]], + }, + Equation { // Cx1 = t*Cx0 + (-tr)*X0 + r*X1 + lhs: Cx1, + rhs: vec![ + vec![GENERATORS.X1.clone(), GENERATORS.X0.clone(), Cx0,] + ] + }, + Equation { // Ca = r_a*Gz_attribute + r_a*G_blind + a*G_amount + // MULTI-ROW: `r` witness is used twice for Gz_amount and G_blind + lhs: Ca, + rhs: vec![ + vec![GENERATORS.Gz_attribute.clone(), O.clone(), O.clone(), GENERATORS.G_amount.clone()], + vec![GENERATORS.G_blind.clone()] + ] + }, + ] + } + } + + pub fn new( + mint_publickey: (&GroupElement, &GroupElement), + coin: &Coin, + randomized_coin: &RandomizedCoin, + transcript: &mut CashuTranscript, + ) -> ZKP { + let r_a = coin.amount_attribute.r.clone(); + let a = coin.amount_attribute.a.clone(); + let t = coin.mac.t.clone(); + let r0 = -r_a.clone()*t.as_ref(); + let (_Cw, I) = mint_publickey; + let Z = I.clone() * &coin.amount_attribute.r; + let statement = MacProof::statement(Z, I.clone(), randomized_coin); + SchnorrProver::new( + transcript, + vec![ + r_a, r0, t, a + ], + ).add_statement(statement).prove() + } + + pub fn verify( + mint_privkey: &MintPrivateKey, + randomized_coin: &RandomizedCoin, + script: Option<&[u8]>, + proof: ZKP, + transcript: &mut CashuTranscript, + ) -> bool { + let (w, x0, x1, ya, ys) = ( + &mint_privkey.w, + &mint_privkey.x0, + &mint_privkey.x1, + &mint_privkey.ya, + &mint_privkey.ys + ); + let (Cx0, Cx1, Ca, Cs, Cv) = ( + randomized_coin.Cx0.clone(), + randomized_coin.Cx1.clone(), + randomized_coin.Ca.clone(), + randomized_coin.Cs.clone(), + randomized_coin.Cv.clone(), + ); + let mut S = GroupElement::new(&GROUP_ELEMENT_ZERO); + if let Some(scr) = script { + let s = Scalar::new(&Sha256Hash::hash(&scr).to_byte_array()); + S = GENERATORS.G_script.clone()*s.as_ref(); + } + let Z = Cv - &( + GENERATORS.W.clone() * w + &( + Cx0 * x0 + &( + Cx1 * x1 + &( + Ca * ya + &( + (Cs + &S) * ys + ) + ) + ) + ) + ); + let (_Cw, I) = mint_privkey.pubkey(); + let statement = MacProof::statement(Z, I.clone(), randomized_coin); + SchnorrVerifier::new(transcript, proof) + .add_statement(statement) + .verify() + } +} + +pub struct IParamsProof; + +#[allow(non_snake_case)] +impl IParamsProof { + + pub fn statement(mint_publickey: (&GroupElement, &GroupElement), coin: &Coin) -> Statement { + let O = GroupElement::new(&GROUP_ELEMENT_ZERO); + let t_tag_bytes: [u8; 32] = coin.mac.t.as_ref().into(); + let t = coin.mac.t.as_ref(); + let U = hash_to_curve(&t_tag_bytes).expect("Couldn't get map MAC tag to GroupElement"); + let V = coin.mac.V.clone(); + let (Cw, I) = mint_publickey; + let Ma = coin.amount_attribute.commitment().clone(); + let mut Ms = O.clone(); + if let Some(scr_attr) = &coin.script_attribute { + Ms = scr_attr.commitment().clone(); + } + Statement { + domain_separator: b"Iparams_Statement_", + equations: vec![ + Equation { // Cw = w*W + w_*W_ + lhs: Cw.clone(), + rhs: vec![vec![GENERATORS.W.clone(), GENERATORS.W_.clone()]] + }, + Equation { // I = Gz_mac - x0*X0 - x1*X1 - ya*Gz_attribute - ys*Gz_script + lhs: I.clone() - &GENERATORS.Gz_mac, + rhs: vec![vec![ + O.clone(), + O.clone(), + -GENERATORS.X0.clone(), + -GENERATORS.X1.clone(), + -GENERATORS.Gz_attribute.clone(), + -GENERATORS.Gz_script.clone(), + ]] + }, + Equation { // V = w*W + x0*U + x1*t*U + ya*Ma + ys*Ms + lhs: V, + rhs: vec![vec![ + GENERATORS.W.clone(), + O, + U.clone(), + U * t.as_ref(), + Ma, + Ms, + ]] + } + ] + } + } + + pub fn new(mint_privkey: &mut MintPrivateKey, coin: &mut Coin, transcript: &mut CashuTranscript) -> ZKP { + let mint_pubkey = mint_privkey.pubkey(); + let statement = IParamsProof::statement(mint_pubkey, coin); + SchnorrProver::new(transcript, mint_privkey.to_scalars()) + .add_statement(statement) + .prove() + } + + pub fn verify( + mint_publickey: (&GroupElement, &GroupElement), + coin: &Coin, + proof: ZKP, + transcript: &mut CashuTranscript, + ) -> bool { + let statement = IParamsProof::statement(mint_publickey, coin); + SchnorrVerifier::new(transcript, proof) + .add_statement(statement) + .verify() + } +} + +pub struct BalanceProof; + +#[allow(non_snake_case)] +impl BalanceProof { + + pub fn statement(B: GroupElement) -> Statement { + Statement { + domain_separator: b"Balance_Statement_", + equations: vec![ + Equation { + lhs: B, + rhs: vec![vec![GENERATORS.Gz_attribute.clone(), GENERATORS.G_blind.clone()]] + } + ] + } + } + + pub fn new( + inputs: &Vec, + outputs: &Vec, + transcript: &mut CashuTranscript) -> ZKP { + let mut r_sum = Scalar::new(&SCALAR_ZERO); + for input in inputs.into_iter() { + r_sum = r_sum + &input.r; + } + let mut r_sum_ = Scalar::new(&SCALAR_ZERO); + for output in outputs.into_iter() { + r_sum_ = r_sum_ + &output.r; + } + let delta_r = (-r_sum_) + r_sum.as_ref(); + let B = GENERATORS.Gz_attribute.clone() * r_sum.as_ref() + ( + GENERATORS.G_blind.clone() * delta_r.as_ref() + ).as_ref(); + let statement = BalanceProof::statement(B); + SchnorrProver::new(transcript, vec![r_sum, delta_r]) + .add_statement(statement) + .prove() + } + + pub fn verify( + inputs: &Vec, + outputs: &Vec, + delta_amount: i64, + proof: ZKP, + transcript: &mut CashuTranscript + ) -> bool { + let delta_a = Scalar::from(delta_amount.abs() as u64); + let mut B = GENERATORS.G_amount.clone() * &delta_a; + if delta_amount >= 0 { + B.negate(); + } + for input in inputs.iter() { + B = B + input.Ca.as_ref(); + } + for output in outputs.iter() { + B = B - output.as_ref(); + } + let statement = BalanceProof::statement(B); + SchnorrVerifier::new(transcript, proof) + .add_statement(statement) + .verify() + + } +} + + +pub struct ScriptEqualityProof; + +#[allow(non_snake_case)] +impl ScriptEqualityProof { + pub fn statement( + inputs: &Vec, + outputs: &Vec<(GroupElement, GroupElement)> + ) -> Statement { + let O: GroupElement = GENERATORS.O.clone(); + let mut equations: Vec = Vec::new(); + + for (i, zcoin) in inputs.iter().enumerate() { + let construction = vec![ + vec![GENERATORS.G_script.clone()], + vec![O.clone(); i], + vec![GENERATORS.Gz_script.clone()], + vec![O.clone(); inputs.len() - 1], + vec![GENERATORS.G_blind.clone()], + ] + .into_iter() + .flatten() + .collect::>(); + + equations.push(Equation { + lhs: zcoin.Cs.clone(), + rhs: vec![construction], + }); + } + for (i, commitments) in outputs.iter().enumerate() { + let construction = vec![ + vec![GENERATORS.G_script.clone()], + vec![O.clone(); 2*inputs.len()+i], + vec![GENERATORS.G_blind.clone()], + ] + .into_iter() + .flatten() + .collect::>(); + + let (_Ma, Ms) = commitments; + equations.push(Equation { + lhs: Ms.clone(), + rhs: vec![construction], + }); + } + Statement { + domain_separator: b"Script_Equality_Statement_", + equations, + } + } + + pub fn new( + inputs: &Vec, + randomized_inputs: &Vec, + outputs: &Vec<(AmountAttribute, ScriptAttribute)>, + transcript: &mut CashuTranscript, + ) -> Result { + if inputs.is_empty() || + randomized_inputs.is_empty() || + outputs.is_empty() + { + return Err(Error::EmptyList); + } + let commitments: Vec<(GroupElement, GroupElement)> = outputs + .iter() + .map(|(aa, sa)| (aa.commitment().clone(), sa.commitment().clone())) + .collect(); + let statement = ScriptEqualityProof::statement(randomized_inputs, &commitments); + let s = inputs[0].script_attribute.as_ref().ok_or(Error::NoScriptProvided)?.s.clone(); + let r_a_list = inputs + .iter() + .map(|coin| coin.amount_attribute.r.clone()) + .collect(); + let r_s_list = inputs + .iter() + .map(|coin| coin.script_attribute.as_ref().expect("Expected Script Attribute").r.clone()) + .collect(); + let new_r_s_list = outputs + .iter() + .map(|(_, script_attr)| script_attr.r.clone()) + .collect(); + Ok(SchnorrProver::new( + transcript, + vec![vec![s], r_a_list, r_s_list, new_r_s_list] + .into_iter() + .flatten() + .collect() + ).add_statement(statement).prove() + ) + } + + pub fn verify( + randomized_inputs: &Vec, + outputs: &Vec<(GroupElement, GroupElement)>, + proof: ZKP, + transcript: &mut CashuTranscript, + ) -> bool { + if randomized_inputs.is_empty() || + outputs.is_empty() + { + return false; + } + let statement = ScriptEqualityProof::statement(randomized_inputs, outputs); + SchnorrVerifier::new(transcript, proof) + .add_statement(statement) + .verify() + } +} + +#[cfg(test)] +mod tests{ + + use crate::{errors::Error, generators::{hash_to_curve, GENERATORS}, models::{AmountAttribute, Coin, MintPrivateKey, RandomizedCoin, ScriptAttribute, MAC}, secp::{GroupElement, Scalar, GROUP_ELEMENT_ZERO}, transcript::CashuTranscript}; + + use super::{BalanceProof, BootstrapProof, IParamsProof, MacProof, ScriptEqualityProof}; + + fn transcripts() -> (CashuTranscript, CashuTranscript) { + let mint_transcript = CashuTranscript::new(); + let client_transcript = CashuTranscript::new(); + (mint_transcript, client_transcript) + } + + fn privkey() -> MintPrivateKey { + let scalars = [ + Scalar::random(), + Scalar::random(), + Scalar::random(), + Scalar::random(), + Scalar::random(), + Scalar::random() + ]; + MintPrivateKey::from_scalars(&scalars) + } + + #[test] + fn test_bootstrap() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let mut bootstrap_attr = AmountAttribute::new(0, None); + let proof = BootstrapProof::create(&mut bootstrap_attr, client_transcript.as_mut()); + assert!(BootstrapProof::verify(bootstrap_attr.commitment().as_ref(), proof, &mut mint_transcript)) + } + + #[test] + fn test_wrong_bootstrap() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let mut bootstrap_attr = AmountAttribute::new(1, None); + let proof = BootstrapProof::create(&mut bootstrap_attr, client_transcript.as_mut()); + assert!(!BootstrapProof::verify(bootstrap_attr.commitment().as_ref(), proof, &mut mint_transcript)) + } + + #[test] + fn test_iparams() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let mut mint_privkey = privkey(); + let amount_attr = AmountAttribute::new(12, None); + let mac = MAC::generate(&mint_privkey, &amount_attr.commitment(), None, None).expect("Couldn't generate MAC"); + let mut coin = Coin::new(amount_attr, None, mac); + let proof = IParamsProof::new(&mut mint_privkey, &mut coin, &mut client_transcript); + assert!(IParamsProof::verify(mint_privkey.pubkey(), &mut coin, proof, &mut mint_transcript)); + } + + #[test] + fn test_wrong_iparams() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let mut mint_privkey = privkey(); + let mint_privkey_1 = privkey(); + let amount_attr = AmountAttribute::new(12, None); + let mac = MAC::generate(&mint_privkey, &amount_attr.commitment(), None, None).expect("Couldn't generate MAC"); + let mut coin = Coin::new(amount_attr, None, mac); + let proof = IParamsProof::new(&mut mint_privkey, &mut coin, &mut client_transcript); + assert!(!IParamsProof::verify(mint_privkey_1.pubkey(), &mut coin, proof, &mut mint_transcript)) + } + + #[test] + fn test_mac() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let mint_privkey = privkey(); + let amount_attr = AmountAttribute::new(12, None); + let mac = MAC::generate(&mint_privkey, &amount_attr.commitment(), None, None).expect("Couldn't generate MAC"); + let coin = Coin::new(amount_attr, None, mac); + let randomized_coin = RandomizedCoin::from_coin(&coin, false).expect("Expected a randomized coin"); + let proof = MacProof::new(mint_privkey.pubkey(), &coin, &randomized_coin, &mut client_transcript); + assert!(MacProof::verify(&mint_privkey, &randomized_coin, None, proof, &mut mint_transcript)); + } + + #[test] + fn test_wrong_mac() { + #[allow(non_snake_case)] + fn generate_custom_rand(coin: &Coin) -> Result { + let t = coin.mac.t.clone(); + let V = coin.mac.V.as_ref(); + let t_bytes: [u8; 32] = (&coin.mac.t).into(); + let U = hash_to_curve(&t_bytes)?; + let Ma = coin.amount_attribute.commitment(); + // We try and randomize differently. + let z = Scalar::random(); + let Ms: GroupElement = GroupElement::new(&GROUP_ELEMENT_ZERO); + + let Ca = GENERATORS.Gz_attribute.clone() * z.as_ref() + Ma; + let Cs = GENERATORS.Gz_script.clone() * z.as_ref() + &Ms; + let Cx0 = GENERATORS.X0.clone() * z.as_ref() + &U; + let Cx1 = GENERATORS.X1.clone() * z.as_ref() + &(U * &t); + let Cv = GENERATORS.Gz_mac.clone() * z.as_ref() + V; + + Ok(RandomizedCoin { Ca, Cs, Cx0, Cx1, Cv }) + } + + let (mut mint_transcript, mut client_transcript) = transcripts(); + let mut mint_privkey = privkey(); + let amount_attr = AmountAttribute::new(12, None); + let mac = MAC::generate(&mint_privkey, &amount_attr.commitment(), None, None).expect("Couldn't generate MAC"); + let mut coin = Coin::new(amount_attr, None, mac); + let randomized_coin = generate_custom_rand(&mut coin).expect("Expected a randomized coin"); + let proof = MacProof::new(mint_privkey.pubkey(), &coin, &randomized_coin, &mut client_transcript); + assert!(!MacProof::verify(&mut mint_privkey, &randomized_coin, None, proof, &mut mint_transcript)); + } + + #[test] + fn test_balance() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let privkey = privkey(); + let inputs = vec![AmountAttribute::new(12, None), AmountAttribute::new(11, None)]; + let outputs = vec![AmountAttribute::new(23, None)]; + // We assume the inputs were already attributed a MAC previously + let macs: Vec = inputs + .iter() + .map(|input| MAC::generate(&privkey, input.commitment(), None, None).expect("MAC expected")) + .collect(); + let proof = BalanceProof::new(&inputs, &outputs, &mut client_transcript); + let mut coins: Vec = macs.into_iter().zip(inputs.into_iter()) + .map(|(mac, input)| Coin::new(input, None, mac)) + .collect(); + let randomized_coins: Vec = coins.iter_mut() + .map(|coin| RandomizedCoin::from_coin(coin, false).expect("RandomzedCoin expected")) + .collect(); + let outputs: Vec = outputs.into_iter().map(|output| output.commitment().clone()).collect(); + assert!(BalanceProof::verify(&randomized_coins, &outputs, 0, proof, &mut mint_transcript)); + } + + #[test] + fn test_wrong_balance() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let privkey = privkey(); + let mut inputs = vec![AmountAttribute::new(12, None), AmountAttribute::new(11, None)]; + let outputs = vec![AmountAttribute::new(23, None)]; + // We assume the inputs were already attributed a MAC previously + let macs: Vec = inputs + .iter_mut() + .map(|input| MAC::generate(&privkey, &input.commitment(), None, None).expect("MAC expected")) + .collect(); + let proof = BalanceProof::new(&inputs, &outputs, &mut client_transcript); + let mut coins: Vec = macs.into_iter().zip(inputs.into_iter()) + .map(|(mac, input)| Coin::new(input, None, mac)) + .collect(); + let randomized_coins: Vec = coins.iter_mut() + .map(|coin| RandomizedCoin::from_coin(coin, false).expect("RandomzedCoin expected")) + .collect(); + let outputs: Vec = outputs.into_iter().map(|output| output.commitment().clone()).collect(); + assert!(!BalanceProof::verify(&randomized_coins, &outputs, 1, proof, &mut mint_transcript)); + } + + #[test] + fn test_script_equality() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let script = b"testscript"; + let privkey = privkey(); + let inputs = vec![ + (AmountAttribute::new(12, None), ScriptAttribute::new(script, None)), + (AmountAttribute::new(11, None), ScriptAttribute::new(script, None)) + ]; + let outputs = vec![ + (AmountAttribute::new(6, None), ScriptAttribute::new(script, None)), + (AmountAttribute::new(11, None), ScriptAttribute::new(script, None)), + (AmountAttribute::new(12, None), ScriptAttribute::new(script, None)), + ]; + let macs: Vec = inputs + .iter() + .map(|(amount_attr, script_attr)| + MAC::generate(&privkey, &amount_attr.commitment(), Some(&script_attr.commitment()), None).expect("")) + .collect(); + let coins: Vec = inputs.into_iter().zip(macs.into_iter()) + .map(|((aa, sa), mac)| Coin::new(aa, Some(sa), mac)) + .collect(); + let randomized_coins: Vec = coins + .iter() + .map(|coin| RandomizedCoin::from_coin(coin, false).expect("")) + .collect(); + let proof = ScriptEqualityProof::new( + &coins, + &randomized_coins, + &outputs, + client_transcript.as_mut(), + ).expect(""); + let outputs = outputs + .into_iter() + .map(|(aa, sa)| (aa.commitment().clone(), sa.commitment().clone())) + .collect(); + assert!(ScriptEqualityProof::verify(&randomized_coins, &outputs, proof, mint_transcript.as_mut())) + } + + #[test] + fn test_script_inequality() { + let (mut mint_transcript, mut client_transcript) = transcripts(); + let script = b"testscript"; + let privkey = privkey(); + let inputs = vec![ + (AmountAttribute::new(12, None), ScriptAttribute::new(script, None)), + (AmountAttribute::new(11, None), ScriptAttribute::new(script, None)) + ]; + let outputs = vec![ + (AmountAttribute::new(6, None), ScriptAttribute::new(b"testscript_", None)), + (AmountAttribute::new(11, None), ScriptAttribute::new(script, None)), + (AmountAttribute::new(12, None), ScriptAttribute::new(script, None)), + ]; + let macs: Vec = inputs + .iter() + .map(|(amount_attr, script_attr)| + MAC::generate(&privkey, &amount_attr.commitment(), Some(&script_attr.commitment()), None).expect("")) + .collect(); + let coins: Vec = inputs.into_iter().zip(macs.into_iter()) + .map(|((aa, sa), mac)| Coin::new(aa, Some(sa), mac)) + .collect(); + let randomized_coins: Vec = coins + .iter() + .map(|coin| RandomizedCoin::from_coin(coin, false).expect("")) + .collect(); + let proof = ScriptEqualityProof::new( + &coins, + &randomized_coins, + &outputs, + client_transcript.as_mut(), + ).expect(""); + let outputs = outputs + .into_iter() + .map(|(aa, sa)| (aa.commitment().clone(), sa.commitment().clone())) + .collect(); + assert!(!ScriptEqualityProof::verify(&randomized_coins, &outputs, proof, mint_transcript.as_mut())) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..8e3add1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod errors; +pub mod generators; +pub mod models; +pub mod secp; +pub mod transcript; +pub mod kvac; \ No newline at end of file diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..dfaf8f0 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,252 @@ +use crate::{errors::Error, generators::{hash_to_curve, GENERATORS}, secp::{GroupElement, Scalar, GROUP_ELEMENT_ZERO}}; +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; + +pub const RANGE_LIMIT: u64 = std::u32::MAX as u64; + +#[allow(non_snake_case)] +pub struct MintPrivateKey { + pub w: Scalar, + pub w_: Scalar, + pub x0: Scalar, + pub x1: Scalar, + pub ya: Scalar, + pub ys: Scalar, + + // Public parameters + pub Cw: GroupElement, + pub I: GroupElement +} + +#[allow(non_snake_case)] +impl MintPrivateKey { + + pub fn from_scalars(scalars: &[Scalar; 6]) -> Self { + let [w, w_, x0, x1, ya, ys] = scalars; + let Cw = GENERATORS.W.clone()*w + &(GENERATORS.W_.clone()*w_); + let I = + GENERATORS.Gz_mac.clone() - &( + GENERATORS.X0.clone()*x0 + + &( + GENERATORS.X1.clone()*x1 + + &( + GENERATORS.Gz_attribute.clone()*ya + + &( + GENERATORS.Gz_script.clone()*ys + ) + ) + ) + ); + MintPrivateKey { + w: w.clone(), + w_: w_.clone(), + x0: x0.clone(), + x1: x1.clone(), + ya: ya.clone(), + ys: ys.clone(), + Cw, + I, + } + } + + pub fn to_scalars(&self) -> Vec { + vec![self.w.clone(), self.w_.clone(), self.x0.clone(), self.x1.clone(), self.ya.clone(), self.ys.clone()] + } + + pub fn pubkey(&self) -> (&GroupElement, &GroupElement) { + (self.Cw.as_ref(), self.I.as_ref()) + } +} + + +pub struct ZKP { + pub s: Vec, + pub c: Scalar +} + +#[allow(non_snake_case)] +pub struct ScriptAttribute { + pub r: Scalar, + pub s: Scalar, + Ms: GroupElement, +} + +#[allow(non_snake_case)] +impl ScriptAttribute { + pub fn new(script: &[u8], blinding_factor: Option<&[u8; 32]>) -> Self { + let s = Scalar::new(&Sha256Hash::hash(&script).to_byte_array()); + if let Some(b_factor) = blinding_factor { + let r = Scalar::new(b_factor); + let Ms = GENERATORS.G_script.clone() * &s + &( + GENERATORS.G_blind.clone() * &r + ); + ScriptAttribute { r, s, Ms } + } else { + let r = Scalar::random(); + let Ms = GENERATORS.G_script.clone() * &s + &( + GENERATORS.G_blind.clone() * &r + ); + ScriptAttribute { r, s, Ms } + } + } + + pub fn commitment(&self) -> &GroupElement { + self.Ms.as_ref() + } +} + +#[allow(non_snake_case)] +pub struct AmountAttribute { + pub a: Scalar, + pub r: Scalar, + Ma: GroupElement, +} + +#[allow(non_snake_case)] +impl AmountAttribute { + pub fn new(amount: u64, blinding_factor: Option<&[u8; 32]>) -> Self { + let a = Scalar::from(amount); + if let Some(b_factor) = blinding_factor { + let r = Scalar::new(b_factor); + let Ma = GENERATORS.G_amount.clone() * &a + &( + GENERATORS.G_blind.clone() * &r + ); + AmountAttribute { r, a, Ma } + } else { + let r = Scalar::random(); + let Ma = GENERATORS.G_amount.clone() * &a + &( + GENERATORS.G_blind.clone() * &r + ); + AmountAttribute { r, a, Ma } + } + } + + pub fn commitment(&self) -> &GroupElement { + self.Ma.as_ref() + } +} + +#[allow(non_snake_case)] +pub struct MAC { + pub t: Scalar, + pub V: GroupElement, +} + +impl MAC { + #[allow(non_snake_case)] + pub fn generate( + privkey: &MintPrivateKey, + amount_commitment: &GroupElement, + script_commitment: Option<&GroupElement>, + t_tag: Option<&Scalar>, + ) -> Result { + let t: Scalar; + if let Some(t_tag_some) = t_tag { + t = t_tag_some.clone(); + } else { + t = Scalar::random(); + } + let t_bytes: [u8; 32] = t.as_ref().into(); + let U = hash_to_curve(&t_bytes)?; + let Ma = amount_commitment.clone(); + let Ms: GroupElement; + if let Some(com) = script_commitment { + Ms = com.clone(); + } else { + Ms = GroupElement::new(&GROUP_ELEMENT_ZERO); + } + let V = + GENERATORS.W.clone() * &privkey.w + &( + U.clone() * &privkey.x0 + &( + U.clone() * &(t.clone() * &privkey.x1) + &( + Ma * &(privkey.ya) + &( + Ms * &(privkey.ys) + ) + ) + ) + ); + Ok(MAC { t, V }) + } +} + +/// Spendable coin. +/// Contains `AmountAttribute`, `ScriptAttribute` +/// and the `MAC` approval by the Mint. +pub struct Coin { + pub amount_attribute: AmountAttribute, + pub script_attribute: Option, + pub mac: MAC, +} + +impl Coin { + pub fn new( + amount_attribute: AmountAttribute, + script_attribute: Option, + mac: MAC, + ) -> Self { + Coin { amount_attribute, script_attribute, mac } + } +} + +/// Contains randomized commitments of a `Coin`. +/// Used for unlinkable multi-show. +#[allow(non_snake_case)] +pub struct RandomizedCoin { + /// Randomized Attribute Commitment + pub Ca: GroupElement, + /// Randomized Script Commitment + pub Cs: GroupElement, + /// Randomized MAC-specific Generator "U" + pub Cx0: GroupElement, + /// Randomized tag commitment + pub Cx1: GroupElement, + /// Randomized MAC + pub Cv: GroupElement, +} + +impl RandomizedCoin { + #[allow(non_snake_case)] + pub fn from_coin( + coin: &Coin, + reveal_script: bool, + ) -> Result { + let t = coin.mac.t.clone(); + let V = coin.mac.V.as_ref(); + let t_bytes: [u8; 32] = (&coin.mac.t).into(); + let U = hash_to_curve(&t_bytes)?; + let Ma = coin.amount_attribute.commitment(); + let r = &coin.amount_attribute.r; + let Ms: GroupElement; + if let Some(attr) = &coin.script_attribute { + if reveal_script { + Ms = GENERATORS.G_blind.clone() * attr.r.as_ref(); + } else { + Ms = attr.commitment().clone(); + } + } else { + Ms = GroupElement::new(&GROUP_ELEMENT_ZERO); + } + + let Ca = GENERATORS.Gz_attribute.clone() * r + &Ma; + let Cs = GENERATORS.Gz_script.clone() * r + &Ms; + let Cx0 = GENERATORS.X0.clone() * r + &U; + let Cx1 = GENERATORS.X1.clone() * r + &(U * &t); + let Cv = GENERATORS.Gz_mac.clone() * r + V; + + Ok(RandomizedCoin { Ca, Cs, Cx0, Cx1, Cv }) + } +} + +pub struct Equation { + /// Left-hand side of the equation (public input) + pub lhs: GroupElement, + /// Right-hand side of the equation (construction of the relation) + pub rhs: Vec>, +} + +pub struct Statement { + /// Domain Separator of the proof + pub domain_separator: &'static [u8], + /// Relations + pub equations: Vec +} diff --git a/src/secp.py b/src/secp.py index 6ffd241..6f657e9 100644 --- a/src/secp.py +++ b/src/secp.py @@ -10,7 +10,6 @@ def div2(M, x): """Helper routine to compute x/2 mod M (where M is odd).""" - assert M & 1 if x & 1: # If x is odd, make it even by adding M. x += M # x must be even now, so a clean division by 2 is possible. @@ -30,10 +29,6 @@ def modinv(M, x): delta, f, g, d, e = 1 + delta, f, (g + f) // 2, d, div2(M, e + d) else: delta, f, g, d, e = 1 + delta, f, (g ) // 2, d, div2(M, e ) - # Verify that the invariants d=f/x mod M, e=g/x mod M are maintained. - assert f % M == (d * x) % M - assert g % M == (e * x) % M - assert f == 1 or f == -1 # |f| is the GCD, it must be 1 # Because of invariant d = f/x (mod M), 1/x = d/f (mod M). As |f|=1, d/f = d*f. return (d * f) % M @@ -197,4 +192,11 @@ def to_data(self): return [self.public_key.data[i] for i in range(64)] scalar_one = Scalar(int(1).to_bytes(32, 'big')) -scalar_zero = Scalar(SCALAR_ZERO) \ No newline at end of file +scalar_zero = Scalar(SCALAR_ZERO) + +if __name__ == '__main__': + s = Scalar(bytes.fromhex("00"*28+"deadbeef")) + s_inv = s.invert() + s_inv_num = int.from_bytes(s_inv.to_bytes(), "big") + print(f"{s_inv_num = }") + print(f"{s_inv.serialize() = }") \ No newline at end of file diff --git a/src/secp.rs b/src/secp.rs new file mode 100755 index 0000000..d25fcfd --- /dev/null +++ b/src/secp.rs @@ -0,0 +1,697 @@ +use bitcoin::secp256k1::constants::CURVE_ORDER; +use bitcoin::secp256k1::{rand, All, PublicKey, Scalar as SecpScalar, Secp256k1, SecretKey}; +use once_cell::sync::Lazy; +use rug::ops::RemRounding; +use rug::Integer; +use std::cmp::PartialEq; + +pub const SCALAR_ZERO: [u8; 32] = [0; 32]; +pub const SCALAR_ONE: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, +]; +pub const GROUP_ELEMENT_ZERO: [u8; 33] = [0; 33]; + +/// Secp256k1 global context +pub static SECP256K1: Lazy> = Lazy::new(|| { + let mut ctx = Secp256k1::new(); + let mut rng = rand::thread_rng(); + ctx.randomize(&mut rng); + ctx +}); + +#[derive(Clone)] +pub struct Scalar { + inner: Option, + is_zero: bool, +} + +#[derive(Clone)] +pub struct GroupElement { + inner: Option, + is_zero: bool, +} + +fn div2(m: &Integer, mut x: Integer) -> Integer { + if x.is_odd() { + x += m; + } + x >> 1 +} + +fn modinv(m: &Integer, x: &Integer) -> Integer { + assert!(m.is_odd(), "M must be odd"); + let mut delta = 1; + let mut f = m.clone(); + let mut g = x.clone(); + let mut d = Integer::from(0); + let mut e = Integer::from(1); + + while !g.is_zero() { + if delta > 0 && g.is_odd() { + let tmp_g = g.clone(); + g = (g - &f) >> 1; + f = tmp_g; + let tmp_e = e.clone(); + e = div2(m, e - &d); + d = tmp_e; + delta = 1 - delta; + } else if g.is_odd() { + g = (g + &f) >> 1; + e = div2(m, e + &d); + delta = 1 + delta; + } else { + g >>= 1; + e = div2(m, e); + delta = 1 + delta; + } + } + + // Result: (d * f) % m + (d * f).rem_euc(m) +} + +impl Scalar { + pub fn new(data: &[u8; 32]) -> Self { + if *data == SCALAR_ZERO { + Scalar { + inner: None, + is_zero: true, + } + } else { + let inner = SecretKey::from_slice(data).expect("Could not instantiate Scalar"); + Scalar { + inner: Some(inner), + is_zero: false, + } + } + } + + pub fn random() -> Self { + let inner = SecretKey::new(&mut rand::thread_rng()); + Scalar { + inner: Some(inner), + is_zero: false, + } + } + + pub fn clone(&self) -> Self { + Scalar { + inner: Some(self.inner.unwrap().clone()), + is_zero: self.is_zero, + } + } + + pub fn tweak_mul(&mut self, other: &Scalar) -> &Self { + if other.is_zero || self.is_zero { + self.is_zero = true; + self.inner = None; + return self; + } + let b = SecpScalar::from_be_bytes(other.inner.unwrap().secret_bytes()).unwrap(); + let result = self + .inner + .unwrap() + .mul_tweak(&b) + .expect("Could not multiply Scalars"); + self.inner = Some(result); + self + } + + pub fn tweak_add(&mut self, other: &Scalar) -> &Self { + if other.is_zero { + self + } else if self.is_zero { + self.inner = Some(other.inner.unwrap().clone()); + self.is_zero = false; + self + } else { + let b = SecpScalar::from_be_bytes(other.inner.unwrap().secret_bytes()).unwrap(); + let result_key = self + .inner + .unwrap() + .add_tweak(&b) + .expect("Could not add to Scalar"); + self.inner = Some(result_key); + self + } + } + + pub fn tweak_neg(&mut self) -> &Self { + if self.is_zero { + self + } else { + let result = self.inner.unwrap().negate(); + self.inner = Some(result); + self + } + } + + pub fn invert(&mut self) -> &Self { + if self.is_zero { + panic!("Scalar 0 doesn't have an inverse") + } else { + let x = Integer::from_digits( + &self.inner.unwrap().secret_bytes(), + rug::integer::Order::Msf, + ); + let q = Integer::from_digits(&CURVE_ORDER, rug::integer::Order::Msf); + let x_inv = modinv(&q, &x); + //let x_inv = x.clone().invert(&q).unwrap(); + let vec = x_inv.to_digits(rug::integer::Order::Msf); + let inner = SecretKey::from_slice(&vec).expect("Could not instantiate Scalar"); + self.inner = Some(inner); + self + } + } +} + +impl GroupElement { + pub fn new(data: &[u8; 33]) -> Self { + if *data == GROUP_ELEMENT_ZERO { + GroupElement { + inner: None, + is_zero: true, + } + } else { + let inner = PublicKey::from_slice(data).expect("Cannot create GroupElement"); + GroupElement { + inner: Some(inner), + is_zero: false, + } + } + } + + pub fn clone(&self) -> Self { + if self.is_zero { + GroupElement { + inner: None, + is_zero: true, + } + } else { + GroupElement { + inner: Some(self.inner.unwrap().clone()), + is_zero: self.is_zero, + } + } + } + + pub fn combine_add(&mut self, other: &GroupElement) -> &Self { + if other.is_zero { + self + } else if self.is_zero { + self.inner = other.inner.clone(); + self.is_zero = other.is_zero; + self + } else { + let result = self + .inner + .unwrap() + .combine(&other.inner.unwrap()) + .expect("Error combining GroupElements"); + self.inner = Some(result); + self + } + } + + pub fn multiply(&mut self, scalar: &Scalar) -> &Self { + if scalar.is_zero || self.is_zero { + self.is_zero = true; + self.inner = None; + self + } else { + let b = bitcoin::secp256k1::Scalar::from_be_bytes(scalar.inner.unwrap().secret_bytes()) + .unwrap(); + let result = self + .inner + .unwrap() + .mul_tweak(&SECP256K1, &b) + .expect("Could not multiply Scalar to GroupElement"); + self.inner = Some(result); + self + } + } + + pub fn negate(&mut self) -> &Self { + if self.is_zero { + self + } else { + let result = self.inner.unwrap().negate(&SECP256K1); + self.inner = Some(result); + self + } + } +} + +impl std::ops::Add<&Scalar> for Scalar { + type Output = Scalar; + + fn add(mut self, other: &Scalar) -> Scalar { + self.tweak_add(&other); + self + } +} + +impl std::ops::Neg for Scalar { + type Output = Scalar; + + fn neg(mut self) -> Scalar { + self.tweak_neg(); + self + } +} + +impl std::ops::Sub<&Scalar> for Scalar { + type Output = Scalar; + + fn sub(self, other: &Scalar) -> Scalar { + if other.is_zero { + self + } else if self.is_zero { + -(other.clone()) + } else { + let other_neg = -(other.clone()); + self + &other_neg + } + } +} + +impl std::ops::Mul<&Scalar> for Scalar { + type Output = Scalar; + + fn mul(mut self, other: &Scalar) -> Scalar { + if other.is_zero || self.is_zero { + self.inner = None; + self.is_zero = true; + self + } else { + // Multiplication is masked with random `r` + let mut r = Scalar::random(); + self.tweak_add(&r); + self.tweak_mul(&other); + r.tweak_mul(&other); + self - &r + } + } +} + +impl Into> for Scalar { + fn into(self) -> Vec { + if self.is_zero { + SCALAR_ZERO.to_vec() + } else { + self.inner.unwrap().secret_bytes().to_vec() + } + } +} + +impl Into<[u8; 32]> for &Scalar { + fn into(self) -> [u8; 32] { + if self.is_zero { + SCALAR_ZERO + } else { + self.inner.as_ref().expect("Expected inner Scalar").secret_bytes() + } + } +} + +impl Into for &Scalar { + fn into(self) -> String { + if self.is_zero { + hex::encode(SCALAR_ZERO) + } else { + hex::encode(self.inner.unwrap().secret_bytes()) + } + } +} + +impl From for Scalar { + fn from(value: u64) -> Self { + let mut bytes = [0u8; 32]; + bytes[24] = (value >> 56) as u8; + bytes[25] = (value >> 48) as u8; + bytes[26] = (value >> 40) as u8; + bytes[27] = (value >> 32) as u8; + bytes[28] = (value >> 24) as u8; + bytes[29] = (value >> 16) as u8; + bytes[30] = (value >> 8) as u8; + bytes[31] = value as u8; + Scalar::new(&bytes) + } +} + +impl From<&str> for Scalar { + fn from(hex_string: &str) -> Self { + let bytes = hex::decode(hex_string).expect("Invalid hex string"); + if bytes.len() > 32 { + panic!("Hex string is too long"); + } + let mut padded_bytes = [0u8; 32]; + padded_bytes[32 - bytes.len()..32].copy_from_slice(&bytes); + Scalar::new(&padded_bytes) + } +} + +impl AsRef for Scalar { + fn as_ref(&self) -> &Scalar { + self + } +} + +impl PartialEq for Scalar { + fn eq(&self, other: &Self) -> bool { + if self.is_zero && other.is_zero { + return true; + } + if self.is_zero || other.is_zero { + return false; + } + let mut b = 0u8; + for (x, y) in self + .inner + .as_ref() + .unwrap() + .secret_bytes() + .iter() + .zip(other.inner.as_ref().unwrap().secret_bytes().iter()) + { + b |= x ^ y; + } + b == 0 + } +} + +impl std::ops::Add<&GroupElement> for GroupElement { + type Output = GroupElement; + + fn add(mut self, other: &GroupElement) -> GroupElement { + self.combine_add(other); + self + } +} + +impl std::ops::Neg for GroupElement { + type Output = GroupElement; + + fn neg(mut self) -> GroupElement { + self.negate(); + self + } +} + +impl std::ops::Sub<&GroupElement> for GroupElement { + type Output = GroupElement; + + fn sub(self, other: &GroupElement) -> GroupElement { + if other.is_zero { + self + } else if self.is_zero { + -(other.clone()) + } else { + let other_neg = -(other.clone()); + self + &other_neg + } + } +} + +impl std::ops::Mul<&Scalar> for GroupElement { + type Output = GroupElement; + + fn mul(mut self, other: &Scalar) -> GroupElement { + if self.is_zero || other.is_zero { + self.is_zero = true; + self.inner = None; + self + } else { + // Multiplication is masked with random `r` + let r = Scalar::random(); + let r_copy = r.clone(); + let mut self_copy = self.clone(); + self.multiply(&(r + other)); + self_copy.multiply(&r_copy); + self - &self_copy + } + } +} + +impl PartialEq for GroupElement { + fn eq(&self, other: &Self) -> bool { + if self.is_zero && other.is_zero { + return true; + } + if self.is_zero || other.is_zero { + return false; + } else { + self.inner.unwrap().eq(&other.inner.unwrap()) + } + } +} + +impl From<&str> for GroupElement { + fn from(hex_string: &str) -> Self { + let bytes = hex::decode(hex_string).expect("Invalid hex string"); + if bytes.len() > 33 { + panic!("Hex string is too long"); + } + let mut padded_bytes = [0u8; 33]; + padded_bytes[33 - bytes.len()..33].copy_from_slice(&bytes); + GroupElement::new(&padded_bytes) + } +} + +impl Into<[u8; 33]> for &GroupElement { + fn into(self) -> [u8; 33] { + if self.is_zero { + GROUP_ELEMENT_ZERO + } else { + self.inner + .as_ref() + .expect("Expected inner PublicKey") + .serialize() + } + } +} + +impl Into for &GroupElement { + fn into(self) -> String { + if self.is_zero { + hex::encode(GROUP_ELEMENT_ZERO) + } else { + hex::encode(self.inner + .as_ref() + .expect("Expected inner PublicKey") + .serialize() + ) + } + } +} + +impl AsRef for GroupElement { + fn as_ref(&self) -> &GroupElement { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_scalar() { + let data = [1u8; 32]; + let scalar = Scalar::new(&data); + assert!(!scalar.is_zero); + } + + #[test] + fn test_new_zero_scalar() { + let scalar = Scalar::new(&SCALAR_ZERO); + assert!(scalar.is_zero); + } + + #[test] + fn test_random_scalar() { + let scalar = Scalar::random(); + assert!(!scalar.is_zero); + } + + #[test] + fn test_clone_scalar() { + let scalar = Scalar::random(); + let cloned_scalar = scalar.clone(); + assert_eq!(scalar.inner, cloned_scalar.inner); + assert_eq!(scalar.is_zero, cloned_scalar.is_zero); + } + + #[test] + fn test_scalar_tweak_mul() { + let mut scalar1 = Scalar::from("02"); + let scalar2 = Scalar::from("03"); + let result = Scalar::from("06"); + let result_ = scalar1.tweak_mul(&scalar2); + assert!(*result_ == result); + } + + #[test] + fn test_scalar_tweak_add() { + let mut scalar1 = Scalar::from("02"); + let scalar2 = Scalar::from("03"); + let result = Scalar::from("05"); + let result_ = scalar1.tweak_add(&scalar2); + assert!(result == *result_); + } + + #[test] + fn test_scalar_add() { + let scalar1 = Scalar::from("02"); + let scalar2 = Scalar::from("03"); + let result = Scalar::from("05"); + let result_ = scalar1 + &scalar2; + assert!(result_ == result); + } + + #[test] + fn test_scalar_sub() { + let scalar1 = Scalar::from("10"); + let scalar2 = Scalar::from("02"); + let result = Scalar::from("0e"); + let result_ = scalar1 - &scalar2; + assert!(result == result_); + } + + #[test] + fn test_scalar_mul() { + let scalar1 = Scalar::from("02"); + let scalar2 = Scalar::from("03"); + let result = Scalar::from("06"); + let result_ = scalar1 * &scalar2; + assert!(result_ == result); + } + + #[test] + fn test_scalar_mul_zero() { + let scalar1 = Scalar::random(); + let scalar2 = Scalar::new(&SCALAR_ZERO); + let result = scalar1 * &scalar2; + assert!(result.is_zero); + } + + #[test] + fn test_scalar_mul_by_zero() { + let scalar1 = Scalar::new(&SCALAR_ZERO); + let scalar2 = Scalar::random(); + let result = scalar1 * &scalar2; + assert!(result.is_zero); + } + + #[test] + fn test_mul_cmp() { + let a = Scalar::random(); + let b = Scalar::random(); + let mut a_clone = a.clone(); + let c = a_clone.tweak_mul(&b); + let c_ = a * &b; + assert!(*c == c_); + } + + #[test] + fn test_scalar_into_vec() { + let scalar = Scalar::random(); + let bytes: Vec = scalar.clone().into(); + assert_eq!(bytes.len(), 32); + assert!(bytes.iter().any(|&b| b != 0)); // Ensure it's not all zeros + } + + #[test] + fn test_zero_scalar_into_vec() { + let scalar = Scalar::new(&SCALAR_ZERO); + let bytes: Vec = scalar.into(); + assert_eq!(bytes, SCALAR_ZERO.to_vec()); + } + + #[test] + fn test_scalar_into_string() { + let scalar = Scalar::random(); + let hex_str: String = scalar.as_ref().into(); + assert_eq!(hex_str.len(), 64); + assert!(hex::decode(&hex_str).is_ok()); + } + + #[test] + fn test_zero_scalar_into_string() { + let scalar = Scalar::new(&SCALAR_ZERO); + let hex_str: String = scalar.as_ref().into(); + assert_eq!(hex_str, hex::encode(SCALAR_ZERO)); + } + + #[test] + fn test_div2_even() { + let m = Integer::from(29); + let x = Integer::from(20); + assert_eq!(div2(&m, x), Integer::from(10)); + } + + #[test] + fn test_div2_odd() { + let m = Integer::from(29); + let x = Integer::from(21); + assert_eq!(div2(&m, x), Integer::from(25)); + } + + #[test] + fn test_scalar_modular_inversion() { + let one = Scalar::new(&SCALAR_ONE); + let scalar = Scalar::from("deadbeef"); + let mut scalar_inv = scalar.clone(); + scalar_inv.invert(); + let prod = scalar * &scalar_inv; + assert!(one == prod); + } + + #[test] + fn test_ge_from_hex() { + let g = GroupElement::from( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ); + assert!(!g.is_zero) + } + + #[test] + fn test_ge_into() { + let hex_str = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; + let g = GroupElement::from(hex_str); + let g_string: String = g.as_ref().into(); + assert!(hex_str == g_string) + } + + #[test] + fn test_cmp_neq() { + let g1 = GroupElement::from( + "0264f39fbee428ab6165e907b5d463a17e315b9f06f6200ed7e9c4bcbe0df73383", + ); + let g2 = GroupElement::from( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ); + assert!(g1 != g2); + } + + #[test] + fn test_ge_add_mul() { + let g = GroupElement::from( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ); + let scalar_2 = Scalar::from("02"); + let result = g.clone() + &g; + let result_ = g * &scalar_2; + assert!(result == result_) + } + + #[test] + fn test_ge_sub_mul() { + let g = GroupElement::from( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ); + let scalar_2 = Scalar::from("02"); + let result = g.clone() * &scalar_2 - &g; + assert!(result == g) + } +} diff --git a/src/transcript.rs b/src/transcript.rs new file mode 100644 index 0000000..a5c1bac --- /dev/null +++ b/src/transcript.rs @@ -0,0 +1,35 @@ +use merlin::Transcript; + +use crate::secp::{GroupElement, Scalar}; + +pub struct CashuTranscript { + inner: Transcript +} + +impl CashuTranscript { + pub fn new() -> Self { + let inner = Transcript::new(b"Secp256k1_Cashu_"); + CashuTranscript { inner } + } + + pub fn domain_sep(&mut self, message: &[u8]) { + self.inner.append_message(b"dom-sep", message); + } + + pub fn append_element(&mut self, label: &'static [u8], element: &GroupElement) { + let element_bytes_compressed: [u8; 33] = element.into(); + self.inner.append_message(label, &element_bytes_compressed); + } + + pub fn get_challenge(&mut self, label: &'static [u8]) -> Scalar { + let mut challenge: [u8; 32] = [0; 32]; + self.inner.challenge_bytes(label, &mut challenge); + Scalar::new(&challenge) + } +} + +impl AsMut for CashuTranscript { + fn as_mut(&mut self) -> &mut Self { + self + } +} \ No newline at end of file