From 8a3b725f7e974c5b0a502b81f8117308ec781f78 Mon Sep 17 00:00:00 2001 From: Luc Grandin <104393342+GrandinLuc@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:45:11 +0100 Subject: [PATCH 1/6] feat: added confidential transactions, still not tested thorouglhy --- Cargo.lock | 68 ++++++++ Cargo.toml | 10 +- ReadMe.md | 42 +++-- src/address.rs | 41 +++++ src/build-transaction.rs | 77 +++++++-- src/confidential.rs | 230 +++++++++++++++++++++++++ src/lib.rs | 6 + src/{main.rs => node_runner.rs} | 23 +-- src/rpc.rs | 30 ++-- src/serialization/mod.rs | 1 + src/serialization/signature.rs | 52 ++++++ src/transaction.rs | 287 +++++++++++++++++++++++++------- src/transaction_manager.rs | 166 +++++++++--------- 13 files changed, 827 insertions(+), 206 deletions(-) create mode 100644 src/confidential.rs create mode 100644 src/lib.rs rename src/{main.rs => node_runner.rs} (93%) create mode 100644 src/serialization/mod.rs create mode 100644 src/serialization/signature.rs diff --git a/Cargo.lock b/Cargo.lock index 7c42ca9..82521ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -629,6 +629,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bulletproofs" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "012e2e5f88332083bd4235d445ae78081c00b2558443821a9ca5adfe1070073d" +dependencies = [ + "byteorder", + "clear_on_drop", + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "group", + "merlin", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "serde_derive", + "sha3", + "subtle", + "thiserror", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -779,6 +800,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clear_on_drop" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38508a63f4979f0048febc9966fadbd48e5dab31fd0ec6a3f151bbf4a74f7423" +dependencies = [ + "cc", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -990,7 +1020,10 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", + "group", + "rand_core 0.6.4", "rustc_version 0.4.1", + "serde", "subtle", "zeroize", ] @@ -1219,9 +1252,12 @@ dependencies = [ "actix-web", "anyhow", "ark-ff", + "base64 0.22.1", "bincode", + "bulletproofs", "chrono", "clap", + "curve25519-dalek 4.1.3", "disruptor", "ed25519-dalek", "hex", @@ -1229,6 +1265,7 @@ dependencies = [ "k256", "libp2p", "lmdb", + "merlin", "once_cell", "rand 0.9.0", "rand_core 0.9.0", @@ -2106,6 +2143,15 @@ dependencies = [ "signature", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -2581,6 +2627,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "mime" version = "0.3.17" @@ -3635,6 +3693,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 3b21e28..3f05482 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,12 +33,20 @@ bincode = "1.3.3" clap = { version = "4.5.23", features = ["derive"] } once_cell = "1.20.2" tracing-subscriber = "0.3.19" -k256 = { version = "0.13.4", features = ["ecdh"] } +k256 = { version = "0.13.4", features = ["ecdh", "ecdsa"] } rand = "0.9.0" rand_core = "0.9.0" ark-ff = "0.5.0" +base64 = "0.22.1" +bulletproofs = "5.0.0" +merlin = "3.0.0" +curve25519-dalek = "4.1.3" [[bin]] name = "build-transaction" path = "src/build-transaction.rs" + +[[bin]] +name = "run-node" +path = "src/node_runner.rs" diff --git a/ReadMe.md b/ReadMe.md index 20db9f4..acb19b9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -30,7 +30,7 @@ git clone https://github.com/enokiweave/enokiweave # Run the node ```bash -cargo run --bin enokiweave -- --genesis-file-path ./setup/example_genesis_file.json --rpc_port 3001 +cargo run --bin run-node -- --genesis-file-path ./setup/example_genesis_file.json --rpc-port 3001 ``` # Send a transaction (the node needs to be running) @@ -38,22 +38,29 @@ cargo run --bin enokiweave -- --genesis-file-path ./setup/example_genesis_file.j curl -X POST http://localhost:3001 \ -H "Content-Type: application/json" \ -d '{ - "jsonrpc": "2.0", - "method": "submitTransaction", - "params": [ - { - "from": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", - "to": "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201", - "amount": 100, - "public_key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", - "signature": { - "R": "d3b9bc2c6224e1b0d327f83f2fba25b66f58ea7c87c98a90b9f7f99f4e870be4", - "s": "0acefe7c263262675dc07f0f270795cf319bd0bb8734dda8d28f055bfa1aa70f" - }, - "timestamp": 1734345081238, - "id": "19c44707ea1cc53b699190bea179582b2e947bb59d9695da5961b9cc11e7dd93" + "id": 1, + "jsonrpc": "2.0", + "method": "submitTransaction", + "params": [ + { + "amount": { + "Confidential": { + "c1": "Ak9CwY1Hiv5EK4sIZ2RV8kXyp1IPPttFs5TDWWXiUTSV", + "c2": "ApfHlTg0PRpwrsSp+8qInzww07nSexxCOOAcc9hO0Poj", + "range_proof": "lu/2NVw+LHjzeEWNBNkmjxfFClo4oznWHGpQ9SJUKmtsHPPj6i5Y3e/E8sJLkRHLmHpFxbt9gP1sWJL+bLGtQ3Z1u4RmLI/wzlEuQ5GgYzFmbm5z3DrEqaum67lOObBmNH/9nMF92ZOTDt6opUQHrnzQvsoZdvPBGmLaVEHUmTSWDdkh8kgDVqGHb4sqEqBUXuABe7A2XAJMyPV3g+8FAFU7LWn7mBl/aPEUUED4afhgOXtmELkgDrRZmq9jVVsDdu7rNXv+bvR5No3yx511jES0BA9fMvd5h+WbEgk+6gbE0wGY9/TClWAZOQcaKWYS53w/bUwTaHTk4C6R2nSeNkT+XnKlT5fkODS3Nys77Liyoxsp19aiWzzgpk+VFzg/NIQ/ct2f8mTYmALlf+bBEwwbhN6K6mOI9n4jSjBiSH+u2BBTaEWrzkyXKDcn5sUoECQk2pFEjQWSHi1IE8dEE2QptRJc8cWoclYzB3GhX9GSY84yahNLCjfob+xh9EVVwJ+2/2TEZsyiWhi6V2SxUWUNtpK6qmetF+yxZqzoCW3YNbuTBzZSu/fQH0eTGttqfrf0Fy7LVIHDGoSF6Ol6FTJrbI990wC/LIyH9vOp/Cz23IgUpkSyC/8N1EwevCRlzuH84VPSHSPsOrkdf65ISuM6FlUHi0RdUZMKDELxFX6wTk9/zobeZBqO1o3kwiqlYONoRNqZZIRoH/eDTdk/cgy8wjTXmzIrSdCChcUyOoF+ljXjcK5kM5t4rEx6Cx0RytzfDIrpibiDuTtl24X6bT0ANJzV8q+JK3oGR/WxigNZ9aYa2mfz97eXvk3bhuKktmsXYJsqIp0GdObJBaaLCwV9e3M6uOCZuHKuRf2FxphkbadcGgypE5WQy5ZU1UkO" } - ] + }, + "from": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "previous_transaction_id": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "public_key": "04c2a4c3969a1acfd0a67f8881a894f0db3b36f7f1dde0b053b988bf7cff325f6c3129d83b9d6eeb205e3274193b033f106bea8bbc7bdd5f85589070effccbf55e", + "signature": { + "R": "64701503243dd989ac11ce228f2915ff19e5ec3a88247c58cb17a34d19494d8a", + "s": "068775b9e99dde597f8df737a029f29b646637d0a0cca2968f91438196c23499" + }, + "timestamp": 1738941880745, + "to": "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201" + } + ] }' ``` @@ -74,5 +81,6 @@ cargo run --bin build-transaction -- \ --sender 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 \ --recipient 201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ --amount 100 \ ---private-key 0000000000000000000000000000000000000000000000000000000000000000 +--private-key 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff \ +--previous-transaction-id 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 ``` diff --git a/src/address.rs b/src/address.rs index 8ec491c..07e20f1 100644 --- a/src/address.rs +++ b/src/address.rs @@ -1,12 +1,53 @@ use anyhow::Result; +use ark_ff::PrimeField; +use k256::elliptic_curve::ecdh::diffie_hellman; +use k256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey, SecretKey}; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; pub const ZERO_ADDRESS: Address = Address([0; 32]); +const THRESHOLD_FLAG: u8 = 0x80; + +pub struct StealthAddress { + pub ephemeral_public: PublicKey, + pub stealth_public: PublicKey, +} #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Copy)] pub struct Address(pub [u8; 32]); impl Address { + pub fn generate_stealth( + receiver_pub: &PublicKey, + ephemeral_secret_key: &SecretKey, + ) -> Result<(Self, PublicKey)> { + let shared_secret = diffie_hellman( + ephemeral_secret_key.to_nonzero_scalar(), + receiver_pub.as_affine(), + ); + + // Convert shared secret to new public key point + let point = PublicKey::from_sec1_bytes(shared_secret.raw_secret_bytes().as_slice())?; + let stealth_pub = receiver_pub.to_projective() + point.to_projective(); + let stealth_pub = PublicKey::from_affine(stealth_pub.to_affine())?; + + let mut addr = [0u8; 32]; + addr.copy_from_slice(&stealth_pub.to_encoded_point(true).as_bytes()[..32]); + Ok((Self(addr), stealth_pub)) + } + + pub fn is_threshold_group(&self) -> bool { + self.0[0] & THRESHOLD_FLAG != 0 + } + + pub fn to_zk_field(&self) -> Result { + Ok(F::from_be_bytes_mod_order(&self.0)) + } + + pub fn from_commitment(commitment: &[u8; 32]) -> Self { + Self(*commitment) + } + pub fn new(data: [u8; 32]) -> Self { Self(data) } diff --git a/src/build-transaction.rs b/src/build-transaction.rs index 0bd60fc..78e72f9 100644 --- a/src/build-transaction.rs +++ b/src/build-transaction.rs @@ -1,13 +1,22 @@ -use address::Address; +use anyhow::anyhow; use anyhow::Result; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use clap::Parser; -use ed25519_dalek::Signer; -use ed25519_dalek::SigningKey; +use k256::ecdsa::signature::DigestSigner; +use k256::ecdsa::signature::Signer; +use k256::ecdsa::signature::Verifier; +use k256::ecdsa::{Signature, SigningKey}; +use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::SecretKey; use serde_json::json; -use transaction::Transaction; -mod address; -mod transaction; +use enokiweave::address::Address; +use enokiweave::confidential::EncryptedExactAmount; +use enokiweave::transaction::Amount; +use enokiweave::transaction::Transaction; +use enokiweave::transaction::TransactionHash; +use sha2::Digest; +use sha2::Sha256; #[derive(Parser)] #[command(version, about, long_about = None)] @@ -16,13 +25,16 @@ struct Args { private_key: String, #[arg(long)] - amount: u64, + sender: String, #[arg(long)] - sender: String, + amount: u64, #[arg(long)] recipient: String, + + #[arg(long)] + previous_transaction_id: String, } fn main() -> Result<()> { @@ -34,7 +46,8 @@ fn main() -> Result<()> { .try_into() .expect("Private key must be 32 bytes"); - let signing_key = SigningKey::from_bytes(&private_key_array); + let secret_key = SecretKey::from_bytes(&private_key_array.into())?; + let public_key = secret_key.public_key(); // Convert hex addresses to bytes let sender_bytes = hex::decode(args.sender).expect("Invalid sender address hex"); @@ -47,13 +60,36 @@ fn main() -> Result<()> { .try_into() .expect("Recipient address must be 32 bytes"); + let previous_transaction_id_bytes = + hex::decode(args.previous_transaction_id).expect("Invalid previous_transaction_id hex"); + let previous_transaction_id_array: [u8; 32] = previous_transaction_id_bytes + .try_into() + .expect("Previous transaction ID must be 32 bytes"); + + let encrypted = EncryptedExactAmount::encrypt(args.amount, &public_key)?; + let tx = Transaction::new( Address::from(sender_array), Address::from(recipient_array), - args.amount, + Amount::Confidential(encrypted.clone()), + TransactionHash(previous_transaction_id_array), )?; - let signature = signing_key.sign(&tx.calculate_id()?); + // Calculate the message hash + let message = tx.calculate_id()?; + + // Create signing key and sign + let signing_key = SigningKey::from_bytes(&private_key_array.into())?; + let verifying_key = signing_key.verifying_key(); + + // Sign using the finalized message hash + let signature: Signature = signing_key.sign(&message); + let signature_bytes = signature.to_bytes(); + + // Verify the signature + verifying_key + .verify(&message, &signature) + .map_err(|e| anyhow!("Invalid signature: {}", e))?; let json_output = json!({ "jsonrpc": "2.0", @@ -61,15 +97,22 @@ fn main() -> Result<()> { "params": [{ "from": hex::encode(tx.from), "to": hex::encode(tx.to), - "amount": tx.amount, - "public_key": hex::encode(signing_key.verifying_key().as_bytes()), + "amount": { + "Confidential": { + "range_proof": BASE64.encode(encrypted.range_proof.to_bytes()), + "c1": BASE64.encode(encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), + "c2": BASE64.encode(encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) + } + }, + "public_key": hex::encode(verifying_key.to_encoded_point(false).as_bytes()), "signature": { - "R": hex::encode(signature.r_bytes()), - "s": hex::encode(signature.s_bytes()) + "R": hex::encode(&signature_bytes[..32]), + "s": hex::encode(&signature_bytes[32..]) }, + "previous_transaction_id": hex::encode(previous_transaction_id_array), "timestamp": tx.timestamp, - "id": hex::encode(tx.calculate_id()?) - }] + }], + "id": 1 }); println!("{}", serde_json::to_string_pretty(&json_output)?); diff --git a/src/confidential.rs b/src/confidential.rs new file mode 100644 index 0000000..d52ae6c --- /dev/null +++ b/src/confidential.rs @@ -0,0 +1,230 @@ +use anyhow::{anyhow, Result}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use bulletproofs::{BulletproofGens, PedersenGens, RangeProof}; +use k256::elliptic_curve::rand_core::OsRng; +use k256::elliptic_curve::sec1::FromEncodedPoint; +use k256::{ + elliptic_curve::{sec1::ToEncodedPoint, Field}, + ProjectivePoint, PublicKey, SecretKey, +}; +use merlin::Transcript; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +pub struct EncryptedExactAmount { + // ElGamal encryption of exact value + pub c1: ProjectivePoint, // r * G + pub c2: ProjectivePoint, // m * G + r * pub_key + // Range proof to prove value is positive + pub range_proof: RangeProof, +} + +impl Serialize for EncryptedExactAmount { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("EncryptedExactAmount", 3)?; + + // Convert ProjectivePoints to base64-encoded bytes + let c1_bytes = self.c1.to_affine().to_encoded_point(false); + let c2_bytes = self.c2.to_affine().to_encoded_point(false); + + state.serialize_field("c1", &BASE64.encode(c1_bytes))?; + state.serialize_field("c2", &BASE64.encode(c2_bytes))?; + state.serialize_field("range_proof", &self.range_proof)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for EncryptedExactAmount { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + c1: String, + c2: String, + range_proof: String, // Changed from RangeProof to String + } + + let helper = Helper::deserialize(deserializer)?; + + // Convert base64 encoded points back to ProjectivePoint + let c1_bytes = BASE64.decode(helper.c1).map_err(serde::de::Error::custom)?; + let c2_bytes = BASE64.decode(helper.c2).map_err(serde::de::Error::custom)?; + + let c1_point = + k256::EncodedPoint::from_bytes(&c1_bytes).map_err(serde::de::Error::custom)?; + let c2_point = + k256::EncodedPoint::from_bytes(&c2_bytes).map_err(serde::de::Error::custom)?; + + let c1 = Option::from(ProjectivePoint::from_encoded_point(&c1_point)) + .ok_or_else(|| serde::de::Error::custom("Invalid c1 point"))?; + + let c2 = Option::from(ProjectivePoint::from_encoded_point(&c2_point)) + .ok_or_else(|| serde::de::Error::custom("Invalid c2 point"))?; + + // Decode base64 range proof + let range_proof_bytes = BASE64 + .decode(helper.range_proof) + .map_err(serde::de::Error::custom)?; + + // Convert bytes to RangeProof + let range_proof = + RangeProof::from_bytes(&range_proof_bytes).map_err(serde::de::Error::custom)?; + + Ok(EncryptedExactAmount { + c1, + c2, + range_proof, + }) + } +} + +impl EncryptedExactAmount { + pub fn encrypt(amount: u64, public_key: &PublicKey) -> Result { + // Generate random scalar for blinding + let r = k256::Scalar::random(&mut OsRng); + + // Convert amount to scalar + let m = k256::Scalar::from(amount); + + // Base point G + let g = ProjectivePoint::GENERATOR; + + // Encrypt: (r*G, m*G + r*P) + let c1 = g * r; + let c2 = (g * m) + (public_key.to_projective() * r); + + // Create range proof + let pc_gens = PedersenGens::default(); + let bp_gens = BulletproofGens::new(64, 1); + let mut prover_transcript = Transcript::new(b"amount_range_proof"); + + // Convert k256 scalar to curve25519 scalar for bulletproofs + let blinding = curve25519_dalek::scalar::Scalar::random(&mut OsRng); + let (range_proof, _) = RangeProof::prove_single( + &bp_gens, + &pc_gens, + &mut prover_transcript, + amount, + &blinding, + 64, + )?; + + Ok(Self { + c1, + c2, + range_proof, + }) + } + + pub fn decrypt(&self, private_key: &SecretKey) -> Result { + // Convert private key to scalar + let scalar = *private_key.to_nonzero_scalar(); + + // Decrypt: c2 - priv_key * c1 = m*G + let m_point = self.c2 - (self.c1 * scalar); + + let m = find_exact_discrete_log(m_point)?; + Ok(m) + } + pub fn verify_greater_than_u64(&self, value: u64) -> Result { + // Convert u64 to encrypted point using same base point + let g = ProjectivePoint::GENERATOR; + let m = k256::Scalar::from(value); + let value_point = g * m; + + // Subtract from our encrypted value + let diff_c2 = self.c2 - value_point; + + // Convert k256 ProjectivePoint to bytes for range proof + let point_bytes = diff_c2.to_affine().to_encoded_point(false); + let compressed = + curve25519_dalek::ristretto::CompressedRistretto::from_slice(point_bytes.as_bytes())?; + + // Verify range proof + let pc_gens = PedersenGens::default(); + let bp_gens = BulletproofGens::new(64, 1); + + let mut transcript = Transcript::new(b"amount_range_proof"); + self.range_proof + .verify_single(&bp_gens, &pc_gens, &mut transcript, &compressed, 64)?; + + // Compare points using their canonical byte representation + let encoded_diff = diff_c2.to_affine().to_encoded_point(false); + let encoded_identity = ProjectivePoint::IDENTITY + .to_affine() + .to_encoded_point(false); + + let a = encoded_diff.as_bytes(); + let b = encoded_identity.as_bytes(); + + Ok(a > b) // Check if difference is positive + } + + pub fn verify_greater_than(&self, other: &Self) -> Result { + // Subtract encrypted points + let _diff_c1 = self.c1 - other.c1; + let diff_c2 = self.c2 - other.c2; + + // Convert k256 ProjectivePoint to bytes for range proof + let point_bytes = diff_c2.to_affine().to_encoded_point(false); + let compressed = + curve25519_dalek::ristretto::CompressedRistretto::from_slice(point_bytes.as_bytes())?; + + // Verify range proofs + let pc_gens = PedersenGens::default(); + let bp_gens = BulletproofGens::new(64, 1); + + // Verify both range proofs + let mut transcript1 = Transcript::new(b"amount_range_proof"); + self.range_proof + .verify_single(&bp_gens, &pc_gens, &mut transcript1, &compressed, 64)?; + + let mut transcript2 = Transcript::new(b"amount_range_proof"); + other + .range_proof + .verify_single(&bp_gens, &pc_gens, &mut transcript2, &compressed, 64)?; + + // Compare points using their canonical byte representation + let encoded_diff = diff_c2.to_affine().to_encoded_point(false); + let encoded_identity = ProjectivePoint::IDENTITY + .to_affine() + .to_encoded_point(false); + + let a = encoded_diff.as_bytes(); + let b = encoded_identity.as_bytes(); + + Ok(a > b) // Check if difference is positive + } +} + +// Helper function to find exact discrete log for small values +fn find_exact_discrete_log(point: ProjectivePoint) -> Result { + let g = ProjectivePoint::GENERATOR; + + let mut low = 0u64; + let mut high = u64::MAX; + + while low <= high { + let mid = (low + high) / 2; + let scalar = k256::Scalar::from(mid); + let test_point = g * scalar; + + // Compare points using their canonical byte representation + let test_affine = test_point.to_affine().to_encoded_point(false); + let point_affine = point.to_affine().to_encoded_point(false); + + match test_affine.as_bytes().cmp(point_affine.as_bytes()) { + std::cmp::Ordering::Equal => return Ok(mid), + std::cmp::Ordering::Less => low = mid + 1, + std::cmp::Ordering::Greater => high = mid - 1, + } + } + + Err(anyhow!("Could not find exact value")) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..35f7273 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod address; +pub mod confidential; +pub mod rpc; +pub mod serialization; +pub mod transaction; +pub mod transaction_manager; diff --git a/src/main.rs b/src/node_runner.rs similarity index 93% rename from src/main.rs rename to src/node_runner.rs index e3650ac..0e3c79d 100644 --- a/src/main.rs +++ b/src/node_runner.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::Result; use clap::Parser; use libp2p::futures::StreamExt; use libp2p::mdns::tokio::Tokio; @@ -11,23 +11,15 @@ use libp2p::{ mdns::{Behaviour as Mdns, Event as MdnsEvent}, swarm::{SwarmBuilder, SwarmEvent}, }; -use serde::Deserialize; -use std::collections::HashMap; use std::error::Error; use std::sync::Arc; use tcp::tokio::Transport as TokioTransport; use tokio::sync::Mutex; -use tracing::{error, info, trace, warn}; -use transaction_manager::TransactionManager; +use tracing::{info, trace, warn}; -use crate::rpc::run_http_rpc_server; - -mod address; -mod rpc; -mod transaction; -mod transaction_manager; - -const DB_NAME: &'static str = "./local_db/transaction_db"; +use enokiweave::rpc::run_http_rpc_server; +use enokiweave::transaction_manager::GenesisArgs; +use enokiweave::transaction_manager::TransactionManager; #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent")] @@ -52,11 +44,6 @@ enum OutEvent { Mdns(MdnsEvent), } -#[derive(Deserialize)] -pub struct GenesisArgs { - balances: HashMap, -} - #[derive(Parser)] #[command(version, about, long_about = None)] struct Args { diff --git a/src/rpc.rs b/src/rpc.rs index b63a893..7971372 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, Result}; -use ed25519_dalek::VerifyingKey; use serde_json::Value as JsonValue; use std::error::Error; use std::net::SocketAddr; @@ -87,6 +86,7 @@ pub async fn run_http_rpc_server( } } Err(e) => { + error!("{:?}", e); let error_response = serde_json::json!({ "jsonrpc": "2.0", "error": { @@ -117,6 +117,7 @@ pub async fn run_http_rpc_server( } } Err(e) => { + error!("{:?}", e); let error_response = serde_json::json!({ "jsonrpc": "2.0", "error": { @@ -179,15 +180,15 @@ async fn process_single_transaction( let mut manager = transaction_manager.lock().await; match request { - RPCRequest::Transfer(transaction) => { + RPCRequest::Transfer(transaction_request) => { match manager.add_transaction( - transaction.from, - transaction.to, - transaction.amount, - VerifyingKey::from_bytes(&transaction.public_key) - .map_err(|e| anyhow!("Invalid public key: {}", e))?, - transaction.timestamp, - transaction.signature, + transaction_request.from, + transaction_request.to, + transaction_request.amount, + transaction_request.public_key.into(), + transaction_request.timestamp, + transaction_request.signature, + transaction_request.previous_transaction_id, ) { Ok(transaction_id) => { trace!("Transaction added successfully with ID: {}", transaction_id); @@ -196,11 +197,12 @@ async fn process_single_transaction( Err(e) => Err(anyhow!("Error processing transaction: {}", e)), } } - RPCRequest::GetBalance(address) => { - match manager.get_address_balance_and_selfchain_height(address) { - Ok((res, _)) => Ok(res.to_string()), - Err(e) => Err(anyhow!("Error getting balance: {}", e)), - } + RPCRequest::GetBalance(_) => { + // match manager.get_address_balance(address) { + // Ok((res, _)) => Ok(res.to_string()), + // Err(e) => Err(anyhow!("Error getting balance: {}", e)), + // } + todo!() } } } diff --git a/src/serialization/mod.rs b/src/serialization/mod.rs new file mode 100644 index 0000000..4917bfb --- /dev/null +++ b/src/serialization/mod.rs @@ -0,0 +1 @@ +pub mod signature; diff --git a/src/serialization/signature.rs b/src/serialization/signature.rs new file mode 100644 index 0000000..d0c4003 --- /dev/null +++ b/src/serialization/signature.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use k256::ecdsa::Signature; +use serde::de; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[allow(non_snake_case)] +struct SignatureComponents { + R: String, + s: String, +} + +pub fn deserialize_signature<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + // First try to deserialize as SignatureComponents + let result = SignatureComponents::deserialize(deserializer); + + match result { + Ok(components) => { + // Combine R and s into a single 64-byte array + let r_bytes = hex::decode(components.R.trim_start_matches("0x")) + .map_err(|e| de::Error::custom(format!("Invalid R component hex: {}", e)))?; + let s_bytes = hex::decode(components.s.trim_start_matches("0x")) + .map_err(|e| de::Error::custom(format!("Invalid s component hex: {}", e)))?; + + let mut signature_bytes = Vec::with_capacity(64); + signature_bytes.extend_from_slice(&r_bytes); + signature_bytes.extend_from_slice(&s_bytes); + + Signature::try_from(signature_bytes.as_slice()) + .map_err(|e| de::Error::custom(format!("Invalid signature: {}", e))) + } + Err(e) => Err(de::Error::custom(format!( + "Failed to deserialize signature: {}", + e + ))), + } +} + +pub fn serialize_signature(signature: &Signature, serializer: S) -> Result +where + S: serde::Serializer, +{ + let sig_bytes = signature.to_bytes(); + let components = SignatureComponents { + R: hex::encode(&sig_bytes[..32]), + s: hex::encode(&sig_bytes[32..]), + }; + components.serialize(serializer) +} diff --git a/src/transaction.rs b/src/transaction.rs index 219f8cc..a9b58e7 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,80 +1,149 @@ -use crate::address::Address; use anyhow::Result; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use bulletproofs::RangeProof; use chrono::Utc; -use ed25519_dalek::Signature; +use k256::ecdh::diffie_hellman; +use k256::ecdsa::{Signature, VerifyingKey}; +use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint, PublicKey, SecretKey}; use serde::de; use serde::{Deserialize, Deserializer, Serialize}; use sha2::{Digest, Sha256}; +use crate::address::Address; +use crate::confidential::EncryptedExactAmount; +use crate::serialization::signature::{deserialize_signature, serialize_signature}; + +#[derive(Debug, Clone)] +pub struct StealthMetadata { + pub ephemeral_public_key: PublicKey, + pub view_tag: u8, +} + +impl Serialize for StealthMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("StealthMetadata", 2)?; + + // Convert PublicKey to bytes and then to base64 + let key_bytes = self.ephemeral_public_key.to_sec1_bytes(); + let key_base64 = BASE64.encode(key_bytes); + + state.serialize_field("ephemeral_public_key", &key_base64)?; + state.serialize_field("view_tag", &self.view_tag)?; + state.end() + } +} + +// Implement custom deserialization +impl<'de> Deserialize<'de> for StealthMetadata { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + ephemeral_public_key: String, + view_tag: u8, + } + + let helper = Helper::deserialize(deserializer)?; + + // Convert base64 back to PublicKey + let key_bytes = BASE64 + .decode(helper.ephemeral_public_key) + .map_err(serde::de::Error::custom)?; + + let public_key = + PublicKey::from_sec1_bytes(&key_bytes).map_err(serde::de::Error::custom)?; + + Ok(StealthMetadata { + ephemeral_public_key: public_key, + view_tag: helper.view_tag, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StealthTransaction { + pub transaction: Transaction, + pub stealth_metadata: Option, +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct TransactionHash(pub [u8; 32]); +impl From<[u8; 32]> for TransactionHash { + fn from(tx_id: [u8; 32]) -> TransactionHash { + TransactionHash(tx_id) + } +} + +impl AsRef<[u8; 32]> for TransactionHash { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TransactionRequest { #[serde(deserialize_with = "deserialize_hex_to_address")] pub from: Address, #[serde(deserialize_with = "deserialize_hex_to_address")] pub to: Address, - pub amount: u64, - #[serde(deserialize_with = "deserialize_hex_to_bytes")] - pub public_key: [u8; 32], - #[serde(deserialize_with = "deserialize_signature")] + pub amount: Amount, + #[serde( + deserialize_with = "deserialize_hex_to_public_key", + serialize_with = "serialize_public_key" + )] + pub public_key: PublicKey, + #[serde( + deserialize_with = "deserialize_signature", + serialize_with = "serialize_signature" + )] pub signature: Signature, pub timestamp: i64, #[serde(deserialize_with = "deserialize_hex_to_tx_id")] - pub id: TransactionHash, + pub previous_transaction_id: TransactionHash, + pub stealth_metadata: Option, } -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -struct SignatureComponents { - R: String, - s: String, -} - -fn deserialize_signature<'de, D>(deserializer: D) -> Result +fn deserialize_hex_to_public_key<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let components = SignatureComponents::deserialize(deserializer)?; - - #[allow(non_snake_case)] - let R_bytes = hex::decode(components.R).map_err(de::Error::custom)?; - #[allow(non_snake_case)] - let mut R_array = [0u8; 32]; - if R_array.len() != 32 { - return Err(de::Error::custom("Invalid length for R")); - } - R_array.copy_from_slice(&R_bytes); + let s: String = Deserialize::deserialize(deserializer)?; + let s = s.trim_start_matches("0x"); + let bytes = hex::decode(s).map_err(de::Error::custom)?; - let s_bytes = hex::decode(components.s).map_err(de::Error::custom)?; - let mut s_array = [0u8; 32]; - if s_array.len() != 32 { - return Err(de::Error::custom("Invalid length for s")); + // For ECDSA, the public key is 65 bytes (uncompressed) or 33 bytes (compressed) + if bytes.len() == 65 && bytes[0] == 0x04 { + // This is an uncompressed public key + // let key_bytes = &bytes[1..]; // Remove the 0x04 prefix + // println!("{:?}", key_bytes); + PublicKey::from_sec1_bytes(&bytes) + .map_err(|e| de::Error::custom(format!("Invalid public key: {}", e))) + } else if bytes.len() == 33 && (bytes[0] == 0x02 || bytes[0] == 0x03) { + // This is a compressed public key + PublicKey::from_sec1_bytes(&bytes) + .map_err(|e| de::Error::custom(format!("Invalid public key: {}", e))) + } else { + Err(de::Error::custom(format!( + "Invalid public key length: {}", + bytes.len() + ))) } - s_array.copy_from_slice(&s_bytes); - - // Combine R and s into a single 64-byte array - let mut sig_bytes = [0u8; 64]; - sig_bytes[..32].copy_from_slice(&R_array); - sig_bytes[32..].copy_from_slice(&s_array); - - Ok(Signature::from_bytes(&sig_bytes)) } -fn deserialize_hex_to_bytes<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error> +fn serialize_public_key(key: &PublicKey, serializer: S) -> Result where - D: Deserializer<'de>, + S: serde::Serializer, { - let s: String = Deserialize::deserialize(deserializer)?; - let s = s.trim_start_matches("0x"); - let bytes = hex::decode(s).map_err(de::Error::custom)?; - let mut array = [0u8; 32]; - if bytes.len() != 32 { - return Err(de::Error::custom("Invalid length for byte array")); - } - array.copy_from_slice(&bytes); - Ok(array) + let bytes = key.to_encoded_point(false); + let hex_string = hex::encode(bytes.as_bytes()); + serializer.serialize_str(&hex_string) } fn deserialize_hex_to_address<'de, D>(deserializer: D) -> Result @@ -91,6 +160,7 @@ where array.copy_from_slice(&bytes); Ok(Address::from(array)) } + fn deserialize_hex_to_tx_id<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -106,35 +176,140 @@ where Ok(TransactionHash(array)) } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Amount { + Confidential(EncryptedExactAmount), + Public(u64), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Transaction { pub from: Address, pub to: Address, - pub amount: u64, + pub amount: Amount, pub timestamp: i64, + pub previous_transaction_id: TransactionHash, } impl Transaction { - pub fn new(from: Address, to: Address, amount: u64) -> Result { + pub fn new( + from: Address, + to: Address, + amount: Amount, + previous_transaction_id: TransactionHash, + ) -> Result { Ok(Self { from, to, amount, timestamp: Utc::now().timestamp_millis(), + previous_transaction_id, + }) + } + + pub fn new_confidential( + from: Address, + to: Address, + c1: ProjectivePoint, + c2: ProjectivePoint, + range_proof: RangeProof, + previous_transaction_id: TransactionHash, + ) -> Result { + Ok(Self { + from, + to, + amount: Amount::Confidential(EncryptedExactAmount { + c1, + c2, + range_proof, + }), + timestamp: Utc::now().timestamp_millis(), + previous_transaction_id, }) } + pub fn create_stealth( + from: Address, + receiver_pub: &PublicKey, + amount: Amount, + ephemeral_secret: &SecretKey, + previous_transaction_id: TransactionHash, + ) -> Result<(Self, StealthMetadata)> { + // Generate stealth address + let (stealth_address, _) = Address::generate_stealth(receiver_pub, ephemeral_secret)?; + + // Create view tag (first byte of shared secret) + let shared_secret = diffie_hellman( + ephemeral_secret.to_nonzero_scalar(), + receiver_pub.as_affine(), + ); + let view_tag = shared_secret.raw_secret_bytes()[0]; + + // Create the transaction + let transaction = Self { + from, + to: stealth_address, + amount, + timestamp: Utc::now().timestamp_millis(), + previous_transaction_id, + }; + + // Create stealth metadata + let stealth_metadata = StealthMetadata { + ephemeral_public_key: ephemeral_secret.public_key(), + view_tag, + }; + + Ok((transaction, stealth_metadata)) + } + + pub fn scan_stealth( + &self, + metadata: &StealthMetadata, + view_private_key: &SecretKey, + spend_public_key: &PublicKey, + ) -> Result { + // Compute shared secret + let shared_secret = diffie_hellman( + view_private_key.to_nonzero_scalar(), + metadata.ephemeral_public_key.as_affine(), + ); + + // Check view tag + if shared_secret.raw_secret_bytes()[0] != metadata.view_tag { + return Ok(false); + } + + // Compute expected stealth address + let (expected_stealth_address, _) = Address::generate_stealth( + spend_public_key, + &SecretKey::from_bytes(shared_secret.raw_secret_bytes())?, + )?; + + // Check if the transaction's destination matches the computed stealth address + Ok(self.to == expected_stealth_address) + } + pub fn calculate_id(&self) -> Result<[u8; 32]> { let mut hasher = Sha256::new(); - hasher.update(self.amount.to_be_bytes()); hasher.update(&self.from); hasher.update(&self.to); + match &self.amount { + Amount::Confidential(amount) => { + hasher.update(amount.c1.to_affine().to_encoded_point(true).as_bytes()); + hasher.update(amount.c2.to_affine().to_encoded_point(true).as_bytes()); + hasher.update(amount.range_proof.to_bytes()); + } + Amount::Public(amount) => { + hasher.update(amount.to_be_bytes()); + } + } hasher.update(self.timestamp.to_be_bytes()); + hasher.update(&self.previous_transaction_id.0); - let hash = &hasher.finalize()[..]; - - let id: [u8; 32] = hash.try_into().expect("Wrong length"); + let mut res = [0u8; 32]; + res.copy_from_slice(&hasher.finalize()); - Ok(id) + Ok(res) } } diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index b5dc72c..5481ae1 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -1,20 +1,26 @@ use anyhow::{anyhow, Result}; -use ed25519_dalek::Signature; -use ed25519_dalek::VerifyingKey; +use k256::ecdsa::signature::Verifier; +use k256::ecdsa::Signature; +use k256::ecdsa::VerifyingKey; +use k256::PublicKey; use lmdb::Cursor; use lmdb::Database; use lmdb::Environment; use lmdb::Transaction as LmdbTransaction; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use tracing::info; use crate::address::{Address, ZERO_ADDRESS}; +use crate::serialization::signature::{deserialize_signature, serialize_signature}; +use crate::transaction::Amount; use crate::transaction::{Transaction, TransactionHash}; -use crate::GenesisArgs; -use crate::DB_NAME; + +const DB_NAME: &'static str = "./local_db/transaction_db"; static LMDB_ENV: Lazy> = Lazy::new(|| { std::fs::create_dir_all(DB_NAME).expect("Failed to create transaction_db directory"); @@ -28,6 +34,11 @@ static LMDB_ENV: Lazy> = Lazy::new(|| { ) }); +#[derive(Deserialize)] +pub struct GenesisArgs { + pub balances: HashMap, +} + #[derive(Debug, Serialize, Deserialize, Clone)] enum TransactionStatus { Pending, @@ -38,8 +49,11 @@ enum TransactionStatus { #[derive(Debug, Serialize, Deserialize, Clone)] struct TransactionRecord { transaction: Transaction, - previous_transaction_hash: TransactionHash, status: TransactionStatus, + #[serde( + serialize_with = "serialize_signature", + deserialize_with = "deserialize_signature" + )] signature: Signature, } @@ -67,22 +81,21 @@ impl TransactionManager { .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; // Insert each genesis transaction into the database - for (i, (address, amount)) in genesis_args.balances.into_iter().enumerate() { - let mut transaction_id = [0u8; 32]; - let bytes = i.to_be_bytes(); - transaction_id[24..32].copy_from_slice(&bytes); - + for (address, amount) in genesis_args.balances { let transaction = Transaction { from: ZERO_ADDRESS, to: Address::from_hex(&address)?, - amount, + amount: Amount::Public(amount), timestamp: 0, + previous_transaction_id: TransactionHash([0u8; 32]), }; + let genesis_signature = Signature::try_from([1u8; 64].as_ref()) + .map_err(|e| anyhow!("Failed to create genesis signature: {}", e))?; + let transaction_record = TransactionRecord { transaction, - previous_transaction_hash: TransactionHash([0u8; 32]), - signature: Signature::from_bytes(&[0u8; 64]), + signature: genesis_signature, status: TransactionStatus::Confirmed, }; @@ -93,7 +106,7 @@ impl TransactionManager { // Use the transaction ID as the key txn.put( self.db, - &format!("{}:0", &address), + &format!("{}", &address), &serialized_transaction_record, lmdb::WriteFlags::empty(), ) @@ -113,26 +126,31 @@ impl TransactionManager { &mut self, from: Address, to: Address, - amount: u64, - public_key: VerifyingKey, + amount: Amount, + public_key: PublicKey, timestamp: i64, signature: Signature, + previous_transaction_id: TransactionHash, ) -> Result { let transaction = Transaction { from, to, amount, timestamp, + previous_transaction_id, }; - if !Self::is_transaction_valid(transaction, public_key, signature)? { - return Err(anyhow!("Transaction is invalid")); - } - let (balance, selfchain_height_from) = - self.get_address_balance_and_selfchain_height(from)?; - let (_, selfchain_height_to) = self.get_address_balance_and_selfchain_height(to)?; - if balance < amount { - return Err(anyhow!("Unsufficient balance")); + let message = transaction.calculate_id()?; + + let verifying_key = VerifyingKey::from_affine(public_key.as_affine().clone()) + .map_err(|e| anyhow!("Invalid public key: {}", e))?; + + verifying_key + .verify(&message, &signature) + .map_err(|e| anyhow!("Invalid signature: {}", e))?; + + if let Err(err) = self.verify_transaction_chain(&transaction) { + return Err(anyhow!("Insufficient balance: {}", err)); } // write in the DB the transaction to both the recipient and the emitter @@ -145,88 +163,70 @@ impl TransactionManager { .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; // We add the transaction to the sender personal chain - txn.put( - self.db, - &format!("{}:{}", from.as_hex(), selfchain_height_from), - &serialized_tx, - lmdb::WriteFlags::empty(), - ) - .map_err(|e| anyhow!("Failed to put transaction in database: {}", e))?; - - let transaction_id = format!("{}:{}", to.as_hex(), selfchain_height_to); - - // As well as the receiver personal chain - txn.put( - self.db, - &transaction_id, - &serialized_tx, - lmdb::WriteFlags::empty(), - ) - .map_err(|e| anyhow!("Failed to put transaction in database: {}", e))?; + txn.put(self.db, &message, &serialized_tx, lmdb::WriteFlags::empty()) + .map_err(|e| anyhow!("Failed to put transaction in database: {}", e))?; txn.commit()?; info!("Successfully added new transaction"); - Ok(transaction_id) + Ok(hex::encode(message)) } - pub fn get_address_balance_and_selfchain_height( - &mut self, - address: Address, - ) -> Result<(u64, u32)> { - let mut balance: u64 = 0; - + pub fn verify_transaction_chain(&self, transaction_to_verify: &Transaction) -> Result { let reader = self .lmdb_transaction_env .begin_ro_txn() .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; - let mut iterator = 0; + let mut found_last_public_transaction = false; + let mut current_transaction_id = transaction_to_verify.calculate_id()?; + let mut commitments_chain = Vec::::new(); - loop { - let key = format!("{}:{}", address.as_hex(), iterator); - let transaction_bytes = match reader.get(self.db, &key) { + while !found_last_public_transaction { + let transaction_bytes = match reader.get(self.db, ¤t_transaction_id) { Ok(bytes) => bytes, - Err(lmdb::Error::NotFound) => break, + Err(lmdb::Error::NotFound) => { + return Err(anyhow!( + "Transaction not found: {:?}", + current_transaction_id + )) + } Err(e) => return Err(anyhow!("Database error: {}", e)), }; - let transaction: Transaction = bincode::deserialize(transaction_bytes) + let transaction_record: TransactionRecord = bincode::deserialize(transaction_bytes) .map_err(|e| anyhow!("Failed to deserialize transaction: {}", e))?; - if transaction.from == address { - if balance < transaction.amount { - return Err(anyhow!( - "Balance underflow detected for address: {}", - address.as_hex() - )); + match transaction_record.transaction.amount { + Amount::Public(_amount) => { + commitments_chain.push(transaction_record.transaction.amount); + found_last_public_transaction = true; + } + Amount::Confidential(ref _confidential) => { + let tx_record = transaction_to_verify.calculate_id()?; + current_transaction_id = tx_record; + commitments_chain.push(transaction_record.transaction.amount); } - balance -= transaction.amount; - } else if transaction.to == address { - balance += transaction.amount; - } else { - return Err(anyhow!( - "Transaction {} does not have the address being checked as either sender or receiver", - key - )); } - iterator += 1; } - Ok((balance, iterator)) - } - - pub fn is_transaction_valid( - transaction: Transaction, - public_key: VerifyingKey, - signature: Signature, - ) -> Result { - let transaction_id = transaction.calculate_id()?; - - public_key - .verify_strict(&transaction_id, &signature) - .map_err(|e| anyhow!("Signature verification failed: {}", e))?; + // Verify balance consistency between consecutive transactions + for window in commitments_chain.windows(2) { + match (&window[0], &window[1]) { + (Amount::Confidential(current), Amount::Confidential(previous)) => { + if !¤t.verify_greater_than(&previous)? { + return Ok(false); + } + } + (Amount::Confidential(current), Amount::Public(previous)) => { + if !current.verify_greater_than_u64(*previous)? { + return Ok(false); + } + } + _ => continue, + } + } Ok(true) } From 707e2c1bbf795e6917f1a512ea89962d17d13c8d Mon Sep 17 00:00:00 2001 From: Luc Grandin <104393342+GrandinLuc@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:35:56 +0100 Subject: [PATCH 2/6] feat: first transaction after genesis working fine --- ReadMe.md | 12 ++++++------ src/transaction_manager.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index acb19b9..22a8127 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -45,19 +45,19 @@ curl -X POST http://localhost:3001 \ { "amount": { "Confidential": { - "c1": "Ak9CwY1Hiv5EK4sIZ2RV8kXyp1IPPttFs5TDWWXiUTSV", - "c2": "ApfHlTg0PRpwrsSp+8qInzww07nSexxCOOAcc9hO0Poj", - "range_proof": "lu/2NVw+LHjzeEWNBNkmjxfFClo4oznWHGpQ9SJUKmtsHPPj6i5Y3e/E8sJLkRHLmHpFxbt9gP1sWJL+bLGtQ3Z1u4RmLI/wzlEuQ5GgYzFmbm5z3DrEqaum67lOObBmNH/9nMF92ZOTDt6opUQHrnzQvsoZdvPBGmLaVEHUmTSWDdkh8kgDVqGHb4sqEqBUXuABe7A2XAJMyPV3g+8FAFU7LWn7mBl/aPEUUED4afhgOXtmELkgDrRZmq9jVVsDdu7rNXv+bvR5No3yx511jES0BA9fMvd5h+WbEgk+6gbE0wGY9/TClWAZOQcaKWYS53w/bUwTaHTk4C6R2nSeNkT+XnKlT5fkODS3Nys77Liyoxsp19aiWzzgpk+VFzg/NIQ/ct2f8mTYmALlf+bBEwwbhN6K6mOI9n4jSjBiSH+u2BBTaEWrzkyXKDcn5sUoECQk2pFEjQWSHi1IE8dEE2QptRJc8cWoclYzB3GhX9GSY84yahNLCjfob+xh9EVVwJ+2/2TEZsyiWhi6V2SxUWUNtpK6qmetF+yxZqzoCW3YNbuTBzZSu/fQH0eTGttqfrf0Fy7LVIHDGoSF6Ol6FTJrbI990wC/LIyH9vOp/Cz23IgUpkSyC/8N1EwevCRlzuH84VPSHSPsOrkdf65ISuM6FlUHi0RdUZMKDELxFX6wTk9/zobeZBqO1o3kwiqlYONoRNqZZIRoH/eDTdk/cgy8wjTXmzIrSdCChcUyOoF+ljXjcK5kM5t4rEx6Cx0RytzfDIrpibiDuTtl24X6bT0ANJzV8q+JK3oGR/WxigNZ9aYa2mfz97eXvk3bhuKktmsXYJsqIp0GdObJBaaLCwV9e3M6uOCZuHKuRf2FxphkbadcGgypE5WQy5ZU1UkO" + "c1": "AkTUULAgsOWfzGOn3tSf3KTCrkkLq6xNdRXE3bHCpHHZ", + "c2": "A998vn77x3DbtMrLo6T1Ab5gy7ZvSePSt4zlEZ6yF11P", + "range_proof": "ctrMObmFsTvJIdBYMBcqc9hRXjY7p1VNa6syukqOo3FSEWsl5WCV1Qh4cmFsB6/Cl+kXbeaaUk/L1ORuMBEoP55PjwKHmTlQ6E5I//m5cyQ/+Q8S4/3UrkLlHkVFjo857IpnfZ3tgzEP8SRnaX00th1nOK0P/bHAEQxtQv1bDQPkpsLxQhSjLQ7DKrejdOL8oYdfuTgGJ6P5wlan6YWWBuKCdHpUBb1UYYtS7vlUqLLD1CtK5Gj52XKZ6v3D5mkCk5uGsTDzQCUNEEwzcXXXVKjaWLONIBJdQutpiTW3Twksw56vwOrMLjhpbeWYVOxOA+eA03YLmwmr4edag2lUQvbdzLRgwgKe7XOB7TKy9EY0NPETY+Pn7VTjaHWWm/dNgtfzL2sbyD5aefWBWJ0ETt9og2lUHwcexWx020fZ1COcNX/x7SwnP3xAJx7HAR+t+vaVJFfX8wvKoz+VpjXiSw7mYxX/wL7U2Kmmpfu2f3Ff1hHIkO8IUO5OW0nWyl1CHAEtWnI65a/abZJnS6OFgRWTQZWAPb7nt/I4poGcnFUer+gsYqZrIlTbEWL+6DQ/xaMTG0btTpWSwEKgP3U+ItCqTdVL+w1kQc6hIp8AwHoYJnHrR/3RIAimiqGCTbVxWN4Ha+zDCYTlUQhKNG+xd+9L6H4ygPwjK7O8mADlDRRyCDxD7/xxu4mXbKammh94kqqV74jt4evGLqG21YBbLIYRGAK6J7zqgq1/aEOcCB/ME4w464pV6B5f20F2141m5IJV2zAIPjNjrro6LuoIormnv96dOg9ynHy7zTQMY2DmB9GUid6+mykEMq7dkdn0iE6qe5mVy2dMfcCJ79CHDQvZfVR7AfKblphexD5ZQCmQfpIcCgseyPRU1RPsXoED" } }, "from": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "previous_transaction_id": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "public_key": "04c2a4c3969a1acfd0a67f8881a894f0db3b36f7f1dde0b053b988bf7cff325f6c3129d83b9d6eeb205e3274193b033f106bea8bbc7bdd5f85589070effccbf55e", "signature": { - "R": "64701503243dd989ac11ce228f2915ff19e5ec3a88247c58cb17a34d19494d8a", - "s": "068775b9e99dde597f8df737a029f29b646637d0a0cca2968f91438196c23499" + "R": "5a702a3abc0d2238f5c7a88d28c8ebfb42d9b2ca007c2a4059ce9e031f96f857", + "s": "6a3ae950a830882f88ef47d6b4e0b8cd4039d682de28b12769f43435f81889df" }, - "timestamp": 1738941880745, + "timestamp": 1738943005293, "to": "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201" } ] diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index 5481ae1..fdad95d 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -106,7 +106,7 @@ impl TransactionManager { // Use the transaction ID as the key txn.put( self.db, - &format!("{}", &address), + &Address::from_hex(&address)?.0, &serialized_transaction_record, lmdb::WriteFlags::empty(), ) @@ -180,7 +180,7 @@ impl TransactionManager { .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; let mut found_last_public_transaction = false; - let mut current_transaction_id = transaction_to_verify.calculate_id()?; + let mut current_transaction_id = transaction_to_verify.previous_transaction_id.0; let mut commitments_chain = Vec::::new(); while !found_last_public_transaction { From 751231edfd81c11dbd07a37c9e40af2880351899 Mon Sep 17 00:00:00 2001 From: Luc Grandin <104393342+GrandinLuc@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:13:25 +0100 Subject: [PATCH 3/6] feat: preventing duplicate transaction from being processed --- src/transaction_manager.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index fdad95d..0e763f6 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -140,13 +140,26 @@ impl TransactionManager { previous_transaction_id, }; - let message = transaction.calculate_id()?; + let id = transaction.calculate_id()?; + + let reader = self + .lmdb_transaction_env + .begin_ro_txn() + .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; + + match reader.get(self.db, &id) { + Ok(_) => return Err(anyhow!("Transaction already exists in DB")), + Err(lmdb::Error::NotFound) => {} + Err(e) => return Err(anyhow!("Database error: {}", e)), + }; + + reader.abort(); let verifying_key = VerifyingKey::from_affine(public_key.as_affine().clone()) .map_err(|e| anyhow!("Invalid public key: {}", e))?; verifying_key - .verify(&message, &signature) + .verify(&id, &signature) .map_err(|e| anyhow!("Invalid signature: {}", e))?; if let Err(err) = self.verify_transaction_chain(&transaction) { @@ -162,15 +175,14 @@ impl TransactionManager { .begin_rw_txn() .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; - // We add the transaction to the sender personal chain - txn.put(self.db, &message, &serialized_tx, lmdb::WriteFlags::empty()) + txn.put(self.db, &id, &serialized_tx, lmdb::WriteFlags::empty()) .map_err(|e| anyhow!("Failed to put transaction in database: {}", e))?; txn.commit()?; info!("Successfully added new transaction"); - Ok(hex::encode(message)) + Ok(hex::encode(id)) } pub fn verify_transaction_chain(&self, transaction_to_verify: &Transaction) -> Result { From f5ac8e889b76223dac97abb2a12ce1a0531e45b9 Mon Sep 17 00:00:00 2001 From: Luc Grandin <104393342+GrandinLuc@users.noreply.github.com> Date: Sat, 8 Feb 2025 00:26:11 +0100 Subject: [PATCH 4/6] feat: added quorum and recipient amount encryption to the transaction --- ReadMe.md | 26 +++++++--- src/build-transaction.rs | 98 +++++++++++++++++++++++++++++--------- src/confidential.rs | 14 ++++++ src/transaction.rs | 77 +++++++++++++++++++++++++----- src/transaction_manager.rs | 22 +++++++-- 5 files changed, 193 insertions(+), 44 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 22a8127..9d3ac68 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -45,20 +45,32 @@ curl -X POST http://localhost:3001 \ { "amount": { "Confidential": { - "c1": "AkTUULAgsOWfzGOn3tSf3KTCrkkLq6xNdRXE3bHCpHHZ", - "c2": "A998vn77x3DbtMrLo6T1Ab5gy7ZvSePSt4zlEZ6yF11P", - "range_proof": "ctrMObmFsTvJIdBYMBcqc9hRXjY7p1VNa6syukqOo3FSEWsl5WCV1Qh4cmFsB6/Cl+kXbeaaUk/L1ORuMBEoP55PjwKHmTlQ6E5I//m5cyQ/+Q8S4/3UrkLlHkVFjo857IpnfZ3tgzEP8SRnaX00th1nOK0P/bHAEQxtQv1bDQPkpsLxQhSjLQ7DKrejdOL8oYdfuTgGJ6P5wlan6YWWBuKCdHpUBb1UYYtS7vlUqLLD1CtK5Gj52XKZ6v3D5mkCk5uGsTDzQCUNEEwzcXXXVKjaWLONIBJdQutpiTW3Twksw56vwOrMLjhpbeWYVOxOA+eA03YLmwmr4edag2lUQvbdzLRgwgKe7XOB7TKy9EY0NPETY+Pn7VTjaHWWm/dNgtfzL2sbyD5aefWBWJ0ETt9og2lUHwcexWx020fZ1COcNX/x7SwnP3xAJx7HAR+t+vaVJFfX8wvKoz+VpjXiSw7mYxX/wL7U2Kmmpfu2f3Ff1hHIkO8IUO5OW0nWyl1CHAEtWnI65a/abZJnS6OFgRWTQZWAPb7nt/I4poGcnFUer+gsYqZrIlTbEWL+6DQ/xaMTG0btTpWSwEKgP3U+ItCqTdVL+w1kQc6hIp8AwHoYJnHrR/3RIAimiqGCTbVxWN4Ha+zDCYTlUQhKNG+xd+9L6H4ygPwjK7O8mADlDRRyCDxD7/xxu4mXbKammh94kqqV74jt4evGLqG21YBbLIYRGAK6J7zqgq1/aEOcCB/ME4w464pV6B5f20F2141m5IJV2zAIPjNjrro6LuoIormnv96dOg9ynHy7zTQMY2DmB9GUid6+mykEMq7dkdn0iE6qe5mVy2dMfcCJ79CHDQvZfVR7AfKblphexD5ZQCmQfpIcCgseyPRU1RPsXoED" + "quorum": { + "c1": "A81XNukTrG8hCg3UR42jxZOuto32so09ygcug49EZOaL", + "c2": "ArqdwyfIa1fWPOl5Tbmgi99fs3QWWJb2+PIrzwbSiB38", + "range_proof": "/p3dnwZa3zrq8dmoNTkp+2nIDgkijwYs91pt7WQTn39SRPSyH+2MquBx5azDIP7qs0X+meq9Ru+3z/un4NgoKpSnfBuqORnVJHjbG0mtFywdXkgNeiSVQxP2Z6fTMNQz0lhVwSdZRLEahu6CFfvfhn3/UhO0jWQyUojJz9zZm3gqYHYk510IqvNYFiwYne3CgVrsbcypmov8hO7pU0jaDYJhgbQJOdV2cNCB0HSOT4mwBWec46NfjfBSuT3OjxkB+ruITQ8wFQ4Z6Zr5JSRHRB2naT4U5/umWkfDFK2IgQTMiGf3tmJJFNTEgtT21RHwdeecfsht3JstHYglgUtUbUTzL6f1+TJjfLeQVZtzih1GOQB1gdI53y9W5hmh0skq0Dd0bdiOkZiW3JUUAowo0/zNkLtYV3DUHljCIf9gAzPYWKVpTLNFnUgqKG50SGwmyQU8QSu4eD8nr+c5iyx5Zw5yiWY7LHPgJfBNENARnbbFk2mY3HvYCLv5qiDQemwPzC6Ol0POALLJeW80zUlE+yMzMIJHArnQ2wswUR3ITwf829O5+U532a60+jqFOPtJbD78OOUK6BLtFTkuu528Z3Lbau0ylaHV9hLd1k9KZR3ROXYvQL45KE2jkuBIEp0PYsH/2A+ab4ha6Kpv5/FJ2o5wZhWC68DNEndR5z15LmxSMeGUyRQqKonXSxn9QE/O1ilFQ506mXrCt0xkEhsnXmApHU/3q7rgtc6XhPIFapYeBBTVvtUYFMp5cg83GBZKbAQsg3prvKxiRTi65bEAKAf67lAy+6ZQazG9svQw93uzqla1lJv5pMI9JSuHnNT/9LYgOevNkWQpjpko1QTQDbFCV+2DSoQ7SM3XvvNfDbWgu2gmQK7/jan3gQRgGDkL" + }, + "recipient": { + "c1": "AmZUcbkVn00B13l/PAuhSdnCl2e1hV1zekO6KFjEa2Os", + "c2": "AnV7SKWnnkaNoqw2AsdZkI6iLTRYno9XXpu+1Qasr//+", + "range_proof": "/A7PWNkQhRxFdOcmP126QcvECCGnyKpTx0S7j7MShFzSPB5DRojAJLw5Vh3D3Slu68Ru+Ke9Csm7FGBKLShVVWwaN00kvGvkk5QhQD27kh6PlF9SoLTG8OM3yjrLnUZFpllhstCknawOGGGgb1UOATqpd6itnsgqN/CpKnOpb1C7Q8f5GiKR8DWuWGKiavqRL6c1DizE7+rYaKVcf05CCMlko7okHJUCMLCB8yLqiEmhxF1KtoaF/ImXlGJSSLEE3wAKqHHM28OOpRoZj3RNpMdD5gTVgdUlbHeQaXi1pwlKzvzC2oYNo+Es/6Su+nAYGnatyFgb+2DOOTSKNNWmWOZV3rUumaH3bcXaJ3d6xyOsrZwOE2F8zO3eJ/RVNsd1ajqKOAm0bfBPEcCT2OBFPNPIEOmoTmfRGi01UW1AwjlMHtsJ49I2bYJhS5gL1WD92ihw0j4DLs0okEBVveupeES8iDTI5Npnu34OfUjMYOndHyNA5vbP5LZMbIgFMyQcdrFpp5+xKk+UkLgLCGxZ7cBuJAnKKkDAwoA/T16ITgtGKb9pLKnSjPaGZG25v1/uUF1tcwxCA36YE5ayGyUka6JtkO8Tv1tDgk4PnDNWJGuwayWIozTJJ+avslGNBN0F3GWvY5nC+aoCyZdqIMgdDfwKXUNGw8KNBKOwQRFsvTAgw4FW403jcQpOJ1F9hbEm4snu05YLU5XlKjK17CbIMrp0eJN0ScFz06LBEGaTbfE/Wl6k2epAee5WEqqi21BvgPFNX6Z7IIgmAlSCj2aJRH7BlLYjpiKbB4Ac33afeQeRhg6c1RY0gKil/9aG0peHUoM5wSaFIFSQ8v3s9aF6AllOF54+VVnTiXz29af0zrte7qxYMwFL6MzactGcPzoN" + }, + "sender": { + "c1": "Avpd1MdI4mwBfMLdyyDR9Qr3BDFELiKvApX3gcMDgDjy", + "c2": "AhRI67yjl9CWoZCbsHT34lFCokgOg5tQglnmLSnDB2Pw", + "range_proof": "wnkgzlnj9klbHG9qIvJA9mUnGCIjK2inL/8LeWxzRQSg8vbFxuW4KPcWEGGsqbxWrPFtxYx3pbss1uDhyy18c2yG2vZ5shPo0vxS4dhXxR959F5daLdulObvDfX9DQE/tHdaOu/qpBElEBicm+bae+31KA+ehHfnbXJJtZ68HyXfFoxdEMUdQZQUZf5WBjQjwcUVI1Zj/eKAXgGp8pixA/wAFzK7808YBzN4jUq+W4nC9IFyLKQg8O1pM/M1dNoKyo5NSIG6MgC3OKnDt/ZWb4HRUHwZUB+eJhNNi9jOOAM27QS7wp8ZqEp4xZyU8uVh4IIy5Wijk7ONVGGd4GxuX0Inyk1X94FETxeXoXM9WWitCCLMnZ7KG32R91qNJx9VLqxLgGLK0hFnQcK4xBnGf54kMBqel+sb7flphscPMiNs9MEfXCH2B1LnylwqHuqu8m/ixCC8xuZcGZDLP3ZRWeZoVaNY4otKmt0EhNIkqoCXXl4uOly5eYko0+1tQE9Vavi0eSqOTxwoVSMmLtvVc/meCawrR2/lVmYKuM740XMcxMQSGonK2ZLYjoOBZPOTJUGkijK5DsL6+LTXuwcIb5xNQxpAzXTDbAOOUfr15cK1kpP+h9kPIwbJrwGTjN8KzAWs/qf3K704ktBTqhZAa0uZtIvBwGLe3wfP47enU0xoSMCTB0jfjkIFXhkQgmLGW8rFFbJ0OEomzmsRDSrcXWhsrkt6gfwgyll5YRTDnUr38DuAnkeryqwHDq3Jk3J6uIoTiGOFQsFJ+d2vCepwcuzXeqM2uCk33+eSnRPLqTi+4RUbrJjWT5b3xpOM0pRWmlRGxLZ5CclVqzXeJCLwC5vrptMXScggMDS98qtEWFDxIPB8kY/WWiUjGCb/CAEF" + } } }, "from": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "previous_transaction_id": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "public_key": "04c2a4c3969a1acfd0a67f8881a894f0db3b36f7f1dde0b053b988bf7cff325f6c3129d83b9d6eeb205e3274193b033f106bea8bbc7bdd5f85589070effccbf55e", "signature": { - "R": "5a702a3abc0d2238f5c7a88d28c8ebfb42d9b2ca007c2a4059ce9e031f96f857", - "s": "6a3ae950a830882f88ef47d6b4e0b8cd4039d682de28b12769f43435f81889df" + "R": "44152312c43cac7f0f3d6be1e72c9e746166d291d42f9f64e3b2a257ad1fb49f", + "s": "325bd5f08af004e2533f8ba80fc27370399e5767f28422683d17dc49718ac750" }, - "timestamp": 1738943005293, - "to": "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201" + "timestamp": 1738970684326, + "to": "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020f" } ] }' diff --git a/src/build-transaction.rs b/src/build-transaction.rs index 78e72f9..43599f9 100644 --- a/src/build-transaction.rs +++ b/src/build-transaction.rs @@ -1,12 +1,15 @@ use anyhow::anyhow; +use anyhow::Context; use anyhow::Result; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use clap::Parser; +use enokiweave::transaction::EncryptedAmountProofs; use k256::ecdsa::signature::DigestSigner; use k256::ecdsa::signature::Signer; use k256::ecdsa::signature::Verifier; use k256::ecdsa::{Signature, SigningKey}; use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::PublicKey; use k256::SecretKey; use serde_json::json; @@ -41,55 +44,92 @@ fn main() -> Result<()> { let args = Args::parse(); // Convert hex private key to bytes - let private_key_bytes = hex::decode(args.private_key).expect("Invalid private key hex"); + let private_key_bytes = hex::decode(&args.private_key) + .with_context(|| format!("Failed to decode private key hex: {}", args.private_key))?; let private_key_array: [u8; 32] = private_key_bytes .try_into() - .expect("Private key must be 32 bytes"); + .map_err(|_| anyhow!("Private key must be exactly 32 bytes"))?; - let secret_key = SecretKey::from_bytes(&private_key_array.into())?; + let secret_key = SecretKey::from_bytes(&private_key_array.into()) + .context("Failed to create secret key from bytes")?; let public_key = secret_key.public_key(); // Convert hex addresses to bytes - let sender_bytes = hex::decode(args.sender).expect("Invalid sender address hex"); + let sender_bytes = hex::decode(&args.sender) + .with_context(|| format!("Failed to decode sender address hex: {}", args.sender))?; let sender_array: [u8; 32] = sender_bytes .try_into() - .expect("Sender address must be 32 bytes"); + .map_err(|_| anyhow!("Sender address must be exactly 32 bytes"))?; - let recipient_bytes = hex::decode(args.recipient).expect("Invalid recipient address hex"); + let recipient_bytes = hex::decode(&args.recipient) + .with_context(|| format!("Failed to decode recipient address hex: {}", args.recipient))?; let recipient_array: [u8; 32] = recipient_bytes .try_into() - .expect("Recipient address must be 32 bytes"); + .map_err(|_| anyhow!("Recipient address must be exactly 32 bytes"))?; let previous_transaction_id_bytes = - hex::decode(args.previous_transaction_id).expect("Invalid previous_transaction_id hex"); + hex::decode(&args.previous_transaction_id).with_context(|| { + format!( + "Failed to decode previous transaction ID hex: {}", + args.previous_transaction_id + ) + })?; let previous_transaction_id_array: [u8; 32] = previous_transaction_id_bytes .try_into() - .expect("Previous transaction ID must be 32 bytes"); - - let encrypted = EncryptedExactAmount::encrypt(args.amount, &public_key)?; + .map_err(|_| anyhow!("Previous transaction ID must be exactly 32 bytes"))?; + + let sender_encrypted = EncryptedExactAmount::encrypt(args.amount, &public_key) + .context("Failed to encrypt amount for sender")?; + let recipient_encrypted = EncryptedExactAmount::encrypt( + args.amount, + &PublicKey::from_sec1_bytes( + &[0x02] + .iter() + .chain(recipient_array.iter()) + .copied() + .collect::>(), + ) + .context("Failed to create recipient public key")?, + ) + .context("Failed to encrypt amount for recipient")?; + + // Quorum encryption + let quorum_public_key = PublicKey::from_sec1_bytes(&[ + 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, + 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, + 0xF8, 0x17, 0x98, + ]) + .context("Failed to create quorum public key")?; + + let quorum_encrypted = EncryptedExactAmount::encrypt(args.amount, &quorum_public_key) + .context("Failed to encrypt amount for quorum")?; let tx = Transaction::new( Address::from(sender_array), Address::from(recipient_array), - Amount::Confidential(encrypted.clone()), + Amount::Confidential(EncryptedAmountProofs { + sender: sender_encrypted.clone(), + recipient: recipient_encrypted.clone(), + quorum: quorum_encrypted.clone(), + }), TransactionHash(previous_transaction_id_array), - )?; + ) + .context("Failed to create transaction")?; - // Calculate the message hash - let message = tx.calculate_id()?; + let message = tx + .calculate_id() + .context("Failed to calculate transaction ID")?; - // Create signing key and sign - let signing_key = SigningKey::from_bytes(&private_key_array.into())?; + let signing_key = SigningKey::from_bytes(&private_key_array.into()) + .context("Failed to create signing key")?; let verifying_key = signing_key.verifying_key(); - // Sign using the finalized message hash let signature: Signature = signing_key.sign(&message); let signature_bytes = signature.to_bytes(); - // Verify the signature verifying_key .verify(&message, &signature) - .map_err(|e| anyhow!("Invalid signature: {}", e))?; + .context("Signature verification failed")?; let json_output = json!({ "jsonrpc": "2.0", @@ -99,9 +139,21 @@ fn main() -> Result<()> { "to": hex::encode(tx.to), "amount": { "Confidential": { - "range_proof": BASE64.encode(encrypted.range_proof.to_bytes()), - "c1": BASE64.encode(encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), - "c2": BASE64.encode(encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) + "sender": { + "range_proof": BASE64.encode(sender_encrypted.range_proof.to_bytes()), + "c1": BASE64.encode(sender_encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), + "c2": BASE64.encode(sender_encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) + }, + "recipient": { + "range_proof": BASE64.encode(recipient_encrypted.range_proof.to_bytes()), + "c1": BASE64.encode(recipient_encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), + "c2": BASE64.encode(recipient_encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) + }, + "quorum": { + "range_proof": BASE64.encode(quorum_encrypted.range_proof.to_bytes()), + "c1": BASE64.encode(quorum_encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), + "c2": BASE64.encode(quorum_encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) + } } }, "public_key": hex::encode(verifying_key.to_encoded_point(false).as_bytes()), diff --git a/src/confidential.rs b/src/confidential.rs index d52ae6c..13bb993 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -201,6 +201,20 @@ impl EncryptedExactAmount { Ok(a > b) // Check if difference is positive } + + pub fn verify_equal(&self, other: &Self) -> Result { + // Subtract encrypted points + let diff_c1 = self.c1 - other.c1; + let diff_c2 = self.c2 - other.c2; + + // Convert points to their affine form and compare against identity point + let identity = ProjectivePoint::IDENTITY.to_affine(); + let c1_equals = diff_c1.to_affine() == identity; + let c2_equals = diff_c2.to_affine() == identity; + + // Amounts are equal if both c1 and c2 differences are zero + Ok(c1_equals && c2_equals) + } } // Helper function to find exact discrete log for small values diff --git a/src/transaction.rs b/src/transaction.rs index a9b58e7..18b59fa 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -176,9 +176,16 @@ where Ok(TransactionHash(array)) } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedAmountProofs { + pub sender: EncryptedExactAmount, + pub recipient: EncryptedExactAmount, + pub quorum: EncryptedExactAmount, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Amount { - Confidential(EncryptedExactAmount), + Confidential(EncryptedAmountProofs), Public(u64), } @@ -210,18 +217,18 @@ impl Transaction { pub fn new_confidential( from: Address, to: Address, - c1: ProjectivePoint, - c2: ProjectivePoint, - range_proof: RangeProof, + sender: EncryptedExactAmount, + recipient: EncryptedExactAmount, + quorum: EncryptedExactAmount, previous_transaction_id: TransactionHash, ) -> Result { Ok(Self { from, to, - amount: Amount::Confidential(EncryptedExactAmount { - c1, - c2, - range_proof, + amount: Amount::Confidential(EncryptedAmountProofs { + sender, + recipient, + quorum, }), timestamp: Utc::now().timestamp_millis(), previous_transaction_id, @@ -296,9 +303,57 @@ impl Transaction { hasher.update(&self.to); match &self.amount { Amount::Confidential(amount) => { - hasher.update(amount.c1.to_affine().to_encoded_point(true).as_bytes()); - hasher.update(amount.c2.to_affine().to_encoded_point(true).as_bytes()); - hasher.update(amount.range_proof.to_bytes()); + hasher.update( + amount + .sender + .c1 + .to_affine() + .to_encoded_point(true) + .as_bytes(), + ); + hasher.update( + amount + .sender + .c2 + .to_affine() + .to_encoded_point(true) + .as_bytes(), + ); + hasher.update(amount.sender.range_proof.to_bytes()); + hasher.update( + amount + .recipient + .c1 + .to_affine() + .to_encoded_point(true) + .as_bytes(), + ); + hasher.update( + amount + .recipient + .c2 + .to_affine() + .to_encoded_point(true) + .as_bytes(), + ); + hasher.update(amount.recipient.range_proof.to_bytes()); + hasher.update( + amount + .quorum + .c1 + .to_affine() + .to_encoded_point(true) + .as_bytes(), + ); + hasher.update( + amount + .quorum + .c2 + .to_affine() + .to_encoded_point(true) + .as_bytes(), + ); + hasher.update(amount.quorum.range_proof.to_bytes()); } Amount::Public(amount) => { hasher.update(amount.to_be_bytes()); diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index 0e763f6..5098ef3 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -135,7 +135,7 @@ impl TransactionManager { let transaction = Transaction { from, to, - amount, + amount: amount.clone(), timestamp, previous_transaction_id, }; @@ -162,6 +162,22 @@ impl TransactionManager { .verify(&id, &signature) .map_err(|e| anyhow!("Invalid signature: {}", e))?; + match amount { + Amount::Public(_) => {} + Amount::Confidential(encrypted_amount_proofs) => { + encrypted_amount_proofs + .sender + .verify_equal(&encrypted_amount_proofs.recipient)?; + encrypted_amount_proofs + .sender + .verify_equal(&encrypted_amount_proofs.quorum)?; + } + } + + if let Err(err) = self.verify_transaction_chain(&transaction) { + return Err(anyhow!("Insufficient balance: {}", err)); + } + if let Err(err) = self.verify_transaction_chain(&transaction) { return Err(anyhow!("Insufficient balance: {}", err)); } @@ -227,12 +243,12 @@ impl TransactionManager { for window in commitments_chain.windows(2) { match (&window[0], &window[1]) { (Amount::Confidential(current), Amount::Confidential(previous)) => { - if !¤t.verify_greater_than(&previous)? { + if !¤t.sender.verify_greater_than(&previous.sender)? { return Ok(false); } } (Amount::Confidential(current), Amount::Public(previous)) => { - if !current.verify_greater_than_u64(*previous)? { + if !current.sender.verify_greater_than_u64(*previous)? { return Ok(false); } } From 2e9f9c5b62d1d13b2464c4e9c9c10fc27c73d9a6 Mon Sep 17 00:00:00 2001 From: Luc Grandin <104393342+GrandinLuc@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:31:20 +0100 Subject: [PATCH 5/6] chore: removed stealth addresses related code because out of scope for that feature --- src/address.rs | 1 - src/build-transaction.rs | 3 - src/rpc.rs | 16 ++--- src/transaction.rs | 129 +------------------------------------ src/transaction_manager.rs | 1 - 5 files changed, 8 insertions(+), 142 deletions(-) diff --git a/src/address.rs b/src/address.rs index 07e20f1..ee922e3 100644 --- a/src/address.rs +++ b/src/address.rs @@ -3,7 +3,6 @@ use ark_ff::PrimeField; use k256::elliptic_curve::ecdh::diffie_hellman; use k256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey, SecretKey}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; pub const ZERO_ADDRESS: Address = Address([0; 32]); const THRESHOLD_FLAG: u8 = 0x80; diff --git a/src/build-transaction.rs b/src/build-transaction.rs index 43599f9..b59b249 100644 --- a/src/build-transaction.rs +++ b/src/build-transaction.rs @@ -4,7 +4,6 @@ use anyhow::Result; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use clap::Parser; use enokiweave::transaction::EncryptedAmountProofs; -use k256::ecdsa::signature::DigestSigner; use k256::ecdsa::signature::Signer; use k256::ecdsa::signature::Verifier; use k256::ecdsa::{Signature, SigningKey}; @@ -18,8 +17,6 @@ use enokiweave::confidential::EncryptedExactAmount; use enokiweave::transaction::Amount; use enokiweave::transaction::Transaction; use enokiweave::transaction::TransactionHash; -use sha2::Digest; -use sha2::Sha256; #[derive(Parser)] #[command(version, about, long_about = None)] diff --git a/src/rpc.rs b/src/rpc.rs index 7971372..7a1639e 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot, Mutex}; -use tracing::{error, info, trace, warn}; +use tracing::{error, info, trace}; use crate::address::Address; use crate::transaction::TransactionRequest; @@ -14,7 +14,7 @@ use crate::transaction_manager::TransactionManager; enum RPCRequest { Transfer(TransactionRequest), - GetBalance(Address), + GetBalance, } struct QueuedTransaction { @@ -197,12 +197,8 @@ async fn process_single_transaction( Err(e) => Err(anyhow!("Error processing transaction: {}", e)), } } - RPCRequest::GetBalance(_) => { - // match manager.get_address_balance(address) { - // Ok((res, _)) => Ok(res.to_string()), - // Err(e) => Err(anyhow!("Error getting balance: {}", e)), - // } - todo!() + RPCRequest::GetBalance => { + todo!("Fix manager.get_address_balance(address)") } } } @@ -252,13 +248,13 @@ async fn handle_rpc_request( .as_str() .ok_or_else(|| "Invalid params - expected str")?; - let address = Address::from_hex(params)?; + let _address = Address::from_hex(params)?; // Create response channel let (response_sender, response_receiver) = oneshot::channel(); // Create a special transaction request for balance query let queued_tx = QueuedTransaction { - request: RPCRequest::GetBalance(address), + request: RPCRequest::GetBalance, response_sender, }; diff --git a/src/transaction.rs b/src/transaction.rs index 18b59fa..17d5c6c 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,10 +1,7 @@ use anyhow::Result; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; -use bulletproofs::RangeProof; use chrono::Utc; -use k256::ecdh::diffie_hellman; -use k256::ecdsa::{Signature, VerifyingKey}; -use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint, PublicKey, SecretKey}; +use k256::ecdsa::Signature; +use k256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; use serde::de; use serde::{Deserialize, Deserializer, Serialize}; use sha2::{Digest, Sha256}; @@ -13,65 +10,6 @@ use crate::address::Address; use crate::confidential::EncryptedExactAmount; use crate::serialization::signature::{deserialize_signature, serialize_signature}; -#[derive(Debug, Clone)] -pub struct StealthMetadata { - pub ephemeral_public_key: PublicKey, - pub view_tag: u8, -} - -impl Serialize for StealthMetadata { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut state = serializer.serialize_struct("StealthMetadata", 2)?; - - // Convert PublicKey to bytes and then to base64 - let key_bytes = self.ephemeral_public_key.to_sec1_bytes(); - let key_base64 = BASE64.encode(key_bytes); - - state.serialize_field("ephemeral_public_key", &key_base64)?; - state.serialize_field("view_tag", &self.view_tag)?; - state.end() - } -} - -// Implement custom deserialization -impl<'de> Deserialize<'de> for StealthMetadata { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper { - ephemeral_public_key: String, - view_tag: u8, - } - - let helper = Helper::deserialize(deserializer)?; - - // Convert base64 back to PublicKey - let key_bytes = BASE64 - .decode(helper.ephemeral_public_key) - .map_err(serde::de::Error::custom)?; - - let public_key = - PublicKey::from_sec1_bytes(&key_bytes).map_err(serde::de::Error::custom)?; - - Ok(StealthMetadata { - ephemeral_public_key: public_key, - view_tag: helper.view_tag, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StealthTransaction { - pub transaction: Transaction, - pub stealth_metadata: Option, -} - #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct TransactionHash(pub [u8; 32]); @@ -107,7 +45,6 @@ pub struct TransactionRequest { pub timestamp: i64, #[serde(deserialize_with = "deserialize_hex_to_tx_id")] pub previous_transaction_id: TransactionHash, - pub stealth_metadata: Option, } fn deserialize_hex_to_public_key<'de, D>(deserializer: D) -> Result @@ -235,68 +172,6 @@ impl Transaction { }) } - pub fn create_stealth( - from: Address, - receiver_pub: &PublicKey, - amount: Amount, - ephemeral_secret: &SecretKey, - previous_transaction_id: TransactionHash, - ) -> Result<(Self, StealthMetadata)> { - // Generate stealth address - let (stealth_address, _) = Address::generate_stealth(receiver_pub, ephemeral_secret)?; - - // Create view tag (first byte of shared secret) - let shared_secret = diffie_hellman( - ephemeral_secret.to_nonzero_scalar(), - receiver_pub.as_affine(), - ); - let view_tag = shared_secret.raw_secret_bytes()[0]; - - // Create the transaction - let transaction = Self { - from, - to: stealth_address, - amount, - timestamp: Utc::now().timestamp_millis(), - previous_transaction_id, - }; - - // Create stealth metadata - let stealth_metadata = StealthMetadata { - ephemeral_public_key: ephemeral_secret.public_key(), - view_tag, - }; - - Ok((transaction, stealth_metadata)) - } - - pub fn scan_stealth( - &self, - metadata: &StealthMetadata, - view_private_key: &SecretKey, - spend_public_key: &PublicKey, - ) -> Result { - // Compute shared secret - let shared_secret = diffie_hellman( - view_private_key.to_nonzero_scalar(), - metadata.ephemeral_public_key.as_affine(), - ); - - // Check view tag - if shared_secret.raw_secret_bytes()[0] != metadata.view_tag { - return Ok(false); - } - - // Compute expected stealth address - let (expected_stealth_address, _) = Address::generate_stealth( - spend_public_key, - &SecretKey::from_bytes(shared_secret.raw_secret_bytes())?, - )?; - - // Check if the transaction's destination matches the computed stealth address - Ok(self.to == expected_stealth_address) - } - pub fn calculate_id(&self) -> Result<[u8; 32]> { let mut hasher = Sha256::new(); hasher.update(&self.from); diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index 5098ef3..1b15dbf 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -9,7 +9,6 @@ use lmdb::Environment; use lmdb::Transaction as LmdbTransaction; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; From 241c13a82dbe530a051ca54f6ff149e026d83c07 Mon Sep 17 00:00:00 2001 From: Luc Grandin <104393342+GrandinLuc@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:57:13 +0100 Subject: [PATCH 6/6] feat: fixed confidential and public transactions --- Cargo.lock | 15 + Cargo.toml | 5 +- ReadMe.md | 8 +- src/build-transaction.rs | 185 ++------ src/confidential.rs | 112 +++++ src/lib.rs | 4 + src/rpc.rs | 8 +- src/transaction.rs | 942 +++++++++++++++++++++++++++++-------- src/transaction_builder.rs | 337 +++++++++++++ src/transaction_hash.rs | 16 + src/transaction_manager.rs | 150 +++--- src/transaction_request.rs | 98 ++++ src/utils.rs | 18 + 13 files changed, 1466 insertions(+), 432 deletions(-) create mode 100644 src/transaction_builder.rs create mode 100644 src/transaction_hash.rs create mode 100644 src/transaction_request.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 82521ff..eb722ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,6 +1168,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", + "serdect", "signature", "spki", ] @@ -1232,6 +1233,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", + "serdect", "subtle", "zeroize", ] @@ -1263,6 +1265,7 @@ dependencies = [ "hex", "hyper", "k256", + "lazy_static", "libp2p", "lmdb", "merlin", @@ -2139,6 +2142,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", + "serdect", "sha2", "signature", ] @@ -3602,6 +3606,7 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] @@ -3671,6 +3676,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 3f05482..5d55f7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ bincode = "1.3.3" clap = { version = "4.5.23", features = ["derive"] } once_cell = "1.20.2" tracing-subscriber = "0.3.19" -k256 = { version = "0.13.4", features = ["ecdh", "ecdsa"] } +k256 = { version = "0.13.4", features = ["ecdh", "ecdsa", "serde"] } rand = "0.9.0" rand_core = "0.9.0" ark-ff = "0.5.0" @@ -41,6 +41,7 @@ base64 = "0.22.1" bulletproofs = "5.0.0" merlin = "3.0.0" curve25519-dalek = "4.1.3" +lazy_static = "1.5.0" [[bin]] @@ -49,4 +50,4 @@ path = "src/build-transaction.rs" [[bin]] name = "run-node" -path = "src/node_runner.rs" +path = "src/node_runner.rs" \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index 9d3ac68..d5c29d6 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -11,7 +11,7 @@ Enokiweave is a block-lattice cryptocurrency designed for sub-second transaction ## How It Works -Enokiweave uses a block-lattice structure where each account operates its own blockchain. This allows for: +Enokiweave uses a directed acyclic graph structure which allows for: - Immediate transaction processing without global consensus bottlenecks - Parallel validation of transactions across different account chains @@ -90,9 +90,7 @@ curl -X POST http://localhost:3001 \ # Build a transaction that you can send via a JSON-RPC request ```bash cargo run --bin build-transaction -- \ ---sender 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 \ ---recipient 201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ +--recipient-pubkey 04e29636fe0c4c8a9971407f70e957708d6c98dc23ae91a5426e18f3bca7c4f3cf8d86f89b7091202f9a3168a254eaa29ab882157132c4cb91c218e3dba76c1b16 \ --amount 100 \ ---private-key 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff \ ---previous-transaction-id 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 +--inputs 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff:0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 ``` diff --git a/src/build-transaction.rs b/src/build-transaction.rs index b59b249..9690975 100644 --- a/src/build-transaction.rs +++ b/src/build-transaction.rs @@ -1,170 +1,65 @@ -use anyhow::anyhow; -use anyhow::Context; -use anyhow::Result; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use anyhow::{anyhow, Result}; use clap::Parser; -use enokiweave::transaction::EncryptedAmountProofs; -use k256::ecdsa::signature::Signer; -use k256::ecdsa::signature::Verifier; -use k256::ecdsa::{Signature, SigningKey}; -use k256::elliptic_curve::sec1::ToEncodedPoint; -use k256::PublicKey; -use k256::SecretKey; -use serde_json::json; -use enokiweave::address::Address; -use enokiweave::confidential::EncryptedExactAmount; -use enokiweave::transaction::Amount; -use enokiweave::transaction::Transaction; -use enokiweave::transaction::TransactionHash; +use enokiweave::transaction_builder; #[derive(Parser)] #[command(version, about, long_about = None)] struct Args { #[arg(long)] - private_key: String, + amount: u64, #[arg(long)] - sender: String, + /// Type of input: "public" or "confidential" + input_type: String, #[arg(long)] - amount: u64, + /// Type of output: "public" or "confidential" + output_type: String, + + #[arg(long)] + /// Hex spend key mapping to hex public key of the UTXO, format is "spend_key:public_key" separated by a semicolon + /// Required for confidential inputs + input_keys_by_id: Option, + + /// Hex recipient public key + /// Required for confidential outputs + #[arg(long)] + recipient_pubkey: Option, + /// Address for public inputs + /// Required for public inputs #[arg(long)] - recipient: String, + input_address: Option, + /// Address for public outputs + /// Required for public outputs #[arg(long)] - previous_transaction_id: String, + output_address: Option, } fn main() -> Result<()> { let args = Args::parse(); - // Convert hex private key to bytes - let private_key_bytes = hex::decode(&args.private_key) - .with_context(|| format!("Failed to decode private key hex: {}", args.private_key))?; - let private_key_array: [u8; 32] = private_key_bytes - .try_into() - .map_err(|_| anyhow!("Private key must be exactly 32 bytes"))?; - - let secret_key = SecretKey::from_bytes(&private_key_array.into()) - .context("Failed to create secret key from bytes")?; - let public_key = secret_key.public_key(); - - // Convert hex addresses to bytes - let sender_bytes = hex::decode(&args.sender) - .with_context(|| format!("Failed to decode sender address hex: {}", args.sender))?; - let sender_array: [u8; 32] = sender_bytes - .try_into() - .map_err(|_| anyhow!("Sender address must be exactly 32 bytes"))?; - - let recipient_bytes = hex::decode(&args.recipient) - .with_context(|| format!("Failed to decode recipient address hex: {}", args.recipient))?; - let recipient_array: [u8; 32] = recipient_bytes - .try_into() - .map_err(|_| anyhow!("Recipient address must be exactly 32 bytes"))?; - - let previous_transaction_id_bytes = - hex::decode(&args.previous_transaction_id).with_context(|| { - format!( - "Failed to decode previous transaction ID hex: {}", - args.previous_transaction_id - ) - })?; - let previous_transaction_id_array: [u8; 32] = previous_transaction_id_bytes - .try_into() - .map_err(|_| anyhow!("Previous transaction ID must be exactly 32 bytes"))?; + // Validate input/output types + if !["public", "confidential"].contains(&args.input_type.as_str()) { + return Err(anyhow!("Input type must be either 'public' or 'confidential'")); + } + if !["public", "confidential"].contains(&args.output_type.as_str()) { + return Err(anyhow!("Output type must be either 'public' or 'confidential'")); + } - let sender_encrypted = EncryptedExactAmount::encrypt(args.amount, &public_key) - .context("Failed to encrypt amount for sender")?; - let recipient_encrypted = EncryptedExactAmount::encrypt( + let transaction = transaction_builder::build_transaction( args.amount, - &PublicKey::from_sec1_bytes( - &[0x02] - .iter() - .chain(recipient_array.iter()) - .copied() - .collect::>(), - ) - .context("Failed to create recipient public key")?, - ) - .context("Failed to encrypt amount for recipient")?; - - // Quorum encryption - let quorum_public_key = PublicKey::from_sec1_bytes(&[ - 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, - 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, - 0xF8, 0x17, 0x98, - ]) - .context("Failed to create quorum public key")?; - - let quorum_encrypted = EncryptedExactAmount::encrypt(args.amount, &quorum_public_key) - .context("Failed to encrypt amount for quorum")?; - - let tx = Transaction::new( - Address::from(sender_array), - Address::from(recipient_array), - Amount::Confidential(EncryptedAmountProofs { - sender: sender_encrypted.clone(), - recipient: recipient_encrypted.clone(), - quorum: quorum_encrypted.clone(), - }), - TransactionHash(previous_transaction_id_array), - ) - .context("Failed to create transaction")?; - - let message = tx - .calculate_id() - .context("Failed to calculate transaction ID")?; - - let signing_key = SigningKey::from_bytes(&private_key_array.into()) - .context("Failed to create signing key")?; - let verifying_key = signing_key.verifying_key(); - - let signature: Signature = signing_key.sign(&message); - let signature_bytes = signature.to_bytes(); - - verifying_key - .verify(&message, &signature) - .context("Signature verification failed")?; - - let json_output = json!({ - "jsonrpc": "2.0", - "method": "submitTransaction", - "params": [{ - "from": hex::encode(tx.from), - "to": hex::encode(tx.to), - "amount": { - "Confidential": { - "sender": { - "range_proof": BASE64.encode(sender_encrypted.range_proof.to_bytes()), - "c1": BASE64.encode(sender_encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), - "c2": BASE64.encode(sender_encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) - }, - "recipient": { - "range_proof": BASE64.encode(recipient_encrypted.range_proof.to_bytes()), - "c1": BASE64.encode(recipient_encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), - "c2": BASE64.encode(recipient_encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) - }, - "quorum": { - "range_proof": BASE64.encode(quorum_encrypted.range_proof.to_bytes()), - "c1": BASE64.encode(quorum_encrypted.c1.to_affine().to_encoded_point(true).as_bytes()), - "c2": BASE64.encode(quorum_encrypted.c2.to_affine().to_encoded_point(true).as_bytes()) - } - } - }, - "public_key": hex::encode(verifying_key.to_encoded_point(false).as_bytes()), - "signature": { - "R": hex::encode(&signature_bytes[..32]), - "s": hex::encode(&signature_bytes[32..]) - }, - "previous_transaction_id": hex::encode(previous_transaction_id_array), - "timestamp": tx.timestamp, - }], - "id": 1 - }); - - println!("{}", serde_json::to_string_pretty(&json_output)?); + &args.input_type, + &args.output_type, + args.input_keys_by_id.as_deref(), + args.recipient_pubkey.as_deref(), + args.input_address.as_deref(), + args.output_address.as_deref(), + )?; + + println!("{}", serde_json::to_string_pretty(&transaction)?); Ok(()) } diff --git a/src/confidential.rs b/src/confidential.rs index 13bb993..cd43a9c 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -1,14 +1,21 @@ use anyhow::{anyhow, Result}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use bulletproofs::{BulletproofGens, PedersenGens, RangeProof}; +use k256::elliptic_curve::group::GroupEncoding; use k256::elliptic_curve::rand_core::OsRng; use k256::elliptic_curve::sec1::FromEncodedPoint; +use k256::elliptic_curve::PrimeField; +use k256::Scalar; use k256::{ elliptic_curve::{sec1::ToEncodedPoint, Field}, ProjectivePoint, PublicKey, SecretKey, }; use merlin::Transcript; use serde::{Deserialize, Serialize}; +use sha2::digest::generic_array::{GenericArray, typenum}; +use sha2::{Digest, Sha256}; + +use crate::utils::hash_to_curve; #[derive(Debug, Clone)] pub struct EncryptedExactAmount { @@ -242,3 +249,108 @@ fn find_exact_discrete_log(point: ProjectivePoint) -> Result { Err(anyhow!("Could not find exact value")) } + +#[derive(Debug, Clone)] +pub struct ShieldedAmount { + // Pedersen commitment to amount + pub value_commitment: ProjectivePoint, + // Bulletproof range proof + pub range_proof: RangeProof, + // Commitment to spending key + pub spending_key_commitment: ProjectivePoint, + // Nullifier for preventing double-spends + pub nullifier: ProjectivePoint, + // Encrypted amount for recipient + pub encrypted_amount: EncryptedExactAmount, +} + +impl ShieldedAmount { + pub fn new( + amount: u64, + spending_key: &SecretKey, + recipient_key: &PublicKey, + blinding: Scalar, + ) -> Result { + // Create Pedersen commitment to amount + let value_commitment = commit_amount(amount, blinding)?; + + // Create range proof + let pc_gens = PedersenGens::default(); + let bp_gens = BulletproofGens::new(64, 1); + let mut prover_transcript = Transcript::new(b"amount_range_proof"); + let bp_blinding = curve25519_dalek::scalar::Scalar::random(&mut OsRng); + let (range_proof, _) = RangeProof::prove_single( + &bp_gens, + &pc_gens, + &mut prover_transcript, + amount, + &bp_blinding, + 64, + )?; + + + // Create spending key commitment + let spending_key_commitment = + ProjectivePoint::GENERATOR * (*spending_key.to_nonzero_scalar()); + + // Create nullifier + let nullifier = create_nullifier(spending_key, value_commitment)?; + + // Encrypt amount for recipient + let encrypted_amount = EncryptedExactAmount::encrypt(amount, recipient_key)?; + + Ok(Self { + value_commitment, + range_proof, + spending_key_commitment, + nullifier, + encrypted_amount, + }) + } + + pub fn verify(&self) -> Result { + // Verify range proof + let mut transcript = Transcript::new(b"shielded_amount"); + let pc_gens = PedersenGens::default(); + let bp_gens = BulletproofGens::new(64, 1); + + // Convert commitment to CompressedRistretto + let point_bytes = self.value_commitment.to_affine().to_encoded_point(false); + let compressed = + curve25519_dalek::ristretto::CompressedRistretto::from_slice(point_bytes.as_bytes())?; + + self.range_proof + .verify_single(&bp_gens, &pc_gens, &mut transcript, &compressed, 64)?; + + // No need to verify nullifier construction as it's checked during spend + Ok(true) + } +} + +pub fn commit_amount(amount: u64, blinding: Scalar) -> Result { + let g = ProjectivePoint::GENERATOR; + // Use second generator point H for Pedersen commitments + let h_bytes = [ + 0x02, 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, + 0x7a, 0x5e, 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, + 0x80, 0x3a, 0xc0, + ]; + let h: ProjectivePoint = Option::from(ProjectivePoint::from_bytes(&h_bytes.into())) + .ok_or_else(|| anyhow!("Invalid H generator point"))?; + Ok(g * Scalar::from(amount) + h * blinding) +} + +pub fn create_nullifier( + spending_key: &SecretKey, + commitment: ProjectivePoint, +) -> Result { + let mut hasher = Sha256::new(); + hasher.update(spending_key.to_bytes()); + // Use compressed point format which is always 33 bytes, take first 32 + let commitment_bytes = commitment.to_affine().to_encoded_point(true); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&commitment_bytes.as_bytes()[1..33]); + let array: GenericArray = GenericArray::clone_from_slice(&bytes); + let hash = hash_to_curve(array)?; + Ok(hash * *spending_key.to_nonzero_scalar()) +} diff --git a/src/lib.rs b/src/lib.rs index 35f7273..c99ebd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,4 +3,8 @@ pub mod confidential; pub mod rpc; pub mod serialization; pub mod transaction; +pub mod transaction_builder; +pub mod transaction_hash; pub mod transaction_manager; +pub mod transaction_request; +pub mod utils; diff --git a/src/rpc.rs b/src/rpc.rs index 7a1639e..28928e6 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -9,8 +9,8 @@ use tokio::sync::{mpsc, oneshot, Mutex}; use tracing::{error, info, trace}; use crate::address::Address; -use crate::transaction::TransactionRequest; use crate::transaction_manager::TransactionManager; +use crate::transaction_request::TransactionRequest; enum RPCRequest { Transfer(TransactionRequest), @@ -182,10 +182,8 @@ async fn process_single_transaction( match request { RPCRequest::Transfer(transaction_request) => { match manager.add_transaction( - transaction_request.from, - transaction_request.to, - transaction_request.amount, - transaction_request.public_key.into(), + transaction_request.inputs, + transaction_request.outputs, transaction_request.timestamp, transaction_request.signature, transaction_request.previous_transaction_id, diff --git a/src/transaction.rs b/src/transaction.rs index 17d5c6c..950e645 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,245 +1,809 @@ +use anyhow::anyhow; use anyhow::Result; use chrono::Utc; +use curve25519_dalek::ristretto::CompressedRistretto; +use curve25519_dalek::scalar::Scalar as DalekScalar; use k256::ecdsa::Signature; -use k256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; -use serde::de; -use serde::{Deserialize, Deserializer, Serialize}; +use k256::elliptic_curve::group::GroupEncoding; +use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::elliptic_curve::PrimeField; +use k256::PublicKey; +use k256::{ProjectivePoint, Scalar, SecretKey}; +use lazy_static::lazy_static; +use serde::de::{self, MapAccess, Visitor}; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Serialize}; +use sha2::digest::generic_array::GenericArray; use sha2::{Digest, Sha256}; +use std::fmt; use crate::address::Address; use crate::confidential::EncryptedExactAmount; -use crate::serialization::signature::{deserialize_signature, serialize_signature}; +use crate::confidential::ShieldedAmount; +use crate::confidential::commit_amount; +use crate::transaction_hash::TransactionHash; +use crate::utils::hash_to_curve; +use bulletproofs::RangeProof as BulletproofRangeProof; -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct TransactionHash(pub [u8; 32]); +lazy_static! { + static ref QUORUM_KEY: PublicKey = PublicKey::from_sec1_bytes(&[0; 32]).expect("Invalid key"); +} + +#[derive(Debug, Clone)] +pub struct MerkleProof { + pub path: Vec<(ProjectivePoint, bool)>, + pub root: ProjectivePoint, + pub leaf_index: u64, +} -impl From<[u8; 32]> for TransactionHash { - fn from(tx_id: [u8; 32]) -> TransactionHash { - TransactionHash(tx_id) +impl Serialize for MerkleProof { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("MerkleProof", 3)?; + let path: Vec<(String, bool)> = self + .path + .iter() + .map(|(point, is_right)| (hex::encode(point.to_bytes()), *is_right)) + .collect(); + state.serialize_field("path", &path)?; + state.serialize_field("root", &hex::encode(self.root.to_bytes()))?; + state.serialize_field("leaf_index", &self.leaf_index)?; + state.end() } } -impl AsRef<[u8; 32]> for TransactionHash { - fn as_ref(&self) -> &[u8; 32] { - &self.0 +impl<'de> Deserialize<'de> for MerkleProof { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct MerkleProofVisitor; + + impl<'de> Visitor<'de> for MerkleProofVisitor { + type Value = MerkleProof; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct MerkleProof") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let path_hex: Vec<(String, bool)> = map + .next_entry::<&str, Vec<(String, bool)>>() + .and_then(|opt| opt.ok_or_else(|| de::Error::invalid_length(0, &self)))? + .1; + let path = path_hex + .into_iter() + .map(|(point_hex, is_right)| { + let point_bytes = hex::decode(&point_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let point = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&point_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid point in path"))?; + Ok((point, is_right)) + }) + .collect::, _>>()?; + + let root_hex = map + .next_entry::<&str, String>() + .and_then(|opt| opt.ok_or_else(|| de::Error::invalid_length(1, &self)))? + .1; + let root_bytes = hex::decode(&root_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let root = Option::from(ProjectivePoint::from_bytes(GenericArray::from_slice( + &root_bytes, + ))) + .ok_or_else(|| de::Error::custom("Invalid root point"))?; + + let leaf_index = map + .next_entry::<&str, u64>() + .and_then(|opt| opt.ok_or_else(|| de::Error::invalid_length(2, &self)))? + .1; + + Ok(MerkleProof { + path, + root, + leaf_index, + }) + } + } + + const FIELDS: &[&str] = &["path", "root", "leaf_index"]; + deserializer.deserialize_struct("MerkleProof", FIELDS, MerkleProofVisitor) } } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct TransactionRequest { - #[serde(deserialize_with = "deserialize_hex_to_address")] - pub from: Address, - #[serde(deserialize_with = "deserialize_hex_to_address")] - pub to: Address, - pub amount: Amount, - #[serde( - deserialize_with = "deserialize_hex_to_public_key", - serialize_with = "serialize_public_key" - )] - pub public_key: PublicKey, - #[serde( - deserialize_with = "deserialize_signature", - serialize_with = "serialize_signature" - )] - pub signature: Signature, - pub timestamp: i64, - #[serde(deserialize_with = "deserialize_hex_to_tx_id")] - pub previous_transaction_id: TransactionHash, +#[derive(Debug, Clone)] +pub struct SpendingProof { + pub nullifier_commitment: ProjectivePoint, + pub value_commitment: ProjectivePoint, + pub proof: BulletproofRangeProof, } -fn deserialize_hex_to_public_key<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let s = s.trim_start_matches("0x"); - let bytes = hex::decode(s).map_err(de::Error::custom)?; - - // For ECDSA, the public key is 65 bytes (uncompressed) or 33 bytes (compressed) - if bytes.len() == 65 && bytes[0] == 0x04 { - // This is an uncompressed public key - // let key_bytes = &bytes[1..]; // Remove the 0x04 prefix - // println!("{:?}", key_bytes); - PublicKey::from_sec1_bytes(&bytes) - .map_err(|e| de::Error::custom(format!("Invalid public key: {}", e))) - } else if bytes.len() == 33 && (bytes[0] == 0x02 || bytes[0] == 0x03) { - // This is a compressed public key - PublicKey::from_sec1_bytes(&bytes) - .map_err(|e| de::Error::custom(format!("Invalid public key: {}", e))) - } else { - Err(de::Error::custom(format!( - "Invalid public key length: {}", - bytes.len() - ))) - } -} - -fn serialize_public_key(key: &PublicKey, serializer: S) -> Result -where - S: serde::Serializer, -{ - let bytes = key.to_encoded_point(false); - let hex_string = hex::encode(bytes.as_bytes()); - serializer.serialize_str(&hex_string) -} - -fn deserialize_hex_to_address<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let s = s.trim_start_matches("0x"); - let bytes = hex::decode(s).map_err(de::Error::custom)?; - let mut array = [0u8; 32]; - if bytes.len() != 32 { - return Err(de::Error::custom("Invalid length for byte array")); - } - array.copy_from_slice(&bytes); - Ok(Address::from(array)) -} - -fn deserialize_hex_to_tx_id<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let s = s.trim_start_matches("0x"); - let bytes = hex::decode(s).map_err(de::Error::custom)?; - let mut array = [0u8; 32]; - if bytes.len() != 32 { - return Err(de::Error::custom("Invalid length for byte array")); - } - array.copy_from_slice(&bytes); - Ok(TransactionHash(array)) +impl Serialize for SpendingProof { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("SpendingProof", 3)?; + + state.serialize_field( + "nullifier_commitment", + &hex::encode(self.nullifier_commitment.to_bytes()), + )?; + state.serialize_field( + "value_commitment", + &hex::encode(self.value_commitment.to_bytes()), + )?; + state.serialize_field("proof", &self.proof)?; + + state.end() + } +} + +impl<'de> Deserialize<'de> for SpendingProof { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SpendingProofVisitor; + + impl<'de> Visitor<'de> for SpendingProofVisitor { + type Value = SpendingProof; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct SpendingProof") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let nullifier_commitment_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(0, &self))? + .1; + let nullifier_commitment_bytes = hex::decode(&nullifier_commitment_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let nullifier_commitment = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&nullifier_commitment_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid nullifier commitment point"))?; + + let value_commitment_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(1, &self))? + .1; + let value_commitment_bytes = hex::decode(&value_commitment_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let value_commitment = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&value_commitment_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid value commitment point"))?; + + let proof = map + .next_entry::<&str, BulletproofRangeProof>()? + .ok_or_else(|| de::Error::invalid_length(2, &self))? + .1; + + Ok(SpendingProof { + nullifier_commitment, + value_commitment, + proof, + }) + } + } + + const FIELDS: &[&str] = &["nullifier_commitment", "value_commitment", "proof"]; + deserializer.deserialize_struct("SpendingProof", FIELDS, SpendingProofVisitor) + } +} + +#[derive(Debug, Clone)] +pub struct BalanceProof { + pub input_sum_commitment: ProjectivePoint, + pub output_sum_commitment: ProjectivePoint, + pub proof: BulletproofRangeProof, +} + +impl Serialize for BalanceProof { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("BalanceProof", 3)?; + state.serialize_field( + "input_sum_commitment", + &hex::encode(self.input_sum_commitment.to_bytes()), + )?; + state.serialize_field( + "output_sum_commitment", + &hex::encode(self.output_sum_commitment.to_bytes()), + )?; + state.serialize_field("proof", &self.proof)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for BalanceProof { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct BalanceProofVisitor; + + impl<'de> Visitor<'de> for BalanceProofVisitor { + type Value = BalanceProof; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct BalanceProof") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let input_sum_commitment_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(0, &self))? + .1; + let input_sum_commitment_bytes = hex::decode(&input_sum_commitment_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let input_sum_commitment = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&input_sum_commitment_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid input sum commitment point"))?; + + let output_sum_commitment_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(1, &self))? + .1; + let output_sum_commitment_bytes = hex::decode(&output_sum_commitment_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let output_sum_commitment = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&output_sum_commitment_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid output sum commitment point"))?; + + let proof = map + .next_entry::<&str, BulletproofRangeProof>()? + .ok_or_else(|| de::Error::invalid_length(2, &self))? + .1; + + Ok(BalanceProof { + input_sum_commitment, + output_sum_commitment, + proof, + }) + } + } + + const FIELDS: &[&str] = &["input_sum_commitment", "output_sum_commitment", "proof"]; + deserializer.deserialize_struct("BalanceProof", FIELDS, BalanceProofVisitor) + } +} + +#[derive(Debug, Clone)] +pub struct ShieldedInput { + pub merkle_proof: MerkleProof, + pub spending_proof: SpendingProof, +} + +impl Serialize for ShieldedInput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("ShieldedInput", 3)?; + state.serialize_field("merkle_proof", &self.merkle_proof)?; + state.serialize_field("spending_proof", &self.spending_proof)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for ShieldedInput { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ShieldedInputVisitor; + + impl<'de> Visitor<'de> for ShieldedInputVisitor { + type Value = ShieldedInput; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct ShieldedInput") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let merkle_proof = map + .next_entry::<&str, MerkleProof>()? + .ok_or_else(|| de::Error::invalid_length(1, &self))? + .1; + + let spending_proof = map + .next_entry::<&str, SpendingProof>()? + .ok_or_else(|| de::Error::invalid_length(2, &self))? + .1; + + Ok(ShieldedInput { + merkle_proof, + spending_proof, + }) + } + } + + const FIELDS: &[&str] = &["nullifier", "merkle_proof", "spending_proof"]; + deserializer.deserialize_struct("ShieldedInput", FIELDS, ShieldedInputVisitor) + } +} + +#[derive(Debug, Clone)] +pub struct ShieldedOutput { + pub value_commitment: ProjectivePoint, + pub range_proof: BulletproofRangeProof, + pub spending_key_commitment: ProjectivePoint, + pub nullifier: ProjectivePoint, + pub recipient_public_key: PublicKey, + pub encrypted_amount: EncryptedExactAmount, +} + +impl Serialize for ShieldedOutput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("ShieldedOutput", 6)?; + state.serialize_field( + "value_commitment", + &hex::encode(self.value_commitment.to_bytes()), + )?; + state.serialize_field("range_proof", &self.range_proof)?; + state.serialize_field( + "spending_key_commitment", + &hex::encode(self.spending_key_commitment.to_bytes()), + )?; + state.serialize_field("nullifier", &hex::encode(self.nullifier.to_bytes()))?; + state.serialize_field("recipient_public_key", &self.recipient_public_key)?; + state.serialize_field("encrypted_amount", &self.encrypted_amount)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for ShieldedOutput { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ShieldedOutputVisitor; + + impl<'de> Visitor<'de> for ShieldedOutputVisitor { + type Value = ShieldedOutput; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct ShieldedOutput") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let value_commitment_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(0, &self))? + .1; + let value_commitment_bytes = hex::decode(&value_commitment_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let value_commitment = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&value_commitment_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid value commitment point"))?; + + let range_proof = map + .next_entry::<&str, BulletproofRangeProof>()? + .ok_or_else(|| de::Error::invalid_length(1, &self))? + .1; + + let spending_key_commitment_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(2, &self))? + .1; + let spending_key_commitment_bytes = hex::decode(&spending_key_commitment_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let spending_key_commitment = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&spending_key_commitment_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid spending key commitment point"))?; + + let nullifier_hex = map + .next_entry::<&str, String>()? + .ok_or_else(|| de::Error::invalid_length(3, &self))? + .1; + let nullifier_bytes = hex::decode(&nullifier_hex) + .map_err(|e| de::Error::custom(format!("Invalid hex: {}", e)))?; + let nullifier = Option::from(ProjectivePoint::from_bytes( + GenericArray::from_slice(&nullifier_bytes), + )) + .ok_or_else(|| de::Error::custom("Invalid nullifier point"))?; + + let recipient_public_key = map + .next_entry::<&str, PublicKey>()? + .ok_or_else(|| de::Error::invalid_length(4, &self))? + .1; + + let encrypted_amount = map + .next_entry::<&str, EncryptedExactAmount>()? + .ok_or_else(|| de::Error::invalid_length(5, &self))? + .1; + + Ok(ShieldedOutput { + value_commitment, + range_proof, + spending_key_commitment, + nullifier, + recipient_public_key, + encrypted_amount, + }) + } + } + + const FIELDS: &[&str] = &[ + "value_commitment", + "range_proof", + "spending_key_commitment", + "nullifier", + "recipient_public_key", + "encrypted_amount" + ]; + deserializer.deserialize_struct("ShieldedOutput", FIELDS, ShieldedOutputVisitor) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublicInput { + pub amount: u64, + pub owner: Address, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EncryptedAmountProofs { - pub sender: EncryptedExactAmount, - pub recipient: EncryptedExactAmount, - pub quorum: EncryptedExactAmount, +pub struct PublicOutput { + pub amount: u64, + pub recipient: Address, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Amount { - Confidential(EncryptedAmountProofs), - Public(u64), +pub enum Input { + Public(PublicInput), + Confidential(ShieldedInput), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedPublicInput { + pub input: PublicInput, + pub signature: Signature, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedConfidentialInput { + pub input: ShieldedInput, + pub signature: Signature, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Output { + Public(PublicOutput), + Confidential(ShieldedOutput), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Transaction { - pub from: Address, - pub to: Address, - pub amount: Amount, + pub inputs: Vec, + pub outputs: Vec, pub timestamp: i64, pub previous_transaction_id: TransactionHash, } impl Transaction { pub fn new( - from: Address, - to: Address, - amount: Amount, + inputs: Vec, + outputs: Vec, previous_transaction_id: TransactionHash, ) -> Result { Ok(Self { - from, - to, - amount, + inputs, + outputs, timestamp: Utc::now().timestamp_millis(), previous_transaction_id, }) } - pub fn new_confidential( - from: Address, - to: Address, - sender: EncryptedExactAmount, - recipient: EncryptedExactAmount, - quorum: EncryptedExactAmount, - previous_transaction_id: TransactionHash, - ) -> Result { - Ok(Self { - from, - to, - amount: Amount::Confidential(EncryptedAmountProofs { - sender, - recipient, - quorum, - }), - timestamp: Utc::now().timestamp_millis(), - previous_transaction_id, - }) + pub fn verify_amounts_consistency(&self) -> Result { + let mut public_input_amount = 0u64; + let mut public_output_amount = 0u64; + let mut confidential_input_amount = ProjectivePoint::IDENTITY; + let mut confidential_output_amount = ProjectivePoint::IDENTITY; + + // Collect all input amounts + for input in self.inputs.iter() { + match input { + Input::Public(public_input) => { + public_input_amount = public_input_amount.checked_add(public_input.amount) + .ok_or_else(|| anyhow!("Input amount overflow"))?; + } + Input::Confidential(shielded_input) => { + confidential_input_amount += shielded_input.spending_proof.value_commitment; + } + } + } + + // Collect all output amounts + for output in self.outputs.iter() { + match output { + Output::Public(public_output) => { + public_output_amount = public_output_amount.checked_add(public_output.amount) + .ok_or_else(|| anyhow!("Output amount overflow"))?; + } + Output::Confidential(shielded_output) => { + confidential_output_amount += shielded_output.value_commitment; + } + } + } + + // For public amounts, we need to convert them to commitments + // We use the same blinding factor for both input and output to ensure they match + let public_input_commitment = commit_amount(public_input_amount, Scalar::ZERO)?; + let public_output_commitment = commit_amount(public_output_amount, Scalar::ZERO)?; + + // Add public and confidential amounts together and verify they match + let total_input = confidential_input_amount + public_input_commitment; + let total_output = confidential_output_amount + public_output_commitment; + + if total_input != total_output { + return Err(anyhow!("Total input amount does not equal total output amount")); + } + + Ok(true) } pub fn calculate_id(&self) -> Result<[u8; 32]> { let mut hasher = Sha256::new(); - hasher.update(&self.from); - hasher.update(&self.to); - match &self.amount { - Amount::Confidential(amount) => { - hasher.update( - amount - .sender - .c1 - .to_affine() - .to_encoded_point(true) - .as_bytes(), - ); - hasher.update( - amount - .sender - .c2 - .to_affine() - .to_encoded_point(true) - .as_bytes(), - ); - hasher.update(amount.sender.range_proof.to_bytes()); - hasher.update( - amount - .recipient - .c1 - .to_affine() - .to_encoded_point(true) - .as_bytes(), - ); - hasher.update( - amount - .recipient - .c2 - .to_affine() - .to_encoded_point(true) - .as_bytes(), - ); - hasher.update(amount.recipient.range_proof.to_bytes()); - hasher.update( - amount - .quorum - .c1 - .to_affine() - .to_encoded_point(true) - .as_bytes(), - ); - hasher.update( - amount - .quorum - .c2 - .to_affine() - .to_encoded_point(true) - .as_bytes(), - ); - hasher.update(amount.quorum.range_proof.to_bytes()); + + // Hash inputs + for input in self.inputs.iter() { + match input { + Input::Public(public_input) => { + hasher.update(public_input.amount.to_be_bytes()); + hasher.update(&public_input.owner.0); + } + Input::Confidential(shielded_input) => { + hash_merkle_proof(&mut hasher, &shielded_input.merkle_proof); + hash_spending_proof(&mut hasher, &shielded_input.spending_proof); + } } - Amount::Public(amount) => { - hasher.update(amount.to_be_bytes()); + } + + // Hash outputs + for output in self.outputs.iter() { + match output { + Output::Public(public_output) => { + hasher.update(public_output.amount.to_be_bytes()); + hasher.update(&public_output.recipient.0); + } + Output::Confidential(shielded_output) => { + hasher.update(shielded_output.nullifier.to_bytes()); + hasher.update(shielded_output.range_proof.to_bytes()); + hasher.update(shielded_output.spending_key_commitment.to_bytes()); + hasher.update(shielded_output.recipient_public_key.to_sec1_bytes()); + hasher.update(shielded_output.encrypted_amount.range_proof.to_bytes()); + hasher.update(shielded_output.encrypted_amount.c1.to_bytes()); + hasher.update(shielded_output.encrypted_amount.c2.to_bytes()); + } } } + hasher.update(self.timestamp.to_be_bytes()); hasher.update(&self.previous_transaction_id.0); let mut res = [0u8; 32]; res.copy_from_slice(&hasher.finalize()); - Ok(res) } } + +// Helper functions for hashing different components +fn hash_merkle_proof(hasher: &mut Sha256, proof: &MerkleProof) -> () { + for (point, is_right) in &proof.path { + hasher.update(point.to_bytes()); + hasher.update(&[*is_right as u8]); + } + hasher.update(proof.root.to_bytes()); + hasher.update(proof.leaf_index.to_be_bytes()); +} + +fn hash_spending_proof(hasher: &mut Sha256, proof: &SpendingProof) { + hasher.update(proof.nullifier_commitment.to_bytes()); + hasher.update(proof.value_commitment.to_bytes()); + hasher.update(proof.proof.to_bytes()); +} + +// Implementation for components +impl ShieldedInput { + pub fn new(note: ShieldedAmount, spending_key: &SecretKey) -> Result { + let merkle_proof = MerkleProof { + path: vec![], // TODO: This should be populated with the actual Merkle path + root: ProjectivePoint::IDENTITY, // TODO: This should be the actual Merkle root + leaf_index: 0, // TODO: This should be the actual leaf index + }; + + let spending_proof = SpendingProof { + nullifier_commitment: note.nullifier, + value_commitment: note.value_commitment, + proof: note.range_proof, + }; + + Ok(Self { + merkle_proof, + spending_proof, + }) + } +} + +impl ShieldedOutput { + pub fn new( + amount: u64, + secret_key: &SecretKey, + recipient_key: &PublicKey, + blinding: Scalar, + ) -> Result { + // Create value commitment using the provided blinding factor + let value_commitment = commit_amount(amount, blinding)?; + + // Create range proof using the same blinding factor + let mut transcript = merlin::Transcript::new(b"example"); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(blinding.to_repr().as_ref()); + let dalek_blinding = curve25519_dalek::scalar::Scalar::from_bytes_mod_order(bytes); + let range_proof = BulletproofRangeProof::prove_single( + &bulletproofs::BulletproofGens::new(64, 1), + &bulletproofs::PedersenGens::default(), + &mut transcript, + amount, + &dalek_blinding, + 64, + )? + .0; + + // Create spending key commitment using the same blinding factor + let spending_key_commitment = commit_amount(amount, blinding + *secret_key.to_nonzero_scalar())?; + + // Create nullifier + let nullifier = create_nullifier(secret_key, &value_commitment)?; + + // Create encrypted amount for recipient + let encrypted_amount = EncryptedExactAmount::encrypt(amount, recipient_key)?; + + Ok(Self { + value_commitment, + range_proof, + spending_key_commitment, + nullifier, + recipient_public_key: recipient_key.clone(), + encrypted_amount, + }) + } + + pub fn verify(&self) -> Result { + Ok(true) + } +} + +impl SpendingProof { + pub fn verify(&self) -> Result { + // Verify the nullifier commitment + let nullifier_commitment_bytes = self.nullifier_commitment.to_affine().to_bytes(); + let nullifier_commitment_scalar: Scalar = Option::from(Scalar::from_repr( + *GenericArray::from_slice(&nullifier_commitment_bytes), + )) + .ok_or_else(|| anyhow!("Invalid nullifier"))?; + let expected_nullifier_commitment = + ProjectivePoint::GENERATOR * nullifier_commitment_scalar; + if self.value_commitment != expected_nullifier_commitment { + return Err(anyhow!("Invalid nullifier commitment")); + } + + // Verify the range proof + let mut transcript = merlin::Transcript::new(b"example"); + let pc_gens = bulletproofs::PedersenGens::default(); + let bp_gens = bulletproofs::BulletproofGens::new(64, 1); + self.proof.verify_single( + &bp_gens, + &pc_gens, + &mut transcript, + &CompressedRistretto::from_slice(&self.value_commitment.to_bytes())?, + 64, + )?; + + Ok(true) + } +} + +impl MerkleProof { + pub fn verify(&self) -> Result { + let computed_root = self + .leaf_index + .to_le_bytes() + .iter() + .zip(&self.path) + .try_fold( + self.path[0].0, + |acc, (_, (sibling, is_right))| -> Result { + if *is_right { + let res: ProjectivePoint = + Option::from(ProjectivePoint::from_bytes(&GenericArray::from_slice( + &[acc.to_bytes(), sibling.to_bytes()].concat(), + ))) + .ok_or_else(|| anyhow!("Invalid point conversion"))?; + Ok(res) + } else { + let res: ProjectivePoint = + Option::from(ProjectivePoint::from_bytes(&GenericArray::from_slice( + &[sibling.to_bytes(), acc.to_bytes()].concat(), + ))) + .ok_or_else(|| anyhow!("Invalid point conversion"))?; + Ok(res) + } + }, + )?; + + if computed_root == self.root { + Ok(true) + } else { + Err(anyhow!("Merkle proof verification failed")) + } + } +} + +impl BalanceProof { + pub fn verify(&self) -> Result { + // Verify the range proof + let mut transcript = merlin::Transcript::new(b"balance_proof"); + let pc_gens = bulletproofs::PedersenGens::default(); + let bp_gens = bulletproofs::BulletproofGens::new(64, 1); + self.proof.verify_single( + &bp_gens, + &pc_gens, + &mut transcript, + &CompressedRistretto::from_slice(&self.input_sum_commitment.to_bytes())?, + 32, + )?; + + // Verify that input sum commitment equals output sum commitment + if self.input_sum_commitment != self.output_sum_commitment { + return Err(anyhow!( + "Input sum commitment does not equal output sum commitment" + )); + } + + Ok(true) + } +} + +fn create_nullifier( + spending_key: &SecretKey, + commitment: &ProjectivePoint, +) -> Result { + let mut hasher = Sha256::new(); + hasher.update(spending_key.to_bytes()); + hasher.update(commitment.to_bytes()); + let hash = hasher.finalize(); + let nullifier = hash_to_curve(GenericArray::clone_from_slice(&hash))?; + Ok(nullifier) +} diff --git a/src/transaction_builder.rs b/src/transaction_builder.rs new file mode 100644 index 0000000..0de6b9d --- /dev/null +++ b/src/transaction_builder.rs @@ -0,0 +1,337 @@ +use anyhow::{anyhow, Context, Result}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use k256::ecdsa::{Signature, SigningKey, signature::Signer}; +use k256::elliptic_curve::group::GroupEncoding; +use k256::elliptic_curve::rand_core::OsRng; +use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::{ProjectivePoint, SecretKey as K256SecretKey, PublicKey, Scalar, NonZeroScalar}; +use serde_json::json; +use sha2::digest::generic_array::GenericArray; +use sha2::digest::typenum; +use tokio::runtime::Runtime; + +use crate::address::Address; +use crate::confidential::{EncryptedExactAmount, ShieldedAmount}; +use crate::transaction::{Input, Output, PublicInput, PublicOutput, ShieldedInput, ShieldedOutput, Transaction}; +use crate::transaction_hash::TransactionHash; + +#[derive(Debug)] +pub struct Utxo { + pub amount: Option, // None for confidential amounts + pub owner: Option
, + pub value_commitment: Option, + pub spending_key_commitment: Option, + pub nullifier: Option, + pub encrypted_amount: Option, +} + +pub async fn fetch_utxo(_txid: &str, _output_index: usize) -> Result { + // TODO: Implement actual fetching from chain + // For now return dummy data for testing + Ok(Utxo { + amount: Some(100), // Fixed test amount + owner: None, + value_commitment: None, + spending_key_commitment: None, + nullifier: None, + encrypted_amount: None, + }) +} + +pub fn decode_recipient_pubkey(recipient_pubkey: &str) -> Result { + let recipient_pubkey_bytes = hex::decode(recipient_pubkey) + .context("Failed to decode recipient public key hex")?; + PublicKey::from_sec1_bytes(&recipient_pubkey_bytes) + .map_err(|e| anyhow!("Invalid recipient public key: {}", e)) +} + +pub fn process_confidential_input( + input_keys_by_id: &str, + recipient_pubkey: &PublicKey, + _k256_blinding: Scalar, +) -> Result<(Vec, Option<(K256SecretKey, PublicKey)>)> { + let rt = Runtime::new()?; + let mut first_spending_key = None; + let tx_inputs = input_keys_by_id + .split(';') + .map(|pair| { + let mut split = pair.split(':'); + let spend_key = split + .next() + .ok_or_else(|| anyhow!("Missing spend key in input pair"))? + .to_string(); + let txid = split + .next() + .ok_or_else(|| anyhow!("Missing txid in input pair"))? + .to_string(); + let output_index = split + .next() + .ok_or_else(|| anyhow!("Missing output index in input pair"))? + .parse::()?; + + // Fetch UTXO data + let utxo = rt.block_on(fetch_utxo(&txid, output_index))?; + + // Verify the UTXO exists and is confidential + if utxo.value_commitment.is_none() { + return Err(anyhow!("UTXO is not confidential")); + } + + let spend_key_bytes: [u8; 32] = hex::decode(spend_key.clone()) + .with_context(|| format!("Failed to decode spend key hex: {}", spend_key))? + .try_into() + .map_err(|_| anyhow!("Spend key must be exactly 32 bytes"))?; + + let mut spending_key_array = GenericArray::::default(); + spending_key_array.copy_from_slice(&spend_key_bytes); + let spending_key = K256SecretKey::from_bytes(&spending_key_array)?; + + // Store first spending key for output creation + if first_spending_key.is_none() { + first_spending_key = Some((spending_key.clone(), recipient_pubkey.clone())); + } + + // Create shielded input using the UTXO data + let encrypted_amount = utxo.encrypted_amount.unwrap(); + let shielded_amount = ShieldedAmount { + value_commitment: utxo.value_commitment.unwrap(), + nullifier: utxo.nullifier.unwrap(), + range_proof: encrypted_amount.range_proof.clone(), + spending_key_commitment: utxo.spending_key_commitment.unwrap(), + encrypted_amount: encrypted_amount, + }; + + let shielded_input = ShieldedInput::new(shielded_amount, &spending_key)?; + + Ok(Input::Confidential(shielded_input)) + }) + .collect::>>()?; + + Ok((tx_inputs, first_spending_key)) +} + +pub fn process_public_input( + input_address: &str, + amount: u64, + _txid: &str, + _output_index: usize, +) -> Result { + let address = Address::from_hex(input_address)?; + Ok(Input::Public(PublicInput { + amount, + owner: address, + })) +} + +pub fn create_confidential_output( + amount: u64, + spending_key: &K256SecretKey, + recipient_pubkey: &PublicKey, + k256_blinding: Scalar, +) -> Result { + let output = ShieldedOutput::new( + amount, + spending_key, + recipient_pubkey, + k256_blinding, + )?; + Ok(Output::Confidential(output)) +} + +pub fn create_public_output( + amount: u64, + recipient_address: &str, +) -> Result { + let recipient = Address::from_hex(recipient_address)?; + Ok(Output::Public(PublicOutput { + amount, + recipient, + })) +} + +pub fn generate_blinding_factor() -> Result { + let scalar = NonZeroScalar::random(&mut OsRng); + Ok(*scalar.as_ref()) +} + +pub fn create_and_sign_transaction( + inputs: Vec, + outputs: Vec, +) -> Result<(Transaction, Signature)> { + let tx = Transaction::new(inputs, outputs, TransactionHash([0; 32]))?; + tx.verify_amounts_consistency()?; + + let signing_key = SigningKey::random(&mut OsRng); + let signature = signing_key.sign(&tx.calculate_id()?); + + Ok((tx, signature)) +} + +pub fn create_transaction_json(tx: &Transaction, signature_bytes: &[u8]) -> Result { + let json_inputs: Vec<_> = tx.inputs.iter().map(|input| { + match input { + Input::Confidential(shielded_input) => { + json!({ + "type": "confidential", + "merkle_proof": { + "path": shielded_input.merkle_proof.path.iter().map(|(point, is_right)| { + json!({ + "point": hex::encode(point.to_bytes()), + "is_right": is_right + }) + }).collect::>(), + "root": hex::encode(shielded_input.merkle_proof.root.to_bytes()), + "leaf_index": shielded_input.merkle_proof.leaf_index + }, + "spending_proof": { + "nullifier_commitment": hex::encode(shielded_input.spending_proof.nullifier_commitment.to_bytes()), + "value_commitment": hex::encode(shielded_input.spending_proof.value_commitment.to_bytes()), + "proof": BASE64.encode(shielded_input.spending_proof.proof.to_bytes()) + } + }) + }, + Input::Public(public_input) => { + json!({ + "type": "public", + "amount": public_input.amount, + "owner": public_input.owner.as_hex() + }) + } + } + }).collect(); + + let json_outputs: Vec<_> = tx.outputs.iter().map(|output| { + match output { + Output::Confidential(shielded_output) => { + json!({ + "type": "confidential", + "value_commitment": hex::encode(shielded_output.value_commitment.to_bytes()), + "range_proof": BASE64.encode(shielded_output.range_proof.to_bytes()), + "spending_key_commitment": hex::encode(shielded_output.spending_key_commitment.to_bytes()), + "nullifier": hex::encode(shielded_output.nullifier.to_bytes()), + "recipient_public_key": hex::encode(shielded_output.recipient_public_key.to_encoded_point(false).as_bytes()), + "encrypted_amount": { + "c1": BASE64.encode(shielded_output.encrypted_amount.c1.to_affine().to_encoded_point(false).as_bytes()), + "c2": BASE64.encode(shielded_output.encrypted_amount.c2.to_affine().to_encoded_point(false).as_bytes()), + "range_proof": BASE64.encode(shielded_output.encrypted_amount.range_proof.to_bytes()) + } + }) + }, + Output::Public(public_output) => { + json!({ + "type": "public", + "amount": public_output.amount, + "recipient": public_output.recipient.as_hex() + }) + } + } + }).collect(); + + Ok(json!({ + "jsonrpc": "2.0", + "method": "submitTransaction", + "params": [{ + "inputs": json_inputs, + "outputs": json_outputs, + "signature": { + "R": hex::encode(&signature_bytes[..32]), + "s": hex::encode(&signature_bytes[32..]) + }, + "previous_transaction_id": hex::encode(tx.previous_transaction_id.0), + "timestamp": tx.timestamp, + }], + "id": 1 + })) +} + +pub fn build_transaction( + amount: u64, + input_type: &str, + output_type: &str, + input_keys_by_id: Option<&str>, + recipient_pubkey: Option<&str>, + input_address: Option<&str>, + output_address: Option<&str>, +) -> Result { + let mut outputs = Vec::new(); + let mut first_spending_key = None; + + // For public-to-confidential transactions: + // 1. Public input uses Scalar::ZERO as blinding factor + // 2. Confidential output uses -Scalar::ZERO as blinding factor to ensure they sum to zero + let k256_blinding = if input_type == "public" && output_type == "confidential" { + -Scalar::ZERO // Negative of zero is still zero, but explicitly showing the logic + } else { + generate_blinding_factor()? + }; + + let inputs = match input_type { + "confidential" => { + let input_keys_by_id = input_keys_by_id + .ok_or_else(|| anyhow!("input_keys_by_id required for confidential inputs"))?; + + let recipient_pubkey = recipient_pubkey + .ok_or_else(|| anyhow!("recipient_pubkey required for confidential outputs"))?; + let recipient_pubkey = decode_recipient_pubkey(recipient_pubkey)?; + + // Parse input keys and create inputs + let (tx_inputs, first_spending_key_opt) = process_confidential_input( + input_keys_by_id, + &recipient_pubkey, + k256_blinding, + )?; + + first_spending_key = first_spending_key_opt; + + Ok(tx_inputs) + } + "public" => { + let input_address = input_address + .ok_or_else(|| anyhow!("input_address required for public inputs"))?; + Ok(vec![process_public_input(input_address, amount, "", 0)?]) + } + _ => Err(anyhow!("Invalid input type")), + }?; + + // Create output based on input type + match output_type { + "confidential" => { + let (spending_key, recipient_pubkey) = if let Some(key_pair) = first_spending_key { + // Use existing key pair from confidential input + key_pair + } else { + // Generate new spending key for public-to-confidential transactions + let recipient_pubkey = recipient_pubkey + .ok_or_else(|| anyhow!("recipient_pubkey required for confidential outputs"))?; + let recipient_pubkey = decode_recipient_pubkey(recipient_pubkey)?; + + // Generate a new random spending key + let spending_key = K256SecretKey::random(&mut OsRng); + + (spending_key, recipient_pubkey) + }; + + let output = create_confidential_output( + amount, + &spending_key, + &recipient_pubkey, + k256_blinding, + )?; + outputs.push(output); + } + "public" => { + let recipient_address = output_address + .ok_or_else(|| anyhow!("output_address required for public outputs"))?; + let output = create_public_output(amount, recipient_address)?; + outputs.push(output); + } + _ => return Err(anyhow!("Invalid output type")), + } + + // Create and sign transaction + let (tx, signature) = create_and_sign_transaction(inputs, outputs)?; + let signature_bytes = signature.to_bytes(); + + // Create JSON output + create_transaction_json(&tx, &signature_bytes) +} diff --git a/src/transaction_hash.rs b/src/transaction_hash.rs new file mode 100644 index 0000000..43c14a4 --- /dev/null +++ b/src/transaction_hash.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TransactionHash(pub [u8; 32]); + +impl From<[u8; 32]> for TransactionHash { + fn from(tx_id: [u8; 32]) -> TransactionHash { + TransactionHash(tx_id) + } +} + +impl AsRef<[u8; 32]> for TransactionHash { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} diff --git a/src/transaction_manager.rs b/src/transaction_manager.rs index 1b15dbf..c77f3fb 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -7,6 +7,7 @@ use lmdb::Cursor; use lmdb::Database; use lmdb::Environment; use lmdb::Transaction as LmdbTransaction; +use lmdb::{DatabaseFlags, WriteFlags}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -16,10 +17,15 @@ use tracing::info; use crate::address::{Address, ZERO_ADDRESS}; use crate::serialization::signature::{deserialize_signature, serialize_signature}; -use crate::transaction::Amount; -use crate::transaction::{Transaction, TransactionHash}; +use crate::transaction::Input; +use crate::transaction::Output; +use crate::transaction::PublicInput; +use crate::transaction::PublicOutput; +use crate::transaction::Transaction; +use crate::transaction_hash::TransactionHash; const DB_NAME: &'static str = "./local_db/transaction_db"; +const MERKLE_DB_NAME: &'static str = "./local_db/merkle_db"; static LMDB_ENV: Lazy> = Lazy::new(|| { std::fs::create_dir_all(DB_NAME).expect("Failed to create transaction_db directory"); @@ -33,6 +39,18 @@ static LMDB_ENV: Lazy> = Lazy::new(|| { ) }); +static MERKLE_LMDB_ENV: Lazy> = Lazy::new(|| { + std::fs::create_dir_all(MERKLE_DB_NAME).expect("Failed to create merkle_db directory"); + Arc::new( + lmdb::Environment::new() + .set_max_dbs(1) + .set_map_size(10 * 1024 * 1024) + .set_max_readers(126) + .open(&Path::new(MERKLE_DB_NAME)) + .expect("Failed to create LMDB environment"), + ) +}); + #[derive(Deserialize)] pub struct GenesisArgs { pub balances: HashMap, @@ -59,16 +77,20 @@ struct TransactionRecord { pub struct TransactionManager { pub lmdb_transaction_env: Arc, pub db: Database, + pub merkle_db: Database, } impl TransactionManager { pub fn new() -> Result { let env = LMDB_ENV.clone(); - let db = env.create_db(Some(DB_NAME), lmdb::DatabaseFlags::empty())?; + let db = env.create_db(Some(DB_NAME), DatabaseFlags::empty())?; + let merkle_env = MERKLE_LMDB_ENV.clone(); + let merkle_db = merkle_env.create_db(Some(MERKLE_DB_NAME), DatabaseFlags::empty())?; Ok(TransactionManager { lmdb_transaction_env: env, db, + merkle_db, }) } @@ -82,9 +104,14 @@ impl TransactionManager { // Insert each genesis transaction into the database for (address, amount) in genesis_args.balances { let transaction = Transaction { - from: ZERO_ADDRESS, - to: Address::from_hex(&address)?, - amount: Amount::Public(amount), + inputs: vec![Input::Public(PublicInput { + amount, + owner: ZERO_ADDRESS, + })], + outputs: vec![Output::Public(PublicOutput { + amount, + recipient: Address::from_hex(&address)?, + })], timestamp: 0, previous_transaction_id: TransactionHash([0u8; 32]), }; @@ -123,18 +150,15 @@ impl TransactionManager { pub fn add_transaction( &mut self, - from: Address, - to: Address, - amount: Amount, - public_key: PublicKey, + inputs: Vec, + outputs: Vec, timestamp: i64, signature: Signature, previous_transaction_id: TransactionHash, ) -> Result { let transaction = Transaction { - from, - to, - amount: amount.clone(), + inputs, + outputs, timestamp, previous_transaction_id, }; @@ -154,31 +178,8 @@ impl TransactionManager { reader.abort(); - let verifying_key = VerifyingKey::from_affine(public_key.as_affine().clone()) - .map_err(|e| anyhow!("Invalid public key: {}", e))?; - - verifying_key - .verify(&id, &signature) - .map_err(|e| anyhow!("Invalid signature: {}", e))?; - - match amount { - Amount::Public(_) => {} - Amount::Confidential(encrypted_amount_proofs) => { - encrypted_amount_proofs - .sender - .verify_equal(&encrypted_amount_proofs.recipient)?; - encrypted_amount_proofs - .sender - .verify_equal(&encrypted_amount_proofs.quorum)?; - } - } - - if let Err(err) = self.verify_transaction_chain(&transaction) { - return Err(anyhow!("Insufficient balance: {}", err)); - } - - if let Err(err) = self.verify_transaction_chain(&transaction) { - return Err(anyhow!("Insufficient balance: {}", err)); + if let Err(err) = transaction.verify_amounts_consistency() { + return Err(anyhow!("{}", err)); } // write in the DB the transaction to both the recipient and the emitter @@ -200,62 +201,39 @@ impl TransactionManager { Ok(hex::encode(id)) } - pub fn verify_transaction_chain(&self, transaction_to_verify: &Transaction) -> Result { + pub fn add_merkle_root(&self, root: &[u8; 32]) -> Result<()> { + let mut txn = self + .lmdb_transaction_env + .begin_rw_txn() + .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; + + txn.put(self.merkle_db, root, &[], WriteFlags::empty()) + .map_err(|e| anyhow!("Failed to put Merkle root in database: {}", e))?; + + txn.commit() + .map_err(|e| anyhow!("Failed to commit Merkle root: {}", e))?; + + Ok(()) + } + + pub fn get_merkle_roots(&self) -> Result> { let reader = self .lmdb_transaction_env .begin_ro_txn() .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; - let mut found_last_public_transaction = false; - let mut current_transaction_id = transaction_to_verify.previous_transaction_id.0; - let mut commitments_chain = Vec::::new(); - - while !found_last_public_transaction { - let transaction_bytes = match reader.get(self.db, ¤t_transaction_id) { - Ok(bytes) => bytes, - Err(lmdb::Error::NotFound) => { - return Err(anyhow!( - "Transaction not found: {:?}", - current_transaction_id - )) - } - Err(e) => return Err(anyhow!("Database error: {}", e)), - }; - - let transaction_record: TransactionRecord = bincode::deserialize(transaction_bytes) - .map_err(|e| anyhow!("Failed to deserialize transaction: {}", e))?; - - match transaction_record.transaction.amount { - Amount::Public(_amount) => { - commitments_chain.push(transaction_record.transaction.amount); - found_last_public_transaction = true; - } - Amount::Confidential(ref _confidential) => { - let tx_record = transaction_to_verify.calculate_id()?; - current_transaction_id = tx_record; - commitments_chain.push(transaction_record.transaction.amount); - } - } - } + let mut roots = Vec::new(); + let mut cursor = reader + .open_ro_cursor(self.merkle_db) + .map_err(|e| anyhow!("Failed to create cursor: {}", e))?; - // Verify balance consistency between consecutive transactions - for window in commitments_chain.windows(2) { - match (&window[0], &window[1]) { - (Amount::Confidential(current), Amount::Confidential(previous)) => { - if !¤t.sender.verify_greater_than(&previous.sender)? { - return Ok(false); - } - } - (Amount::Confidential(current), Amount::Public(previous)) => { - if !current.sender.verify_greater_than_u64(*previous)? { - return Ok(false); - } - } - _ => continue, - } + for (key, _) in cursor.iter() { + let mut root = [0u8; 32]; + root.copy_from_slice(key); + roots.push(root); } - Ok(true) + Ok(roots) } pub fn get_transaction(&self, id: String) -> Result { diff --git a/src/transaction_request.rs b/src/transaction_request.rs new file mode 100644 index 0000000..7cc3aaa --- /dev/null +++ b/src/transaction_request.rs @@ -0,0 +1,98 @@ +use anyhow::Result; +use k256::ecdsa::Signature; +use k256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; +use serde::de; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::address::Address; +use crate::serialization::signature::{deserialize_signature, serialize_signature}; +use crate::transaction_hash::TransactionHash; +use crate::transaction::{Input, Output}; +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TransactionRequest { + #[serde(deserialize_with = "deserialize_hex_to_address")] + pub from: Address, + #[serde(deserialize_with = "deserialize_hex_to_address")] + pub to: Address, + pub inputs: Vec, + pub outputs: Vec, + #[serde( + deserialize_with = "deserialize_hex_to_public_key", + serialize_with = "serialize_public_key" + )] + pub public_key: PublicKey, + #[serde( + deserialize_with = "deserialize_signature", + serialize_with = "serialize_signature" + )] + pub signature: Signature, + pub timestamp: i64, + #[serde(deserialize_with = "deserialize_hex_to_tx_id")] + pub previous_transaction_id: TransactionHash, +} + +fn deserialize_hex_to_public_key<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let s = s.trim_start_matches("0x"); + let bytes = hex::decode(s).map_err(de::Error::custom)?; + + // For ECDSA, the public key is 65 bytes (uncompressed) or 33 bytes (compressed) + if bytes.len() == 65 && bytes[0] == 0x04 { + // This is an uncompressed public key + // let key_bytes = &bytes[1..]; // Remove the 0x04 prefix + // println!("{:?}", key_bytes); + PublicKey::from_sec1_bytes(&bytes) + .map_err(|e| de::Error::custom(format!("Invalid public key: {}", e))) + } else if bytes.len() == 33 && (bytes[0] == 0x02 || bytes[0] == 0x03) { + // This is a compressed public key + PublicKey::from_sec1_bytes(&bytes) + .map_err(|e| de::Error::custom(format!("Invalid public key: {}", e))) + } else { + Err(de::Error::custom(format!( + "Invalid public key length: {}", + bytes.len() + ))) + } +} + +fn serialize_public_key(key: &PublicKey, serializer: S) -> Result +where + S: serde::Serializer, +{ + let bytes = key.to_encoded_point(false); + let hex_string = hex::encode(bytes.as_bytes()); + serializer.serialize_str(&hex_string) +} + +fn deserialize_hex_to_address<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let s = s.trim_start_matches("0x"); + let bytes = hex::decode(s).map_err(de::Error::custom)?; + let mut array = [0u8; 32]; + if bytes.len() != 32 { + return Err(de::Error::custom("Invalid length for byte array")); + } + array.copy_from_slice(&bytes); + Ok(Address::from(array)) +} + +fn deserialize_hex_to_tx_id<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let s = s.trim_start_matches("0x"); + let bytes = hex::decode(s).map_err(de::Error::custom)?; + let mut array = [0u8; 32]; + if bytes.len() != 32 { + return Err(de::Error::custom("Invalid length for byte array")); + } + array.copy_from_slice(&bytes); + Ok(TransactionHash(array)) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b87e844 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,18 @@ +use anyhow::{anyhow, Result}; +use k256::elliptic_curve::PrimeField; +use k256::{ProjectivePoint, Scalar}; +use sha2::{Digest, Sha256}; +use sha2::digest::generic_array::{GenericArray, typenum::U32}; + +pub fn hash_to_curve(to_bytes: GenericArray) -> Result { + // Hash input bytes to scalar using SHA-256 + let mut hasher = Sha256::new(); + hasher.update(&to_bytes); + let hash = hasher.finalize(); // This produces a 32-byte output + + // Convert hash to scalar and multiply generator point + let scalar: Scalar = Option::from(Scalar::from_repr(*GenericArray::from_slice(&hash))) + .ok_or_else(|| anyhow!("Invalid scalar conversion"))?; + + Ok(ProjectivePoint::GENERATOR * &scalar) +} \ No newline at end of file