mirror of
https://github.com/serai-dex/serai.git
synced 2024-11-16 17:07:35 +00:00
Add a database of all Monero outs into the processor
Enables synchronous transaction creation (which requires synchronous decoy selection).
This commit is contained in:
parent
e56af7fc51
commit
2edc2f3612
8 changed files with 457 additions and 156 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -8515,6 +8515,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"dkg",
|
"dkg",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
|
|
|
@ -249,7 +249,7 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
||||||
/// While no implementors are directly provided, [monero-simple-request-rpc](
|
/// While no implementors are directly provided, [monero-simple-request-rpc](
|
||||||
/// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request
|
/// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request
|
||||||
/// ) is recommended.
|
/// ) is recommended.
|
||||||
pub trait Rpc: Sync + Clone + Debug {
|
pub trait Rpc: Sync + Clone {
|
||||||
/// Perform a POST request to the specified route with the specified body.
|
/// Perform a POST request to the specified route with the specified body.
|
||||||
///
|
///
|
||||||
/// The implementor is left to handle anything such as authentication.
|
/// The implementor is left to handle anything such as authentication.
|
||||||
|
@ -1003,10 +1003,10 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||||
/// An implementation is provided for any satisfier of `Rpc`. It is not recommended to use an `Rpc`
|
/// An implementation is provided for any satisfier of `Rpc`. It is not recommended to use an `Rpc`
|
||||||
/// object to satisfy this. This should be satisfied by a local store of the output distribution,
|
/// object to satisfy this. This should be satisfied by a local store of the output distribution,
|
||||||
/// both for performance and to prevent potential attacks a remote node can perform.
|
/// both for performance and to prevent potential attacks a remote node can perform.
|
||||||
pub trait DecoyRpc: Sync + Clone + Debug {
|
pub trait DecoyRpc: Sync {
|
||||||
/// Get the height the output distribution ends at.
|
/// Get the height the output distribution ends at.
|
||||||
///
|
///
|
||||||
/// This is equivalent to the hight of the blockchain it's for. This is intended to be cheaper
|
/// This is equivalent to the height of the blockchain it's for. This is intended to be cheaper
|
||||||
/// than fetching the entire output distribution.
|
/// than fetching the entire output distribution.
|
||||||
fn get_output_distribution_end_height(
|
fn get_output_distribution_end_height(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -25,6 +25,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
|
||||||
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
||||||
|
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
|
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "ed25519"] }
|
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "ed25519"] }
|
||||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ed25519"] }
|
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ed25519"] }
|
||||||
|
|
294
processor/monero/src/decoys.rs
Normal file
294
processor/monero/src/decoys.rs
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -107,146 +107,6 @@ impl Monero {
|
||||||
Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap())
|
Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn make_signable_transaction(
|
|
||||||
&self,
|
|
||||||
block_number: usize,
|
|
||||||
plan_id: &[u8; 32],
|
|
||||||
inputs: &[Output],
|
|
||||||
payments: &[Payment<Self>],
|
|
||||||
change: &Option<Address>,
|
|
||||||
calculating_fee: bool,
|
|
||||||
) -> Result<Option<MakeSignableTransactionResult>, NetworkError> {
|
|
||||||
for payment in payments {
|
|
||||||
assert_eq!(payment.balance.coin, Coin::Monero);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO2: Use an fee representative of several blocks, cached inside Self
|
|
||||||
let block_for_fee = self.get_block(block_number).await?;
|
|
||||||
let fee_rate = self.median_fee(&block_for_fee).await?;
|
|
||||||
|
|
||||||
// Determine the RCT proofs to make based off the hard fork
|
|
||||||
// TODO: Make a fn for this block which is duplicated with tests
|
|
||||||
let rct_type = match block_for_fee.header.hardfork_version {
|
|
||||||
14 => RctType::ClsagBulletproof,
|
|
||||||
15 | 16 => RctType::ClsagBulletproofPlus,
|
|
||||||
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut transcript =
|
|
||||||
RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript");
|
|
||||||
transcript.append_message(b"plan", plan_id);
|
|
||||||
|
|
||||||
// All signers need to select the same decoys
|
|
||||||
// All signers use the same height and a seeded RNG to make sure they do so.
|
|
||||||
let mut inputs_actual = Vec::with_capacity(inputs.len());
|
|
||||||
for input in inputs {
|
|
||||||
inputs_actual.push(
|
|
||||||
OutputWithDecoys::fingerprintable_deterministic_new(
|
|
||||||
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
|
|
||||||
&self.rpc,
|
|
||||||
// TODO: Have Decoys take RctType
|
|
||||||
match rct_type {
|
|
||||||
RctType::ClsagBulletproof => 11,
|
|
||||||
RctType::ClsagBulletproofPlus => 16,
|
|
||||||
_ => panic!("selecting decoys for an unsupported RctType"),
|
|
||||||
},
|
|
||||||
block_number + 1,
|
|
||||||
input.0.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(map_rpc_err)?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monero requires at least two outputs
|
|
||||||
// If we only have one output planned, add a dummy payment
|
|
||||||
let mut payments = payments.to_vec();
|
|
||||||
let outputs = payments.len() + usize::from(u8::from(change.is_some()));
|
|
||||||
if outputs == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
} else if outputs == 1 {
|
|
||||||
payments.push(Payment {
|
|
||||||
address: Address::new(
|
|
||||||
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
|
|
||||||
.unwrap()
|
|
||||||
.legacy_address(MoneroNetwork::Mainnet),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
balance: Balance { coin: Coin::Monero, amount: Amount(0) },
|
|
||||||
data: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let payments = payments
|
|
||||||
.into_iter()
|
|
||||||
.map(|payment| (payment.address.into(), payment.balance.amount.0))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
match MSignableTransaction::new(
|
|
||||||
rct_type,
|
|
||||||
// Use the plan ID as the outgoing view key
|
|
||||||
Zeroizing::new(*plan_id),
|
|
||||||
inputs_actual,
|
|
||||||
payments,
|
|
||||||
Change::fingerprintable(change.as_ref().map(|change| change.clone().into())),
|
|
||||||
vec![],
|
|
||||||
fee_rate,
|
|
||||||
) {
|
|
||||||
Ok(signable) => Ok(Some({
|
|
||||||
if calculating_fee {
|
|
||||||
MakeSignableTransactionResult::Fee(signable.necessary_fee())
|
|
||||||
} else {
|
|
||||||
MakeSignableTransactionResult::SignableTransaction(signable)
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
Err(e) => match e {
|
|
||||||
SendError::UnsupportedRctType => {
|
|
||||||
panic!("trying to use an RctType unsupported by monero-wallet")
|
|
||||||
}
|
|
||||||
SendError::NoInputs |
|
|
||||||
SendError::InvalidDecoyQuantity |
|
|
||||||
SendError::NoOutputs |
|
|
||||||
SendError::TooManyOutputs |
|
|
||||||
SendError::NoChange |
|
|
||||||
SendError::TooMuchArbitraryData |
|
|
||||||
SendError::TooLargeTransaction |
|
|
||||||
SendError::WrongPrivateKey => {
|
|
||||||
panic!("created an invalid Monero transaction: {e}");
|
|
||||||
}
|
|
||||||
SendError::MultiplePaymentIds => {
|
|
||||||
panic!("multiple payment IDs despite not supporting integrated addresses");
|
|
||||||
}
|
|
||||||
SendError::NotEnoughFunds { inputs, outputs, necessary_fee } => {
|
|
||||||
log::debug!(
|
|
||||||
"Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, necessary_fee: {necessary_fee:?}",
|
|
||||||
inputs,
|
|
||||||
outputs
|
|
||||||
);
|
|
||||||
match necessary_fee {
|
|
||||||
Some(necessary_fee) => {
|
|
||||||
// If we're solely calculating the fee, return the fee this TX will cost
|
|
||||||
if calculating_fee {
|
|
||||||
Ok(Some(MakeSignableTransactionResult::Fee(necessary_fee)))
|
|
||||||
} else {
|
|
||||||
// If we're actually trying to make the TX, return None
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We didn't have enough funds to even cover the outputs
|
|
||||||
None => {
|
|
||||||
// Ensure we're not misinterpreting this
|
|
||||||
assert!(outputs > inputs);
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SendError::MaliciousSerialization | SendError::ClsagError(_) | SendError::FrostError(_) => {
|
|
||||||
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn test_view_pair() -> ViewPair {
|
fn test_view_pair() -> ViewPair {
|
||||||
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
|
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
|
||||||
|
|
|
@ -15,6 +15,8 @@ mod key_gen;
|
||||||
use crate::key_gen::KeyGenParams;
|
use crate::key_gen::KeyGenParams;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
use rpc::Rpc;
|
use rpc::Rpc;
|
||||||
|
|
||||||
|
mod decoys;
|
||||||
/*
|
/*
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
use scheduler::Scheduler;
|
use scheduler::Scheduler;
|
||||||
|
|
|
@ -5,6 +5,7 @@ use monero_simple_request_rpc::SimpleRequestRpc;
|
||||||
|
|
||||||
use serai_client::primitives::{NetworkId, Coin, Amount};
|
use serai_client::primitives::{NetworkId, Coin, Amount};
|
||||||
|
|
||||||
|
use serai_db::Db;
|
||||||
use scanner::ScannerFeed;
|
use scanner::ScannerFeed;
|
||||||
use signers::TransactionPublisher;
|
use signers::TransactionPublisher;
|
||||||
|
|
||||||
|
@ -14,11 +15,12 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Rpc {
|
pub(crate) struct Rpc<D: Db> {
|
||||||
|
pub(crate) db: D,
|
||||||
pub(crate) rpc: SimpleRequestRpc,
|
pub(crate) rpc: SimpleRequestRpc,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScannerFeed for Rpc {
|
impl<D: Db> ScannerFeed for Rpc<D> {
|
||||||
const NETWORK: NetworkId = NetworkId::Monero;
|
const NETWORK: NetworkId = NetworkId::Monero;
|
||||||
// Outputs aren't spendable until 10 blocks later due to the 10-block lock
|
// 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
|
// Since we assumed scanned outputs are spendable, that sets a minimum confirmation depth of 10
|
||||||
|
@ -37,16 +39,15 @@ impl ScannerFeed for Rpc {
|
||||||
&self,
|
&self,
|
||||||
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
||||||
async move {
|
async move {
|
||||||
Ok(
|
// The decoys task only indexes finalized blocks
|
||||||
self
|
crate::decoys::NextToIndexBlock::get(&self.db)
|
||||||
.rpc
|
.ok_or_else(|| {
|
||||||
.get_height()
|
RpcError::InternalError("decoys task hasn't indexed any blocks yet".to_string())
|
||||||
.await?
|
})?
|
||||||
.checked_sub(1)
|
.checked_sub(1)
|
||||||
.expect("connected to an invalid Monero RPC")
|
.ok_or_else(|| {
|
||||||
.try_into()
|
RpcError::InternalError("only the genesis block has been indexed".to_string())
|
||||||
.unwrap(),
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +128,7 @@ impl ScannerFeed for Rpc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionPublisher<Transaction> for Rpc {
|
impl<D: Db> TransactionPublisher<Transaction> for Rpc<D> {
|
||||||
type EphemeralError = RpcError;
|
type EphemeralError = RpcError;
|
||||||
|
|
||||||
fn publish(
|
fn publish(
|
||||||
|
|
|
@ -1,3 +1,144 @@
|
||||||
|
async fn make_signable_transaction(
|
||||||
|
block_number: usize,
|
||||||
|
plan_id: &[u8; 32],
|
||||||
|
inputs: &[Output],
|
||||||
|
payments: &[Payment<Self>],
|
||||||
|
change: &Option<Address>,
|
||||||
|
calculating_fee: bool,
|
||||||
|
) -> Result<Option<MakeSignableTransactionResult>, NetworkError> {
|
||||||
|
for payment in payments {
|
||||||
|
assert_eq!(payment.balance.coin, Coin::Monero);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO2: Use an fee representative of several blocks, cached inside Self
|
||||||
|
let block_for_fee = self.get_block(block_number).await?;
|
||||||
|
let fee_rate = self.median_fee(&block_for_fee).await?;
|
||||||
|
|
||||||
|
// Determine the RCT proofs to make based off the hard fork
|
||||||
|
// TODO: Make a fn for this block which is duplicated with tests
|
||||||
|
let rct_type = match block_for_fee.header.hardfork_version {
|
||||||
|
14 => RctType::ClsagBulletproof,
|
||||||
|
15 | 16 => RctType::ClsagBulletproofPlus,
|
||||||
|
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut transcript =
|
||||||
|
RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript");
|
||||||
|
transcript.append_message(b"plan", plan_id);
|
||||||
|
|
||||||
|
// All signers need to select the same decoys
|
||||||
|
// All signers use the same height and a seeded RNG to make sure they do so.
|
||||||
|
let mut inputs_actual = Vec::with_capacity(inputs.len());
|
||||||
|
for input in inputs {
|
||||||
|
inputs_actual.push(
|
||||||
|
OutputWithDecoys::fingerprintable_deterministic_new(
|
||||||
|
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
|
||||||
|
&self.rpc,
|
||||||
|
// TODO: Have Decoys take RctType
|
||||||
|
match rct_type {
|
||||||
|
RctType::ClsagBulletproof => 11,
|
||||||
|
RctType::ClsagBulletproofPlus => 16,
|
||||||
|
_ => panic!("selecting decoys for an unsupported RctType"),
|
||||||
|
},
|
||||||
|
block_number + 1,
|
||||||
|
input.0.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(map_rpc_err)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monero requires at least two outputs
|
||||||
|
// If we only have one output planned, add a dummy payment
|
||||||
|
let mut payments = payments.to_vec();
|
||||||
|
let outputs = payments.len() + usize::from(u8::from(change.is_some()));
|
||||||
|
if outputs == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
} else if outputs == 1 {
|
||||||
|
payments.push(Payment {
|
||||||
|
address: Address::new(
|
||||||
|
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
|
||||||
|
.unwrap()
|
||||||
|
.legacy_address(MoneroNetwork::Mainnet),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
balance: Balance { coin: Coin::Monero, amount: Amount(0) },
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let payments = payments
|
||||||
|
.into_iter()
|
||||||
|
.map(|payment| (payment.address.into(), payment.balance.amount.0))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
match MSignableTransaction::new(
|
||||||
|
rct_type,
|
||||||
|
// Use the plan ID as the outgoing view key
|
||||||
|
Zeroizing::new(*plan_id),
|
||||||
|
inputs_actual,
|
||||||
|
payments,
|
||||||
|
Change::fingerprintable(change.as_ref().map(|change| change.clone().into())),
|
||||||
|
vec![],
|
||||||
|
fee_rate,
|
||||||
|
) {
|
||||||
|
Ok(signable) => Ok(Some({
|
||||||
|
if calculating_fee {
|
||||||
|
MakeSignableTransactionResult::Fee(signable.necessary_fee())
|
||||||
|
} else {
|
||||||
|
MakeSignableTransactionResult::SignableTransaction(signable)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
Err(e) => match e {
|
||||||
|
SendError::UnsupportedRctType => {
|
||||||
|
panic!("trying to use an RctType unsupported by monero-wallet")
|
||||||
|
}
|
||||||
|
SendError::NoInputs |
|
||||||
|
SendError::InvalidDecoyQuantity |
|
||||||
|
SendError::NoOutputs |
|
||||||
|
SendError::TooManyOutputs |
|
||||||
|
SendError::NoChange |
|
||||||
|
SendError::TooMuchArbitraryData |
|
||||||
|
SendError::TooLargeTransaction |
|
||||||
|
SendError::WrongPrivateKey => {
|
||||||
|
panic!("created an invalid Monero transaction: {e}");
|
||||||
|
}
|
||||||
|
SendError::MultiplePaymentIds => {
|
||||||
|
panic!("multiple payment IDs despite not supporting integrated addresses");
|
||||||
|
}
|
||||||
|
SendError::NotEnoughFunds { inputs, outputs, necessary_fee } => {
|
||||||
|
log::debug!(
|
||||||
|
"Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, necessary_fee: {necessary_fee:?}",
|
||||||
|
inputs,
|
||||||
|
outputs
|
||||||
|
);
|
||||||
|
match necessary_fee {
|
||||||
|
Some(necessary_fee) => {
|
||||||
|
// If we're solely calculating the fee, return the fee this TX will cost
|
||||||
|
if calculating_fee {
|
||||||
|
Ok(Some(MakeSignableTransactionResult::Fee(necessary_fee)))
|
||||||
|
} else {
|
||||||
|
// If we're actually trying to make the TX, return None
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We didn't have enough funds to even cover the outputs
|
||||||
|
None => {
|
||||||
|
// Ensure we're not misinterpreting this
|
||||||
|
assert!(outputs > inputs);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SendError::MaliciousSerialization | SendError::ClsagError(_) | SendError::FrostError(_) => {
|
||||||
|
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
use ciphersuite::{Ciphersuite, Secp256k1};
|
use ciphersuite::{Ciphersuite, Secp256k1};
|
||||||
|
|
||||||
use bitcoin_serai::{
|
use bitcoin_serai::{
|
||||||
|
@ -186,3 +327,4 @@ impl TransactionPlanner<Rpc, ()> for Planner {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
||||||
|
*/
|
||||||
|
|
Loading…
Reference in a new issue