From 49c5799649a5fe58a6651b6fbc0ba15aa2e9595c Mon Sep 17 00:00:00 2001 From: jowparks Date: Fri, 12 Jan 2024 16:19:12 -0800 Subject: [PATCH] feat: Add build method proposed transaction (#4529) * add build() to ProposedTransaction to build UnsignedTransaction * add build method to proposed transaction --- ironfish-rust/src/transaction/mod.rs | 117 +++++++++++++++++++++- ironfish-rust/src/transaction/tests.rs | 53 ++++++++++ ironfish-rust/src/transaction/unsigned.rs | 20 ++-- 3 files changed, 179 insertions(+), 11 deletions(-) diff --git a/ironfish-rust/src/transaction/mod.rs b/ironfish-rust/src/transaction/mod.rs index d283005750..bf6efd09b5 100644 --- a/ironfish-rust/src/transaction/mod.rs +++ b/ironfish-rust/src/transaction/mod.rs @@ -18,7 +18,7 @@ use crate::{ note::Note, sapling_bls12::SAPLING, witness::WitnessTrait, - OutputDescription, SpendDescription, + OutgoingViewKey, OutputDescription, SpendDescription, ViewKey, }; use bellperson::groth16::{verify_proofs_batch, PreparedVerifyingKey}; @@ -34,6 +34,7 @@ use ironfish_zkp::{ VALUE_COMMITMENT_RANDOMNESS_GENERATOR, }, redjubjub::{self, PrivateKey, PublicKey, Signature}, + ProofGenerationKey, }; use std::{ @@ -45,6 +46,7 @@ use std::{ use self::{ burns::{BurnBuilder, BurnDescription}, mints::{MintBuilder, MintDescription, UnsignedMintDescription}, + unsigned::UnsignedTransaction, }; pub mod burns; @@ -188,6 +190,119 @@ impl ProposedTransaction { Ok(()) } + pub fn build( + &mut self, + proof_generation_key: ProofGenerationKey, + view_key: ViewKey, + outgoing_view_key: OutgoingViewKey, + public_address: PublicAddress, + change_goes_to: Option, + intended_transaction_fee: u64, + ) -> Result { + // The public key after randomization has been applied. This is used + // during signature verification. Referred to as `rk` in the literature + // Calculated from the authorizing key and the public_key_randomness. + let randomized_public_key = redjubjub::PublicKey(view_key.authorizing_key.into()) + .randomize(self.public_key_randomness, *SPENDING_KEY_GENERATOR); + + let mut change_notes = vec![]; + + for (asset_id, value) in self.value_balances.iter() { + let is_native_asset = asset_id == &NATIVE_ASSET; + + let change_amount = match is_native_asset { + true => *value - i64::try_from(intended_transaction_fee)?, + false => *value, + }; + + if change_amount < 0 { + return Err(IronfishError::new(IronfishErrorKind::InvalidBalance)); + } + if change_amount > 0 { + let change_address = change_goes_to.unwrap_or(public_address); + let change_note = Note::new( + change_address, + change_amount as u64, // we checked it was positive + "", + *asset_id, + public_address, + ); + + change_notes.push(change_note); + } + } + + for change_note in change_notes { + self.add_output(change_note)?; + } + + let mut unsigned_spends = Vec::with_capacity(self.spends.len()); + for spend in &self.spends { + unsigned_spends.push(spend.build( + &proof_generation_key, + &view_key, + &self.public_key_randomness, + &randomized_public_key, + )?); + } + + let mut output_descriptions = Vec::with_capacity(self.outputs.len()); + for output in &self.outputs { + output_descriptions.push(output.build( + &proof_generation_key, + &outgoing_view_key, + &self.public_key_randomness, + &randomized_public_key, + )?); + } + + let mut unsigned_mints = Vec::with_capacity(self.mints.len()); + for mint in &self.mints { + unsigned_mints.push(mint.build( + &proof_generation_key, + &public_address, + &self.public_key_randomness, + &randomized_public_key, + )?); + } + + let mut burn_descriptions = Vec::with_capacity(self.burns.len()); + for burn in &self.burns { + burn_descriptions.push(burn.build()); + } + + let data_to_sign = self.transaction_signature_hash( + &unsigned_spends, + &output_descriptions, + &unsigned_mints, + &burn_descriptions, + &randomized_public_key, + )?; + + // Create and verify binding signature keys + let (binding_signature_private_key, binding_signature_public_key) = + self.binding_signature_keys(&unsigned_mints, &burn_descriptions)?; + + let binding_signature = self.binding_signature( + &binding_signature_private_key, + &binding_signature_public_key, + &data_to_sign, + )?; + + Ok(UnsignedTransaction { + burns: burn_descriptions, + mints: unsigned_mints, + outputs: output_descriptions, + spends: unsigned_spends, + version: self.version, + fee: i64::try_from(intended_transaction_fee)?, + binding_signature, + randomized_public_key, + public_key_randomness: self.public_key_randomness, + expiration: self.expiration, + }) + } + /// Post the transaction. This performs a bit of validation, and signs /// the spends with a signature that proves the spends are part of this /// transaction. diff --git a/ironfish-rust/src/transaction/tests.rs b/ironfish-rust/src/transaction/tests.rs index 2dadbe7894..355254ae14 100644 --- a/ironfish-rust/src/transaction/tests.rs +++ b/ironfish-rust/src/transaction/tests.rs @@ -198,6 +198,59 @@ fn test_transaction_simple() { assert_eq!(received_note.sender, spender_key_clone.public_address()); } +#[test] +fn test_proposed_transaction_build() { + let spender_key = SaplingKey::generate_key(); + let receiver_key = SaplingKey::generate_key(); + let sender_key = SaplingKey::generate_key(); + let spender_key_clone = spender_key.clone(); + + let in_note = Note::new( + spender_key.public_address(), + 42, + "", + NATIVE_ASSET, + sender_key.public_address(), + ); + let out_note = Note::new( + receiver_key.public_address(), + 40, + "", + NATIVE_ASSET, + spender_key.public_address(), + ); + let witness = make_fake_witness(&in_note); + + let mut transaction = ProposedTransaction::new(TransactionVersion::latest()); + transaction.add_spend(in_note, &witness).unwrap(); + assert_eq!(transaction.spends.len(), 1); + transaction.add_output(out_note).unwrap(); + assert_eq!(transaction.outputs.len(), 1); + + let unsigned_transaction = transaction + .build( + spender_key.sapling_proof_generation_key(), + spender_key.view_key().clone(), + spender_key.outgoing_view_key().clone(), + spender_key.public_address(), + Some(spender_key.public_address()), + 1, + ) + .expect("should be able to build unsigned transaction"); + + // A change note was created + assert_eq!(unsigned_transaction.outputs.len(), 2); + assert_eq!(unsigned_transaction.spends.len(), 1); + assert_eq!(unsigned_transaction.mints.len(), 0); + assert_eq!(unsigned_transaction.burns.len(), 0); + + let received_note = unsigned_transaction.outputs[1] + .merkle_note() + .decrypt_note_for_owner(&spender_key_clone.incoming_viewing_key) + .unwrap(); + assert_eq!(received_note.sender, spender_key_clone.public_address()); +} + #[test] fn test_miners_fee() { let spender_key = SaplingKey::generate_key(); diff --git a/ironfish-rust/src/transaction/unsigned.rs b/ironfish-rust/src/transaction/unsigned.rs index c516bf7de7..cf309bd696 100644 --- a/ironfish-rust/src/transaction/unsigned.rs +++ b/ironfish-rust/src/transaction/unsigned.rs @@ -20,28 +20,28 @@ use super::{ pub struct UnsignedTransaction { /// The transaction serialization version. This can be incremented when /// changes need to be made to the transaction format - version: TransactionVersion, + pub(crate) version: TransactionVersion, /// List of spends, or input notes, that have been destroyed. - spends: Vec, + pub(crate) spends: Vec, /// List of outputs, or output notes that have been created. - outputs: Vec, + pub(crate) outputs: Vec, /// List of mint descriptions - mints: Vec, + pub(crate) mints: Vec, /// List of burn descriptions - burns: Vec, + pub(crate) burns: Vec, /// Signature calculated from accumulating randomness with all the spends /// and outputs when the transaction was created. - binding_signature: Signature, + pub(crate) binding_signature: Signature, /// This is the sequence in the chain the transaction will expire at and be /// removed from the mempool. A value of 0 indicates the transaction will /// not expire. - expiration: u32, + pub(crate) expiration: u32, /// Randomized public key of the sender of the Transaction /// currently this value is the same for all spends[].owner and outputs[].sender @@ -49,13 +49,13 @@ pub struct UnsignedTransaction { /// well as signing of the SpendDescriptions. Referred to as /// `rk` in the literature Calculated from the authorizing key and /// the public_key_randomness. - randomized_public_key: redjubjub::PublicKey, + pub(crate) randomized_public_key: redjubjub::PublicKey, // TODO: Verify if this is actually okay to store on the unsigned transaction - public_key_randomness: jubjub::Fr, + pub(crate) public_key_randomness: jubjub::Fr, /// The balance of total spends - outputs, which is the amount that the miner gets to keep - fee: i64, + pub(crate) fee: i64, } impl UnsignedTransaction {