Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add test to demonstrate pair of "half" kernels sharing same public excess #3314

Merged
merged 3 commits into from
May 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 35 additions & 24 deletions core/src/libtx/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,47 +199,58 @@ where
}

/// Builds a complete transaction.
/// NOTE: We only use this in tests (for convenience).
/// In the real world we use signature aggregation across multiple participants.
pub fn transaction<K, B>(
quentinlesceller marked this conversation as resolved.
Show resolved Hide resolved
features: KernelFeatures,
elems: Vec<Box<Append<K, B>>>,
keychain: &K,
builder: &B,
) -> Result<Transaction, Error>
where
K: Keychain,
B: ProofBuild,
{
let mut kernel = TxKernel::with_features(features);

// Construct the message to be signed.
let msg = kernel.msg_to_sign()?;

// Generate kernel public excess and associated signature.
let excess = BlindingFactor::rand(&keychain.secp());
let skey = excess.secret_key(&keychain.secp())?;
kernel.excess = keychain.secp().commit(0, skey)?;
let pubkey = &kernel.excess.to_pubkey(&keychain.secp())?;
kernel.excess_sig = aggsig::sign_with_blinding(&keychain.secp(), &msg, &excess, Some(&pubkey))?;
kernel.verify()?;
transaction_with_kernel(elems, kernel, excess, keychain, builder)
}

/// Build a complete transaction with the provided kernel and corresponding private excess.
/// NOTE: Only used in tests (for convenience).
/// Cannot recommend passing private excess around like this in the real world.
pub fn transaction_with_kernel<K, B>(
elems: Vec<Box<Append<K, B>>>,
kernel: TxKernel,
excess: BlindingFactor,
keychain: &K,
builder: &B,
) -> Result<Transaction, Error>
where
K: Keychain,
B: ProofBuild,
{
let mut ctx = Context { keychain, builder };
let (mut tx, sum) = elems
let (tx, sum) = elems
.iter()
.fold(Ok((Transaction::empty(), BlindSum::new())), |acc, elem| {
elem(&mut ctx, acc)
})?;
let blind_sum = ctx.keychain.blind_sum(&sum)?;

// Split the key so we can generate an offset for the tx.
let split = blind_sum.split(&keychain.secp())?;
let k1 = split.blind_1;
let k2 = split.blind_2;

let mut kern = TxKernel::with_features(features);

// Construct the message to be signed.
let msg = kern.msg_to_sign()?;

// Generate kernel excess and excess_sig using the split key k1.
let skey = k1.secret_key(&keychain.secp())?;
kern.excess = ctx.keychain.secp().commit(0, skey)?;
let pubkey = &kern.excess.to_pubkey(&keychain.secp())?;
kern.excess_sig = aggsig::sign_with_blinding(&keychain.secp(), &msg, &k1, Some(&pubkey))?;

// Store the kernel offset (k2) on the tx.
// Commitments will sum correctly when accounting for the offset.
tx.offset = k2;

// Set the kernel on the tx.
let tx = tx.replace_kernel(kern);

// Update tx with new kernel and offset.
let mut tx = tx.replace_kernel(kernel);
tx.offset = blind_sum.split(&excess, &keychain.secp())?;
Ok(tx)
}

Expand Down
77 changes: 76 additions & 1 deletion core/tests/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use self::core::core::{
aggregate, deaggregate, KernelFeatures, Output, Transaction, TxKernel, Weighting,
};
use self::core::libtx::build::{self, initial_tx, input, output, with_excess};
use self::core::libtx::ProofBuilder;
use self::core::libtx::{aggsig, ProofBuilder};
use self::core::{global, ser};
use crate::common::{new_block, tx1i1o, tx1i2o, tx2i1o};
use grin_core as core;
Expand Down Expand Up @@ -148,6 +148,81 @@ fn build_tx_kernel() {
assert_eq!(2, tx.fee());
}

// Proof of concept demonstrating we can build two transactions that share
// the *same* kernel public excess. This is a key part of building a transaction as two
// "halves" for NRD kernels.
// Note: In a real world scenario multiple participants would build the kernel signature
// using signature aggregation. No party would see the full private kernel excess and
// the halves would need to be constructed with carefully crafted individual offsets to
// adjust the excess as required.
// For the sake of convenience we are simply constructing the kernel directly and we have access
// to the full private excess.
#[test]
fn build_two_half_kernels() {
test_setup();
let keychain = ExtKeychain::from_random_seed(false).unwrap();
let builder = ProofBuilder::new(&keychain);
let key_id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0);
let key_id3 = ExtKeychain::derive_key_id(1, 3, 0, 0, 0);

// build kernel with associated private excess
let mut kernel = TxKernel::with_features(KernelFeatures::Plain { fee: 2 });

// Construct the message to be signed.
let msg = kernel.msg_to_sign().unwrap();

// Generate a kernel with public excess and associated signature.
let excess = BlindingFactor::rand(&keychain.secp());
let skey = excess.secret_key(&keychain.secp()).unwrap();
kernel.excess = keychain.secp().commit(0, skey).unwrap();
let pubkey = &kernel.excess.to_pubkey(&keychain.secp()).unwrap();
kernel.excess_sig =
aggsig::sign_with_blinding(&keychain.secp(), &msg, &excess, Some(&pubkey)).unwrap();
kernel.verify().unwrap();

let tx1 = build::transaction_with_kernel(
vec![input(10, key_id1), output(8, key_id2.clone())],
kernel.clone(),
excess.clone(),
&keychain,
&builder,
)
.unwrap();

let tx2 = build::transaction_with_kernel(
vec![input(8, key_id2), output(6, key_id3)],
kernel.clone(),
excess.clone(),
&keychain,
&builder,
)
.unwrap();

assert_eq!(
tx1.validate(Weighting::AsTransaction, verifier_cache()),
Ok(()),
);

assert_eq!(
tx2.validate(Weighting::AsTransaction, verifier_cache()),
Ok(()),
);

// The transactions share an identical kernel.
assert_eq!(tx1.kernels()[0], tx2.kernels()[0]);

// The public kernel excess is shared between both "halves".
assert_eq!(tx1.kernels()[0].excess(), tx2.kernels()[0].excess());

// Each transaction is built from different inputs and outputs.
// The offset differs to compensate for the shared excess commitments.
assert!(tx1.offset != tx2.offset);

// For completeness, these are different transactions.
assert!(tx1.hash() != tx2.hash());
}

// Combine two transactions into one big transaction (with multiple kernels)
// and check it still validates.
#[test]
Expand Down
114 changes: 52 additions & 62 deletions keychain/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//! Keychain trait and its main supporting types. The Identifier is a
//! semi-opaque structure (just bytes) to track keys within the Keychain.
//! BlindingFactor is a useful wrapper around a private key to help with
//! commitment generation.

use rand::thread_rng;
use std::cmp::min;
use std::convert::TryFrom;
use std::io::Cursor;
use std::ops::Add;
/// Keychain trait and its main supporting types. The Identifier is a
/// semi-opaque structure (just bytes) to track keys within the Keychain.
/// BlindingFactor is a useful wrapper around a private key to help with
/// commitment generation.
use std::{error, fmt};

use crate::blake2::blake2b::blake2b;
use crate::extkey_bip32::{self, ChildNumber};
use serde::{de, ser}; //TODO: Convert errors to use ErrorKind

use crate::util::secp::constants::SECRET_KEY_SIZE;
use crate::util::secp::key::{PublicKey, SecretKey};
use crate::util::secp::key::{PublicKey, SecretKey, ZERO_KEY};
use crate::util::secp::pedersen::Commitment;
use crate::util::secp::{self, Message, Secp256k1, Signature};
use crate::util::static_secp_instance;
use crate::util::ToHex;
use zeroize::Zeroize;

Expand Down Expand Up @@ -242,34 +241,8 @@ impl AsRef<[u8]> for BlindingFactor {
}
}

impl Add for BlindingFactor {
type Output = Result<BlindingFactor, Error>;

// Convenient (and robust) way to add two blinding_factors together.
// Handles "zero" blinding_factors correctly.
//
// let bf = (bf1 + bf2)?;
//
fn add(self, other: BlindingFactor) -> Self::Output {
let secp = static_secp_instance();
let secp = secp.lock();
let keys = vec![self, other]
.into_iter()
.filter(|x| *x != BlindingFactor::zero())
.filter_map(|x| x.secret_key(&secp).ok())
.collect::<Vec<_>>();

if keys.is_empty() {
Ok(BlindingFactor::zero())
} else {
let sum = secp.blind_sum(keys, vec![])?;
Ok(BlindingFactor::from_secret_key(sum))
}
}
}

impl BlindingFactor {
pub fn from_secret_key(skey: secp::key::SecretKey) -> BlindingFactor {
pub fn from_secret_key(skey: SecretKey) -> BlindingFactor {
BlindingFactor::from_slice(&skey.as_ref())
}

Expand All @@ -281,22 +254,45 @@ impl BlindingFactor {
}

pub fn zero() -> BlindingFactor {
BlindingFactor::from_secret_key(secp::key::ZERO_KEY)
BlindingFactor::from_secret_key(ZERO_KEY)
}

pub fn is_zero(&self) -> bool {
self.0 == ZERO_KEY.as_ref()
}

pub fn rand(secp: &Secp256k1) -> BlindingFactor {
BlindingFactor::from_secret_key(SecretKey::new(secp, &mut thread_rng()))
}
antiochp marked this conversation as resolved.
Show resolved Hide resolved

pub fn from_hex(hex: &str) -> Result<BlindingFactor, Error> {
let bytes = util::from_hex(hex).unwrap();
Ok(BlindingFactor::from_slice(&bytes))
}

pub fn secret_key(&self, secp: &Secp256k1) -> Result<secp::key::SecretKey, Error> {
if *self == BlindingFactor::zero() {
// TODO - need this currently for tx tests
// the "zero" secret key is not actually a valid secret_key
// and secp lib checks this
Ok(secp::key::ZERO_KEY)
// Handle "zero" blinding_factor correctly, by returning the "zero" key.
// We need this for some of the tests.
pub fn secret_key(&self, secp: &Secp256k1) -> Result<SecretKey, Error> {
if self.is_zero() {
Ok(ZERO_KEY)
} else {
secp::key::SecretKey::from_slice(secp, &self.0).map_err(Error::Secp)
SecretKey::from_slice(secp, &self.0).map_err(Error::Secp)
}
}

// Convenient (and robust) way to add two blinding_factors together.
// Handles "zero" blinding_factors correctly.
pub fn add(&self, other: &BlindingFactor, secp: &Secp256k1) -> Result<BlindingFactor, Error> {
antiochp marked this conversation as resolved.
Show resolved Hide resolved
let keys = vec![self, other]
.into_iter()
.filter(|x| !x.is_zero())
.filter_map(|x| x.secret_key(&secp).ok())
.collect::<Vec<_>>();
if keys.is_empty() {
Ok(BlindingFactor::zero())
} else {
let sum = secp.blind_sum(keys, vec![])?;
Ok(BlindingFactor::from_secret_key(sum))
}
}

Expand All @@ -306,26 +302,19 @@ impl BlindingFactor {
/// This prevents an actor from being able to sum a set of inputs, outputs
/// and kernels from a block to identify and reconstruct a particular tx
/// from a block. You would need both k1, k2 to do this.
pub fn split(&self, secp: &Secp256k1) -> Result<SplitBlindingFactor, Error> {
let skey_1 = secp::key::SecretKey::new(secp, &mut thread_rng());

// use blind_sum to subtract skey_1 from our key (to give k = k1 + k2)
pub fn split(
&self,
blind_1: &BlindingFactor,
secp: &Secp256k1,
) -> Result<BlindingFactor, Error> {
// use blind_sum to subtract skey_1 from our key such that skey = skey_1 + skey_2
let skey = self.secret_key(secp)?;
let skey_2 = secp.blind_sum(vec![skey], vec![skey_1.clone()])?;

let blind_1 = BlindingFactor::from_secret_key(skey_1);
let blind_2 = BlindingFactor::from_secret_key(skey_2);

Ok(SplitBlindingFactor { blind_1, blind_2 })
let skey_1 = blind_1.secret_key(secp)?;
let skey_2 = secp.blind_sum(vec![skey], vec![skey_1])?;
Ok(BlindingFactor::from_secret_key(skey_2))
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SplitBlindingFactor {
pub blind_1: BlindingFactor,
pub blind_2: BlindingFactor,
}

/// Accumulator to compute the sum of blinding factors. Keeps track of each
/// factor as well as the "sign" with which they should be combined.
#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -557,16 +546,17 @@ mod test {
assert!(all_zeros)
}

// split a key, sum the split keys and confirm the sum matches the original key
#[test]
fn split_blinding_factor() {
let secp = Secp256k1::new();
let skey_in = SecretKey::new(&secp, &mut thread_rng());
let blind = BlindingFactor::from_secret_key(skey_in.clone());
let split = blind.split(&secp).unwrap();
let blind_1 = BlindingFactor::rand(&secp);
let blind_2 = blind.split(&blind_1, &secp).unwrap();

// split a key, sum the split keys and confirm the sum matches the original key
let mut skey_sum = split.blind_1.secret_key(&secp).unwrap();
let skey_2 = split.blind_2.secret_key(&secp).unwrap();
let mut skey_sum = blind_1.secret_key(&secp).unwrap();
let skey_2 = blind_2.secret_key(&secp).unwrap();
skey_sum.add_assign(&secp, &skey_2).unwrap();
assert_eq!(skey_in, skey_sum);
}
Expand Down
8 changes: 7 additions & 1 deletion pool/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use grin_util as util;
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use util::static_secp_instance;

pub struct Pool<B, V>
where
Expand Down Expand Up @@ -272,7 +273,12 @@ where
header: &BlockHeader,
) -> Result<BlockSums, PoolError> {
let overage = tx.overage();
let offset = (header.total_kernel_offset() + tx.offset.clone())?;

let offset = {
let secp = static_secp_instance();
antiochp marked this conversation as resolved.
Show resolved Hide resolved
let secp = secp.lock();
header.total_kernel_offset().add(&tx.offset, &secp)
}?;

let block_sums = self.blockchain.get_block_sums(&header.hash())?;

Expand Down