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-wallet",
"parity-scale-codec",
"rand_chacha",
"rand_core",
"serai-client",
"serai-db",

View file

@ -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()

View file

@ -49,7 +49,7 @@ impl scheduler::Transaction for Transaction {
#[derive(Clone, Debug)]
pub(crate) struct SignableTransaction {
pub(crate) inputs: Vec<ReceivedOutput>,
pub(crate) payments: Vec<(Address, u64)>,
pub(crate) payments: Vec<(ScriptBuf, u64)>,
pub(crate) change: Option<Address>,
pub(crate) fee_per_vbyte: u64,
}
@ -58,12 +58,7 @@ impl SignableTransaction {
fn signable(self) -> Result<BSignableTransaction, TransactionError> {
BSignableTransaction::new(
self.inputs,
&self
.payments
.iter()
.cloned()
.map(|(address, amount)| (ScriptBuf::from(address), amount))
.collect::<Vec<_>>(),
&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<u8>, 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)?;

View file

@ -35,7 +35,7 @@ fn address_from_serai_key(key: <Secp256k1 as Ciphersuite>::G, kind: OutputType)
}
fn signable_transaction<D: Db>(
fee_per_vbyte: u64,
_reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<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
);
// TODO
let fee_per_vbyte = 1;
let inputs = inputs.into_iter().map(|input| input.output).collect::<Vec<_>>();
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<D: Db>(
*/
payments.push((
// 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
// TODO: Add a test for this comparing to bitcoin's `minimal_non_dust`
bitcoin_serai::wallet::DUST,
@ -79,11 +82,7 @@ fn signable_transaction<D: Db>(
BSignableTransaction::new(
inputs.clone(),
&payments
.iter()
.cloned()
.map(|(address, amount)| (ScriptBuf::from(address), amount))
.collect::<Vec<_>>(),
&payments,
change.clone().map(ScriptBuf::from),
None,
fee_per_vbyte,
@ -95,7 +94,6 @@ fn signable_transaction<D: Db>(
pub(crate) struct Planner;
impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Planner {
type EphemeralError = ();
type FeeRate = u64;
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.
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>> {
address_from_serai_key(key, OutputType::Branch)
}
@ -136,29 +128,32 @@ impl<D: Db> TransactionPlanner<Rpc<D>, EffectedReceivedOutputs<Rpc<D>>> for Plan
}
fn calculate_fee(
fee_rate: Self::FeeRate,
&self,
reference_block: &BlockFor<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<Rpc<D>>>>,
change: Option<KeyFor<Rpc<D>>>,
) -> 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),
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
async move {
Ok(match signable_transaction::<D>(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<Rpc<D>>,
inputs: Vec<OutputFor<Rpc<D>>>,
payments: Vec<Payment<AddressFor<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());
match signable_transaction::<D>(fee_rate, inputs.clone(), payments, change) {
match signable_transaction::<D>(reference_block, inputs.clone(), payments, change) {
Ok(tx) => Ok(PlannedTransaction {
signable: tx.0,
eventuality: Eventuality { txid: tx.1.txid(), singular_spent_output },

View file

@ -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"] }

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;
}
/*
#[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)]
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 {
type Id = OutputId;
type TransactionId = [u8; 32];

View file

@ -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<OutputId>,
pub(crate) id: [u8; 32],
pub(crate) singular_spent_output: Option<OutputId>,
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 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: <Ed25519 as Ciphersuite>::G, kind: OutputType) ->
.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)]
pub(crate) struct Planner(pub(crate) Rpc);
impl TransactionPlanner<Rpc, ()> 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<Rpc, ()> for Planner {
const MAX_INPUTS: usize = 120;
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> {
address_from_serai_key(key, OutputType::Branch)
}
@ -212,218 +170,101 @@ impl TransactionPlanner<Rpc, ()> for Planner {
}
fn calculate_fee(
fee_rate: Self::FeeRate,
&self,
reference_block: &BlockFor<Rpc>,
inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>,
) -> Amount {
todo!("TODO")
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>> {
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<Rpc>,
inputs: Vec<OutputFor<Rpc>>,
payments: Vec<Payment<AddressFor<Rpc>>>,
change: Option<KeyFor<Rpc>>,
) -> impl Send
+ 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());
match signable_transaction::<D>(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<Rpc, Planner>;
*/

View file

@ -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<S: ScannerFeed, A>: '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<S: ScannerFeed, A>: '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<S>, coin: Coin) -> Self::FeeRate;
/// The branch address for this key of Serai's.
fn branch_address(key: KeyFor<S>) -> AddressFor<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
/// denominated in this coin.
fn calculate_fee(
fee_rate: Self::FeeRate,
&self,
reference_block: &BlockFor<S>,
inputs: Vec<OutputFor<S>>,
payments: Vec<Payment<AddressFor<S>>>,
change: Option<KeyFor<S>>,
) -> Amount;
) -> impl Send + Future<Output = Result<Amount, Self::EphemeralError>>;
/// Plan a transaction.
///
@ -91,7 +84,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
/// output must be created.
fn plan(
&self,
fee_rate: Self::FeeRate,
reference_block: &BlockFor<S>,
inputs: Vec<OutputFor<S>>,
payments: Vec<Payment<AddressFor<S>>>,
change: Option<KeyFor<S>>,
@ -112,7 +105,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
fn plan_transaction_with_fee_amortization(
&self,
operating_costs: &mut u64,
fee_rate: Self::FeeRate,
reference_block: &BlockFor<S>,
inputs: Vec<OutputFor<S>>,
mut payments: Vec<Payment<AddressFor<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
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<S: ScannerFeed, A>: '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<S: ScannerFeed, A>: '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,

View file

@ -56,7 +56,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.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<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.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<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.planner
.plan_transaction_with_fee_amortization(
&mut operating_costs,
P::fee_rate(block, coin),
block,
outputs.clone(),
tree[0]
.payments::<S>(coin, &branch_address, tree[0].value())
@ -327,7 +327,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> Scheduler<S, P> {
.planner
.plan_transaction_with_fee_amortization(
&mut operating_costs,
P::fee_rate(block, coin),
block,
outputs,
vec![],
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
// 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<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> 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,

View file

@ -86,7 +86,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> 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<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
.planner
.plan_transaction_with_fee_amortization(
&mut operating_costs,
P::fee_rate(block, coin),
block,
outputs.clone(),
tree[0]
.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(
// 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<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> 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<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> 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<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> 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,