mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-20 19:11:08 +03:00
add test to demonstrate pair of "half" kernels sharing same public excess (#3314)
* cleanup how we handle key splitting for transaction offset add test to demonstrate a pair of transaction halves sharing same kernel excess * cleanup * cleanup
This commit is contained in:
parent
b570ac9925
commit
a8b8dc3a7f
4 changed files with 170 additions and 88 deletions
|
@ -199,47 +199,58 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a complete transaction.
|
/// 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>(
|
pub fn transaction<K, B>(
|
||||||
features: KernelFeatures,
|
features: KernelFeatures,
|
||||||
elems: Vec<Box<Append<K, B>>>,
|
elems: Vec<Box<Append<K, B>>>,
|
||||||
keychain: &K,
|
keychain: &K,
|
||||||
builder: &B,
|
builder: &B,
|
||||||
) -> Result<Transaction, Error>
|
) -> 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
|
where
|
||||||
K: Keychain,
|
K: Keychain,
|
||||||
B: ProofBuild,
|
B: ProofBuild,
|
||||||
{
|
{
|
||||||
let mut ctx = Context { keychain, builder };
|
let mut ctx = Context { keychain, builder };
|
||||||
let (mut tx, sum) = elems
|
let (tx, sum) = elems
|
||||||
.iter()
|
.iter()
|
||||||
.fold(Ok((Transaction::empty(), BlindSum::new())), |acc, elem| {
|
.fold(Ok((Transaction::empty(), BlindSum::new())), |acc, elem| {
|
||||||
elem(&mut ctx, acc)
|
elem(&mut ctx, acc)
|
||||||
})?;
|
})?;
|
||||||
let blind_sum = ctx.keychain.blind_sum(&sum)?;
|
let blind_sum = ctx.keychain.blind_sum(&sum)?;
|
||||||
|
|
||||||
// Split the key so we can generate an offset for the tx.
|
// Update tx with new kernel and offset.
|
||||||
let split = blind_sum.split(&keychain.secp())?;
|
let mut tx = tx.replace_kernel(kernel);
|
||||||
let k1 = split.blind_1;
|
tx.offset = blind_sum.split(&excess, &keychain.secp())?;
|
||||||
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);
|
|
||||||
|
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ use self::core::core::{
|
||||||
aggregate, deaggregate, KernelFeatures, Output, Transaction, TxKernel, Weighting,
|
aggregate, deaggregate, KernelFeatures, Output, Transaction, TxKernel, Weighting,
|
||||||
};
|
};
|
||||||
use self::core::libtx::build::{self, initial_tx, input, output, with_excess};
|
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 self::core::{global, ser};
|
||||||
use crate::common::{new_block, tx1i1o, tx1i2o, tx2i1o};
|
use crate::common::{new_block, tx1i1o, tx1i2o, tx2i1o};
|
||||||
use grin_core as core;
|
use grin_core as core;
|
||||||
|
@ -148,6 +148,81 @@ fn build_tx_kernel() {
|
||||||
assert_eq!(2, tx.fee());
|
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)
|
// Combine two transactions into one big transaction (with multiple kernels)
|
||||||
// and check it still validates.
|
// and check it still validates.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -12,15 +12,15 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 rand::thread_rng;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::io::Cursor;
|
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 std::{error, fmt};
|
||||||
|
|
||||||
use crate::blake2::blake2b::blake2b;
|
use crate::blake2::blake2b::blake2b;
|
||||||
|
@ -28,10 +28,9 @@ use crate::extkey_bip32::{self, ChildNumber};
|
||||||
use serde::{de, ser}; //TODO: Convert errors to use ErrorKind
|
use serde::{de, ser}; //TODO: Convert errors to use ErrorKind
|
||||||
|
|
||||||
use crate::util::secp::constants::SECRET_KEY_SIZE;
|
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::pedersen::Commitment;
|
||||||
use crate::util::secp::{self, Message, Secp256k1, Signature};
|
use crate::util::secp::{self, Message, Secp256k1, Signature};
|
||||||
use crate::util::static_secp_instance;
|
|
||||||
use crate::util::ToHex;
|
use crate::util::ToHex;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
@ -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 {
|
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())
|
BlindingFactor::from_slice(&skey.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +254,15 @@ impl BlindingFactor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zero() -> 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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_hex(hex: &str) -> Result<BlindingFactor, Error> {
|
pub fn from_hex(hex: &str) -> Result<BlindingFactor, Error> {
|
||||||
|
@ -289,14 +270,29 @@ impl BlindingFactor {
|
||||||
Ok(BlindingFactor::from_slice(&bytes))
|
Ok(BlindingFactor::from_slice(&bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn secret_key(&self, secp: &Secp256k1) -> Result<secp::key::SecretKey, Error> {
|
// Handle "zero" blinding_factor correctly, by returning the "zero" key.
|
||||||
if *self == BlindingFactor::zero() {
|
// We need this for some of the tests.
|
||||||
// TODO - need this currently for tx tests
|
pub fn secret_key(&self, secp: &Secp256k1) -> Result<SecretKey, Error> {
|
||||||
// the "zero" secret key is not actually a valid secret_key
|
if self.is_zero() {
|
||||||
// and secp lib checks this
|
Ok(ZERO_KEY)
|
||||||
Ok(secp::key::ZERO_KEY)
|
|
||||||
} else {
|
} 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> {
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,26 +302,19 @@ impl BlindingFactor {
|
||||||
/// This prevents an actor from being able to sum a set of inputs, outputs
|
/// 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
|
/// and kernels from a block to identify and reconstruct a particular tx
|
||||||
/// from a block. You would need both k1, k2 to do this.
|
/// from a block. You would need both k1, k2 to do this.
|
||||||
pub fn split(&self, secp: &Secp256k1) -> Result<SplitBlindingFactor, Error> {
|
pub fn split(
|
||||||
let skey_1 = secp::key::SecretKey::new(secp, &mut thread_rng());
|
&self,
|
||||||
|
blind_1: &BlindingFactor,
|
||||||
// use blind_sum to subtract skey_1 from our key (to give k = k1 + k2)
|
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 = self.secret_key(secp)?;
|
||||||
let skey_2 = secp.blind_sum(vec![skey], vec![skey_1.clone()])?;
|
let skey_1 = blind_1.secret_key(secp)?;
|
||||||
|
let skey_2 = secp.blind_sum(vec![skey], vec![skey_1])?;
|
||||||
let blind_1 = BlindingFactor::from_secret_key(skey_1);
|
Ok(BlindingFactor::from_secret_key(skey_2))
|
||||||
let blind_2 = BlindingFactor::from_secret_key(skey_2);
|
|
||||||
|
|
||||||
Ok(SplitBlindingFactor { blind_1, blind_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
|
/// Accumulator to compute the sum of blinding factors. Keeps track of each
|
||||||
/// factor as well as the "sign" with which they should be combined.
|
/// factor as well as the "sign" with which they should be combined.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -557,16 +546,17 @@ mod test {
|
||||||
assert!(all_zeros)
|
assert!(all_zeros)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// split a key, sum the split keys and confirm the sum matches the original key
|
||||||
#[test]
|
#[test]
|
||||||
fn split_blinding_factor() {
|
fn split_blinding_factor() {
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
let skey_in = SecretKey::new(&secp, &mut thread_rng());
|
let skey_in = SecretKey::new(&secp, &mut thread_rng());
|
||||||
let blind = BlindingFactor::from_secret_key(skey_in.clone());
|
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 = blind_1.secret_key(&secp).unwrap();
|
||||||
let mut skey_sum = split.blind_1.secret_key(&secp).unwrap();
|
let skey_2 = blind_2.secret_key(&secp).unwrap();
|
||||||
let skey_2 = split.blind_2.secret_key(&secp).unwrap();
|
|
||||||
skey_sum.add_assign(&secp, &skey_2).unwrap();
|
skey_sum.add_assign(&secp, &skey_2).unwrap();
|
||||||
assert_eq!(skey_in, skey_sum);
|
assert_eq!(skey_in, skey_sum);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ use grin_util as util;
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use util::static_secp_instance;
|
||||||
|
|
||||||
pub struct Pool<B, V>
|
pub struct Pool<B, V>
|
||||||
where
|
where
|
||||||
|
@ -272,7 +273,12 @@ where
|
||||||
header: &BlockHeader,
|
header: &BlockHeader,
|
||||||
) -> Result<BlockSums, PoolError> {
|
) -> Result<BlockSums, PoolError> {
|
||||||
let overage = tx.overage();
|
let overage = tx.overage();
|
||||||
let offset = (header.total_kernel_offset() + tx.offset.clone())?;
|
|
||||||
|
let offset = {
|
||||||
|
let secp = static_secp_instance();
|
||||||
|
let secp = secp.lock();
|
||||||
|
header.total_kernel_offset().add(&tx.offset, &secp)
|
||||||
|
}?;
|
||||||
|
|
||||||
let block_sums = self.blockchain.get_block_sums(&header.hash())?;
|
let block_sums = self.blockchain.get_block_sums(&header.hash())?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue