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
This commit is contained in:
Quentin Le Sceller 2018-04-17 16:40:51 -04:00 committed by Antioch Peverell
parent 7bad33d249
commit bd64c6099f
3 changed files with 177 additions and 21 deletions

View file

@ -353,26 +353,7 @@ mod test {
assert!(tx2.validate().is_ok()); assert!(tx2.validate().is_ok());
// now build a "cut_through" tx from tx1 and tx2 // now build a "cut_through" tx from tx1 and tx2
let mut tx3 = tx1.clone(); let tx3 = aggregate(vec![tx1, tx2]).unwrap();
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)
};
assert!(tx3.validate().is_ok()); assert!(tx3.validate().is_ok());
} }

View file

@ -16,6 +16,7 @@
use util::secp::{self, Message, Signature}; use util::secp::{self, Message, Signature};
use util::{kernel_sig_msg, static_secp_instance}; use util::{kernel_sig_msg, static_secp_instance};
use util::secp::pedersen::{Commitment, ProofMessage, RangeProof}; use util::secp::pedersen::{Commitment, ProofMessage, RangeProof};
use std::collections::HashSet;
use std::cmp::max; use std::cmp::max;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::{error, fmt}; 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<Transaction>) -> Result<Transaction, Error> {
let mut inputs: Vec<Input> = vec![];
let mut outputs: Vec<Output> = vec![];
let mut kernels: Vec<TxKernel> = 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::<Vec<_>>();
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::<HashSet<_>>();
let out_set = outputs
.iter()
.filter(|out| !out.features.contains(OutputFeatures::COINBASE_OUTPUT))
.map(|out| out.commitment())
.collect::<HashSet<_>>();
let to_cut_through = in_set.intersection(&out_set).collect::<HashSet<_>>();
let mut new_inputs = inputs
.iter()
.filter(|inp| !to_cut_through.contains(&inp.commitment()))
.cloned()
.collect::<Vec<_>>();
let mut new_outputs = outputs
.iter()
.filter(|out| !to_cut_through.contains(&out.commitment()))
.cloned()
.collect::<Vec<_>>();
// 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. /// A transaction input.
/// ///
/// Primarily a reference to an output being spent by the transaction. /// Primarily a reference to an output being spent by the transaction.

View file

@ -860,7 +860,7 @@ mod tests {
use core::core::hash::{Hash, Hashed}; use core::core::hash::{Hash, Hashed};
use core::core::pmmr::MerkleProof; use core::core::pmmr::MerkleProof;
use core::core::target::Difficulty; use core::core::target::Difficulty;
use core::core::transaction::ProofMessageElements; use core::core::transaction::{self, ProofMessageElements};
use types::PoolError::InvalidTx; use types::PoolError::InvalidTx;
macro_rules! expect_output_parent { 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] #[test]
/// A basic test; add a transaction to the pool and add the child to the /// A basic test; add a transaction to the pool and add the child to the
/// stempool /// stempool