mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-18 08:45:00 +00:00
Monero Processor scan, check_for_eventuality_resolutions
This commit is contained in:
parent
b4e94f3d51
commit
947e1067d9
7 changed files with 44 additions and 296 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -5105,6 +5105,7 @@ dependencies = [
|
|||
"hex",
|
||||
"modular-frost",
|
||||
"monero-address",
|
||||
"monero-clsag",
|
||||
"monero-rpc",
|
||||
"monero-serai",
|
||||
"monero-simple-request-rpc",
|
||||
|
@ -8534,8 +8535,10 @@ dependencies = [
|
|||
"serai-processor-signers",
|
||||
"serai-processor-utxo-scheduler",
|
||||
"serai-processor-utxo-scheduler-primitives",
|
||||
"serai-processor-view-keys",
|
||||
"tokio",
|
||||
"zalloc",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -18,6 +18,7 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
rand_core = { version = "0.6", default-features = false }
|
||||
zeroize = { version = "1", default-features = false, features = ["std"] }
|
||||
|
||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
|
||||
|
@ -41,6 +42,7 @@ tokio = { version = "1", default-features = false, features = ["rt-multi-thread"
|
|||
serai-db = { path = "../../common/db" }
|
||||
|
||||
key-gen = { package = "serai-processor-key-gen", path = "../key-gen" }
|
||||
view-keys = { package = "serai-processor-view-keys", path = "../view-keys" }
|
||||
|
||||
primitives = { package = "serai-processor-primitives", path = "../primitives" }
|
||||
scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" }
|
||||
|
|
|
@ -1,119 +1,4 @@
|
|||
/*
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::{time::Duration, collections::HashMap, io};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
use transcript::{Transcript, RecommendedTranscript};
|
||||
|
||||
use ciphersuite::group::{ff::Field, Group};
|
||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
||||
use frost::{curve::Ed25519, ThresholdKeys};
|
||||
|
||||
use monero_simple_request_rpc::SimpleRequestRpc;
|
||||
use monero_wallet::{
|
||||
ringct::RctType,
|
||||
transaction::Transaction,
|
||||
block::Block,
|
||||
rpc::{FeeRate, RpcError, Rpc},
|
||||
address::{Network as MoneroNetwork, SubaddressIndex},
|
||||
ViewPair, GuaranteedViewPair, WalletOutput, OutputWithDecoys, GuaranteedScanner,
|
||||
send::{
|
||||
SendError, Change, SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine,
|
||||
},
|
||||
};
|
||||
#[cfg(test)]
|
||||
use monero_wallet::Scanner;
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub use serai_client::{
|
||||
primitives::{MAX_DATA_LEN, Coin, NetworkId, Amount, Balance},
|
||||
networks::monero::Address,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Payment, additional_key,
|
||||
networks::{
|
||||
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
|
||||
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
|
||||
Eventuality as EventualityTrait, EventualitiesTracker, Network, UtxoNetwork,
|
||||
},
|
||||
multisigs::scheduler::utxo::Scheduler,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Output(WalletOutput);
|
||||
|
||||
const EXTERNAL_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(0, 0);
|
||||
const BRANCH_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(1, 0);
|
||||
const CHANGE_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(2, 0);
|
||||
const FORWARD_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(3, 0);
|
||||
|
||||
impl OutputTrait<Monero> for Output {
|
||||
// While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug.
|
||||
// While we already are immune, thanks to using featured address, this doesn't hurt and is
|
||||
// technically more efficient.
|
||||
type Id = [u8; 32];
|
||||
|
||||
fn kind(&self) -> OutputType {
|
||||
match self.0.subaddress() {
|
||||
EXTERNAL_SUBADDRESS => OutputType::External,
|
||||
BRANCH_SUBADDRESS => OutputType::Branch,
|
||||
CHANGE_SUBADDRESS => OutputType::Change,
|
||||
FORWARD_SUBADDRESS => OutputType::Forwarded,
|
||||
_ => panic!("unrecognized address was scanned for"),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.0.key().compress().to_bytes()
|
||||
}
|
||||
|
||||
fn tx_id(&self) -> [u8; 32] {
|
||||
self.0.transaction()
|
||||
}
|
||||
|
||||
fn key(&self) -> EdwardsPoint {
|
||||
EdwardsPoint(self.0.key() - (EdwardsPoint::generator().0 * self.0.key_offset()))
|
||||
}
|
||||
|
||||
fn presumed_origin(&self) -> Option<Address> {
|
||||
None
|
||||
}
|
||||
|
||||
fn balance(&self) -> Balance {
|
||||
Balance { coin: Coin::Monero, amount: Amount(self.0.commitment().amount) }
|
||||
}
|
||||
|
||||
fn data(&self) -> &[u8] {
|
||||
let Some(data) = self.0.arbitrary_data().first() else { return &[] };
|
||||
// If the data is too large, prune it
|
||||
// This should cause decoding the instruction to fail, and trigger a refund as appropriate
|
||||
if data.len() > usize::try_from(MAX_DATA_LEN).unwrap() {
|
||||
return &[];
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
self.0.write(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||
Ok(Output(WalletOutput::read(reader)?))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider ([u8; 32], TransactionPruned)
|
||||
#[async_trait]
|
||||
impl TransactionTrait<Monero> for Transaction {
|
||||
|
@ -227,29 +112,6 @@ impl BlockTrait<Monero> for Block {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Monero {
|
||||
rpc: SimpleRequestRpc,
|
||||
}
|
||||
// Shim required for testing/debugging purposes due to generic arguments also necessitating trait
|
||||
// bounds
|
||||
impl PartialEq for Monero {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl Eq for Monero {}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // Needed to satisfy API expectations
|
||||
fn map_rpc_err(err: RpcError) -> NetworkError {
|
||||
if let RpcError::InvalidNode(reason) = &err {
|
||||
log::error!("Monero RpcError::InvalidNode({reason})");
|
||||
} else {
|
||||
log::debug!("Monero RpcError {err:?}");
|
||||
}
|
||||
NetworkError::ConnectionError
|
||||
}
|
||||
|
||||
enum MakeSignableTransactionResult {
|
||||
Fee(u64),
|
||||
SignableTransaction(MSignableTransaction),
|
||||
|
@ -461,20 +323,6 @@ impl Monero {
|
|||
|
||||
#[async_trait]
|
||||
impl Network for Monero {
|
||||
type Curve = Ed25519;
|
||||
|
||||
type Transaction = Transaction;
|
||||
type Block = Block;
|
||||
|
||||
type Output = Output;
|
||||
type SignableTransaction = SignableTransaction;
|
||||
type Eventuality = Eventuality;
|
||||
type TransactionMachine = TransactionMachine;
|
||||
|
||||
type Scheduler = Scheduler<Monero>;
|
||||
|
||||
type Address = Address;
|
||||
|
||||
const NETWORK: NetworkId = NetworkId::Monero;
|
||||
const ID: &'static str = "Monero";
|
||||
const ESTIMATED_BLOCK_TIME_IN_SECONDS: usize = 120;
|
||||
|
@ -488,9 +336,6 @@ impl Network for Monero {
|
|||
// TODO
|
||||
const COST_TO_AGGREGATE: u64 = 0;
|
||||
|
||||
// Monero doesn't require/benefit from tweaking
|
||||
fn tweak_keys(_: &mut ThresholdKeys<Self::Curve>) {}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn external_address(&self, key: EdwardsPoint) -> Address {
|
||||
Self::address_internal(key, EXTERNAL_SUBADDRESS)
|
||||
|
@ -508,121 +353,6 @@ impl Network for Monero {
|
|||
Some(Self::address_internal(key, FORWARD_SUBADDRESS))
|
||||
}
|
||||
|
||||
async fn get_latest_block_number(&self) -> Result<usize, NetworkError> {
|
||||
// Monero defines height as chain length, so subtract 1 for block number
|
||||
Ok(self.rpc.get_height().await.map_err(map_rpc_err)? - 1)
|
||||
}
|
||||
|
||||
async fn get_block(&self, number: usize) -> Result<Self::Block, NetworkError> {
|
||||
Ok(
|
||||
self
|
||||
.rpc
|
||||
.get_block(self.rpc.get_block_hash(number).await.map_err(map_rpc_err)?)
|
||||
.await
|
||||
.map_err(map_rpc_err)?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec<Output> {
|
||||
let outputs = loop {
|
||||
match self
|
||||
.rpc
|
||||
.get_scannable_block(block.clone())
|
||||
.await
|
||||
.map_err(|e| format!("{e:?}"))
|
||||
.and_then(|block| Self::scanner(key).scan(block).map_err(|e| format!("{e:?}")))
|
||||
{
|
||||
Ok(outputs) => break outputs,
|
||||
Err(e) => {
|
||||
log::error!("couldn't scan block {}: {e:?}", hex::encode(block.id()));
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Miner transactions are required to explicitly state their timelock, so this does exclude
|
||||
// those (which have an extended timelock we don't want to deal with)
|
||||
let raw_outputs = outputs.not_additionally_locked();
|
||||
let mut outputs = Vec::with_capacity(raw_outputs.len());
|
||||
for output in raw_outputs {
|
||||
// This should be pointless as we shouldn't be able to scan for any other subaddress
|
||||
// This just helps ensures nothing invalid makes it through
|
||||
assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARD_SUBADDRESS]
|
||||
.contains(&output.subaddress()));
|
||||
|
||||
outputs.push(Output(output));
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
|
||||
async fn get_eventuality_completions(
|
||||
&self,
|
||||
eventualities: &mut EventualitiesTracker<Eventuality>,
|
||||
block: &Block,
|
||||
) -> HashMap<[u8; 32], (usize, [u8; 32], Transaction)> {
|
||||
let mut res = HashMap::new();
|
||||
if eventualities.map.is_empty() {
|
||||
return res;
|
||||
}
|
||||
|
||||
async fn check_block(
|
||||
network: &Monero,
|
||||
eventualities: &mut EventualitiesTracker<Eventuality>,
|
||||
block: &Block,
|
||||
res: &mut HashMap<[u8; 32], (usize, [u8; 32], Transaction)>,
|
||||
) {
|
||||
for hash in &block.transactions {
|
||||
let tx = {
|
||||
let mut tx;
|
||||
while {
|
||||
tx = network.rpc.get_transaction(*hash).await;
|
||||
tx.is_err()
|
||||
} {
|
||||
log::error!("couldn't get transaction {}: {}", hex::encode(hash), tx.err().unwrap());
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
tx.unwrap()
|
||||
};
|
||||
|
||||
if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) {
|
||||
if eventuality.matches(&tx.clone().into()) {
|
||||
res.insert(
|
||||
eventualities.map.remove(&tx.prefix().extra).unwrap().0,
|
||||
(block.number().unwrap(), tx.id(), tx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventualities.block_number += 1;
|
||||
assert_eq!(eventualities.block_number, block.number().unwrap());
|
||||
}
|
||||
|
||||
for block_num in (eventualities.block_number + 1) .. block.number().unwrap() {
|
||||
let block = {
|
||||
let mut block;
|
||||
while {
|
||||
block = self.get_block(block_num).await;
|
||||
block.is_err()
|
||||
} {
|
||||
log::error!("couldn't get block {}: {}", block_num, block.err().unwrap());
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
block.unwrap()
|
||||
};
|
||||
|
||||
check_block(self, eventualities, &block, &mut res).await;
|
||||
}
|
||||
|
||||
// Also check the current block
|
||||
check_block(self, eventualities, block, &mut res).await;
|
||||
assert_eq!(eventualities.block_number, block.number().unwrap());
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn needed_fee(
|
||||
&self,
|
||||
block_number: usize,
|
||||
|
@ -687,19 +417,6 @@ impl Network for Monero {
|
|||
}
|
||||
}
|
||||
|
||||
async fn confirm_completion(
|
||||
&self,
|
||||
eventuality: &Eventuality,
|
||||
id: &[u8; 32],
|
||||
) -> Result<Option<Transaction>, NetworkError> {
|
||||
let tx = self.rpc.get_transaction(*id).await.map_err(map_rpc_err)?;
|
||||
if eventuality.matches(&tx.clone().into()) {
|
||||
Ok(Some(tx))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
|
||||
self.rpc.get_block(*id).await.unwrap().number().unwrap()
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use ciphersuite::{Ciphersuite, Ed25519};
|
||||
|
||||
use monero_wallet::{transaction::Transaction, block::Block as MBlock, ViewPairError, GuaranteedViewPair, GuaranteedScanner};
|
||||
use monero_wallet::{
|
||||
block::Block as MBlock, rpc::ScannableBlock as MScannableBlock,
|
||||
ViewPairError, GuaranteedViewPair, ScanError, GuaranteedScanner,
|
||||
};
|
||||
|
||||
use serai_client::networks::monero::Address;
|
||||
|
||||
use primitives::{ReceivedOutput, EventualityTracker};
|
||||
|
||||
use crate::{EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, output::Output, transaction::Eventuality};
|
||||
use view_keys::view_key;
|
||||
use crate::{
|
||||
EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, output::Output,
|
||||
transaction::Eventuality,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct BlockHeader(pub(crate) MBlock);
|
||||
|
@ -22,7 +30,7 @@ impl primitives::BlockHeader for BlockHeader {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Block(pub(crate) MBlock, Vec<Transaction>);
|
||||
pub(crate) struct Block(pub(crate) MScannableBlock);
|
||||
|
||||
impl primitives::Block for Block {
|
||||
type Header = BlockHeader;
|
||||
|
@ -33,20 +41,26 @@ impl primitives::Block for Block {
|
|||
type Eventuality = Eventuality;
|
||||
|
||||
fn id(&self) -> [u8; 32] {
|
||||
self.0.hash()
|
||||
self.0.block.hash()
|
||||
}
|
||||
|
||||
fn scan_for_outputs_unordered(&self, key: Self::Key) -> Vec<Self::Output> {
|
||||
let view_pair = match GuaranteedViewPair::new(key.0, additional_key) {
|
||||
let view_pair = match GuaranteedViewPair::new(key.0, Zeroizing::new(*view_key::<Ed25519>(0))) {
|
||||
Ok(view_pair) => view_pair,
|
||||
Err(ViewPairError::TorsionedSpendKey) => unreachable!("dalek_ff_group::EdwardsPoint has torsion"),
|
||||
};
|
||||
Err(ViewPairError::TorsionedSpendKey) => {
|
||||
unreachable!("dalek_ff_group::EdwardsPoint had torsion")
|
||||
}
|
||||
};
|
||||
let mut scanner = GuaranteedScanner::new(view_pair);
|
||||
scanner.register_subaddress(EXTERNAL_SUBADDRESS.unwrap());
|
||||
scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap());
|
||||
scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap());
|
||||
scanner.register_subaddress(FORWARDED_SUBADDRESS.unwrap());
|
||||
todo!("TODO")
|
||||
match scanner.scan(self.0.clone()) {
|
||||
Ok(outputs) => outputs.not_additionally_locked().into_iter().map(Output).collect(),
|
||||
Err(ScanError::UnsupportedProtocol(version)) => panic!("Monero unexpectedly hard-forked (version {version})"),
|
||||
Err(ScanError::InvalidScannableBlock(reason)) => panic!("fetched an invalid scannable block from the RPC: {reason}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
@ -57,6 +71,18 @@ impl primitives::Block for Block {
|
|||
<Self::Output as ReceivedOutput<Self::Key, Self::Address>>::TransactionId,
|
||||
Self::Eventuality,
|
||||
> {
|
||||
todo!("TODO")
|
||||
let mut res = HashMap::new();
|
||||
assert_eq!(self.0.block.transactions.len(), self.0.transactions.len());
|
||||
for (hash, tx) in self.0.block.transactions.iter().zip(&self.0.transactions) {
|
||||
if let Some(eventuality) = eventualities.active_eventualities.get(&tx.prefix().extra) {
|
||||
if eventuality.eventuality.matches(tx) {
|
||||
res.insert(
|
||||
*hash,
|
||||
eventualities.active_eventualities.remove(&tx.prefix().extra).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ impl AsMut<[u8]> for OutputId {
|
|||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct Output(WalletOutput);
|
||||
pub(crate) struct Output(pub(crate) WalletOutput);
|
||||
|
||||
impl Output {
|
||||
pub(crate) fn new(output: WalletOutput) -> Self {
|
||||
|
|
|
@ -83,7 +83,7 @@ impl scheduler::SignableTransaction for SignableTransaction {
|
|||
pub(crate) struct Eventuality {
|
||||
id: [u8; 32],
|
||||
singular_spent_output: Option<OutputId>,
|
||||
eventuality: MEventuality,
|
||||
pub(crate) eventuality: MEventuality,
|
||||
}
|
||||
|
||||
impl primitives::Eventuality for Eventuality {
|
||||
|
|
|
@ -54,7 +54,7 @@ impl ScannerFeed for Rpc {
|
|||
&self,
|
||||
number: u64,
|
||||
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
||||
async move{todo!("TODO")}
|
||||
async move { todo!("TODO") }
|
||||
}
|
||||
|
||||
fn unchecked_block_header_by_number(
|
||||
|
|
Loading…
Reference in a new issue