Introduce ConfidentialTransactionBuilder

This commit is contained in:
Thomas Eizinger 2021-05-14 18:14:39 +10:00
parent 3f0f97b9ba
commit 19efd376da
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96
5 changed files with 364 additions and 283 deletions

5
Cargo.lock generated
View File

@ -2265,7 +2265,7 @@ dependencies = [
[[package]]
name = "monero"
version = "0.12.0"
source = "git+https://github.com/comit-network/monero-rs?branch=open-outputs#e86521d9e4038958349a34765881589028c84dbb"
source = "git+https://github.com/comit-network/monero-rs?branch=expose-commitment#c344cf7b46651ddd4d0713317cfba0031892c29c"
dependencies = [
"base58-monero",
"clear_on_drop",
@ -2357,6 +2357,9 @@ dependencies = [
"anyhow",
"async-trait",
"curve25519-dalek",
"hash_edwards_to_edwards",
"hex 0.4.3",
"itertools 0.10.0",
"monero",
"monero-harness",
"monero-rpc",

View File

@ -3,4 +3,4 @@ members = ["monero-adaptor", "monero-harness", "monero-rpc", "swap", "monero-wal
[patch.crates-io]
torut = { git = "https://github.com/bonomat/torut/", branch = "feature-flag-tor-secret-keys", default-features = false, features = [ "v3", "control" ] }
monero = { git = "https://github.com/comit-network/monero-rs", branch = "open-outputs" }
monero = { git = "https://github.com/comit-network/monero-rs", branch = "expose-commitment" }

View File

@ -1,21 +1,11 @@
#![allow(non_snake_case)]
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use curve25519_dalek::scalar::Scalar;
use hash_edwards_to_edwards::hash_point_to_point;
use itertools::{izip, Itertools};
use monero::blockdata::transaction::{ExtraField, KeyImage, SubField, TxOutTarget};
use monero::cryptonote::onetime_key::{KeyGenerator, MONERO_MUL_FACTOR};
use monero::util::key::H;
use monero::util::ringct::{EcdhInfo, RctSig, RctSigBase, RctSigPrunable, RctType};
use monero::{PrivateKey, PublicKey, Transaction, TransactionPrefix, TxIn, TxOut, VarInt};
use monero::ViewPair;
use monero_harness::Monero;
use monero_rpc::monerod::{GetOutputsOut, MonerodRpc};
use monero_wallet::MonerodClientExt;
use monero_rpc::monerod::MonerodRpc;
use monero_wallet::{ConfidentialTransactionBuilder, MonerodClientExt};
use rand::{Rng, SeedableRng};
use std::convert::TryInto;
use std::iter;
use testcontainers::clients::Cli;
#[tokio::test]
@ -32,19 +22,15 @@ async fn monerod_integration_test() {
spend: monero::PrivateKey::from_scalar(signing_key),
};
let lock_amount = 1_000_000_000_000;
let fee = 400_000_000;
let spend_amount = lock_amount - fee;
let spend_amount = 999600000000;
let lock_address = monero::Address::from_keypair(monero::Network::Mainnet, &lock_kp);
dbg!(lock_address.to_string());
monero.init_miner().await.unwrap();
let wallet = monero.wallet("miner").expect("wallet to exist");
let transfer = wallet
.transfer(&lock_address.to_string(), lock_amount)
.transfer(&lock_address.to_string(), 1_000_000_000_000)
.await
.expect("lock to succeed");
@ -70,273 +56,43 @@ async fn monerod_integration_test() {
.unwrap();
let output_indices = client.get_o_indexes(lock_tx_hash).await.unwrap().o_indexes;
let our_output = lock_tx
.open_outputs(&lock_kp, 0..1, 0..1)
let lock_vp = ViewPair::from(&lock_kp);
let input_to_spend = lock_tx
.check_outputs(&lock_vp, 0..1, 0..1)
.unwrap()
.pop()
.unwrap();
let actual_signing_key = our_output.signing_key.scalar;
let real_commitment_blinder = our_output.blinding_factor;
let global_output_index = output_indices[input_to_spend.index];
let (lower, upper) = client.calculate_key_offset_boundaries().await.unwrap();
let mut key_offsets = Vec::with_capacity(11);
key_offsets.push(VarInt(output_indices[our_output.index]));
let mut decoy_indices = Vec::with_capacity(10);
for _ in 0..10 {
loop {
let decoy_offset = VarInt(rng.gen_range(lower.0, upper.0));
let decoy_index = rng.gen_range(lower.0, upper.0);
if key_offsets.contains(&decoy_offset) {
if decoy_indices.contains(&decoy_index) && decoy_index != global_output_index {
continue;
}
key_offsets.push(decoy_offset);
decoy_indices.push(decoy_index);
break;
}
}
dbg!(&key_offsets);
let response = client
.get_outs(
key_offsets
.iter()
.map(|offset| GetOutputsOut {
amount: 0,
index: offset.0,
})
.collect(),
)
let decoy_inputs = client
.fetch_decoy_inputs(decoy_indices.try_into().unwrap())
.await
.unwrap();
dbg!(&response);
let target_address = "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".parse().unwrap();
let ring = response
.outs
.iter()
.map(|out| out.key.point.decompress().unwrap());
let commitment_ring = response
.outs
.iter()
.map(|out| CompressedEdwardsY(out.mask.key).decompress().unwrap());
let (key_offsets, ring, commitment_ring) = izip!(key_offsets, ring, commitment_ring)
.sorted_by(|(a, ..), (b, ..)| Ord::cmp(a, b))
.fold(
(Vec::new(), Vec::new(), Vec::new()),
|(mut key_offsets, mut ring, mut commitment_ring), (key_offset, key, commitment)| {
key_offsets.push(key_offset);
ring.push(key);
commitment_ring.push(commitment);
(key_offsets, ring, commitment_ring)
},
);
let ring: [EdwardsPoint; 11] = ring.try_into().unwrap();
let commitment_ring = commitment_ring.try_into().unwrap();
let (signing_index, _) = ring
.iter()
.find_position(|key| **key == actual_signing_key * ED25519_BASEPOINT_POINT)
.unwrap();
let relative_key_offsets = to_relative_offsets(&key_offsets);
dbg!(&relative_key_offsets);
let target_address = "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".parse::<monero::Address>().unwrap();
let ecdh_key_0 = PrivateKey::random(&mut rng);
let (ecdh_info_0, out_blinding_0) = EcdhInfo::new_bulletproof(spend_amount, ecdh_key_0.scalar);
let ecdh_key_1 = PrivateKey::random(&mut rng);
let (ecdh_info_1, out_blinding_1) = EcdhInfo::new_bulletproof(0, ecdh_key_1.scalar);
let (bulletproof, out_pk) = monero::make_bulletproof(
&mut rng,
&[spend_amount, 0],
&[out_blinding_0, out_blinding_1],
)
.unwrap();
let H_p_pk = hash_point_to_point(actual_signing_key * ED25519_BASEPOINT_POINT);
let I = actual_signing_key * H_p_pk;
let prefix = TransactionPrefix {
version: VarInt(2),
unlock_time: Default::default(),
inputs: vec![TxIn::ToKey {
amount: VarInt(0),
key_offsets: relative_key_offsets,
k_image: KeyImage {
image: monero::cryptonote::hash::Hash(I.compress().to_bytes()),
},
}],
outputs: vec![
TxOut {
amount: VarInt(0),
target: TxOutTarget::ToKey {
key: KeyGenerator::from_random(
target_address.public_view,
target_address.public_spend,
ecdh_key_0,
)
.one_time_key(0), // TODO: This must be the output index
},
},
TxOut {
amount: VarInt(0),
target: TxOutTarget::ToKey {
key: KeyGenerator::from_random(
target_address.public_view,
target_address.public_spend,
ecdh_key_1,
)
.one_time_key(1), // TODO: This must be the output index
},
},
],
extra: ExtraField(vec![
SubField::TxPublicKey(PublicKey::from_private_key(&ecdh_key_0)),
SubField::TxPublicKey(PublicKey::from_private_key(&ecdh_key_1)),
]),
};
let out_pk = out_pk // TODO: Should this happen inside the bulletproof module?
.into_iter()
.map(|p| (p.decompress().unwrap() * Scalar::from(MONERO_MUL_FACTOR)).compress())
.collect::<Vec<_>>();
let fee_key = Scalar::from(fee) * H.point.decompress().unwrap();
let pseudo_out = fee_key + out_pk[0].decompress().unwrap() + out_pk[1].decompress().unwrap();
let alpha = Scalar::random(&mut rng);
let responses = random_array(|| Scalar::random(&mut rng));
let out_pk = out_pk
.iter()
.map(|c| monero::util::ringct::CtKey {
mask: monero::util::ringct::Key { key: c.to_bytes() },
})
.collect::<Vec<_>>();
let mut transaction = Transaction {
prefix,
signatures: Vec::new(),
rct_signatures: RctSig {
sig: Some(RctSigBase {
rct_type: RctType::Clsag,
txn_fee: VarInt(fee),
pseudo_outs: Vec::new(),
ecdh_info: vec![ecdh_info_0, ecdh_info_1],
out_pk,
}),
p: Some(RctSigPrunable {
range_sigs: Vec::new(),
bulletproofs: vec![bulletproof],
MGs: Vec::new(),
Clsags: Vec::new(),
pseudo_outs: vec![monero::util::ringct::Key {
key: pseudo_out.compress().0,
}],
}),
},
};
let message = transaction.signature_hash().unwrap();
let sig = monero::clsag::sign(
message.as_fixed_bytes(),
actual_signing_key,
signing_index,
H_p_pk,
alpha,
&ring,
&commitment_ring,
responses,
real_commitment_blinder - (out_blinding_0 + out_blinding_1), // * Scalar::from(MONERO_MUL_FACTOR), TODO DOESN'T VERIFY WITH THIS
pseudo_out,
alpha * ED25519_BASEPOINT_POINT,
alpha * H_p_pk,
I,
);
assert!(monero::clsag::verify(
&sig,
message.as_fixed_bytes(),
&ring,
&commitment_ring,
I,
pseudo_out
));
transaction
.rct_signatures
.p
.as_mut()
.unwrap()
.Clsags
.push(sig);
let transaction =
ConfidentialTransactionBuilder::new(input_to_spend, global_output_index, decoy_inputs, lock_kp)
.with_output(target_address, spend_amount, &mut rng)
.with_output(target_address, 0, &mut rng) // TODO: Do this inside `build`
.build(&mut rng);
client.send_raw_transaction(transaction).await.unwrap();
}
fn to_relative_offsets(offsets: &[VarInt]) -> Vec<VarInt> {
let vals = offsets.iter();
let next_vals = offsets.iter().skip(1);
let diffs = vals
.zip(next_vals)
.map(|(cur, next)| VarInt(next.0 - cur.0));
iter::once(offsets[0].clone()).chain(diffs).collect()
}
fn random_array<T: Default + Copy, const N: usize>(rng: impl FnMut() -> T) -> [T; N] {
let mut ring = [T::default(); N];
ring[..].fill_with(rng);
ring
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calculate_relative_key_offsets() {
let key_offsets = [
VarInt(78),
VarInt(81),
VarInt(91),
VarInt(91),
VarInt(96),
VarInt(98),
VarInt(101),
VarInt(112),
VarInt(113),
VarInt(114),
VarInt(117),
];
let relative_offsets = to_relative_offsets(&key_offsets);
assert_eq!(
&relative_offsets,
&[
VarInt(78),
VarInt(3),
VarInt(10),
VarInt(0),
VarInt(5),
VarInt(2),
VarInt(3),
VarInt(11),
VarInt(1),
VarInt(1),
VarInt(3),
]
)
}
}

View File

@ -11,6 +11,9 @@ monero-rpc = { path = "../monero-rpc" }
rand = "0.7"
curve25519-dalek = "3"
async-trait = "0.1"
itertools = "0.10"
hash_edwards_to_edwards = { git = "https://github.com/comit-network/hash_edwards_to_edwards" }
hex = "0.4"
[dev-dependencies]
monero-harness = { path = "../monero-harness" }

View File

@ -1,36 +1,288 @@
use anyhow::{Context, Result};
use monero::consensus::encode::VarInt;
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use curve25519_dalek::scalar::Scalar;
use hash_edwards_to_edwards::hash_point_to_point;
use itertools::Itertools;
use monero::blockdata::transaction::{KeyImage, SubField, TxOutTarget};
use monero::cryptonote::hash::Hashable;
use monero::cryptonote::onetime_key::KeyGenerator;
use monero::util::key::H;
use monero::util::ringct::{CtKey, EcdhInfo, Key, RctSigBase, RctSigPrunable, RctType};
use monero::{
Address, KeyPair, OwnedTxOut, PrivateKey, PublicKey, Transaction, TransactionPrefix, TxIn,
TxOut, VarInt,
};
use monero_rpc::monerod;
use monero_rpc::monerod::{GetBlockResponse, MonerodRpc as _};
use monero_rpc::monerod::{GetBlockResponse, GetOutputsOut, MonerodRpc as _};
use rand::{CryptoRng, RngCore};
use std::convert::TryInto;
use std::iter;
pub struct Wallet {
key: monero::KeyPair,
pub struct ConfidentialTransactionBuilder {
prefix: TransactionPrefix,
base: RctSigBase,
prunable: RctSigPrunable,
blinding_factors: Vec<Scalar>,
amounts: Vec<u64>,
decoy_inputs: [DecoyInput; 10],
actual_signing_key: Scalar,
real_commitment_blinder: Scalar,
signing_pk: EdwardsPoint,
H_p_pk: EdwardsPoint,
input_commitment: EdwardsPoint,
spend_amount: u64,
global_output_index: u64,
}
impl Wallet {
pub fn new_random<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
impl ConfidentialTransactionBuilder {
pub fn new(
input_to_spend: OwnedTxOut<'_>,
global_output_index: u64,
decoy_inputs: [DecoyInput; 10],
keys: KeyPair,
) -> Self {
let mut prefix = TransactionPrefix::default();
prefix.version = VarInt(2);
let actual_signing_key = input_to_spend.recover_key(&keys).scalar;
let signing_pk = actual_signing_key * ED25519_BASEPOINT_POINT;
Self {
key: monero::KeyPair {
view: monero::PrivateKey::from_scalar(curve25519_dalek::scalar::Scalar::random(
rng,
)),
spend: monero::PrivateKey::from_scalar(curve25519_dalek::scalar::Scalar::random(
rng,
)),
prefix,
base: RctSigBase {
rct_type: RctType::Clsag,
txn_fee: VarInt(0),
pseudo_outs: vec![],
ecdh_info: vec![],
out_pk: vec![],
},
prunable: RctSigPrunable {
range_sigs: vec![],
bulletproofs: vec![],
MGs: vec![],
Clsags: vec![],
pseudo_outs: vec![],
},
blinding_factors: vec![],
amounts: vec![],
decoy_inputs,
actual_signing_key,
signing_pk,
H_p_pk: hash_point_to_point(signing_pk),
input_commitment: input_to_spend.commitment.unwrap(), // TODO: Error handling
spend_amount: input_to_spend.amount.unwrap(), // TODO: Error handling,
global_output_index,
real_commitment_blinder: input_to_spend.blinding_factor.unwrap(), // TODO: Error handling
}
}
pub fn get_address(&self, network: monero::Network) -> monero::Address {
monero::Address::from_keypair(network, &self.key)
pub fn with_output(
mut self,
to: Address,
amount: u64,
rng: &mut (impl RngCore + CryptoRng),
) -> Self {
let next_index = self.prefix.outputs.len();
let ecdh_key = PrivateKey::random(rng);
let (ecdh_info, blinding_factor) = EcdhInfo::new_bulletproof(amount, ecdh_key.scalar);
let out = TxOut {
amount: VarInt(0),
target: TxOutTarget::ToKey {
key: KeyGenerator::from_random(to.public_view, to.public_spend, ecdh_key)
.one_time_key(dbg!(next_index)),
},
};
self.prefix.outputs.push(out);
self.prefix
.extra
.0
.push(SubField::TxPublicKey(PublicKey::from_private_key(
&ecdh_key,
)));
self.base.ecdh_info.push(ecdh_info);
self.blinding_factors.push(blinding_factor);
self.amounts.push(amount);
// sanity checks
debug_assert_eq!(self.prefix.outputs.len(), self.prefix.extra.0.len());
debug_assert_eq!(self.prefix.outputs.len(), self.blinding_factors.len());
debug_assert_eq!(self.prefix.outputs.len(), self.amounts.len());
debug_assert_eq!(self.prefix.outputs.len(), self.base.ecdh_info.len());
self
}
fn compute_fee(&self) -> u64 {
self.spend_amount - self.amounts.iter().sum::<u64>()
}
fn compute_pseudo_out(&mut self, commitments: Vec<EdwardsPoint>) -> EdwardsPoint {
let sum_commitments = commitments
.iter()
.map(|p| p * Scalar::from(8u8)) // TODO: Should this happen inside the bulletproof module?
.sum::<EdwardsPoint>();
let fee = self.compute_fee();
// TODO: Don't mutate in here
self.base.txn_fee = VarInt(fee);
self.base.out_pk = commitments
.iter()
.map(|p| p * Scalar::from(8u8))
.map(|p| CtKey {
mask: Key {
key: p.compress().0,
},
})
.collect();
let fee_key = Scalar::from(fee) * H.point.decompress().unwrap();
let pseudo_out = fee_key + sum_commitments;
self.prunable.pseudo_outs = vec![Key {
key: pseudo_out.compress().0,
}];
pseudo_out
}
pub fn build(mut self, rng: &mut (impl RngCore + CryptoRng)) -> Transaction {
// 0. add dummy output if necessary
// 1. compute fee
// 2. make bullet-proof
// 3. sign
// TODO: move to a function
let (bulletproof, output_commitments) = monero::make_bulletproof(
rng,
self.amounts.as_slice(),
self.blinding_factors.as_slice(),
)
.unwrap();
self.prunable.bulletproofs = vec![bulletproof];
// TODO: move to ctor
let (key_offsets, ring, commitment_ring) = self
.decoy_inputs
.iter()
.copied()
.map(
|DecoyInput {
global_output_index,
key,
commitment,
}| { (VarInt(global_output_index), key, commitment) },
)
.chain(std::iter::once((
VarInt(self.global_output_index),
self.signing_pk,
self.input_commitment,
)))
.sorted_by(|(a, ..), (b, ..)| Ord::cmp(a, b))
.fold(
(Vec::new(), Vec::new(), Vec::new()),
|(mut key_offsets, mut ring, mut commitment_ring),
(key_offset, key, commitment)| {
key_offsets.push(key_offset);
ring.push(key);
commitment_ring.push(commitment);
(key_offsets, ring, commitment_ring)
},
);
let ring: [EdwardsPoint; 11] = ring.try_into().unwrap();
let commitment_ring = commitment_ring.try_into().unwrap();
let (signing_index, _) = ring
.iter()
.find_position(|key| **key == self.signing_pk)
.unwrap();
let relative_key_offsets = to_relative_offsets(&key_offsets);
let I = self.actual_signing_key * self.H_p_pk;
self.prefix.inputs = vec![TxIn::ToKey {
amount: VarInt(0),
key_offsets: relative_key_offsets,
k_image: KeyImage {
image: monero::cryptonote::hash::Hash(I.compress().to_bytes()),
},
}];
let output_commitments = output_commitments
.into_iter()
.map(|p| p.decompress().unwrap())
.collect(); // TODO: Return EdwardsPoints from bulletproof lib
let pseudo_out = self.compute_pseudo_out(output_commitments); // TODO: either mutate or return
let mut transaction = Transaction::default();
transaction.prefix = self.prefix;
transaction.rct_signatures.sig = Some(dbg!(self.base));
transaction.rct_signatures.p = Some(self.prunable);
let alpha = Scalar::random(rng);
let fake_responses = random_array(|| Scalar::random(rng));
let message = transaction.signature_hash().unwrap();
let sig = monero::clsag::sign(
message.as_fixed_bytes(),
self.actual_signing_key,
signing_index,
self.H_p_pk,
alpha,
&ring,
&commitment_ring,
fake_responses,
self.real_commitment_blinder - (self.blinding_factors.iter().sum::<Scalar>()),
pseudo_out,
alpha * ED25519_BASEPOINT_POINT,
alpha * self.H_p_pk,
I,
);
transaction.rct_signatures.p.as_mut().unwrap().Clsags = vec![sig];
dbg!(transaction)
}
}
#[derive(Debug, Copy, Clone)]
pub struct DecoyInput {
global_output_index: u64,
key: EdwardsPoint,
commitment: EdwardsPoint,
}
fn to_relative_offsets(offsets: &[VarInt]) -> Vec<VarInt> {
let vals = offsets.iter();
let next_vals = offsets.iter().skip(1);
let diffs = vals
.zip(next_vals)
.map(|(cur, next)| VarInt(next.0 - cur.0));
iter::once(offsets[0].clone()).chain(diffs).collect()
}
fn random_array<T: Default + Copy, const N: usize>(rng: impl FnMut() -> T) -> [T; N] {
let mut ring = [T::default(); N];
ring[..].fill_with(rng);
ring
}
#[async_trait::async_trait]
pub trait MonerodClientExt {
async fn calculate_key_offset_boundaries(&self) -> Result<(VarInt, VarInt)>;
async fn fetch_decoy_inputs(&self, indices: [u64; 10]) -> Result<[DecoyInput; 10]>;
}
#[async_trait::async_trait]
@ -65,6 +317,37 @@ impl MonerodClientExt for monerod::Client {
Ok((VarInt(0), VarInt(last_index)))
}
async fn fetch_decoy_inputs(&self, indices: [u64; 10]) -> Result<[DecoyInput; 10]> {
let response = self
.get_outs(
indices
.iter()
.map(|offset| GetOutputsOut {
amount: 0,
index: *offset,
})
.collect(),
)
.await?;
let inputs = response
.outs
.into_iter()
.zip(indices.iter())
.map(|(out_key, index)| {
DecoyInput {
global_output_index: *index,
key: out_key.key.point.decompress().unwrap(), // TODO: should decompress on deserialization
commitment: CompressedEdwardsY(out_key.mask.key).decompress().unwrap(),
}
})
.collect::<Vec<_>>()
.try_into()
.expect("exactly 10 elements guaranteed through type-safety of array");
Ok(inputs)
}
}
#[cfg(test)]
@ -75,6 +358,42 @@ mod tests {
use testcontainers::clients::Cli;
use testcontainers::Docker;
#[test]
fn calculate_relative_key_offsets() {
let key_offsets = [
VarInt(78),
VarInt(81),
VarInt(91),
VarInt(91),
VarInt(96),
VarInt(98),
VarInt(101),
VarInt(112),
VarInt(113),
VarInt(114),
VarInt(117),
];
let relative_offsets = to_relative_offsets(&key_offsets);
assert_eq!(
&relative_offsets,
&[
VarInt(78),
VarInt(3),
VarInt(10),
VarInt(0),
VarInt(5),
VarInt(2),
VarInt(3),
VarInt(11),
VarInt(1),
VarInt(1),
VarInt(3),
]
)
}
#[tokio::test]
async fn get_outs_for_key_offsets() {
let cli = Cli::default();