From f4aa3e7c18243422715e1379c23c919626e8ddfb Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Fri, 7 Mar 2025 05:37:15 +0200 Subject: [PATCH] feat: sc core helpers and manipulators --- Cargo.lock | 24 ++ Cargo.toml | 1 + sc_core/Cargo.toml | 36 +++ sc_core/src/lib.rs | 3 + sc_core/src/proofs_circuits.rs | 299 ++++++++++++++++++++++ sc_core/src/transaction_payloads_tools.rs | 92 +++++++ sc_core/src/utxo_manipulator.rs | 110 ++++++++ 7 files changed, 565 insertions(+) create mode 100644 sc_core/Cargo.toml create mode 100644 sc_core/src/lib.rs create mode 100644 sc_core/src/proofs_circuits.rs create mode 100644 sc_core/src/transaction_payloads_tools.rs create mode 100644 sc_core/src/utxo_manipulator.rs diff --git a/Cargo.lock b/Cargo.lock index bed3f10..911d9a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4374,6 +4374,30 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "sc_core" +version = "0.1.0" +dependencies = [ + "accounts", + "anyhow", + "bincode", + "common", + "elliptic-curve", + "env_logger", + "hex", + "k256", + "log", + "monotree", + "rand 0.8.5", + "risc0-zkvm", + "secp256k1-zkp", + "serde", + "serde_json", + "sha2 0.10.8", + "storage", + "utxo", +] + [[package]] name = "schannel" version = "0.1.27" diff --git a/Cargo.toml b/Cargo.toml index 548f216..554bf33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "sequencer_core", "rpc_primitives", "common", + "sc_core", ] [workspace.dependencies] diff --git a/sc_core/Cargo.toml b/sc_core/Cargo.toml new file mode 100644 index 0000000..f894300 --- /dev/null +++ b/sc_core/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sc_core" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +serde_json.workspace = true +env_logger.workspace = true +log.workspace = true +serde.workspace = true +rand.workspace = true +k256.workspace = true +sha2.workspace = true +monotree.workspace = true +bincode.workspace = true +elliptic-curve.workspace = true +hex.workspace = true + +risc0-zkvm = { git = "https://github.com/risc0/risc0.git", branch = "release-1.3" } + +[dependencies.accounts] +path = "../accounts" + +[dependencies.storage] +path = "../storage" + +[dependencies.utxo] +path = "../utxo" + +[dependencies.common] +path = "../common" + +[dependencies.secp256k1-zkp] +workspace = true +features = ["std", "rand-std", "rand", "serde", "global-context"] diff --git a/sc_core/src/lib.rs b/sc_core/src/lib.rs new file mode 100644 index 0000000..7a67a54 --- /dev/null +++ b/sc_core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod proofs_circuits; +pub mod transaction_payloads_tools; +pub mod utxo_manipulator; diff --git a/sc_core/src/proofs_circuits.rs b/sc_core/src/proofs_circuits.rs new file mode 100644 index 0000000..7c646c2 --- /dev/null +++ b/sc_core/src/proofs_circuits.rs @@ -0,0 +1,299 @@ +use bincode; +use k256::Scalar; +use monotree::hasher::Blake3; +use monotree::{Hasher, Monotree}; +use rand::thread_rng; +use secp256k1_zkp::{CommitmentSecrets, Generator, PedersenCommitment, Tag, Tweak, SECP256K1}; +use sha2::{Digest, Sha256}; +use storage::{ + commitment::Commitment, commitments_sparse_merkle_tree::CommitmentsSparseMerkleTree, + nullifier::UTXONullifier, nullifier_sparse_merkle_tree::NullifierSparseMerkleTree, +}; +use utxo::utxo_core::UTXO; + +fn hash(input: &[u8]) -> Vec { + Sha256::digest(input).to_vec() +} + +// Generate nullifiers + +// takes the input_utxo and nsk +// returns the nullifiers[i], where the nullifier[i] = hash(in_commitments[i] || nsk) where the hash function +pub fn generate_nullifiers(input_utxo: &UTXO, nsk: &[u8]) -> Vec { + let mut input = bincode::serialize(input_utxo).unwrap().to_vec(); + input.extend_from_slice(nsk); + hash(&input) +} + +// Generate commitments for output UTXOs + +// uses the list of input_utxos[] +// returns in_commitments[] where each in_commitments[i] = Commitment(in_utxos[i]) where the commitment +pub fn generate_commitments(input_utxos: &[UTXO]) -> Vec> { + input_utxos + .iter() + .map(|utxo| { + let serialized = bincode::serialize(utxo).unwrap(); // Serialize UTXO. + hash(&serialized) + }) + .collect() +} + +// Validate inclusion proof for in_commitments + +// takes the in_commitments[i] as a leaf, the root hash root_commitment and the path in_commitments_proofs[i][], +// returns True if the in_commitments[i] is in the tree with root hash root_commitment otherwise returns False, as membership proof. +pub fn validate_in_commitments_proof( + in_commitment: &Vec, + root_commitment: Vec, + in_commitments_proof: &[Vec], +) -> bool { + // Placeholder implementation. + // Replace with Merkle proof verification logic. + // hash(&[pedersen_commitment.serialize().to_vec(), in_commitments_proof.concat()].concat()) == root_commitment + + let mut nsmt = CommitmentsSparseMerkleTree { + curr_root: Option::Some(root_commitment), + tree: Monotree::default(), + hasher: Blake3::new(), + }; + + let commitments: Vec<_> = in_commitments_proof + .into_iter() + .map(|n_p| Commitment { + commitment_hash: n_p.clone(), + }) + .collect(); + nsmt.insert_items(commitments).unwrap(); + + nsmt.get_non_membership_proof(in_commitment.clone()) + .unwrap() + .1 + .is_some() +} + +// Validate non-membership proof for nullifiers + +// takes the nullifiers[i], path nullifiers_proof[i][] and the root hash root_nullifier, +// returns True if the nullifiers[i] is not in the tree with root hash root_nullifier otherwise returns False, as non-membership proof. +pub fn validate_nullifiers_proof( + nullifier: [u8; 32], + root_nullifier: [u8; 32], + nullifiers_proof: &[[u8; 32]], +) -> bool { + let mut nsmt = NullifierSparseMerkleTree { + curr_root: Option::Some(root_nullifier), + tree: Monotree::default(), + hasher: Blake3::new(), + }; + + let nullifiers: Vec<_> = nullifiers_proof + .into_iter() + .map(|n_p| UTXONullifier { utxo_hash: *n_p }) + .collect(); + nsmt.insert_items(nullifiers).unwrap(); + + nsmt.get_non_membership_proof(nullifier) + .unwrap() + .1 + .is_none() +} + +#[allow(unused)] +fn private_kernel( + root_commitment: &[u8], + root_nullifier: [u8; 32], + input_utxos: &[UTXO], + in_commitments_proof: &[Vec], + nullifiers_proof: &[[u8; 32]], + nullifier_secret_key: Scalar, +) -> (Vec, Vec>) { + let nullifiers: Vec<_> = input_utxos + .into_iter() + .map(|utxo| generate_nullifiers(&utxo, &nullifier_secret_key.to_bytes())) + .collect(); + + let in_commitments = generate_commitments(&input_utxos); + + for in_commitment in in_commitments { + validate_in_commitments_proof( + &in_commitment, + root_commitment.to_vec(), + in_commitments_proof, + ); + } + + for nullifier in nullifiers.iter() { + validate_nullifiers_proof( + nullifier[0..32].try_into().unwrap(), + root_nullifier, + nullifiers_proof, + ); + } + + (vec![], nullifiers) +} + +#[allow(unused)] +fn commitment_secrets_random(value: u64) -> CommitmentSecrets { + CommitmentSecrets { + value, + value_blinding_factor: Tweak::new(&mut thread_rng()), + generator_blinding_factor: Tweak::new(&mut thread_rng()), + } +} + +pub fn tag_random() -> Tag { + use rand::thread_rng; + use rand::RngCore; + + let mut bytes = [0u8; 32]; + thread_rng().fill_bytes(&mut bytes); + + Tag::from(bytes) +} + +pub fn commit(comm: &CommitmentSecrets, tag: Tag) -> PedersenCommitment { + let generator = Generator::new_blinded(SECP256K1, tag, comm.generator_blinding_factor); + + PedersenCommitment::new(SECP256K1, comm.value, comm.value_blinding_factor, generator) +} + +// Check balances + +// takes the public_info and output_utxos[], +// returns the True if the token amount in public_info matches the sum of all output_utxos[], otherwise return False. +pub fn check_balances(public_info: u128, output_utxos: &[UTXO]) -> bool { + let total_output: u128 = output_utxos.iter().map(|utxo| utxo.amount).sum(); + public_info == total_output +} + +// Verify Pedersen commitment + +// takes the public_info, secret_r and pedersen_commitment and +// checks that commitment(public_info,secret_r) is equal pedersen_commitment where the commitment is pedersen commitment. +pub fn verify_commitment( + public_info: u64, + secret_r: &[u8], + pedersen_commitment: &PedersenCommitment, +) -> bool { + let commitment_secrets = CommitmentSecrets { + value: public_info, + value_blinding_factor: Tweak::from_slice(secret_r).unwrap(), + generator_blinding_factor: Tweak::new(&mut thread_rng()), + }; + + let tag = tag_random(); + let commitment = commit(&commitment_secrets, tag); + + commitment == *pedersen_commitment +} + +#[allow(unused)] +fn de_kernel( + root_commitment: &[u8], + root_nullifier: [u8; 32], + public_info: u64, + input_utxos: &[UTXO], + in_commitments_proof: &[Vec], + nullifiers_proof: &[[u8; 32]], + nullifier_secret_key: Scalar, +) -> (Vec, Vec>) { + check_balances(public_info as u128, input_utxos); + + let nullifiers: Vec<_> = input_utxos + .into_iter() + .map(|utxo| generate_nullifiers(&utxo, &nullifier_secret_key.to_bytes())) + .collect(); + + let in_commitments = generate_commitments(&input_utxos); + + for in_commitment in in_commitments { + validate_in_commitments_proof( + &in_commitment, + root_commitment.to_vec(), + in_commitments_proof, + ); + } + + for nullifier in nullifiers.iter() { + validate_nullifiers_proof( + nullifier[0..32].try_into().unwrap(), + root_nullifier, + nullifiers_proof, + ); + } + + (vec![], nullifiers) +} + +// Validate inclusion proof for in_commitments + +// takes the pedersen_commitment as a leaf, the root hash root_commitment and the path in_commitments_proof[], +// returns True if the pedersen_commitment is in the tree with root hash root_commitment +// otherwise +// returns False, as membership proof. +pub fn validate_in_commitments_proof_se( + pedersen_commitment: &PedersenCommitment, + root_commitment: Vec, + in_commitments_proof: &[Vec], +) -> bool { + let mut nsmt = CommitmentsSparseMerkleTree { + curr_root: Option::Some(root_commitment), + tree: Monotree::default(), + hasher: Blake3::new(), + }; + + let commitments: Vec<_> = in_commitments_proof + .into_iter() + .map(|n_p| Commitment { + commitment_hash: n_p.clone(), + }) + .collect(); + nsmt.insert_items(commitments).unwrap(); + + nsmt.get_non_membership_proof(pedersen_commitment.serialize().to_vec()) + .unwrap() + .1 + .is_some() +} + +// Generate nullifiers SE + +// takes the pedersen_commitment and nsk then +// returns a list of nullifiers, where the nullifier = hash(pedersen_commitment || nsk) where the hash function will be determined + +pub fn generate_nullifiers_se(pedersen_commitment: &PedersenCommitment, nsk: &[u8]) -> Vec { + let mut input = pedersen_commitment.serialize().to_vec(); + input.extend_from_slice(nsk); + hash(&input) +} + +#[allow(unused)] +fn se_kernel( + root_commitment: &[u8], + root_nullifier: [u8; 32], + public_info: u64, + pedersen_commitment: PedersenCommitment, + secret_r: &[u8], + output_utxos: &[UTXO], + in_commitments_proof: &[Vec], + nullifiers_proof: &[[u8; 32]], + nullifier_secret_key: Scalar, +) -> (Vec, Vec>, Vec) { + check_balances(public_info as u128, output_utxos); + + let out_commitments = generate_commitments(output_utxos); + + let nullifier = generate_nullifiers_se(&pedersen_commitment, &nullifier_secret_key.to_bytes()); + + validate_in_commitments_proof_se( + &pedersen_commitment, + root_commitment.to_vec(), + in_commitments_proof, + ); + + verify_commitment(public_info, secret_r, &pedersen_commitment); + + (vec![], out_commitments, nullifier) +} diff --git a/sc_core/src/transaction_payloads_tools.rs b/sc_core/src/transaction_payloads_tools.rs new file mode 100644 index 0000000..f1a99e0 --- /dev/null +++ b/sc_core/src/transaction_payloads_tools.rs @@ -0,0 +1,92 @@ +use accounts::account_core::Account; +use anyhow::Result; +use rand::thread_rng; +use risc0_zkvm::Receipt; +use secp256k1_zkp::{CommitmentSecrets, PedersenCommitment, Tweak}; +use storage::transaction::{TransactionPayload, TxKind}; +use utxo::utxo_core::UTXO; + +use crate::proofs_circuits::{commit, generate_nullifiers, tag_random}; + +pub fn create_public_transaction_payload(execution_input: Vec) -> TransactionPayload { + TransactionPayload { + tx_kind: TxKind::Public, + execution_input, + execution_output: vec![], + utxo_commitments_spent_hashes: vec![], + utxo_commitments_created_hashes: vec![], + nullifier_created_hashes: vec![], + execution_proof_private: "".to_string(), + encoded_data: vec![], + ephemeral_pub_key: vec![], + } +} + +pub fn encode_utxos_to_receivers( + utxos_receivers: Vec<(UTXO, &Account)>, +) -> Vec<(Vec, Vec)> { + let mut all_encoded_data = vec![]; + + for (utxo, receiver) in utxos_receivers { + let ephm_key_holder = &receiver.produce_ephemeral_key_holder(); + + let encoded_data = Account::encrypt_data( + &ephm_key_holder, + receiver.key_holder.viewing_public_key, + &serde_json::to_vec(&utxo).unwrap(), + ); + + let encoded_data_vec = (encoded_data.0, encoded_data.1.to_vec()); + + all_encoded_data.push(encoded_data_vec); + } + + all_encoded_data +} + +pub fn generate_nullifiers_spent_utxos(utxos_spent: Vec<(UTXO, &Account)>) -> Vec> { + let mut all_nullifiers = vec![]; + + for (utxo, spender) in utxos_spent { + let nullifier = generate_nullifiers( + &utxo, + &spender + .key_holder + .utxo_secret_key_holder + .nullifier_secret_key + .to_bytes() + .to_vec(), + ); + + all_nullifiers.push(nullifier); + } + + all_nullifiers +} + +pub fn encode_receipt(receipt: Receipt) -> Result { + Ok(hex::encode(serde_json::to_vec(&receipt)?)) +} + +pub fn generate_secret_random_commitment( + value: u64, + account: &Account, +) -> Result { + let commitment_secrets = CommitmentSecrets { + value, + value_blinding_factor: Tweak::from_slice( + &account + .key_holder + .utxo_secret_key_holder + .viewing_secret_key + .to_bytes() + .to_vec(), + )?, + generator_blinding_factor: Tweak::new(&mut thread_rng()), + }; + + let tag = tag_random(); + let commitment = commit(&commitment_secrets, tag); + + Ok(commitment) +} diff --git a/sc_core/src/utxo_manipulator.rs b/sc_core/src/utxo_manipulator.rs new file mode 100644 index 0000000..b59c129 --- /dev/null +++ b/sc_core/src/utxo_manipulator.rs @@ -0,0 +1,110 @@ +use anyhow::Result; +use storage::nullifier::UTXONullifier; +use utxo::utxo_core::{UTXOPayload, UTXO}; + +pub fn utxo_change_owner( + utxo: &mut UTXO, + nullifier: UTXONullifier, + new_owner: [u8; 32], +) -> Result { + let new_payload = UTXOPayload { + owner: new_owner, + asset: utxo.asset.clone(), + amount: utxo.amount, + privacy_flag: utxo.privacy_flag, + }; + + utxo.consume_utxo(nullifier)?; + + Ok(UTXO::create_utxo_from_payload(new_payload)?) +} + +pub fn utxo_substact_part_another_owner( + utxo: &mut UTXO, + nullifier: UTXONullifier, + amount: u128, + new_owner: [u8; 32], +) -> Result<(UTXO, UTXO)> { + if amount > utxo.amount { + anyhow::bail!("Amount too big"); + } + + let diff = utxo.amount - amount; + + let new_payload1 = UTXOPayload { + owner: utxo.owner, + asset: utxo.asset.clone(), + amount: diff, + privacy_flag: utxo.privacy_flag, + }; + + let new_payload2 = UTXOPayload { + owner: new_owner, + asset: utxo.asset.clone(), + amount, + privacy_flag: utxo.privacy_flag, + }; + + utxo.consume_utxo(nullifier)?; + + Ok(( + UTXO::create_utxo_from_payload(new_payload1)?, + UTXO::create_utxo_from_payload(new_payload2)?, + )) +} + +pub fn utxo_substract_part( + utxo: &mut UTXO, + nullifier: UTXONullifier, + amount: u128, +) -> Result<(UTXO, UTXO)> { + let new_owner = utxo.owner; + + utxo_substact_part_another_owner(utxo, nullifier, amount, new_owner) +} + +pub fn utxo_split_n_users( + utxo: &mut UTXO, + nullifier: UTXONullifier, + users_amounts: Vec<([u8; 32], u128)>, +) -> Result> { + let cumulative_diff = users_amounts + .iter() + .fold(0, |acc, (_, amount)| acc + *amount); + + if cumulative_diff > utxo.amount { + anyhow::bail!("Amount too big"); + } + + let mut utxo_res = vec![]; + + for (new_owner, amount) in users_amounts { + let new_payload = UTXOPayload { + owner: new_owner, + asset: utxo.asset.clone(), + amount, + privacy_flag: utxo.privacy_flag, + }; + + let new_utxo = UTXO::create_utxo_from_payload(new_payload)?; + + utxo_res.push(new_utxo); + } + + if cumulative_diff != utxo.amount { + let new_payload = UTXOPayload { + owner: utxo.owner, + asset: utxo.asset.clone(), + amount: utxo.amount - cumulative_diff, + privacy_flag: utxo.privacy_flag, + }; + + let new_utxo = UTXO::create_utxo_from_payload(new_payload)?; + + utxo_res.push(new_utxo); + } + + utxo.consume_utxo(nullifier)?; + + Ok(utxo_res) +}