Skip to content

Commit

Permalink
feat: Add build method proposed transaction (#4529)
Browse files Browse the repository at this point in the history
* add build() to ProposedTransaction to build UnsignedTransaction

* add build method to proposed transaction
  • Loading branch information
jowparks authored Jan 13, 2024
1 parent 4f8f05f commit 49c5799
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 11 deletions.
117 changes: 116 additions & 1 deletion ironfish-rust/src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -34,6 +34,7 @@ use ironfish_zkp::{
VALUE_COMMITMENT_RANDOMNESS_GENERATOR,
},
redjubjub::{self, PrivateKey, PublicKey, Signature},
ProofGenerationKey,
};

use std::{
Expand All @@ -45,6 +46,7 @@ use std::{
use self::{
burns::{BurnBuilder, BurnDescription},
mints::{MintBuilder, MintDescription, UnsignedMintDescription},
unsigned::UnsignedTransaction,
};

pub mod burns;
Expand Down Expand Up @@ -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<PublicAddress>,
intended_transaction_fee: u64,
) -> Result<UnsignedTransaction, IronfishError> {
// 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.
Expand Down
53 changes: 53 additions & 0 deletions ironfish-rust/src/transaction/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
20 changes: 10 additions & 10 deletions ironfish-rust/src/transaction/unsigned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,42 @@ 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<UnsignedSpendDescription>,
pub(crate) spends: Vec<UnsignedSpendDescription>,

/// List of outputs, or output notes that have been created.
outputs: Vec<OutputDescription>,
pub(crate) outputs: Vec<OutputDescription>,

/// List of mint descriptions
mints: Vec<UnsignedMintDescription>,
pub(crate) mints: Vec<UnsignedMintDescription>,

/// List of burn descriptions
burns: Vec<BurnDescription>,
pub(crate) burns: Vec<BurnDescription>,

/// 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
/// This is used during verification of SpendDescriptions and OutputDescriptions, as
/// 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 {
Expand Down

0 comments on commit 49c5799

Please sign in to comment.