#![allow(non_snake_case)] use monero::blockdata::transaction::KeyImage; use monero::util::key::H; use monero::ViewPair; 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 monero::blockdata::transaction::{ExtraField, SubField, TxOutTarget}; use monero::cryptonote::hash::Hashable; use monero::cryptonote::onetime_key::KeyGenerator; use monero::util::ringct::{EcdhInfo, RctSig, RctSigBase, RctSigPrunable, RctType}; use monero::{PrivateKey, PublicKey}; use monero::{Transaction, TransactionPrefix, TxIn, TxOut, VarInt}; use monero_rpc::wallet::MoneroWalletRpc as _; use monero_rpc::monerod; use monero_rpc::monerod::{GetOutputsOut, MonerodRpc}; use monero_wallet::{MonerodClientExt, Wallet}; use rand::rngs::OsRng; use rand::{Rng, SeedableRng}; use std::convert::TryInto; use std::iter; // [0u8; 32] = 466iKkx7MqVGD46dje3kwvSQRMfhNCvGaXTRATbQgz7kS8XTMmRmoTw9oJRRj523kTdQj8gXnF2xU9fmEPy9WXTr6pwetQj // [1u8; 32] = 47HCnKkBEeYfX5pScvBETAKdjBEPN7FcXEJPUqDPzWGCc6wC8VAdS8CjdtgKuSaY72K8fkoswjp176vbSPS8hzS17EZv8gj #[tokio::test] async fn make_blocks() { let client = monerod::Client::localhost(18081).unwrap(); client.generateblocks(10, "47HCnKkBEeYfX5pScvBETAKdjBEPN7FcXEJPUqDPzWGCc6wC8VAdS8CjdtgKuSaY72K8fkoswjp176vbSPS8hzS17EZv8gj".to_owned()).await.unwrap(); } #[tokio::test] async fn monerod_integration_test() { let client = monerod::Client::localhost(18081).unwrap(); let mut rng = rand::rngs::StdRng::from_seed([0u8; 32]); let s_prime_a = curve25519_dalek::scalar::Scalar::random(&mut rng); let s_b = curve25519_dalek::scalar::Scalar::random(&mut rng); let lock_kp = monero::KeyPair { view: monero::PrivateKey::from_scalar(curve25519_dalek::scalar::Scalar::random(&mut rng)), spend: monero::PrivateKey::from_scalar(s_prime_a + s_b), }; let lock_address = monero::Address::from_keypair(monero::Network::Mainnet, &lock_kp); let spend_tx = "c9b8c57097fe3af0bffcc7470355afa804be2cad0c559a99506ac040cb93d62d" .parse() .unwrap(); let mut o_indexes_response = client.get_o_indexes(spend_tx).await.unwrap(); // TODO: Cannot rely on this, because outputs are shuffled let real_key_offset = o_indexes_response.o_indexes.pop().unwrap(); let (lower, upper) = client.calculate_key_offset_boundaries().await.unwrap(); let mut key_offsets = Vec::with_capacity(11); key_offsets.push(VarInt(real_key_offset)); for _ in 0..10 { loop { let decoy_offset = VarInt(rng.gen_range(lower.0, upper.0)); if key_offsets.contains(&decoy_offset) { continue; } key_offsets.push(decoy_offset); break; } } let response = client .get_outs( key_offsets .iter() .map(|offset| GetOutputsOut { amount: 0, index: offset.0, }) .collect(), ) .await .unwrap(); let ring = response .outs .iter() .map(|out| out.key.point.decompress().unwrap()) .collect::>() .try_into() .unwrap(); key_offsets.sort(); let relative_key_offsets = to_relative_offsets(&key_offsets); let lock_amount = 10_000_000; let fee = 10_000; let spend_amount = lock_amount - fee; // TODO: Pay lock amount to shared address (s_prime_a + s_b) let target_address = "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".parse::().unwrap(); let ecdh_key = PrivateKey::random(&mut rng); let (ecdh_info, out_blinding) = EcdhInfo::new_bulletproof(spend_amount, ecdh_key.scalar); // TODO: Modify API to let us determine the blindings ahead of time let (bulletproof, out_pk) = monero::make_bulletproof(&mut rng, &[spend_amount], &[out_blinding]).unwrap(); let out_pk = out_pk .iter() .map(|c| monero::util::ringct::CtKey { mask: monero::util::ringct::Key { key: c.to_bytes() }, }) .collect(); let k_image = { let k = lock_kp.spend.scalar; let K = ViewPair::from(&lock_kp).spend.point; let k_image = k * hash_point_to_point(K.decompress().unwrap()); KeyImage { image: monero::cryptonote::hash::Hash(k_image.compress().to_bytes()) } }; let prefix = TransactionPrefix { version: VarInt(2), unlock_time: Default::default(), inputs: vec![TxIn::ToKey { amount: VarInt(0), key_offsets: relative_key_offsets, k_image, }], outputs: vec![TxOut { amount: VarInt(0), target: TxOutTarget::ToKey { key: KeyGenerator::from_random( target_address.public_view, target_address.public_spend, ecdh_key, ) .one_time_key(0), // TODO: It works with 1 output, but we must choose it based on the output index }, }], extra: ExtraField(vec![SubField::TxPublicKey(PublicKey::from_private_key( &ecdh_key, ))]), }; let (adaptor_sig, adaptor) = single_party_adaptor_sig(s_prime_a, s_b, ring, &prefix.hash().to_bytes()); let sig = adaptor_sig.adapt(adaptor); let pseudo_out = { let amount = Scalar::from(lock_amount); let blinding = -out_blinding; let commitment = (blinding * ED25519_BASEPOINT_POINT) + (amount * H.point.decompress().unwrap()); monero::util::ringct::Key { key: commitment.compress().to_bytes() } }; let 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], out_pk, }), p: Some(RctSigPrunable { range_sigs: Vec::new(), bulletproofs: vec![bulletproof], MGs: Vec::new(), Clsags: vec![sig.into()], pseudo_outs: vec![pseudo_out], }), }, }; let wallet_client = monero_rpc::wallet::Client::localhost(0).unwrap(); let tx_hex = hex::encode(monero::consensus::encode::serialize(&transaction)); let tx_hash = wallet_client.submit_transfer(tx_hex).await.unwrap(); dbg!(tx_hash); } fn to_relative_offsets(offsets: &[VarInt]) -> Vec { 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() } /// First element of ring is the real pk. fn single_party_adaptor_sig( s_prime_a: Scalar, s_b: Scalar, ring: [EdwardsPoint; monero_adaptor::RING_SIZE], msg: &[u8; 32], ) -> (monero_adaptor::AdaptorSignature, Scalar) { let (r_a, R_a, R_prime_a) = { let r_a = Scalar::random(&mut OsRng); let R_a = r_a * ED25519_BASEPOINT_POINT; let pk_hashed_to_point = hash_point_to_point(ring[0]); let R_prime_a = r_a * pk_hashed_to_point; (r_a, R_a, R_prime_a) }; let alice = monero_adaptor::Alice0::new(ring, *msg, R_a, R_prime_a, s_prime_a).unwrap(); let bob = monero_adaptor::Bob0::new(ring, *msg, R_a, R_prime_a, s_b).unwrap(); let msg = alice.next_message(); let bob = bob.receive(msg); let msg = bob.next_message(); let alice = alice.receive(msg).unwrap(); let msg = alice.next_message(); let bob = bob.receive(msg).unwrap(); let msg = bob.next_message(); let alice = alice.receive(msg); (alice.adaptor_sig, r_a) } #[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), ] ) } }