mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 11:39: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 = [
|
||||
"borsh",
|
||||
"ciphersuite",
|
||||
"curve25519-dalek",
|
||||
"dalek-ff-group",
|
||||
"dkg",
|
||||
"flexible-transcript",
|
||||
|
|
|
@ -249,7 +249,7 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
|||
/// While no implementors are directly provided, [monero-simple-request-rpc](
|
||||
/// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request
|
||||
/// ) 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.
|
||||
///
|
||||
/// 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`
|
||||
/// 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.
|
||||
pub trait DecoyRpc: Sync + Clone + Debug {
|
||||
pub trait DecoyRpc: Sync {
|
||||
/// 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.
|
||||
fn get_output_distribution_end_height(
|
||||
&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"] }
|
||||
|
||||
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"] }
|
||||
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "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())
|
||||
}
|
||||
|
||||
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)]
|
||||
fn test_view_pair() -> ViewPair {
|
||||
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
|
||||
|
|
|
@ -15,6 +15,8 @@ mod key_gen;
|
|||
use crate::key_gen::KeyGenParams;
|
||||
mod rpc;
|
||||
use rpc::Rpc;
|
||||
|
||||
mod decoys;
|
||||
/*
|
||||
mod scheduler;
|
||||
use scheduler::Scheduler;
|
||||
|
|
|
@ -5,6 +5,7 @@ use monero_simple_request_rpc::SimpleRequestRpc;
|
|||
|
||||
use serai_client::primitives::{NetworkId, Coin, Amount};
|
||||
|
||||
use serai_db::Db;
|
||||
use scanner::ScannerFeed;
|
||||
use signers::TransactionPublisher;
|
||||
|
||||
|
@ -14,11 +15,12 @@ use crate::{
|
|||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Rpc {
|
||||
pub(crate) struct Rpc<D: Db> {
|
||||
pub(crate) db: D,
|
||||
pub(crate) rpc: SimpleRequestRpc,
|
||||
}
|
||||
|
||||
impl ScannerFeed for Rpc {
|
||||
impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
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
|
||||
|
@ -37,16 +39,15 @@ impl ScannerFeed for Rpc {
|
|||
&self,
|
||||
) -> impl Send + Future<Output = Result<u64, Self::EphemeralError>> {
|
||||
async move {
|
||||
Ok(
|
||||
self
|
||||
.rpc
|
||||
.get_height()
|
||||
.await?
|
||||
.checked_sub(1)
|
||||
.expect("connected to an invalid Monero RPC")
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
// 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +128,7 @@ impl ScannerFeed for Rpc {
|
|||
}
|
||||
}
|
||||
|
||||
impl TransactionPublisher<Transaction> for Rpc {
|
||||
impl<D: Db> TransactionPublisher<Transaction> for Rpc<D> {
|
||||
type EphemeralError = RpcError;
|
||||
|
||||
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 bitcoin_serai::{
|
||||
|
@ -186,3 +327,4 @@ impl TransactionPlanner<Rpc, ()> for Planner {
|
|||
}
|
||||
|
||||
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue