You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
425 lines
13 KiB
Rust
425 lines
13 KiB
Rust
#![allow(non_snake_case)]
|
|
|
|
#[allow(dead_code, unused_variables)]
|
|
mod v2;
|
|
|
|
use anyhow::{Context, Result};
|
|
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
|
|
use curve25519_dalek::edwards::EdwardsPoint;
|
|
use curve25519_dalek::scalar::Scalar;
|
|
use hash_edwards_to_edwards::hash_point_to_point;
|
|
use itertools::Itertools;
|
|
use monero::blockdata::transaction::{ExtraField, KeyImage, SubField, TxOutTarget};
|
|
use monero::cryptonote::hash::Hashable;
|
|
use monero::cryptonote::onetime_key::KeyGenerator;
|
|
use monero::util::key::{EdwardsPointExt, H};
|
|
use monero::util::ringct::{EcdhInfo, RctSig, RctSigBase, RctSigPrunable, RctType};
|
|
use monero::{
|
|
Address, KeyPair, OwnedTxOut, PrivateKey, PublicKey, Transaction, TransactionPrefix, TxIn,
|
|
TxOut, VarInt,
|
|
};
|
|
use monero_rpc::monerod;
|
|
use monero_rpc::monerod::{GetBlockResponse, GetOutputsOut, MonerodRpc as _};
|
|
use rand::{CryptoRng, RngCore};
|
|
use std::convert::TryInto;
|
|
use std::iter;
|
|
|
|
pub struct ConfidentialTransactionBuilder {
|
|
outputs: Vec<TxOut>,
|
|
ecdh_info: Vec<EcdhInfo>,
|
|
extra: ExtraField,
|
|
|
|
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 ConfidentialTransactionBuilder {
|
|
pub fn new(
|
|
input_to_spend: OwnedTxOut<'_>,
|
|
global_output_index: u64,
|
|
decoy_inputs: [DecoyInput; 10],
|
|
keys: KeyPair,
|
|
) -> Self {
|
|
let actual_signing_key = input_to_spend.recover_key(&keys);
|
|
let signing_pk = actual_signing_key * ED25519_BASEPOINT_POINT;
|
|
|
|
Self {
|
|
outputs: vec![],
|
|
ecdh_info: vec![],
|
|
extra: Default::default(),
|
|
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 with_output(
|
|
mut self,
|
|
to: Address,
|
|
amount: u64,
|
|
rng: &mut (impl RngCore + CryptoRng),
|
|
) -> Self {
|
|
let next_index = self.outputs.len();
|
|
|
|
let ecdh_key = PrivateKey::random(rng);
|
|
let (ecdh_info, blinding_factor) = EcdhInfo::new_bulletproof(amount, ecdh_key);
|
|
|
|
let out = TxOut {
|
|
amount: VarInt(0),
|
|
target: TxOutTarget::ToKey {
|
|
key: KeyGenerator::from_random(
|
|
to.public_view.decompress().unwrap(),
|
|
to.public_spend.decompress().unwrap(),
|
|
ecdh_key,
|
|
)
|
|
.one_time_key(next_index),
|
|
},
|
|
};
|
|
|
|
self.outputs.push(out);
|
|
self.extra
|
|
.0
|
|
.push(SubField::TxPublicKey(PublicKey::from_private_key(
|
|
&ecdh_key,
|
|
)));
|
|
self.ecdh_info.push(ecdh_info);
|
|
self.blinding_factors.push(blinding_factor);
|
|
self.amounts.push(amount);
|
|
|
|
// sanity checks
|
|
debug_assert_eq!(self.outputs.len(), self.extra.0.len());
|
|
debug_assert_eq!(self.outputs.len(), self.blinding_factors.len());
|
|
debug_assert_eq!(self.outputs.len(), self.amounts.len());
|
|
|
|
self
|
|
}
|
|
|
|
fn compute_fee(&self) -> u64 {
|
|
self.spend_amount - self.amounts.iter().sum::<u64>()
|
|
}
|
|
|
|
fn compute_pseudo_out(&mut self, commitments: &[EdwardsPoint]) -> EdwardsPoint {
|
|
let sum_commitments = commitments.iter().sum::<EdwardsPoint>();
|
|
|
|
let fee = self.compute_fee();
|
|
|
|
let fee_key = Scalar::from(fee) * *H;
|
|
|
|
fee_key + sum_commitments
|
|
}
|
|
|
|
fn compute_key_image(&self) -> EdwardsPoint {
|
|
self.actual_signing_key * self.H_p_pk
|
|
}
|
|
|
|
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();
|
|
|
|
// 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.compute_key_image();
|
|
let pseudo_out = self.compute_pseudo_out(output_commitments.as_slice());
|
|
let fee = self.compute_fee();
|
|
|
|
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: self.outputs,
|
|
extra: self.extra,
|
|
};
|
|
let rct_sig_base = RctSigBase {
|
|
rct_type: RctType::Clsag,
|
|
txn_fee: VarInt(fee),
|
|
out_pk: output_commitments,
|
|
ecdh_info: self.ecdh_info,
|
|
pseudo_outs: vec![], // legacy
|
|
};
|
|
let rct_sig_prunable = RctSigPrunable {
|
|
range_sigs: vec![], // legacy
|
|
bulletproofs: vec![bulletproof],
|
|
MGs: vec![], // legacy
|
|
Clsags: vec![],
|
|
pseudo_outs: vec![pseudo_out],
|
|
};
|
|
let mut transaction = Transaction {
|
|
prefix,
|
|
rct_signatures: RctSig {
|
|
sig: Some(rct_sig_base),
|
|
p: Some(rct_sig_prunable),
|
|
},
|
|
signatures: vec![], // legacy
|
|
};
|
|
|
|
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];
|
|
|
|
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 CalculateKeyOffsetBoundaries {
|
|
async fn calculate_key_offset_boundaries(&self) -> Result<(VarInt, VarInt)>;
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
pub trait FetchDecoyInputs {
|
|
async fn fetch_decoy_inputs(&self, indices: [u64; 10]) -> Result<[DecoyInput; 10]>;
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl CalculateKeyOffsetBoundaries for monerod::Client {
|
|
/// Chooses 10 random key offsets for use within a new confidential
|
|
/// transactions.
|
|
///
|
|
/// Choosing these offsets randomly is not ideal for privacy, instead they
|
|
/// should be chosen in a way that mimics a real spending pattern as much as
|
|
/// possible.
|
|
async fn calculate_key_offset_boundaries(&self) -> Result<(VarInt, VarInt)> {
|
|
let latest_block = self.get_block_count().await?;
|
|
let latest_spendable_block = latest_block.count - 10;
|
|
|
|
let block: GetBlockResponse = self.get_block(latest_spendable_block).await?;
|
|
|
|
let tx_hash = block
|
|
.blob
|
|
.tx_hashes
|
|
.first()
|
|
.copied()
|
|
.unwrap_or_else(|| block.blob.miner_tx.hash());
|
|
|
|
let indices = self.get_o_indexes(tx_hash).await?;
|
|
|
|
let last_index = indices
|
|
.o_indexes
|
|
.into_iter()
|
|
.max()
|
|
.context("Expected at least one output index")?;
|
|
// let oldest_index = last_index - (last_index / 100) * 40; // oldest index must
|
|
// be within last 40% TODO: CONFIRM THIS
|
|
|
|
Ok((VarInt(1000000), VarInt(last_index)))
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl FetchDecoyInputs for monerod::Client {
|
|
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,
|
|
commitment: out_key.mask,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.try_into()
|
|
.expect("exactly 10 elements guaranteed through type-safety of array");
|
|
|
|
Ok(inputs)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use monero_harness::image::Monerod;
|
|
use monero_rpc::monerod::Client;
|
|
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();
|
|
let container = cli.run(Monerod::default());
|
|
let rpc_client = Client::localhost(container.get_host_port(18081).unwrap()).unwrap();
|
|
rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap();
|
|
// let wallet = Wallet {
|
|
// client: rpc_client.clone(),
|
|
// key: todo!(),
|
|
// };
|
|
//
|
|
// let (lower, upper) = wallet.CalculateKeyOffsetBoundaries().await.unwrap();
|
|
|
|
todo!("fix");
|
|
// let result = rpc_client
|
|
// .get_outs(
|
|
// key_offsets
|
|
// .to_vec()
|
|
// .into_iter()
|
|
// .map(|varint| GetOutputsOut {
|
|
// amount: 0,
|
|
// index: varint.0,
|
|
// })
|
|
// .collect(),
|
|
// )
|
|
// .await
|
|
// .unwrap();
|
|
|
|
// assert_eq!(result.outs.len(), 10);
|
|
}
|
|
}
|