From 0616085109be26825ed68b8e86c327c52d9df6c5 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 14 Sep 2024 04:24:48 -0400 Subject: [PATCH] Monero Planner Finishes the Monero processor. --- Cargo.lock | 1 + processor/bitcoin/src/main.rs | 9 - .../bitcoin/src/primitives/transaction.rs | 25 +- processor/bitcoin/src/scheduler.rs | 57 +- processor/monero/Cargo.toml | 1 + processor/monero/src/lib.rs | 319 ----------- processor/monero/src/main.rs | 146 +++++ processor/monero/src/primitives/output.rs | 7 - .../monero/src/primitives/transaction.rs | 8 +- processor/monero/src/scheduler.rs | 535 ++++++------------ .../scheduler/utxo/primitives/src/lib.rs | 29 +- processor/scheduler/utxo/standard/src/lib.rs | 12 +- .../utxo/transaction-chaining/src/lib.rs | 12 +- 13 files changed, 406 insertions(+), 755 deletions(-) delete mode 100644 processor/monero/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9e34ea3c..c3e39a09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8525,6 +8525,7 @@ dependencies = [ "monero-simple-request-rpc", "monero-wallet", "parity-scale-codec", + "rand_chacha", "rand_core", "serai-client", "serai-db", diff --git a/processor/bitcoin/src/main.rs b/processor/bitcoin/src/main.rs index d029ad8b..f260c47c 100644 --- a/processor/bitcoin/src/main.rs +++ b/processor/bitcoin/src/main.rs @@ -223,15 +223,6 @@ impl Network for Bitcoin { 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)] async fn get_transaction_by_eventuality(&self, _: usize, id: &Eventuality) -> Transaction { self.rpc.get_transaction(&id.0).await.unwrap() diff --git a/processor/bitcoin/src/primitives/transaction.rs b/processor/bitcoin/src/primitives/transaction.rs index 5fca0b91..8e7a26f6 100644 --- a/processor/bitcoin/src/primitives/transaction.rs +++ b/processor/bitcoin/src/primitives/transaction.rs @@ -49,7 +49,7 @@ impl scheduler::Transaction for Transaction { #[derive(Clone, Debug)] pub(crate) struct SignableTransaction { pub(crate) inputs: Vec, - pub(crate) payments: Vec<(Address, u64)>, + pub(crate) payments: Vec<(ScriptBuf, u64)>, pub(crate) change: Option
, pub(crate) fee_per_vbyte: u64, } @@ -58,12 +58,7 @@ impl SignableTransaction { fn signable(self) -> Result { BSignableTransaction::new( self.inputs, - &self - .payments - .iter() - .cloned() - .map(|(address, amount)| (ScriptBuf::from(address), amount)) - .collect::>(), + &self.payments, self.change.map(ScriptBuf::from), None, self.fee_per_vbyte, @@ -108,11 +103,19 @@ impl scheduler::SignableTransaction for SignableTransaction { inputs }; - let payments = <_>::deserialize_reader(reader)?; + let payments = Vec::<(Vec, u64)>::deserialize_reader(reader)?; let change = <_>::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<()> { 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)?; } - self.payments.serialize(writer)?; + for payment in &self.payments { + (payment.0.as_script().as_bytes(), payment.1).serialize(writer)?; + } self.change.serialize(writer)?; self.fee_per_vbyte.serialize(writer)?; diff --git a/processor/bitcoin/src/scheduler.rs b/processor/bitcoin/src/scheduler.rs index b6554bda..08dc508c 100644 --- a/processor/bitcoin/src/scheduler.rs +++ b/processor/bitcoin/src/scheduler.rs @@ -35,7 +35,7 @@ fn address_from_serai_key(key: ::G, kind: OutputType) } fn signable_transaction( - fee_per_vbyte: u64, + _reference_block: &BlockFor>, inputs: Vec>>, payments: Vec>>>, change: Option>>, @@ -49,12 +49,15 @@ fn signable_transaction( , EffectedReceivedOutputs>>>::MAX_OUTPUTS ); + // TODO + let fee_per_vbyte = 1; + let inputs = inputs.into_iter().map(|input| input.output).collect::>(); let mut payments = payments .into_iter() .map(|payment| { - (payment.address().clone(), { + (ScriptBuf::from(payment.address().clone()), { let balance = payment.balance(); assert_eq!(balance.coin, Coin::Bitcoin); balance.amount.0 @@ -68,7 +71,7 @@ fn signable_transaction( */ payments.push(( // The generator is even so this is valid - Address::new(p2tr_script_buf(::G::GENERATOR).unwrap()).unwrap(), + p2tr_script_buf(::G::GENERATOR).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, @@ -79,11 +82,7 @@ fn signable_transaction( BSignableTransaction::new( inputs.clone(), - &payments - .iter() - .cloned() - .map(|(address, amount)| (ScriptBuf::from(address), amount)) - .collect::>(), + &payments, change.clone().map(ScriptBuf::from), None, fee_per_vbyte, @@ -95,7 +94,6 @@ fn signable_transaction( pub(crate) struct Planner; impl TransactionPlanner, EffectedReceivedOutputs>> for Planner { type EphemeralError = (); - type FeeRate = u64; type SignableTransaction = SignableTransaction; @@ -119,12 +117,6 @@ impl TransactionPlanner, EffectedReceivedOutputs>> for Plan // to unstick any transactions which had too low of a fee. const MAX_OUTPUTS: usize = 519; - fn fee_rate(block: &BlockFor>, coin: Coin) -> Self::FeeRate { - assert_eq!(coin, Coin::Bitcoin); - // TODO - 1 - } - fn branch_address(key: KeyFor>) -> AddressFor> { address_from_serai_key(key, OutputType::Branch) } @@ -136,29 +128,32 @@ impl TransactionPlanner, EffectedReceivedOutputs>> for Plan } fn calculate_fee( - fee_rate: Self::FeeRate, + &self, + reference_block: &BlockFor>, inputs: Vec>>, payments: Vec>>>, change: Option>>, - ) -> Amount { - match signable_transaction::(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), + ) -> impl Send + Future> { + async move { + Ok(match signable_transaction::(reference_block, 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( &self, - fee_rate: Self::FeeRate, + reference_block: &BlockFor>, inputs: Vec>>, payments: Vec>>>, change: Option>>, @@ -176,7 +171,7 @@ impl TransactionPlanner, EffectedReceivedOutputs>> for Plan } let singular_spent_output = (inputs.len() == 1).then(|| inputs[0].id()); - match signable_transaction::(fee_rate, inputs.clone(), payments, change) { + match signable_transaction::(reference_block, inputs.clone(), payments, change) { Ok(tx) => Ok(PlannedTransaction { signable: tx.0, eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output }, diff --git a/processor/monero/Cargo.toml b/processor/monero/Cargo.toml index 436f327e..cc895eda 100644 --- a/processor/monero/Cargo.toml +++ b/processor/monero/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] 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"] } hex = { version = "0.4", default-features = false, features = ["std"] } diff --git a/processor/monero/src/lib.rs b/processor/monero/src/lib.rs deleted file mode 100644 index 0848e08a..00000000 --- a/processor/monero/src/lib.rs +++ /dev/null @@ -1,319 +0,0 @@ -/* -// TODO: Consider ([u8; 32], TransactionPruned) -#[async_trait] -impl TransactionTrait 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 { - self.extra() - } - - fn read(reader: &mut R) -> io::Result { - Eventuality::read(reader) - } - fn serialize(&self) -> Vec { - self.serialize() - } - - fn claim(tx: &Transaction) -> [u8; 32] { - tx.id() - } - fn serialize_completion(completion: &Transaction) -> Vec { - completion.serialize() - } - fn read_completion(reader: &mut R) -> io::Result { - 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::(0).0)).unwrap() - } - - fn address_internal(spend: EdwardsPoint, subaddress: Option) -> 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 { - 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
{ - Some(Self::address_internal(key, BRANCH_SUBADDRESS)) - } - - fn change_address(key: EdwardsPoint) -> Option
{ - Some(Self::address_internal(key, CHANGE_SUBADDRESS)) - } - - fn forward_address(key: EdwardsPoint) -> Option
{ - Some(Self::address_internal(key, FORWARD_SUBADDRESS)) - } - - async fn needed_fee( - &self, - block_number: usize, - inputs: &[Output], - payments: &[Payment], - change: &Option
, - ) -> Result, 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], - change: &Option
, - (): &(), - ) -> Result, 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, - transaction: SignableTransaction, - ) -> Result { - 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() - } -} -*/ diff --git a/processor/monero/src/main.rs b/processor/monero/src/main.rs index daba3255..d36118d0 100644 --- a/processor/monero/src/main.rs +++ b/processor/monero/src/main.rs @@ -41,3 +41,149 @@ async fn main() { ) .await; } + +/* +#[async_trait] +impl TransactionTrait 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 { + 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() + } +} +*/ diff --git a/processor/monero/src/primitives/output.rs b/processor/monero/src/primitives/output.rs index fea042c8..201e75c9 100644 --- a/processor/monero/src/primitives/output.rs +++ b/processor/monero/src/primitives/output.rs @@ -34,13 +34,6 @@ impl AsMut<[u8]> for OutputId { #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) struct Output(pub(crate) WalletOutput); - -impl Output { - pub(crate) fn new(output: WalletOutput) -> Self { - Self(output) - } -} - impl ReceivedOutput<::G, Address> for Output { type Id = OutputId; type TransactionId = [u8; 32]; diff --git a/processor/monero/src/primitives/transaction.rs b/processor/monero/src/primitives/transaction.rs index f6765cd9..eeeef81d 100644 --- a/processor/monero/src/primitives/transaction.rs +++ b/processor/monero/src/primitives/transaction.rs @@ -34,8 +34,8 @@ impl scheduler::Transaction for Transaction { #[derive(Clone, Debug)] pub(crate) struct SignableTransaction { - id: [u8; 32], - signable: MSignableTransaction, + pub(crate) id: [u8; 32], + pub(crate) signable: MSignableTransaction, } #[derive(Clone)] @@ -81,8 +81,8 @@ impl scheduler::SignableTransaction for SignableTransaction { #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) struct Eventuality { - id: [u8; 32], - singular_spent_output: Option, + pub(crate) id: [u8; 32], + pub(crate) singular_spent_output: Option, pub(crate) eventuality: MEventuality, } diff --git a/processor/monero/src/scheduler.rs b/processor/monero/src/scheduler.rs index ef52c413..667840f6 100644 --- a/processor/monero/src/scheduler.rs +++ b/processor/monero/src/scheduler.rs @@ -1,146 +1,9 @@ -/* -async fn make_signable_transaction( -block_number: usize, -plan_id: &[u8; 32], -inputs: &[Output], -payments: &[Payment], -change: &Option
, -calculating_fee: bool, -) -> Result, 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::>(); - -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 zeroize::Zeroizing; +use rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + use ciphersuite::{Ciphersuite, Ed25519}; use monero_wallet::rpc::{FeeRate, RpcError}; @@ -154,11 +17,17 @@ use primitives::{OutputType, ReceivedOutput, Payment}; use scanner::{KeyFor, AddressFor, OutputFor, BlockFor}; 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::{ EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, view_pair, - output::Output, transaction::{SignableTransaction, Eventuality}, rpc::Rpc, }; @@ -179,13 +48,108 @@ fn address_from_serai_key(key: ::G, kind: OutputType) -> .expect("created address which wasn't representable") } +async fn signable_transaction( + rpc: &Rpc, + reference_block: &BlockFor, + inputs: Vec>, + payments: Vec>>, + change: Option>, +) -> Result, RpcError> { + assert!(inputs.len() < >::MAX_INPUTS); + assert!( + (payments.len() + usize::from(u8::from(change.is_some()))) < + >::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::>(); + 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, + ::generator().0, + ::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)] pub(crate) struct Planner(pub(crate) Rpc); impl TransactionPlanner for Planner { type EphemeralError = RpcError; - type FeeRate = FeeRate; - type SignableTransaction = SignableTransaction; // wallet2 will not create a transaction larger than 100 KB, and Monero won't relay a transaction @@ -195,12 +159,6 @@ impl TransactionPlanner for Planner { const MAX_INPUTS: usize = 120; const MAX_OUTPUTS: usize = 16; - fn fee_rate(block: &BlockFor, coin: Coin) -> Self::FeeRate { - assert_eq!(coin, Coin::Monero); - // TODO - todo!("TODO") - } - fn branch_address(key: KeyFor) -> AddressFor { address_from_serai_key(key, OutputType::Branch) } @@ -212,218 +170,101 @@ impl TransactionPlanner for Planner { } fn calculate_fee( - fee_rate: Self::FeeRate, + &self, + reference_block: &BlockFor, inputs: Vec>, payments: Vec>>, change: Option>, - ) -> Amount { - todo!("TODO") + ) -> impl Send + Future> { + 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( &self, - fee_rate: Self::FeeRate, + reference_block: &BlockFor, inputs: Vec>, payments: Vec>>, change: Option>, ) -> impl Send + Future, RpcError>> { - async move { todo!("TODO") } - } -} - -pub(crate) type Scheduler = utxo_standard_scheduler::Scheduler; - -/* -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: ::G, kind: OutputType) -> Address { - let offset = ::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( - fee_per_vbyte: u64, - inputs: Vec>, - payments: Vec>>, - change: Option>, -) -> Result<(SignableTransaction, BSignableTransaction), TransactionError> { - assert!( - inputs.len() < - >::MAX_INPUTS - ); - assert!( - (payments.len() + usize::from(u8::from(change.is_some()))) < - >::MAX_OUTPUTS - ); - - let inputs = inputs.into_iter().map(|input| input.output).collect::>(); - - 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::>(); - /* - 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(::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(>::change_address); - - BSignableTransaction::new( - inputs.clone(), - &payments - .iter() - .cloned() - .map(|(address, amount)| (ScriptBuf::from(address), amount)) - .collect::>(), - 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 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, coin: Coin) -> Self::FeeRate { - assert_eq!(coin, Coin::Monero); - // TODO - 1 - } - - fn branch_address(key: KeyFor) -> AddressFor { - address_from_serai_key(key, OutputType::Branch) - } - fn change_address(key: KeyFor) -> AddressFor { - address_from_serai_key(key, OutputType::Change) - } - fn forwarding_address(key: KeyFor) -> AddressFor { - address_from_serai_key(key, OutputType::Forwarded) - } - - fn calculate_fee( - fee_rate: Self::FeeRate, - inputs: Vec>, - payments: Vec>>, - change: Option>, - ) -> Amount { - match signable_transaction::(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>, - payments: Vec>>, - change: Option>, - ) -> PlannedTransaction { - 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()); - match signable_transaction::(fee_rate, inputs.clone(), payments, change) { - Ok(tx) => PlannedTransaction { - signable: tx.0, - eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output }, - auxilliary: (), - }, - Err( - 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") - } + + 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, + eventuality: Eventuality { + id, + singular_spent_output, + eventuality: MEventuality::from(tx.1), + }, + auxilliary: (), + } + } + 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; -*/ diff --git a/processor/scheduler/utxo/primitives/src/lib.rs b/processor/scheduler/utxo/primitives/src/lib.rs index 00b2d10f..c01baf02 100644 --- a/processor/scheduler/utxo/primitives/src/lib.rs +++ b/processor/scheduler/utxo/primitives/src/lib.rs @@ -4,7 +4,7 @@ use core::{fmt::Debug, future::Future}; -use serai_primitives::{Coin, Amount}; +use serai_primitives::Amount; use primitives::{ReceivedOutput, Payment}; use scanner::{ScannerFeed, KeyFor, AddressFor, OutputFor, EventualityFor, BlockFor}; @@ -48,9 +48,6 @@ pub trait TransactionPlanner: 'static + Send + Sync { /// resolve manual intervention/changing the arguments. type EphemeralError: Debug; - /// The type representing a fee rate to use for transactions. - type FeeRate: Send + Clone + Copy; - /// The type representing a signable transaction. type SignableTransaction: SignableTransaction; @@ -59,11 +56,6 @@ pub trait TransactionPlanner: 'static + Send + Sync { /// The maximum amount of outputs allowed in a transaction, including the change output. const MAX_OUTPUTS: usize; - /// Obtain the fee rate to pay. - /// - /// This must be constant to the block and coin. - fn fee_rate(block: &BlockFor, coin: Coin) -> Self::FeeRate; - /// The branch address for this key of Serai's. fn branch_address(key: KeyFor) -> AddressFor; /// The change address for this key of Serai's. @@ -76,11 +68,12 @@ pub trait TransactionPlanner: 'static + Send + Sync { /// The fee rate, inputs, and payments, will all be for the same coin. The returned fee is /// denominated in this coin. fn calculate_fee( - fee_rate: Self::FeeRate, + &self, + reference_block: &BlockFor, inputs: Vec>, payments: Vec>>, change: Option>, - ) -> Amount; + ) -> impl Send + Future>; /// Plan a transaction. /// @@ -91,7 +84,7 @@ pub trait TransactionPlanner: 'static + Send + Sync { /// output must be created. fn plan( &self, - fee_rate: Self::FeeRate, + reference_block: &BlockFor, inputs: Vec>, payments: Vec>>, change: Option>, @@ -112,7 +105,7 @@ pub trait TransactionPlanner: 'static + Send + Sync { fn plan_transaction_with_fee_amortization( &self, operating_costs: &mut u64, - fee_rate: Self::FeeRate, + reference_block: &BlockFor, inputs: Vec>, mut payments: Vec>>, mut change: Option>, @@ -156,7 +149,8 @@ pub trait TransactionPlanner: 'static + Send + Sync { // Sort payments from high amount to low amount 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; while !payments.is_empty() { // We need to pay the fee, and any accrued operating costs, minus what we've already @@ -176,7 +170,10 @@ pub trait TransactionPlanner: 'static + Send + Sync { if payments.last().unwrap().balance().amount.0 <= (per_payment_fee + S::dust(coin).0) { amortized += payments.pop().unwrap().balance().amount.0; // 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; } // Break since all of these payments shouldn't be dropped @@ -237,7 +234,7 @@ pub trait TransactionPlanner: 'static + Send + Sync { let has_change = change.is_some(); let PlannedTransaction { signable, eventuality, auxilliary } = - self.plan(fee_rate, inputs, payments, change).await?; + self.plan(reference_block, inputs, payments, change).await?; Ok(Some(AmortizePlannedTransaction { effected_payments, has_change, diff --git a/processor/scheduler/utxo/standard/src/lib.rs b/processor/scheduler/utxo/standard/src/lib.rs index 5ff786a7..208ae8a0 100644 --- a/processor/scheduler/utxo/standard/src/lib.rs +++ b/processor/scheduler/utxo/standard/src/lib.rs @@ -56,7 +56,7 @@ impl> Scheduler { .planner .plan_transaction_with_fee_amortization( &mut operating_costs, - P::fee_rate(block, coin), + block, to_aggregate, vec![], Some(key_for_change), @@ -176,7 +176,7 @@ impl> Scheduler { .plan_transaction_with_fee_amortization( // Uses 0 as there's no operating costs to incur/amortize here &mut 0, - P::fee_rate(block, coin), + block, vec![output], payments, None, @@ -254,7 +254,7 @@ impl> Scheduler { .planner .plan_transaction_with_fee_amortization( &mut operating_costs, - P::fee_rate(block, coin), + block, outputs.clone(), tree[0] .payments::(coin, &branch_address, tree[0].value()) @@ -327,7 +327,7 @@ impl> Scheduler { .planner .plan_transaction_with_fee_amortization( &mut operating_costs, - P::fee_rate(block, coin), + block, outputs, vec![], Some(to), @@ -487,7 +487,7 @@ impl> SchedulerTrait for Schedul // 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 &mut 0, - P::fee_rate(block, forward.balance().coin), + block, vec![forward.clone()], vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)], None, @@ -508,7 +508,7 @@ impl> SchedulerTrait for Schedul // 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 &mut 0, - P::fee_rate(block, out_instruction.balance().coin), + block, vec![to_return.output().clone()], vec![out_instruction], None, diff --git a/processor/scheduler/utxo/transaction-chaining/src/lib.rs b/processor/scheduler/utxo/transaction-chaining/src/lib.rs index cb0a8b15..961c6fcb 100644 --- a/processor/scheduler/utxo/transaction-chaining/src/lib.rs +++ b/processor/scheduler/utxo/transaction-chaining/src/lib.rs @@ -86,7 +86,7 @@ impl>> Sched .planner .plan_transaction_with_fee_amortization( &mut operating_costs, - P::fee_rate(block, coin), + block, to_aggregate, vec![], Some(key_for_change), @@ -229,7 +229,7 @@ impl>> Sched .planner .plan_transaction_with_fee_amortization( &mut operating_costs, - P::fee_rate(block, coin), + block, outputs.clone(), tree[0] .payments::(coin, &branch_address, tree[0].value()) @@ -323,7 +323,7 @@ impl>> Sched .plan_transaction_with_fee_amortization( // Uses 0 as there's no operating costs to incur/amortize here &mut 0, - P::fee_rate(block, coin), + block, vec![branch_output], payments, None, @@ -379,7 +379,7 @@ impl>> Sched .planner .plan_transaction_with_fee_amortization( &mut operating_costs, - P::fee_rate(block, coin), + block, outputs, vec![], Some(to), @@ -505,7 +505,7 @@ impl>> Sched // 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 &mut 0, - P::fee_rate(block, forward.balance().coin), + block, vec![forward.clone()], vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)], None, @@ -526,7 +526,7 @@ impl>> Sched // 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 &mut 0, - P::fee_rate(block, out_instruction.balance().coin), + block, vec![to_return.output().clone()], vec![out_instruction], None,