mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Introduce ConfidentialTransactionBuilder
This commit is contained in:
parent
3f0f97b9ba
commit
19efd376da
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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" }
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user