From bd64c6099f7f1798d54c3b7cef8f5acbddf6c2e7 Mon Sep 17 00:00:00 2001 From: Quentin Le Sceller Date: Tue, 17 Apr 2018 16:40:51 -0400 Subject: [PATCH] Add aggregate function for multi-kernel transaction (#966) * Add aggregate function * Rustfmt * Add multikernel tx test * Rustfmt * Add test bad multi-kernel transaction * Add sorting * Modified multikernel test --- core/src/core/mod.rs | 21 +------- core/src/core/transaction.rs | 75 ++++++++++++++++++++++++++ pool/src/pool.rs | 102 ++++++++++++++++++++++++++++++++++- 3 files changed, 177 insertions(+), 21 deletions(-) diff --git a/core/src/core/mod.rs b/core/src/core/mod.rs index 4d6335924..5d1684dcd 100644 --- a/core/src/core/mod.rs +++ b/core/src/core/mod.rs @@ -353,26 +353,7 @@ mod test { assert!(tx2.validate().is_ok()); // now build a "cut_through" tx from tx1 and tx2 - let mut tx3 = tx1.clone(); - tx3.inputs.extend(tx2.inputs.iter().cloned()); - tx3.outputs.extend(tx2.outputs.iter().cloned()); - tx3.kernels.extend(tx2.kernels.iter().cloned()); - - // make sure everything is sorted - tx3.inputs.sort(); - tx3.outputs.sort(); - tx3.kernels.sort(); - - // finally sum the offsets up - // TODO - hide this in a convenience function somewhere - tx3.offset = { - let secp = static_secp_instance(); - let secp = secp.lock().unwrap(); - let skey1 = tx1.offset.secret_key(&secp).unwrap(); - let skey2 = tx2.offset.secret_key(&secp).unwrap(); - let skey3 = secp.blind_sum(vec![skey1, skey2], vec![]).unwrap(); - BlindingFactor::from_secret_key(skey3) - }; + let tx3 = aggregate(vec![tx1, tx2]).unwrap(); assert!(tx3.validate().is_ok()); } diff --git a/core/src/core/transaction.rs b/core/src/core/transaction.rs index a2fcb08aa..10e6b1b87 100644 --- a/core/src/core/transaction.rs +++ b/core/src/core/transaction.rs @@ -16,6 +16,7 @@ use util::secp::{self, Message, Signature}; use util::{kernel_sig_msg, static_secp_instance}; use util::secp::pedersen::{Commitment, ProofMessage, RangeProof}; +use std::collections::HashSet; use std::cmp::max; use std::cmp::Ordering; use std::{error, fmt}; @@ -488,6 +489,80 @@ impl Transaction { } } +/// Aggregate a vec of transactions into a multi-kernel transaction +pub fn aggregate(transactions: Vec) -> Result { + let mut inputs: Vec = vec![]; + let mut outputs: Vec = vec![]; + let mut kernels: Vec = vec![]; + + // we will sum these together at the end to give us the overall offset for the + // transaction + let mut kernel_offsets = vec![]; + + for mut transaction in transactions { + // we will summ these later to give a single aggregate offset + kernel_offsets.push(transaction.offset); + + inputs.append(&mut transaction.inputs); + outputs.append(&mut transaction.outputs); + kernels.append(&mut transaction.kernels); + } + + // now sum the kernel_offsets up to give us an aggregate offset for the + // transaction + let total_kernel_offset = { + let secp = static_secp_instance(); + let secp = secp.lock().unwrap(); + let mut keys = kernel_offsets + .iter() + .cloned() + .filter(|x| *x != BlindingFactor::zero()) + .filter_map(|x| x.secret_key(&secp).ok()) + .collect::>(); + + if keys.is_empty() { + BlindingFactor::zero() + } else { + let sum = secp.blind_sum(keys, vec![])?; + BlindingFactor::from_secret_key(sum) + } + }; + + let in_set = inputs + .iter() + .map(|inp| inp.commitment()) + .collect::>(); + + let out_set = outputs + .iter() + .filter(|out| !out.features.contains(OutputFeatures::COINBASE_OUTPUT)) + .map(|out| out.commitment()) + .collect::>(); + + let to_cut_through = in_set.intersection(&out_set).collect::>(); + + let mut new_inputs = inputs + .iter() + .filter(|inp| !to_cut_through.contains(&inp.commitment())) + .cloned() + .collect::>(); + + let mut new_outputs = outputs + .iter() + .filter(|out| !to_cut_through.contains(&out.commitment())) + .cloned() + .collect::>(); + + // sort them lexicographically + new_inputs.sort(); + new_outputs.sort(); + kernels.sort(); + + let tx = Transaction::new(new_inputs, new_outputs, kernels); + + Ok(tx.with_offset(total_kernel_offset)) +} + /// A transaction input. /// /// Primarily a reference to an output being spent by the transaction. diff --git a/pool/src/pool.rs b/pool/src/pool.rs index 070eac866..f4262d0d2 100644 --- a/pool/src/pool.rs +++ b/pool/src/pool.rs @@ -860,7 +860,7 @@ mod tests { use core::core::hash::{Hash, Hashed}; use core::core::pmmr::MerkleProof; use core::core::target::Difficulty; - use core::core::transaction::ProofMessageElements; + use core::core::transaction::{self, ProofMessageElements}; use types::PoolError::InvalidTx; macro_rules! expect_output_parent { @@ -942,6 +942,106 @@ mod tests { } } + #[test] + /// Attempt to add a multi kernel transaction to the mempool + fn test_multikernel_pool_add() { + let mut dummy_chain = DummyChainImpl::new(); + let head_header = block::BlockHeader { + height: 1, + ..block::BlockHeader::default() + }; + dummy_chain.store_head_header(&head_header); + + let parent_transaction = test_transaction(vec![5, 6, 7], vec![11, 3]); + // We want this transaction to be rooted in the blockchain. + let new_output = DummyOutputSet::empty() + .with_output(test_output(5)) + .with_output(test_output(6)) + .with_output(test_output(7)) + .with_output(test_output(8)); + + // Prepare a second transaction, connected to the first. + let child_transaction = test_transaction(vec![11, 3], vec![12]); + + let txs = vec![parent_transaction, child_transaction]; + let multi_kernel_transaction = transaction::aggregate(txs).unwrap(); + + dummy_chain.update_output_set(new_output); + + // To mirror how this construction is intended to be used, the pool + // is placed inside a RwLock. + let pool = RwLock::new(test_setup(&Arc::new(dummy_chain))); + + // Take the write lock and add a pool entry + { + let mut write_pool = pool.write().unwrap(); + assert_eq!(write_pool.total_size(), 0); + + // First, add the transaction rooted in the blockchain + let result = + write_pool.add_to_memory_pool(test_source(), multi_kernel_transaction, false); + if result.is_err() { + panic!( + "got an error adding multi-kernel tx: {:?}", + result.err().unwrap() + ); + } + } + + // Now take the read lock and use a few exposed methods to check consistency + { + let read_pool = pool.read().unwrap(); + assert_eq!(read_pool.total_size(), 1); + expect_output_parent!(read_pool, Parent::PoolTransaction{tx_ref: _}, 12); + expect_output_parent!(read_pool, Parent::AlreadySpent{other_tx: _}, 5); + expect_output_parent!(read_pool, Parent::BlockTransaction, 8); + expect_output_parent!(read_pool, Parent::Unknown, 11, 3, 20); + } + } + + #[test] + /// Attempt to add a bad multi kernel transaction to the mempool should get rejected + fn test_bad_multikernel_pool_add() { + let mut dummy_chain = DummyChainImpl::new(); + let head_header = block::BlockHeader { + height: 1, + ..block::BlockHeader::default() + }; + dummy_chain.store_head_header(&head_header); + + let parent_transaction = test_transaction(vec![5, 6, 7], vec![11, 3]); + // We want this transaction to be rooted in the blockchain. + let new_output = DummyOutputSet::empty() + .with_output(test_output(5)) + .with_output(test_output(6)) + .with_output(test_output(7)) + .with_output(test_output(8)); + + // Prepare a second transaction, connected to the first. + let child_transaction1 = test_transaction(vec![11, 3], vec![12]); + let child_transaction2 = test_transaction(vec![11, 3], vec![10]); + + let txs = vec![parent_transaction, child_transaction1, child_transaction2]; + let bad_multi_kernel_transaction = transaction::aggregate(txs).unwrap(); + + dummy_chain.update_output_set(new_output); + + // To mirror how this construction is intended to be used, the pool + // is placed inside a RwLock. + let pool = RwLock::new(test_setup(&Arc::new(dummy_chain))); + + // Take the write lock and add a pool entry + { + let mut write_pool = pool.write().unwrap(); + assert_eq!(write_pool.total_size(), 0); + + // First, add the transaction rooted in the blockchain + let result = + write_pool.add_to_memory_pool(test_source(), bad_multi_kernel_transaction, false); + assert!(result.is_err()); + } + } + #[test] /// A basic test; add a transaction to the pool and add the child to the /// stempool