diff --git a/Cargo.lock b/Cargo.lock index 7c42ca9..eb722ab 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", ] @@ -1135,6 +1168,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", + "serdect", "signature", "spki", ] @@ -1199,6 +1233,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", + "serdect", "subtle", "zeroize", ] @@ -1219,16 +1254,21 @@ dependencies = [ "actix-web", "anyhow", "ark-ff", + "base64 0.22.1", "bincode", + "bulletproofs", "chrono", "clap", + "curve25519-dalek 4.1.3", "disruptor", "ed25519-dalek", "hex", "hyper", "k256", + "lazy_static", "libp2p", "lmdb", + "merlin", "once_cell", "rand 0.9.0", "rand_core 0.9.0", @@ -2102,10 +2142,20 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", + "serdect", "sha2", "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 +2631,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" @@ -3544,6 +3606,7 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] @@ -3613,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" @@ -3635,6 +3708,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..5d55f7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,12 +33,21 @@ 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", "serde"] } 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" +lazy_static = "1.5.0" [[bin]] name = "build-transaction" path = "src/build-transaction.rs" + +[[bin]] +name = "run-node" +path = "src/node_runner.rs" \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index 20db9f4..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 @@ -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,41 @@ 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": { + "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": "44152312c43cac7f0f3d6be1e72c9e746166d291d42f9f64e3b2a257ad1fb49f", + "s": "325bd5f08af004e2533f8ba80fc27370399e5767f28422683d17dc49718ac750" + }, + "timestamp": 1738970684326, + "to": "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020f" + } + ] }' ``` @@ -71,8 +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 0000000000000000000000000000000000000000000000000000000000000000 +--inputs 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff:0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 ``` diff --git a/src/address.rs b/src/address.rs index 8ec491c..ee922e3 100644 --- a/src/address.rs +++ b/src/address.rs @@ -1,12 +1,52 @@ 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}; 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..9690975 100644 --- a/src/build-transaction.rs +++ b/src/build-transaction.rs @@ -1,78 +1,65 @@ -use address::Address; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::Parser; -use ed25519_dalek::Signer; -use ed25519_dalek::SigningKey; -use serde_json::json; -use transaction::Transaction; -mod address; -mod transaction; +use enokiweave::transaction_builder; #[derive(Parser)] #[command(version, about, long_about = None)] struct Args { #[arg(long)] - private_key: String, + amount: u64, #[arg(long)] - amount: u64, + /// Type of input: "public" or "confidential" + input_type: String, #[arg(long)] - sender: String, + /// Type of output: "public" or "confidential" + output_type: String, #[arg(long)] - recipient: String, -} + /// 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, -fn main() -> Result<()> { - let args = Args::parse(); + /// Hex recipient public key + /// Required for confidential outputs + #[arg(long)] + recipient_pubkey: Option, - // Convert hex private key to bytes - let private_key_bytes = hex::decode(args.private_key).expect("Invalid private key hex"); - let private_key_array: [u8; 32] = private_key_bytes - .try_into() - .expect("Private key must be 32 bytes"); + /// Address for public inputs + /// Required for public inputs + #[arg(long)] + input_address: Option, - let signing_key = SigningKey::from_bytes(&private_key_array); + /// Address for public outputs + /// Required for public outputs + #[arg(long)] + output_address: Option, +} - // Convert hex addresses to bytes - let sender_bytes = hex::decode(args.sender).expect("Invalid sender address hex"); - let sender_array: [u8; 32] = sender_bytes - .try_into() - .expect("Sender address must be 32 bytes"); +fn main() -> Result<()> { + let args = Args::parse(); - let recipient_bytes = hex::decode(args.recipient).expect("Invalid recipient address hex"); - let recipient_array: [u8; 32] = recipient_bytes - .try_into() - .expect("Recipient address must be 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 tx = Transaction::new( - Address::from(sender_array), - Address::from(recipient_array), + let transaction = transaction_builder::build_transaction( args.amount, + &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(), )?; - let signature = signing_key.sign(&tx.calculate_id()?); - - let json_output = json!({ - "jsonrpc": "2.0", - "method": "submitTransaction", - "params": [{ - "from": hex::encode(tx.from), - "to": hex::encode(tx.to), - "amount": tx.amount, - "public_key": hex::encode(signing_key.verifying_key().as_bytes()), - "signature": { - "R": hex::encode(signature.r_bytes()), - "s": hex::encode(signature.s_bytes()) - }, - "timestamp": tx.timestamp, - "id": hex::encode(tx.calculate_id()?) - }] - }); - - println!("{}", serde_json::to_string_pretty(&json_output)?); + println!("{}", serde_json::to_string_pretty(&transaction)?); Ok(()) } diff --git a/src/confidential.rs b/src/confidential.rs new file mode 100644 index 0000000..cd43a9c --- /dev/null +++ b/src/confidential.rs @@ -0,0 +1,356 @@ +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 { + // 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 + } + + 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 +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")) +} + +#[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 new file mode 100644 index 0000000..c99ebd3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod address; +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/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..28928e6 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; @@ -7,15 +6,15 @@ 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; use crate::transaction_manager::TransactionManager; +use crate::transaction_request::TransactionRequest; enum RPCRequest { Transfer(TransactionRequest), - GetBalance(Address), + GetBalance, } struct QueuedTransaction { @@ -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,13 @@ 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.inputs, + transaction_request.outputs, + transaction_request.timestamp, + transaction_request.signature, + transaction_request.previous_transaction_id, ) { Ok(transaction_id) => { trace!("Transaction added successfully with ID: {}", transaction_id); @@ -196,11 +195,8 @@ 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 => { + todo!("Fix manager.get_address_balance(address)") } } } @@ -250,13 +246,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/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..950e645 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,140 +1,809 @@ -use crate::address::Address; +use anyhow::anyhow; use anyhow::Result; use chrono::Utc; -use ed25519_dalek::Signature; -use serde::de; -use serde::{Deserialize, Deserializer, Serialize}; +use curve25519_dalek::ristretto::CompressedRistretto; +use curve25519_dalek::scalar::Scalar as DalekScalar; +use k256::ecdsa::Signature; +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::confidential::ShieldedAmount; +use crate::confidential::commit_amount; +use crate::transaction_hash::TransactionHash; +use crate::utils::hash_to_curve; +use bulletproofs::RangeProof as BulletproofRangeProof; + +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 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<'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, Clone)] +pub struct SpendingProof { + pub nullifier_commitment: ProjectivePoint, + pub value_commitment: ProjectivePoint, + pub proof: BulletproofRangeProof, +} + +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") + } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct TransactionHash(pub [u8; 32]); + 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"))?; -#[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, + 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, - #[serde(deserialize_with = "deserialize_hex_to_bytes")] - pub public_key: [u8; 32], - #[serde(deserialize_with = "deserialize_signature")] + pub owner: Address, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublicOutput { + pub amount: u64, + pub recipient: Address, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Input { + Public(PublicInput), + Confidential(ShieldedInput), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedPublicInput { + pub input: PublicInput, pub signature: Signature, - pub timestamp: i64, - #[serde(deserialize_with = "deserialize_hex_to_tx_id")] - pub id: TransactionHash, -} - -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_snake_case)] -struct SignatureComponents { - R: String, - s: String, -} - -fn deserialize_signature<'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_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")); - } - 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> -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(array) -} - -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)) -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +} + +#[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: u64, + pub inputs: Vec, + pub outputs: Vec, pub timestamp: i64, + pub previous_transaction_id: TransactionHash, } impl Transaction { - pub fn new(from: Address, to: Address, amount: u64) -> Result { + pub fn new( + 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 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.amount.to_be_bytes()); - hasher.update(&self.from); - hasher.update(&self.to); + + // 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); + } + } + } + + // 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()); +} - let hash = &hasher.finalize()[..]; +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 id: [u8; 32] = hash.try_into().expect("Wrong length"); + let spending_proof = SpendingProof { + nullifier_commitment: note.nullifier, + value_commitment: note.value_commitment, + proof: note.range_proof, + }; - Ok(id) + 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 b5dc72c..c77f3fb 100644 --- a/src/transaction_manager.rs +++ b/src/transaction_manager.rs @@ -1,20 +1,31 @@ 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 lmdb::{DatabaseFlags, WriteFlags}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use tracing::info; use crate::address::{Address, ZERO_ADDRESS}; -use crate::transaction::{Transaction, TransactionHash}; -use crate::GenesisArgs; -use crate::DB_NAME; +use crate::serialization::signature::{deserialize_signature, serialize_signature}; +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"); @@ -28,6 +39,23 @@ 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, +} + #[derive(Debug, Serialize, Deserialize, Clone)] enum TransactionStatus { Pending, @@ -38,24 +66,31 @@ 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, } 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, }) } @@ -67,22 +102,26 @@ 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, + 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]), }; + 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 +132,7 @@ impl TransactionManager { // Use the transaction ID as the key txn.put( self.db, - &format!("{}:0", &address), + &Address::from_hex(&address)?.0, &serialized_transaction_record, lmdb::WriteFlags::empty(), ) @@ -111,28 +150,36 @@ impl TransactionManager { pub fn add_transaction( &mut self, - from: Address, - to: Address, - amount: u64, - public_key: VerifyingKey, + inputs: Vec, + outputs: Vec, timestamp: i64, signature: Signature, + previous_transaction_id: TransactionHash, ) -> Result { let transaction = Transaction { - from, - to, - amount, + inputs, + outputs, 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 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(); + + 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 @@ -144,91 +191,49 @@ 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, - &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, &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(transaction_id) + Ok(hex::encode(id)) } - pub fn get_address_balance_and_selfchain_height( - &mut self, - address: Address, - ) -> Result<(u64, u32)> { - let mut balance: u64 = 0; - - let reader = self + pub fn add_merkle_root(&self, root: &[u8; 32]) -> Result<()> { + let mut txn = self .lmdb_transaction_env - .begin_ro_txn() + .begin_rw_txn() .map_err(|e| anyhow!("Failed to begin transaction: {}", e))?; - let mut iterator = 0; + txn.put(self.merkle_db, root, &[], WriteFlags::empty()) + .map_err(|e| anyhow!("Failed to put Merkle root in database: {}", e))?; - loop { - let key = format!("{}:{}", address.as_hex(), iterator); - let transaction_bytes = match reader.get(self.db, &key) { - Ok(bytes) => bytes, - Err(lmdb::Error::NotFound) => break, - Err(e) => return Err(anyhow!("Database error: {}", e)), - }; - - let transaction: Transaction = 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() - )); - } - 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; - } + txn.commit() + .map_err(|e| anyhow!("Failed to commit Merkle root: {}", e))?; - Ok((balance, iterator)) + Ok(()) } - pub fn is_transaction_valid( - transaction: Transaction, - public_key: VerifyingKey, - signature: Signature, - ) -> Result { - let transaction_id = transaction.calculate_id()?; + 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))?; - public_key - .verify_strict(&transaction_id, &signature) - .map_err(|e| anyhow!("Signature verification failed: {}", e))?; + let mut roots = Vec::new(); + let mut cursor = reader + .open_ro_cursor(self.merkle_db) + .map_err(|e| anyhow!("Failed to create cursor: {}", e))?; + + 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