Monero Planner

Finishes the Monero processor.
This commit is contained in:
Luke Parker 2024-09-14 04:24:48 -04:00
parent e23176deeb
commit 0616085109
13 changed files with 406 additions and 755 deletions

1
Cargo.lock generated
View file

@ -8525,6 +8525,7 @@ dependencies = [
"monero-simple-request-rpc", "monero-simple-request-rpc",
"monero-wallet", "monero-wallet",
"parity-scale-codec", "parity-scale-codec",
"rand_chacha",
"rand_core", "rand_core",
"serai-client", "serai-client",
"serai-db", "serai-db",

View file

@ -223,15 +223,6 @@ impl Network for Bitcoin {
self.rpc.get_block_number(id).await.unwrap() self.rpc.get_block_number(id).await.unwrap()
} }
#[cfg(test)]
async fn check_eventuality_by_claim(
&self,
eventuality: &Self::Eventuality,
_: &EmptyClaim,
) -> bool {
self.rpc.get_transaction(&eventuality.0).await.is_ok()
}
#[cfg(test)] #[cfg(test)]
async fn get_transaction_by_eventuality(&self, _: usize, id: &Eventuality) -> Transaction { async fn get_transaction_by_eventuality(&self, _: usize, id: &Eventuality) -> Transaction {
self.rpc.get_transaction(&id.0).await.unwrap() self.rpc.get_transaction(&id.0).await.unwrap()

View file

@ -49,7 +49,7 @@ impl scheduler::Transaction for Transaction {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct SignableTransaction { pub(crate) struct SignableTransaction {
pub(crate) inputs: Vec<ReceivedOutput>, pub(crate) inputs: Vec<ReceivedOutput>,
pub(crate) payments: Vec<(Address, u64)>, pub(crate) payments: Vec<(ScriptBuf, u64)>,
pub(crate) change: Option<Address>, pub(crate) change: Option<Address>,
pub(crate) fee_per_vbyte: u64, pub(crate) fee_per_vbyte: u64,
} }
@ -58,12 +58,7 @@ impl SignableTransaction {
fn signable(self) -> Result<BSignableTransaction, TransactionError> { fn signable(self) -> Result<BSignableTransaction, TransactionError> {
BSignableTransaction::new( BSignableTransaction::new(
self.inputs, self.inputs,
&self &self.payments,
.payments
.iter()
.cloned()
.map(|(address, amount)| (ScriptBuf::from(address), amount))
.collect::<Vec<_>>(),
self.change.map(ScriptBuf::from), self.change.map(ScriptBuf::from),
None, None,
self.fee_per_vbyte, self.fee_per_vbyte,
@ -108,11 +103,19 @@ impl scheduler::SignableTransaction for SignableTransaction {
inputs inputs
}; };
let payments = <_>::deserialize_reader(reader)?; let payments = Vec::<(Vec<u8>, u64)>::deserialize_reader(reader)?;
let change = <_>::deserialize_reader(reader)?; let change = <_>::deserialize_reader(reader)?;
let fee_per_vbyte = <_>::deserialize_reader(reader)?; let fee_per_vbyte = <_>::deserialize_reader(reader)?;
Ok(Self { inputs, payments, change, fee_per_vbyte }) Ok(Self {
inputs,
payments: payments
.into_iter()
.map(|(address, amount)| (ScriptBuf::from_bytes(address), amount))
.collect(),
change,
fee_per_vbyte,
})
} }
fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(&u32::try_from(self.inputs.len()).unwrap().to_le_bytes())?; writer.write_all(&u32::try_from(self.inputs.len()).unwrap().to_le_bytes())?;
@ -120,7 +123,9 @@ impl scheduler::SignableTransaction for SignableTransaction {
input.write(writer)?; input.write(writer)?;
} }
self.payments.serialize(writer)?; for payment in &self.payments {
(payment.0.as_script().as_bytes(), payment.1).serialize(writer)?;
}
self.change.serialize(writer)?; self.change.serialize(writer)?;
self.fee_per_vbyte.serialize(writer)?; self.fee_per_vbyte.serialize(writer)?;

View file

@ -35,7 +35,7 @@ fn address_from_serai_key(key: <Secp256k1 as Ciphersuite>::G, kind: OutputType)
} }
fn signable_transaction<D: Db>( fn signable_transaction<D: Db>(
fee_per_vbyte: u64, _reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>, inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>, payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>, change: Option<KeyFor<Rpc<D>>>,
@ -49,12 +49,15 @@ fn signable_transaction<D: Db>(
<Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_OUTPUTS <Planner as TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>>>::MAX_OUTPUTS
); );
// TODO
let fee_per_vbyte = 1;
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>(); let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
let mut payments = payments let mut payments = payments
.into_iter() .into_iter()
.map(|payment| { .map(|payment| {
(payment.address().clone(), { (ScriptBuf::from(payment.address().clone()), {
let balance = payment.balance(); let balance = payment.balance();
assert_eq!(balance.coin, Coin::Bitcoin); assert_eq!(balance.coin, Coin::Bitcoin);
balance.amount.0 balance.amount.0
@ -68,7 +71,7 @@ fn signable_transaction<D: Db>(
*/ */
payments.push(( payments.push((
// The generator is even so this is valid // The generator is even so this is valid
Address::new(p2tr_script_buf(<Secp256k1 as Ciphersuite>::G::GENERATOR).unwrap()).unwrap(), p2tr_script_buf(<Secp256k1 as Ciphersuite>::G::GENERATOR).unwrap(),
// This uses the minimum output value allowed, as defined as a constant in bitcoin-serai // This uses the minimum output value allowed, as defined as a constant in bitcoin-serai
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust` // TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
bitcoin_serai::wallet::DUST, bitcoin_serai::wallet::DUST,
@ -79,11 +82,7 @@ fn signable_transaction<D: Db>(
BSignableTransaction::new( BSignableTransaction::new(
inputs.clone(), inputs.clone(),
&payments &payments,
.iter()
.cloned()
.map(|(address, amount)| (ScriptBuf::from(address), amount))
.collect::<Vec<_>>(),
change.clone().map(ScriptBuf::from), change.clone().map(ScriptBuf::from),
None, None,
fee_per_vbyte, fee_per_vbyte,
@ -95,7 +94,6 @@ fn signable_transaction<D: Db>(
pub(crate) struct Planner; pub(crate) struct Planner;
impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner { impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner {
type EphemeralError = (); type EphemeralError = ();
type FeeRate = u64;
type SignableTransaction = SignableTransaction; type SignableTransaction = SignableTransaction;
@ -119,12 +117,6 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
// to unstick any transactions which had too low of a fee. // to unstick any transactions which had too low of a fee.
const MAX_OUTPUTS: usize = 519; const MAX_OUTPUTS: usize = 519;
fn fee_rate(block: &BlockFor<Rpc<D>>, coin: Coin) -> Self::FeeRate {
assert_eq!(coin, Coin::Bitcoin);
// TODO
1
}
fn branch_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> { fn branch_address(key: KeyFor<Rpc<D>>) -> AddressFor<Rpc<D>> {
address_from_serai_key(key, OutputType::Branch) address_from_serai_key(key, OutputType::Branch)
} }
@ -136,12 +128,14 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
} }
fn calculate_fee( fn calculate_fee(
fee_rate: Self::FeeRate, &self,
reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>, inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>, payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>, change: Option<KeyFor<Rpc<D>>>,
) -> Amount { ) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
match signable_transaction::<D>(fee_rate, inputs, payments, change) { async move {
Ok(match signable_transaction::<D>(reference_block, inputs, payments, change) {
Ok(tx) => Amount(tx.1.needed_fee()), Ok(tx) => Amount(tx.1.needed_fee()),
Err( Err(
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment, TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
@ -153,12 +147,13 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
TransactionError::TooLargeTransaction, TransactionError::TooLargeTransaction,
) => unreachable!(), ) => unreachable!(),
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee), Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
})
} }
} }
fn plan( fn plan(
&self, &self,
fee_rate: Self::FeeRate, reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>, inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>, payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>, change: Option<KeyFor<Rpc<D>>>,
@ -176,7 +171,7 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
} }
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id()); let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) { match signable_transaction::<D>(reference_block, inputs.clone(), payments, change) {
Ok(tx) => Ok(PlannedTransaction { Ok(tx) => Ok(PlannedTransaction {
signable: tx.0, signable: tx.0,
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output }, eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },

View file

@ -18,6 +18,7 @@ workspace = true
[dependencies] [dependencies]
rand_core = { version = "0.6", default-features = false } rand_core = { version = "0.6", default-features = false }
rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
zeroize = { version = "1", default-features = false, features = ["std"] } zeroize = { version = "1", default-features = false, features = ["std"] }
hex = { version = "0.4", default-features = false, features = ["std"] } hex = { version = "0.4", default-features = false, features = ["std"] }

View file

@ -1,319 +0,0 @@
/*
// TODO: Consider ([u8; 32], TransactionPruned)
#[async_trait]
impl TransactionTrait<Monero> for Transaction {
type Id = [u8; 32];
fn id(&self) -> Self::Id {
self.hash()
}
#[cfg(test)]
async fn fee(&self, _: &Monero) -> u64 {
match self {
Transaction::V1 { .. } => panic!("v1 TX in test-only function"),
Transaction::V2 { ref proofs, .. } => proofs.as_ref().unwrap().base.fee,
}
}
}
impl EventualityTrait for Eventuality {
type Claim = [u8; 32];
type Completion = Transaction;
// Use the TX extra to look up potential matches
// While anyone can forge this, a transaction with distinct outputs won't actually match
// Extra includess the one time keys which are derived from the plan ID, so a collision here is a
// hash collision
fn lookup(&self) -> Vec<u8> {
self.extra()
}
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
Eventuality::read(reader)
}
fn serialize(&self) -> Vec<u8> {
self.serialize()
}
fn claim(tx: &Transaction) -> [u8; 32] {
tx.id()
}
fn serialize_completion(completion: &Transaction) -> Vec<u8> {
completion.serialize()
}
fn read_completion<R: io::Read>(reader: &mut R) -> io::Result<Transaction> {
Transaction::read(reader)
}
}
#[derive(Clone, Debug)]
pub struct SignableTransaction(MSignableTransaction);
impl SignableTransactionTrait for SignableTransaction {
fn fee(&self) -> u64 {
self.0.necessary_fee()
}
}
enum MakeSignableTransactionResult {
Fee(u64),
SignableTransaction(MSignableTransaction),
}
impl Monero {
pub async fn new(url: String) -> Monero {
let mut res = SimpleRequestRpc::new(url.clone()).await;
while let Err(e) = res {
log::error!("couldn't connect to Monero node: {e:?}");
tokio::time::sleep(Duration::from_secs(5)).await;
res = SimpleRequestRpc::new(url.clone()).await;
}
Monero { rpc: res.unwrap() }
}
fn view_pair(spend: EdwardsPoint) -> GuaranteedViewPair {
GuaranteedViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0)).unwrap()
}
fn address_internal(spend: EdwardsPoint, subaddress: Option<SubaddressIndex>) -> Address {
Address::new(Self::view_pair(spend).address(MoneroNetwork::Mainnet, subaddress, None)).unwrap()
}
fn scanner(spend: EdwardsPoint) -> GuaranteedScanner {
let mut scanner = GuaranteedScanner::new(Self::view_pair(spend));
debug_assert!(EXTERNAL_SUBADDRESS.is_none());
scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap());
scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap());
scanner.register_subaddress(FORWARD_SUBADDRESS.unwrap());
scanner
}
async fn median_fee(&self, block: &Block) -> Result<FeeRate, NetworkError> {
let mut fees = vec![];
for tx_hash in &block.transactions {
let tx =
self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?;
// Only consider fees from RCT transactions, else the fee property read wouldn't be accurate
let fee = match &tx {
Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
_ => continue,
};
fees.push(fee / u64::try_from(tx.weight()).unwrap());
}
fees.sort();
let fee = fees.get(fees.len() / 2).copied().unwrap_or(0);
// TODO: Set a sane minimum fee
const MINIMUM_FEE: u64 = 1_500_000;
Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap())
}
#[cfg(test)]
fn test_view_pair() -> ViewPair {
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
}
#[cfg(test)]
fn test_scanner() -> Scanner {
Scanner::new(Self::test_view_pair())
}
#[cfg(test)]
fn test_address() -> Address {
Address::new(Self::test_view_pair().legacy_address(MoneroNetwork::Mainnet)).unwrap()
}
}
#[async_trait]
impl Network for Monero {
const NETWORK: NetworkId = NetworkId::Monero;
const ID: &'static str = "Monero";
const ESTIMATED_BLOCK_TIME_IN_SECONDS: usize = 120;
const CONFIRMATIONS: usize = 10;
// TODO
const COST_TO_AGGREGATE: u64 = 0;
#[cfg(test)]
async fn external_address(&self, key: EdwardsPoint) -> Address {
Self::address_internal(key, EXTERNAL_SUBADDRESS)
}
fn branch_address(key: EdwardsPoint) -> Option<Address> {
Some(Self::address_internal(key, BRANCH_SUBADDRESS))
}
fn change_address(key: EdwardsPoint) -> Option<Address> {
Some(Self::address_internal(key, CHANGE_SUBADDRESS))
}
fn forward_address(key: EdwardsPoint) -> Option<Address> {
Some(Self::address_internal(key, FORWARD_SUBADDRESS))
}
async fn needed_fee(
&self,
block_number: usize,
inputs: &[Output],
payments: &[Payment<Self>],
change: &Option<Address>,
) -> Result<Option<u64>, NetworkError> {
let res = self
.make_signable_transaction(block_number, &[0; 32], inputs, payments, change, true)
.await?;
let Some(res) = res else { return Ok(None) };
let MakeSignableTransactionResult::Fee(fee) = res else {
panic!("told make_signable_transaction calculating_fee and got transaction")
};
Ok(Some(fee))
}
async fn signable_transaction(
&self,
block_number: usize,
plan_id: &[u8; 32],
_key: EdwardsPoint,
inputs: &[Output],
payments: &[Payment<Self>],
change: &Option<Address>,
(): &(),
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
let res = self
.make_signable_transaction(block_number, plan_id, inputs, payments, change, false)
.await?;
let Some(res) = res else { return Ok(None) };
let MakeSignableTransactionResult::SignableTransaction(signable) = res else {
panic!("told make_signable_transaction not calculating_fee and got fee")
};
let signable = SignableTransaction(signable);
let eventuality = signable.0.clone().into();
Ok(Some((signable, eventuality)))
}
async fn attempt_sign(
&self,
keys: ThresholdKeys<Self::Curve>,
transaction: SignableTransaction,
) -> Result<Self::TransactionMachine, NetworkError> {
match transaction.0.clone().multisig(keys) {
Ok(machine) => Ok(machine),
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
}
}
async fn publish_completion(&self, tx: &Transaction) -> Result<(), NetworkError> {
match self.rpc.publish_transaction(tx).await {
Ok(()) => Ok(()),
Err(RpcError::ConnectionError(e)) => {
log::debug!("Monero ConnectionError: {e}");
Err(NetworkError::ConnectionError)?
}
// TODO: Distinguish already in pool vs double spend (other signing attempt succeeded) vs
// invalid transaction
Err(e) => panic!("failed to publish TX {}: {e}", hex::encode(tx.hash())),
}
}
#[cfg(test)]
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
self.rpc.get_block(*id).await.unwrap().number().unwrap()
}
#[cfg(test)]
async fn check_eventuality_by_claim(
&self,
eventuality: &Self::Eventuality,
claim: &[u8; 32],
) -> bool {
return eventuality.matches(&self.rpc.get_pruned_transaction(*claim).await.unwrap());
}
#[cfg(test)]
async fn get_transaction_by_eventuality(
&self,
block: usize,
eventuality: &Eventuality,
) -> Transaction {
let block = self.rpc.get_block_by_number(block).await.unwrap();
for tx in &block.transactions {
let tx = self.rpc.get_transaction(*tx).await.unwrap();
if eventuality.matches(&tx.clone().into()) {
return tx;
}
}
panic!("block didn't have a transaction for this eventuality")
}
#[cfg(test)]
async fn mine_block(&self) {
// https://github.com/serai-dex/serai/issues/198
sleep(std::time::Duration::from_millis(100)).await;
self.rpc.generate_blocks(&Self::test_address().into(), 1).await.unwrap();
}
#[cfg(test)]
async fn test_send(&self, address: Address) -> Block {
use zeroize::Zeroizing;
use rand_core::{RngCore, OsRng};
use monero_wallet::rpc::FeePriority;
let new_block = self.get_latest_block_number().await.unwrap() + 1;
for _ in 0 .. 80 {
self.mine_block().await;
}
let new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
let mut outputs = Self::test_scanner()
.scan(self.rpc.get_scannable_block(new_block.clone()).await.unwrap())
.unwrap()
.ignore_additional_timelock();
let output = outputs.swap_remove(0);
let amount = output.commitment().amount;
// The dust should always be sufficient for the fee
let fee = Monero::DUST;
let rct_type = match new_block.header.hardfork_version {
14 => RctType::ClsagBulletproof,
15 | 16 => RctType::ClsagBulletproofPlus,
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
};
let output = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng,
&self.rpc,
match rct_type {
RctType::ClsagBulletproof => 11,
RctType::ClsagBulletproofPlus => 16,
_ => panic!("selecting decoys for an unsupported RctType"),
},
self.rpc.get_height().await.unwrap(),
output,
)
.await
.unwrap();
let mut outgoing_view_key = Zeroizing::new([0; 32]);
OsRng.fill_bytes(outgoing_view_key.as_mut());
let tx = MSignableTransaction::new(
rct_type,
outgoing_view_key,
vec![output],
vec![(address.into(), amount - fee)],
Change::fingerprintable(Some(Self::test_address().into())),
vec![],
self.rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
)
.unwrap()
.sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0))
.unwrap();
let block = self.get_latest_block_number().await.unwrap() + 1;
self.rpc.publish_transaction(&tx).await.unwrap();
for _ in 0 .. 10 {
self.mine_block().await;
}
self.get_block(block).await.unwrap()
}
}
*/

View file

@ -41,3 +41,149 @@ async fn main() {
) )
.await; .await;
} }
/*
#[async_trait]
impl TransactionTrait<Monero> for Transaction {
#[cfg(test)]
async fn fee(&self, _: &Monero) -> u64 {
match self {
Transaction::V1 { .. } => panic!("v1 TX in test-only function"),
Transaction::V2 { ref proofs, .. } => proofs.as_ref().unwrap().base.fee,
}
}
}
impl Monero {
async fn median_fee(&self, block: &Block) -> Result<FeeRate, NetworkError> {
let mut fees = vec![];
for tx_hash in &block.transactions {
let tx =
self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?;
// Only consider fees from RCT transactions, else the fee property read wouldn't be accurate
let fee = match &tx {
Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee,
_ => continue,
};
fees.push(fee / u64::try_from(tx.weight()).unwrap());
}
fees.sort();
let fee = fees.get(fees.len() / 2).copied().unwrap_or(0);
// TODO: Set a sane minimum fee
const MINIMUM_FEE: u64 = 1_500_000;
Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap())
}
#[cfg(test)]
fn test_view_pair() -> ViewPair {
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
}
#[cfg(test)]
fn test_scanner() -> Scanner {
Scanner::new(Self::test_view_pair())
}
#[cfg(test)]
fn test_address() -> Address {
Address::new(Self::test_view_pair().legacy_address(MoneroNetwork::Mainnet)).unwrap()
}
}
#[async_trait]
impl Network for Monero {
#[cfg(test)]
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
self.rpc.get_block(*id).await.unwrap().number().unwrap()
}
#[cfg(test)]
async fn get_transaction_by_eventuality(
&self,
block: usize,
eventuality: &Eventuality,
) -> Transaction {
let block = self.rpc.get_block_by_number(block).await.unwrap();
for tx in &block.transactions {
let tx = self.rpc.get_transaction(*tx).await.unwrap();
if eventuality.matches(&tx.clone().into()) {
return tx;
}
}
panic!("block didn't have a transaction for this eventuality")
}
#[cfg(test)]
async fn mine_block(&self) {
// https://github.com/serai-dex/serai/issues/198
sleep(std::time::Duration::from_millis(100)).await;
self.rpc.generate_blocks(&Self::test_address().into(), 1).await.unwrap();
}
#[cfg(test)]
async fn test_send(&self, address: Address) -> Block {
use zeroize::Zeroizing;
use rand_core::{RngCore, OsRng};
use monero_wallet::rpc::FeePriority;
let new_block = self.get_latest_block_number().await.unwrap() + 1;
for _ in 0 .. 80 {
self.mine_block().await;
}
let new_block = self.rpc.get_block_by_number(new_block).await.unwrap();
let mut outputs = Self::test_scanner()
.scan(self.rpc.get_scannable_block(new_block.clone()).await.unwrap())
.unwrap()
.ignore_additional_timelock();
let output = outputs.swap_remove(0);
let amount = output.commitment().amount;
// The dust should always be sufficient for the fee
let fee = Monero::DUST;
let rct_type = match new_block.header.hardfork_version {
14 => RctType::ClsagBulletproof,
15 | 16 => RctType::ClsagBulletproofPlus,
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
};
let output = OutputWithDecoys::fingerprintable_deterministic_new(
&mut OsRng,
&self.rpc,
match rct_type {
RctType::ClsagBulletproof => 11,
RctType::ClsagBulletproofPlus => 16,
_ => panic!("selecting decoys for an unsupported RctType"),
},
self.rpc.get_height().await.unwrap(),
output,
)
.await
.unwrap();
let mut outgoing_view_key = Zeroizing::new([0; 32]);
OsRng.fill_bytes(outgoing_view_key.as_mut());
let tx = MSignableTransaction::new(
rct_type,
outgoing_view_key,
vec![output],
vec![(address.into(), amount - fee)],
Change::fingerprintable(Some(Self::test_address().into())),
vec![],
self.rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
)
.unwrap()
.sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0))
.unwrap();
let block = self.get_latest_block_number().await.unwrap() + 1;
self.rpc.publish_transaction(&tx).await.unwrap();
for _ in 0 .. 10 {
self.mine_block().await;
}
self.get_block(block).await.unwrap()
}
}
*/

View file

@ -34,13 +34,6 @@ impl AsMut<[u8]> for OutputId {
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Output(pub(crate) WalletOutput); pub(crate) struct Output(pub(crate) WalletOutput);
impl Output {
pub(crate) fn new(output: WalletOutput) -> Self {
Self(output)
}
}
impl ReceivedOutput<<Ed25519 as Ciphersuite>::G, Address> for Output { impl ReceivedOutput<<Ed25519 as Ciphersuite>::G, Address> for Output {
type Id = OutputId; type Id = OutputId;
type TransactionId = [u8; 32]; type TransactionId = [u8; 32];

View file

@ -34,8 +34,8 @@ impl scheduler::Transaction for Transaction {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct SignableTransaction { pub(crate) struct SignableTransaction {
id: [u8; 32], pub(crate) id: [u8; 32],
signable: MSignableTransaction, pub(crate) signable: MSignableTransaction,
} }
#[derive(Clone)] #[derive(Clone)]
@ -81,8 +81,8 @@ impl scheduler::SignableTransaction for SignableTransaction {
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Eventuality { pub(crate) struct Eventuality {
id: [u8; 32], pub(crate) id: [u8; 32],
singular_spent_output: Option<OutputId>, pub(crate) singular_spent_output: Option<OutputId>,
pub(crate) eventuality: MEventuality, pub(crate) eventuality: MEventuality,
} }

View file

@ -1,146 +1,9 @@
/*
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 core::future::Future; use core::future::Future;
use zeroize::Zeroizing;
use rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use ciphersuite::{Ciphersuite, Ed25519}; use ciphersuite::{Ciphersuite, Ed25519};
use monero_wallet::rpc::{FeeRate, RpcError}; use monero_wallet::rpc::{FeeRate, RpcError};
@ -154,11 +17,17 @@ use primitives::{OutputType, ReceivedOutput, Payment};
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor}; use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
use utxo_scheduler::{PlannedTransaction, TransactionPlanner}; use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
use monero_wallet::address::Network; use monero_wallet::{
ringct::RctType,
address::{Network, AddressType, MoneroAddress},
OutputWithDecoys,
send::{
Change, SendError, SignableTransaction as MSignableTransaction, Eventuality as MEventuality,
},
};
use crate::{ use crate::{
EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, view_pair, EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, view_pair,
output::Output,
transaction::{SignableTransaction, Eventuality}, transaction::{SignableTransaction, Eventuality},
rpc::Rpc, rpc::Rpc,
}; };
@ -179,13 +48,108 @@ fn address_from_serai_key(key: <Ed25519 as Ciphersuite>::G, kind: OutputType) ->
.expect("created address which wasn't representable") .expect("created address which wasn't representable")
} }
async fn signable_transaction(
rpc: &Rpc,
reference_block: &BlockFor<Rpc>,
inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>,
) -> Result<Result<(SignableTransaction, MSignableTransaction), SendError>, RpcError> {
assert!(inputs.len() < <Planner as TransactionPlanner<Rpc, ()>>::MAX_INPUTS);
assert!(
(payments.len() + usize::from(u8::from(change.is_some()))) <
<Planner as TransactionPlanner<Rpc, ()>>::MAX_OUTPUTS
);
// TODO: Set a sane minimum fee
const MINIMUM_FEE: u64 = 1_500_000;
// TODO: Set a fee rate based on the reference block
let fee_rate = FeeRate::new(MINIMUM_FEE, 10000).unwrap();
// Determine the RCT proofs to make based off the hard fork
let rct_type = match reference_block.0.block.header.hardfork_version {
14 => RctType::ClsagBulletproof,
15 | 16 => RctType::ClsagBulletproofPlus,
_ => panic!("Monero hard forked and the processor wasn't updated for it"),
};
// We need a unique ID to distinguish this transaction from another transaction with an identical
// set of payments (as our Eventualities only match over the payments). The output's ID is
// guaranteed to be unique, making it satisfactory
let id = inputs.first().unwrap().id().0;
let mut inputs_actual = Vec::with_capacity(inputs.len());
for input in inputs {
inputs_actual.push(
OutputWithDecoys::fingerprintable_deterministic_new(
// We need a deterministic RNG here with *some* seed
// The unique ID means we don't pick some static seed
// It is a public value, yet that's fine as this is assumed fully transparent
// It is a reused value (with later code), but that's not an issue. Just an oddity
&mut ChaCha20Rng::from_seed(id),
&rpc.rpc,
// TODO: Have Decoys take RctType
match rct_type {
RctType::ClsagBulletproof => 11,
RctType::ClsagBulletproofPlus => 16,
_ => panic!("selecting decoys for an unsupported RctType"),
},
reference_block.0.block.number().unwrap() + 1,
input.0.clone(),
)
.await?,
);
}
let inputs = inputs_actual;
let mut payments = payments
.into_iter()
.map(|payment| {
(MoneroAddress::from(*payment.address()), {
let balance = payment.balance();
assert_eq!(balance.coin, Coin::Monero);
balance.amount.0
})
})
.collect::<Vec<_>>();
if (payments.len() + usize::from(u8::from(change.is_some()))) == 1 {
// Monero requires at least two outputs, so add a dummy payment
payments.push((
MoneroAddress::new(
Network::Mainnet,
AddressType::Legacy,
<Ed25519 as Ciphersuite>::generator().0,
<Ed25519 as Ciphersuite>::generator().0,
),
0,
));
}
let change = if let Some(change) = change {
Change::guaranteed(view_pair(change), Some(CHANGE_SUBADDRESS))
} else {
Change::fingerprintable(None)
};
Ok(
MSignableTransaction::new(
rct_type,
Zeroizing::new(id),
inputs,
payments,
change,
vec![],
fee_rate,
)
.map(|signable| (SignableTransaction { id, signable: signable.clone() }, signable)),
)
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Planner(pub(crate) Rpc); pub(crate) struct Planner(pub(crate) Rpc);
impl TransactionPlanner<Rpc, ()> for Planner { impl TransactionPlanner<Rpc, ()> for Planner {
type EphemeralError = RpcError; type EphemeralError = RpcError;
type FeeRate = FeeRate;
type SignableTransaction = SignableTransaction; type SignableTransaction = SignableTransaction;
// wallet2 will not create a transaction larger than 100 KB, and Monero won't relay a transaction // wallet2 will not create a transaction larger than 100 KB, and Monero won't relay a transaction
@ -195,12 +159,6 @@ impl TransactionPlanner<Rpc, ()> for Planner {
const MAX_INPUTS: usize = 120; const MAX_INPUTS: usize = 120;
const MAX_OUTPUTS: usize = 16; const MAX_OUTPUTS: usize = 16;
fn fee_rate(block: &BlockFor<Rpc>, coin: Coin) -> Self::FeeRate {
assert_eq!(coin, Coin::Monero);
// TODO
todo!("TODO")
}
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> { fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
address_from_serai_key(key, OutputType::Branch) address_from_serai_key(key, OutputType::Branch)
} }
@ -212,218 +170,101 @@ impl TransactionPlanner<Rpc, ()> for Planner {
} }
fn calculate_fee( fn calculate_fee(
fee_rate: Self::FeeRate, &self,
reference_block: &BlockFor<Rpc>,
inputs: Vec<OutputFor<Rpc>>, inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>, payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>, change: Option<KeyFor<Rpc>>,
) -> Amount { ) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
todo!("TODO") async move {
Ok(match signable_transaction(&self.0, reference_block, inputs, payments, change).await? {
Ok(tx) => Amount(tx.1.necessary_fee()),
Err(SendError::NotEnoughFunds { necessary_fee, .. }) => {
Amount(necessary_fee.expect("outputs value exceeded inputs value"))
}
Err(SendError::UnsupportedRctType) => {
panic!("tried to use an RctType monero-wallet doesn't support")
}
Err(SendError::NoInputs | SendError::NoOutputs | SendError::TooManyOutputs) => {
panic!("malformed plan passed to calculate_fee")
}
Err(SendError::InvalidDecoyQuantity) => panic!("selected the wrong amount of decoys"),
Err(SendError::NoChange) => {
panic!("didn't add a dummy payment to satisfy the 2-output minimum")
}
Err(SendError::MultiplePaymentIds) => {
panic!("included multiple payment IDs despite not supporting addresses with payment IDs")
}
Err(SendError::TooMuchArbitraryData) => {
panic!("included too much arbitrary data despite not including any")
}
Err(SendError::TooLargeTransaction) => {
panic!("too large transaction despite MAX_INPUTS/MAX_OUTPUTS")
}
Err(
SendError::WrongPrivateKey |
SendError::MaliciousSerialization |
SendError::ClsagError(_) |
SendError::FrostError(_),
) => unreachable!("signing/serialization error when not signing/serializing"),
})
}
} }
fn plan( fn plan(
&self, &self,
fee_rate: Self::FeeRate, reference_block: &BlockFor<Rpc>,
inputs: Vec<OutputFor<Rpc>>, inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>, payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>, change: Option<KeyFor<Rpc>>,
) -> impl Send ) -> impl Send
+ Future<Output = Result<PlannedTransaction<Rpc, Self::SignableTransaction, ()>, RpcError>> + Future<Output = Result<PlannedTransaction<Rpc, Self::SignableTransaction, ()>, RpcError>>
{ {
async move { todo!("TODO") }
}
}
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
/*
use ciphersuite::{Ciphersuite, Ed25519};
use bitcoin_serai::{
bitcoin::ScriptBuf,
wallet::{TransactionError, SignableTransaction as BSignableTransaction, p2tr_script_buf},
};
use serai_client::{
primitives::{Coin, Amount},
networks::bitcoin::Address,
};
use serai_db::Db;
use primitives::{OutputType, ReceivedOutput, Payment};
use scanner::{KeyFor, AddressFor, OutputFor, BlockFor};
use utxo_scheduler::{PlannedTransaction, TransactionPlanner};
use crate::{
scan::{offsets_for_key, scanner},
output::Output,
transaction::{SignableTransaction, Eventuality},
rpc::Rpc,
};
fn address_from_serai_key(key: <Ed25519 as Ciphersuite>::G, kind: OutputType) -> Address {
let offset = <Ed25519 as Ciphersuite>::G::GENERATOR * offsets_for_key(key)[&kind];
Address::new(
p2tr_script_buf(key + offset)
.expect("creating address from Serai key which wasn't properly tweaked"),
)
.expect("couldn't create Serai-representable address for P2TR script")
}
fn signable_transaction<D: Db>(
fee_per_vbyte: u64,
inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>,
) -> Result<(SignableTransaction, BSignableTransaction), TransactionError> {
assert!(
inputs.len() <
<Planner as TransactionPlanner<Rpc, ()>>::MAX_INPUTS
);
assert!(
(payments.len() + usize::from(u8::from(change.is_some()))) <
<Planner as TransactionPlanner<Rpc, ()>>::MAX_OUTPUTS
);
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
let mut payments = payments
.into_iter()
.map(|payment| {
(payment.address().clone(), {
let balance = payment.balance();
assert_eq!(balance.coin, Coin::Monero);
balance.amount.0
})
})
.collect::<Vec<_>>();
/*
Push a payment to a key with a known private key which anyone can spend. If this transaction
gets stuck, this lets anyone create a child transaction spending this output, raising the fee,
getting the transaction unstuck (via CPFP).
*/
payments.push((
// The generator is even so this is valid
Address::new(p2tr_script_buf(<Ed25519 as Ciphersuite>::G::GENERATOR).unwrap()).unwrap(),
// This uses the minimum output value allowed, as defined as a constant in bitcoin-serai
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
bitcoin_serai::wallet::DUST,
));
let change = change
.map(<Planner as TransactionPlanner<Rpc, ()>>::change_address);
BSignableTransaction::new(
inputs.clone(),
&payments
.iter()
.cloned()
.map(|(address, amount)| (ScriptBuf::from(address), amount))
.collect::<Vec<_>>(),
change.clone().map(ScriptBuf::from),
None,
fee_per_vbyte,
)
.map(|bst| (SignableTransaction { inputs, payments, change, fee_per_vbyte }, bst))
}
pub(crate) struct Planner;
impl TransactionPlanner<Rpc, ()> for Planner {
type EphemeralError = RpcError;
type FeeRate = u64;
type SignableTransaction = SignableTransaction;
/*
Monero has a max weight of 400,000 (MAX_STANDARD_TX_WEIGHT).
A non-SegWit TX will have 4 weight units per byte, leaving a max size of 100,000 bytes. While
our inputs are entirely SegWit, such fine tuning is not necessary and could create issues in
the future (if the size decreases or we misevaluate it). It also offers a minimal amount of
benefit when we are able to logarithmically accumulate inputs/fulfill payments.
For 128-byte inputs (36-byte output specification, 64-byte signature, whatever overhead) and
64-byte outputs (40-byte script, 8-byte amount, whatever overhead), they together take up 192
bytes.
100,000 / 192 = 520
520 * 192 leaves 160 bytes of overhead for the transaction structure itself.
*/
const MAX_INPUTS: usize = 520;
// We always reserve one output to create an anyone-can-spend output enabling anyone to use CPFP
// to unstick any transactions which had too low of a fee.
const MAX_OUTPUTS: usize = 519;
fn fee_rate(block: &BlockFor<Rpc>, coin: Coin) -> Self::FeeRate {
assert_eq!(coin, Coin::Monero);
// TODO
1
}
fn branch_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
address_from_serai_key(key, OutputType::Branch)
}
fn change_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
address_from_serai_key(key, OutputType::Change)
}
fn forwarding_address(key: KeyFor<Rpc>) -> AddressFor<Rpc> {
address_from_serai_key(key, OutputType::Forwarded)
}
fn calculate_fee(
fee_rate: Self::FeeRate,
inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>,
) -> Amount {
match signable_transaction::<D>(fee_rate, inputs, payments, change) {
Ok(tx) => Amount(tx.1.needed_fee()),
Err(
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
) => panic!("malformed arguments to calculate_fee"),
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
Err(
TransactionError::TooMuchData |
TransactionError::TooLowFee |
TransactionError::TooLargeTransaction,
) => unreachable!(),
Err(TransactionError::NotEnoughFunds { fee, .. }) => Amount(fee),
}
}
fn plan(
fee_rate: Self::FeeRate,
inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>,
) -> PlannedTransaction<Rpc, Self::SignableTransaction, ()> {
let key = inputs.first().unwrap().key();
for input in &inputs {
assert_eq!(key, input.key());
}
let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id()); let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id());
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) {
Ok(tx) => PlannedTransaction { async move {
Ok(match signable_transaction(&self.0, reference_block, inputs, payments, change).await? {
Ok(tx) => {
let id = tx.0.id;
PlannedTransaction {
signable: tx.0, signable: tx.0,
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output }, eventuality: Eventuality {
auxilliary: (), id,
singular_spent_output,
eventuality: MEventuality::from(tx.1),
}, },
Err( auxilliary: (),
TransactionError::NoInputs | TransactionError::NoOutputs | TransactionError::DustPayment,
) => panic!("malformed arguments to plan"),
// No data, we have a minimum fee rate, we checked the amount of inputs/outputs
Err(
TransactionError::TooMuchData |
TransactionError::TooLowFee |
TransactionError::TooLargeTransaction,
) => unreachable!(),
Err(TransactionError::NotEnoughFunds { .. }) => {
panic!("plan called for a transaction without enough funds")
} }
} }
Err(SendError::NotEnoughFunds { .. }) => panic!("failed to successfully amortize the fee"),
Err(SendError::UnsupportedRctType) => {
panic!("tried to use an RctType monero-wallet doesn't support")
}
Err(SendError::NoInputs | SendError::NoOutputs | SendError::TooManyOutputs) => {
panic!("malformed plan passed to calculate_fee")
}
Err(SendError::InvalidDecoyQuantity) => panic!("selected the wrong amount of decoys"),
Err(SendError::NoChange) => {
panic!("didn't add a dummy payment to satisfy the 2-output minimum")
}
Err(SendError::MultiplePaymentIds) => {
panic!("included multiple payment IDs despite not supporting addresses with payment IDs")
}
Err(SendError::TooMuchArbitraryData) => {
panic!("included too much arbitrary data despite not including any")
}
Err(SendError::TooLargeTransaction) => {
panic!("too large transaction despite MAX_INPUTS/MAX_OUTPUTS")
}
Err(
SendError::WrongPrivateKey |
SendError::MaliciousSerialization |
SendError::ClsagError(_) |
SendError::FrostError(_),
) => unreachable!("signing/serialization error when not signing/serializing"),
})
}
} }
} }
pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>; pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler<Rpc, Planner>;
*/

View file

@ -4,7 +4,7 @@
use core::{fmt::Debug, future::Future}; use core::{fmt::Debug, future::Future};
use serai_primitives::{Coin, Amount}; use serai_primitives::Amount;
use primitives::{ReceivedOutput, Payment}; use primitives::{ReceivedOutput, Payment};
use scanner::{ScannerFeed, KeyFor, AddressFor, OutputFor, EventualityFor, BlockFor}; use scanner::{ScannerFeed, KeyFor, AddressFor, OutputFor, EventualityFor, BlockFor};
@ -48,9 +48,6 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
/// resolve manual intervention/changing the arguments. /// resolve manual intervention/changing the arguments.
type EphemeralError: Debug; type EphemeralError: Debug;
/// The type representing a fee rate to use for transactions.
type FeeRate: Send + Clone + Copy;
/// The type representing a signable transaction. /// The type representing a signable transaction.
type SignableTransaction: SignableTransaction; type SignableTransaction: SignableTransaction;
@ -59,11 +56,6 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
/// The maximum amount of outputs allowed in a transaction, including the change output. /// The maximum amount of outputs allowed in a transaction, including the change output.
const MAX_OUTPUTS: usize; const MAX_OUTPUTS: usize;
/// Obtain the fee rate to pay.
///
/// This must be constant to the block and coin.
fn fee_rate(block: &BlockFor<S>, coin: Coin) -> Self::FeeRate;
/// The branch address for this key of Serai's. /// The branch address for this key of Serai's.
fn branch_address(key: KeyFor<S>) -> AddressFor<S>; fn branch_address(key: KeyFor<S>) -> AddressFor<S>;
/// The change address for this key of Serai's. /// The change address for this key of Serai's.
@ -76,11 +68,12 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
/// The fee rate, inputs, and payments, will all be for the same coin. The returned fee is /// The fee rate, inputs, and payments, will all be for the same coin. The returned fee is
/// denominated in this coin. /// denominated in this coin.
fn calculate_fee( fn calculate_fee(
fee_rate: Self::FeeRate, &self,
reference_block: &BlockFor<S>,
inputs: Vec<OutputFor<S>>, inputs: Vec<OutputFor<S>>,
payments: Vec<Payment<AddressFor<S>>>, payments: Vec<Payment<AddressFor<S>>>,
change: Option<KeyFor<S>>, change: Option<KeyFor<S>>,
) -> Amount; ) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>>;
/// Plan a transaction. /// Plan a transaction.
/// ///
@ -91,7 +84,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
/// output must be created. /// output must be created.
fn plan( fn plan(
&self, &self,
fee_rate: Self::FeeRate, reference_block: &BlockFor<S>,
inputs: Vec<OutputFor<S>>, inputs: Vec<OutputFor<S>>,
payments: Vec<Payment<AddressFor<S>>>, payments: Vec<Payment<AddressFor<S>>>,
change: Option<KeyFor<S>>, change: Option<KeyFor<S>>,
@ -112,7 +105,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
fn plan_transaction_with_fee_amortization( fn plan_transaction_with_fee_amortization(
&self, &self,
operating_costs: &mut u64, operating_costs: &mut u64,
fee_rate: Self::FeeRate, reference_block: &BlockFor<S>,
inputs: Vec<OutputFor<S>>, inputs: Vec<OutputFor<S>>,
mut payments: Vec<Payment<AddressFor<S>>>, mut payments: Vec<Payment<AddressFor<S>>>,
mut change: Option<KeyFor<S>>, mut change: Option<KeyFor<S>>,
@ -156,7 +149,8 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
// Sort payments from high amount to low amount // Sort payments from high amount to low amount
payments.sort_by(|a, b| a.balance().amount.0.cmp(&b.balance().amount.0).reverse()); payments.sort_by(|a, b| a.balance().amount.0.cmp(&b.balance().amount.0).reverse());
let mut fee = Self::calculate_fee(fee_rate, inputs.clone(), payments.clone(), change).0; let mut fee =
self.calculate_fee(reference_block, inputs.clone(), payments.clone(), change).await?.0;
let mut amortized = 0; let mut amortized = 0;
while !payments.is_empty() { while !payments.is_empty() {
// We need to pay the fee, and any accrued operating costs, minus what we've already // We need to pay the fee, and any accrued operating costs, minus what we've already
@ -176,7 +170,10 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
if payments.last().unwrap().balance().amount.0 <= (per_payment_fee + S::dust(coin).0) { if payments.last().unwrap().balance().amount.0 <= (per_payment_fee + S::dust(coin).0) {
amortized += payments.pop().unwrap().balance().amount.0; amortized += payments.pop().unwrap().balance().amount.0;
// Recalculate the fee and try again // Recalculate the fee and try again
fee = Self::calculate_fee(fee_rate, inputs.clone(), payments.clone(), change).0; fee = self
.calculate_fee(reference_block, inputs.clone(), payments.clone(), change)
.await?
.0;
continue; continue;
} }
// Break since all of these payments shouldn't be dropped // Break since all of these payments shouldn't be dropped
@ -237,7 +234,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
let has_change = change.is_some(); let has_change = change.is_some();
let PlannedTransaction { signable, eventuality, auxilliary } = let PlannedTransaction { signable, eventuality, auxilliary } =
self.plan(fee_rate, inputs, payments, change).await?; self.plan(reference_block, inputs, payments, change).await?;
Ok(Some(AmortizePlannedTransaction { Ok(Some(AmortizePlannedTransaction {
effected_payments, effected_payments,
has_change, has_change,

View file

@ -56,7 +56,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.planner .planner
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
&mut operating_costs, &mut operating_costs,
P::fee_rate(block, coin), block,
to_aggregate, to_aggregate,
vec![], vec![],
Some(key_for_change), Some(key_for_change),
@ -176,7 +176,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
// Uses 0 as there's no operating costs to incur/amortize here // Uses 0 as there's no operating costs to incur/amortize here
&mut 0, &mut 0,
P::fee_rate(block, coin), block,
vec![output], vec![output],
payments, payments,
None, None,
@ -254,7 +254,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.planner .planner
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
&mut operating_costs, &mut operating_costs,
P::fee_rate(block, coin), block,
outputs.clone(), outputs.clone(),
tree[0] tree[0]
.payments::<S>(coin, &branch_address, tree[0].value()) .payments::<S>(coin, &branch_address, tree[0].value())
@ -327,7 +327,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.planner .planner
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
&mut operating_costs, &mut operating_costs,
P::fee_rate(block, coin), block,
outputs, outputs,
vec![], vec![],
Some(to), Some(to),
@ -487,7 +487,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> for Schedul
// This uses 0 for the operating costs as we don't incur any here // This uses 0 for the operating costs as we don't incur any here
// If the output can't pay for itself to be forwarded, we simply drop it // If the output can't pay for itself to be forwarded, we simply drop it
&mut 0, &mut 0,
P::fee_rate(block, forward.balance().coin), block,
vec![forward.clone()], vec![forward.clone()],
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)], vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
None, None,
@ -508,7 +508,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> for Schedul
// This uses 0 for the operating costs as we don't incur any here // This uses 0 for the operating costs as we don't incur any here
// If the output can't pay for itself to be returned, we simply drop it // If the output can't pay for itself to be returned, we simply drop it
&mut 0, &mut 0,
P::fee_rate(block, out_instruction.balance().coin), block,
vec![to_return.output().clone()], vec![to_return.output().clone()],
vec![out_instruction], vec![out_instruction],
None, None,

View file

@ -86,7 +86,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
.planner .planner
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
&mut operating_costs, &mut operating_costs,
P::fee_rate(block, coin), block,
to_aggregate, to_aggregate,
vec![], vec![],
Some(key_for_change), Some(key_for_change),
@ -229,7 +229,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
.planner .planner
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
&mut operating_costs, &mut operating_costs,
P::fee_rate(block, coin), block,
outputs.clone(), outputs.clone(),
tree[0] tree[0]
.payments::<S>(coin, &branch_address, tree[0].value()) .payments::<S>(coin, &branch_address, tree[0].value())
@ -323,7 +323,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
// Uses 0 as there's no operating costs to incur/amortize here // Uses 0 as there's no operating costs to incur/amortize here
&mut 0, &mut 0,
P::fee_rate(block, coin), block,
vec![branch_output], vec![branch_output],
payments, payments,
None, None,
@ -379,7 +379,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
.planner .planner
.plan_transaction_with_fee_amortization( .plan_transaction_with_fee_amortization(
&mut operating_costs, &mut operating_costs,
P::fee_rate(block, coin), block,
outputs, outputs,
vec![], vec![],
Some(to), Some(to),
@ -505,7 +505,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
// This uses 0 for the operating costs as we don't incur any here // This uses 0 for the operating costs as we don't incur any here
// If the output can't pay for itself to be forwarded, we simply drop it // If the output can't pay for itself to be forwarded, we simply drop it
&mut 0, &mut 0,
P::fee_rate(block, forward.balance().coin), block,
vec![forward.clone()], vec![forward.clone()],
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)], vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
None, None,
@ -526,7 +526,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
// This uses 0 for the operating costs as we don't incur any here // This uses 0 for the operating costs as we don't incur any here
// If the output can't pay for itself to be returned, we simply drop it // If the output can't pay for itself to be returned, we simply drop it
&mut 0, &mut 0,
P::fee_rate(block, out_instruction.balance().coin), block,
vec![to_return.output().clone()], vec![to_return.output().clone()],
vec![out_instruction], vec![out_instruction],
None, None,