From dfd2f624eea53686a367dd007982d5aea438cdf3 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 2 Jun 2022 00:00:26 -0400 Subject: [PATCH] Implement a proper Monero Timelock type Transaction scanning now returns the timelock to ensure it's acknowledged by wallets. Fixes https://github.com/serai-dex/serai/issues/16. --- coins/monero/src/rpc.rs | 11 +++++---- coins/monero/src/transaction.rs | 36 ++++++++++++++++++++++++++--- coins/monero/src/wallet/scan.rs | 15 ++++-------- coins/monero/src/wallet/send/mod.rs | 4 ++-- coins/monero/tests/send.rs | 4 ++-- processor/src/coins/monero.rs | 15 ++++++++++-- 6 files changed, 62 insertions(+), 23 deletions(-) diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs index 13ba9026..a609901c 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc.rs @@ -9,7 +9,7 @@ use serde_json::json; use reqwest; -use crate::{transaction::{Input, Transaction}, block::Block}; +use crate::{transaction::{Input, Timelock, Transaction}, block::Block}; #[derive(Deserialize, Debug)] pub struct EmptyResponse {} @@ -267,9 +267,12 @@ impl Rpc { // get the median time for the given height, yet we do need to in order to be complete outs.outs.iter().enumerate().map( |(i, out)| Ok( - if txs[i].prefix.unlock_time <= u64::try_from(height).unwrap() { - Some([rpc_point(&out.key)?, rpc_point(&out.mask)?]) - } else { None } + Some([rpc_point(&out.key)?, rpc_point(&out.mask)?]).filter(|_| { + match txs[i].prefix.timelock { + Timelock::Block(t_height) => (t_height <= height), + _ => false + } + }) ) ).collect() } diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index da18773d..338a16e5 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -84,10 +84,40 @@ impl Output { } } +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Timelock { + None, + Block(usize), + Time(u64) +} + +impl Timelock { + fn from_raw(raw: u64) -> Timelock { + if raw == 0 { + Timelock::None + } else if raw < 500_000_000 { + Timelock::Block(usize::try_from(raw).unwrap()) + } else { + Timelock::Time(raw) + } + } + + fn serialize(&self, w: &mut W) -> std::io::Result<()> { + write_varint( + &match self { + Timelock::None => 0, + Timelock::Block(block) => (*block).try_into().unwrap(), + Timelock::Time(time) => *time + }, + w + ) + } +} + #[derive(Clone, PartialEq, Debug)] pub struct TransactionPrefix { pub version: u64, - pub unlock_time: u64, + pub timelock: Timelock, pub inputs: Vec, pub outputs: Vec, pub extra: Vec @@ -96,7 +126,7 @@ pub struct TransactionPrefix { impl TransactionPrefix { pub fn serialize(&self, w: &mut W) -> std::io::Result<()> { write_varint(&self.version, w)?; - write_varint(&self.unlock_time, w)?; + self.timelock.serialize(w)?; write_vec(Input::serialize, &self.inputs, w)?; write_vec(Output::serialize, &self.outputs, w)?; write_varint(&self.extra.len().try_into().unwrap(), w)?; @@ -106,7 +136,7 @@ impl TransactionPrefix { pub fn deserialize(r: &mut R) -> std::io::Result { let mut prefix = TransactionPrefix { version: read_varint(r)?, - unlock_time: read_varint(r)?, + timelock: Timelock::from_raw(read_varint(r)?), inputs: read_vec(Input::deserialize, r)?, outputs: read_vec(Output::deserialize, r)?, extra: vec![] diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index 644a3a73..d8feb7da 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -11,7 +11,7 @@ use monero::{consensus::deserialize, blockdata::transaction::ExtraField}; use crate::{ Commitment, serialize::{write_varint, read_32, read_scalar, read_point}, - transaction::Transaction, + transaction::{Timelock, Transaction}, wallet::{uniqueness, shared_key, amount_decryption, commitment_mask} }; @@ -57,13 +57,7 @@ impl Transaction { &self, view: Scalar, spend: EdwardsPoint - ) -> Vec { - // Ignore transactions which utilize a timelock. Almost no transactions on Monero do, - // and they're not worth the effort to track given their complexities - if self.prefix.unlock_time != 0 { - return vec![]; - } - + ) -> (Vec, Timelock) { let mut extra = vec![]; write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap(); extra.extend(&self.prefix.extra); @@ -81,7 +75,7 @@ impl Transaction { pubkeys = m_pubkeys.iter().map(|key| key.point.decompress()).filter_map(|key| key).collect(); } else { - return vec![]; + return (vec![], self.prefix.timelock); }; let mut res = vec![]; @@ -136,6 +130,7 @@ impl Transaction { } } } - res + + (res, self.prefix.timelock) } } diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index a7274b70..8ad88a5e 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -27,7 +27,7 @@ use crate::{ bulletproofs::Bulletproofs, RctBase, RctPrunable, RctSignatures }, - transaction::{Input, Output, TransactionPrefix, Transaction}, + transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, rpc::{Rpc, RpcError}, wallet::{SpendableOutput, Decoys, key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption} }; @@ -255,7 +255,7 @@ impl SignableTransaction { Transaction { prefix: TransactionPrefix { version: 2, - unlock_time: 0, + timelock: Timelock::None, inputs: vec![], outputs: tx_outputs, extra diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index cb7a2b4f..19cc6fdf 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -100,7 +100,7 @@ async fn send_core(test: usize, multisig: bool) { // Grab the largest output available let output = { - let mut outputs = tx.as_ref().unwrap().scan(view, spend_pub); + let mut outputs = tx.as_ref().unwrap().scan(view, spend_pub).0; outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse()); outputs.swap_remove(0) }; @@ -125,7 +125,7 @@ async fn send_core(test: usize, multisig: bool) { for i in (start + 1) .. (start + 9) { let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0); - let output = tx.scan(view, spend_pub).swap_remove(0); + let output = tx.scan(view, spend_pub).0.swap_remove(0); amount += output.commitment.amount; outputs.push(output); } diff --git a/processor/src/coins/monero.rs b/processor/src/coins/monero.rs index 55b5e911..6e9140f9 100644 --- a/processor/src/coins/monero.rs +++ b/processor/src/coins/monero.rs @@ -9,7 +9,7 @@ use frost::MultisigKeys; use monero::util::address::Address; use monero_serai::{ frost::Ed25519, - transaction::Transaction, + transaction::{Timelock, Transaction}, rpc::Rpc, wallet::{SpendableOutput, SignableTransaction} }; @@ -90,7 +90,18 @@ impl Coin for Monero { } async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec { - block.iter().flat_map(|tx| tx.scan(self.view, key.0)).map(Output::from).collect() + block + .iter() + .flat_map(|tx| { + let (outputs, timelock) = tx.scan(self.view, key.0); + if timelock == Timelock::None { + outputs + } else { + vec![] + } + }) + .map(Output::from) + .collect() } async fn prepare_send(