mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Allow scheduler's creation of transactions to be async and error
I don't love this, but it's the only way to select decoys without using a local database. While the prior commit added such a databse, the performance of it presumably wasn't viable, and while TODOs marked the needed improvements, it was still messy with an immense scope re: any auditing. The relevant scheduler functions now take `&self` (intentional, as all mutations should be via the `&mut impl DbTxn` passed). The calls to `&self` are expected to be completely deterministic (as usual).
This commit is contained in:
@@ -1,294 +0,0 @@
|
||||
use core::{
|
||||
future::Future,
|
||||
ops::{Bound, RangeBounds},
|
||||
};
|
||||
|
||||
use curve25519_dalek::{
|
||||
scalar::Scalar,
|
||||
edwards::{CompressedEdwardsY, EdwardsPoint},
|
||||
};
|
||||
use monero_wallet::{
|
||||
DEFAULT_LOCK_WINDOW,
|
||||
primitives::Commitment,
|
||||
transaction::{Timelock, Input, Pruned, Transaction},
|
||||
rpc::{OutputInformation, RpcError, Rpc as MRpcTrait, DecoyRpc},
|
||||
};
|
||||
|
||||
use borsh::{BorshSerialize, BorshDeserialize};
|
||||
use serai_db::{Get, DbTxn, Db, create_db};
|
||||
|
||||
use primitives::task::ContinuallyRan;
|
||||
use scanner::ScannerFeed;
|
||||
|
||||
use crate::Rpc;
|
||||
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
struct EncodableOutputInformation {
|
||||
height: u64,
|
||||
timelocked: bool,
|
||||
key: [u8; 32],
|
||||
commitment: [u8; 32],
|
||||
}
|
||||
|
||||
create_db! {
|
||||
MoneroProcessorDecoys {
|
||||
NextToIndexBlock: () -> u64,
|
||||
PriorIndexedBlock: () -> [u8; 32],
|
||||
DistributionStartBlock: () -> u64,
|
||||
Distribution: () -> Vec<u64>,
|
||||
Out: (index: u64) -> EncodableOutputInformation,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
We want to be able to select decoys when planning transactions, but planning transactions is a
|
||||
synchronous process. We store the decoys to a local database and have our database implement
|
||||
`DecoyRpc` to achieve synchronous decoy selection.
|
||||
|
||||
This is only needed as the transactions we sign must have decoys decided and agreed upon. With
|
||||
FCMP++s, we'll be able to sign transactions without the membership proof, letting any signer
|
||||
prove for membership after the fact (with their local views). Until then, this task remains.
|
||||
*/
|
||||
pub(crate) struct DecoysTask<D: Db> {
|
||||
pub(crate) rpc: Rpc<D>,
|
||||
pub(crate) current_distribution: Vec<u64>,
|
||||
}
|
||||
|
||||
impl<D: Db> ContinuallyRan for DecoysTask<D> {
|
||||
fn run_iteration(&mut self) -> impl Send + Future<Output = Result<bool, String>> {
|
||||
async move {
|
||||
let finalized_block_number = self
|
||||
.rpc
|
||||
.rpc
|
||||
.get_height()
|
||||
.await
|
||||
.map_err(|e| format!("couldn't fetch latest block number: {e:?}"))?
|
||||
.checked_sub(Rpc::<D>::CONFIRMATIONS.try_into().unwrap())
|
||||
.ok_or(format!(
|
||||
"blockchain only just started and doesn't have {} blocks yet",
|
||||
Rpc::<D>::CONFIRMATIONS
|
||||
))?;
|
||||
|
||||
if NextToIndexBlock::get(&self.rpc.db).is_none() {
|
||||
let distribution = self
|
||||
.rpc
|
||||
.rpc
|
||||
.get_output_distribution(..= finalized_block_number)
|
||||
.await
|
||||
.map_err(|e| format!("failed to get output distribution: {e:?}"))?;
|
||||
if distribution.is_empty() {
|
||||
Err("distribution was empty".to_string())?;
|
||||
}
|
||||
|
||||
let distribution_start_block = finalized_block_number - (distribution.len() - 1);
|
||||
// There may have been a reorg between the time of getting the distribution and the time of
|
||||
// getting this block. This is an invariant and assumed not to have happened in the split
|
||||
// second it's possible.
|
||||
let block = self
|
||||
.rpc
|
||||
.rpc
|
||||
.get_block_by_number(distribution_start_block)
|
||||
.await
|
||||
.map_err(|e| format!("failed to get the start block for the distribution: {e:?}"))?;
|
||||
|
||||
let mut txn = self.rpc.db.txn();
|
||||
NextToIndexBlock::set(&mut txn, &distribution_start_block.try_into().unwrap());
|
||||
PriorIndexedBlock::set(&mut txn, &block.header.previous);
|
||||
DistributionStartBlock::set(&mut txn, &u64::try_from(distribution_start_block).unwrap());
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
let next_to_index_block =
|
||||
usize::try_from(NextToIndexBlock::get(&self.rpc.db).unwrap()).unwrap();
|
||||
if next_to_index_block >= finalized_block_number {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
for b in next_to_index_block ..= finalized_block_number {
|
||||
// Fetch the block
|
||||
let block = self
|
||||
.rpc
|
||||
.rpc
|
||||
.get_block_by_number(b)
|
||||
.await
|
||||
.map_err(|e| format!("decoys task failed to fetch block: {e:?}"))?;
|
||||
let prior = PriorIndexedBlock::get(&self.rpc.db).unwrap();
|
||||
if block.header.previous != prior {
|
||||
panic!(
|
||||
"decoys task detected reorg: expected {}, found {}",
|
||||
hex::encode(prior),
|
||||
hex::encode(block.header.previous)
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch the transactions in the block
|
||||
let transactions = self
|
||||
.rpc
|
||||
.rpc
|
||||
.get_pruned_transactions(&block.transactions)
|
||||
.await
|
||||
.map_err(|e| format!("failed to get the pruned transactions within a block: {e:?}"))?;
|
||||
|
||||
fn outputs(
|
||||
list: &mut Vec<EncodableOutputInformation>,
|
||||
block_number: u64,
|
||||
tx: Transaction<Pruned>,
|
||||
) {
|
||||
match tx {
|
||||
Transaction::V1 { .. } => {}
|
||||
Transaction::V2 { prefix, proofs } => {
|
||||
for (i, output) in prefix.outputs.into_iter().enumerate() {
|
||||
list.push(EncodableOutputInformation {
|
||||
// This is correct per the documentation on OutputInformation, which this maps to
|
||||
height: block_number,
|
||||
timelocked: prefix.additional_timelock != Timelock::None,
|
||||
key: output.key.to_bytes(),
|
||||
commitment: if matches!(
|
||||
prefix.inputs.first().expect("Monero transaction had no inputs"),
|
||||
Input::Gen(_)
|
||||
) {
|
||||
Commitment::new(
|
||||
Scalar::ONE,
|
||||
output.amount.expect("miner transaction outputs didn't have amounts set"),
|
||||
)
|
||||
.calculate()
|
||||
.compress()
|
||||
.to_bytes()
|
||||
} else {
|
||||
proofs
|
||||
.as_ref()
|
||||
.expect("non-miner V2 transaction didn't have proofs")
|
||||
.base
|
||||
.commitments
|
||||
.get(i)
|
||||
.expect("amount of commitments didn't match amount of outputs")
|
||||
.compress()
|
||||
.to_bytes()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let block_hash = block.hash();
|
||||
|
||||
let b = u64::try_from(b).unwrap();
|
||||
let mut encodable = Vec::with_capacity(2 * (1 + block.transactions.len()));
|
||||
outputs(&mut encodable, b, block.miner_transaction.into());
|
||||
for transaction in transactions {
|
||||
outputs(&mut encodable, b, transaction);
|
||||
}
|
||||
|
||||
let existing_outputs = self.current_distribution.last().copied().unwrap_or(0);
|
||||
let now_outputs = existing_outputs + u64::try_from(encodable.len()).unwrap();
|
||||
self.current_distribution.push(now_outputs);
|
||||
|
||||
let mut txn = self.rpc.db.txn();
|
||||
NextToIndexBlock::set(&mut txn, &(b + 1));
|
||||
PriorIndexedBlock::set(&mut txn, &block_hash);
|
||||
// TODO: Don't write the entire 10 MB distribution to the DB every two minutes
|
||||
Distribution::set(&mut txn, &self.current_distribution);
|
||||
for (b, out) in (existing_outputs .. now_outputs).zip(encodable) {
|
||||
Out::set(&mut txn, b, &out);
|
||||
}
|
||||
txn.commit();
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Cache the distribution in a static
|
||||
pub(crate) struct Decoys<'a, G: Get>(&'a G);
|
||||
impl<'a, G: Sync + Get> DecoyRpc for Decoys<'a, G> {
|
||||
#[rustfmt::skip]
|
||||
fn get_output_distribution_end_height(
|
||||
&self,
|
||||
) -> impl Send + Future<Output = Result<usize, RpcError>> {
|
||||
async move {
|
||||
Ok(NextToIndexBlock::get(self.0).map_or(0, |b| usize::try_from(b).unwrap() + 1))
|
||||
}
|
||||
}
|
||||
fn get_output_distribution(
|
||||
&self,
|
||||
range: impl Send + RangeBounds<usize>,
|
||||
) -> impl Send + Future<Output = Result<Vec<u64>, RpcError>> {
|
||||
async move {
|
||||
let from = match range.start_bound() {
|
||||
Bound::Included(from) => *from,
|
||||
Bound::Excluded(from) => from.checked_add(1).ok_or_else(|| {
|
||||
RpcError::InternalError("range's from wasn't representable".to_string())
|
||||
})?,
|
||||
Bound::Unbounded => 0,
|
||||
};
|
||||
let to = match range.end_bound() {
|
||||
Bound::Included(to) => *to,
|
||||
Bound::Excluded(to) => to
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| RpcError::InternalError("range's to wasn't representable".to_string()))?,
|
||||
Bound::Unbounded => {
|
||||
panic!("requested distribution till latest block, which is non-deterministic")
|
||||
}
|
||||
};
|
||||
if from > to {
|
||||
Err(RpcError::InternalError(format!(
|
||||
"malformed range: inclusive start {from}, inclusive end {to}"
|
||||
)))?;
|
||||
}
|
||||
|
||||
let distribution_start_block = usize::try_from(
|
||||
DistributionStartBlock::get(self.0).expect("never populated the distribution start block"),
|
||||
)
|
||||
.unwrap();
|
||||
let len_of_distribution_until_to =
|
||||
to.checked_sub(distribution_start_block).ok_or_else(|| {
|
||||
RpcError::InternalError(
|
||||
"requested distribution until a block when the distribution had yet to start"
|
||||
.to_string(),
|
||||
)
|
||||
})? +
|
||||
1;
|
||||
let distribution = Distribution::get(self.0).expect("never populated the distribution");
|
||||
assert!(
|
||||
distribution.len() >= len_of_distribution_until_to,
|
||||
"requested distribution until block we have yet to index"
|
||||
);
|
||||
Ok(
|
||||
distribution[from.saturating_sub(distribution_start_block) .. len_of_distribution_until_to]
|
||||
.to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fn get_outs(
|
||||
&self,
|
||||
_indexes: &[u64],
|
||||
) -> impl Send + Future<Output = Result<Vec<OutputInformation>, RpcError>> {
|
||||
async move { unimplemented!("get_outs is unused") }
|
||||
}
|
||||
fn get_unlocked_outputs(
|
||||
&self,
|
||||
indexes: &[u64],
|
||||
height: usize,
|
||||
fingerprintable_deterministic: bool,
|
||||
) -> impl Send + Future<Output = Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>> {
|
||||
assert!(fingerprintable_deterministic, "processor wasn't using deterministic output selection");
|
||||
async move {
|
||||
let mut res = vec![];
|
||||
for index in indexes {
|
||||
let out = Out::get(self.0, *index).expect("requested output we didn't index");
|
||||
let unlocked = (!out.timelocked) &&
|
||||
((usize::try_from(out.height).unwrap() + DEFAULT_LOCK_WINDOW) <= height);
|
||||
res.push(unlocked.then(|| CompressedEdwardsY(out.key).decompress()).flatten().map(|key| {
|
||||
[
|
||||
key,
|
||||
CompressedEdwardsY(out.commitment)
|
||||
.decompress()
|
||||
.expect("output with invalid commitment"),
|
||||
]
|
||||
}));
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ use crate::key_gen::KeyGenParams;
|
||||
mod rpc;
|
||||
use rpc::Rpc;
|
||||
|
||||
mod decoys;
|
||||
/*
|
||||
mod scheduler;
|
||||
use scheduler::Scheduler;
|
||||
|
||||
@@ -5,7 +5,6 @@ use monero_simple_request_rpc::SimpleRequestRpc;
|
||||
|
||||
use serai_client::primitives::{NetworkId, Coin, Amount};
|
||||
|
||||
use serai_db::Db;
|
||||
use scanner::ScannerFeed;
|
||||
use signers::TransactionPublisher;
|
||||
|
||||
@@ -15,12 +14,11 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Rpc<D: Db> {
|
||||
pub(crate) db: D,
|
||||
pub(crate) struct Rpc {
|
||||
pub(crate) rpc: SimpleRequestRpc,
|
||||
}
|
||||
|
||||
impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
impl ScannerFeed for Rpc {
|
||||
const NETWORK: NetworkId = NetworkId::Monero;
|
||||
// Outputs aren't spendable until 10 blocks later due to the 10-block lock
|
||||
// Since we assumed scanned outputs are spendable, that sets a minimum confirmation depth of 10
|
||||
@@ -39,15 +37,16 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
&self,
|
||||
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
||||
async move {
|
||||
// The decoys task only indexes finalized blocks
|
||||
crate::decoys::NextToIndexBlock::get(&self.db)
|
||||
.ok_or_else(|| {
|
||||
RpcError::InternalError("decoys task hasn't indexed any blocks yet".to_string())
|
||||
})?
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| {
|
||||
RpcError::InternalError("only the genesis block has been indexed".to_string())
|
||||
})
|
||||
Ok(
|
||||
self
|
||||
.rpc
|
||||
.get_height()
|
||||
.await?
|
||||
.checked_sub(1)
|
||||
.expect("connected to an invalid Monero RPC")
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +127,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Db> TransactionPublisher<Transaction> for Rpc<D> {
|
||||
impl TransactionPublisher<Transaction> for Rpc {
|
||||
type EphemeralError = RpcError;
|
||||
|
||||
fn publish(
|
||||
|
||||
Reference in New Issue
Block a user