Track and amortize operating costs to ensure solvency

Implements most of #297 to the point I'm fine closing it. The solution
implemented is distinct than originally designed, yet much simpler.

Since we have a fully-linear view of created transactions, we don't have to
per-output track operating costs incurred by that output. We can track it
across the entire Serai system, without hooking into the Eventuality system.

Also updates documentation.
This commit is contained in:
Luke Parker
2023-10-19 02:53:55 -04:00
parent 057c3b7cf1
commit 3255c0ace5
12 changed files with 186 additions and 73 deletions

View File

@@ -130,7 +130,7 @@ Input accumulation refers to transactions which exist to merge inputs. Just as
there is a `max_outputs_per_tx`, there is a `max_inputs_per_tx`. When the amount there is a `max_outputs_per_tx`, there is a `max_inputs_per_tx`. When the amount
of inputs belonging to Serai exceeds `max_inputs_per_tx`, a TX merging them is of inputs belonging to Serai exceeds `max_inputs_per_tx`, a TX merging them is
created. This TX incurs fees yet has no outputs mapping to burns to amortize created. This TX incurs fees yet has no outputs mapping to burns to amortize
them over, creating an insolvency. them over, accumulating operating costs.
Please note that this merging occurs in parallel to create a logarithmic Please note that this merging occurs in parallel to create a logarithmic
execution, similar to how outputs are also processed in parallel. execution, similar to how outputs are also processed in parallel.
@@ -154,18 +154,16 @@ initially filled, yet requires:
while still risking insolvency, if the actual fees keep increasing in a way while still risking insolvency, if the actual fees keep increasing in a way
preventing successful estimation. preventing successful estimation.
The solution Serai implements is to accrue insolvency, tracking each output with The solution Serai implements is to accrue operating costs, tracking with each
a virtual amount (the amount it represents on Serai) and the actual amount. When created transaction the running operating costs. When a created transaction has
the output, or a descendant of it, is used to handle burns, the discrepancy payments out, all of the operating costs incurred so far, which have yet to be
between the virtual amount and the amount is amortized over outputs. This amortized, are immediately and fully amortized.
restores solvency while solely charging the actual fees, making Serai a
generally insolvent, always eventually solvent system.
There is the concern that a significant amount of outputs could be created, There is the concern that a significant amount of outputs could be created,
which when merged as inputs, create a significant amount of fees as an which when merged as inputs, create a significant amount of operating costs.
insolvency. This would then be forced onto random users, while the party who This would then be forced onto random users who burn `sriXYZ` soon after, while
created the insolvency would then be able to burn their own `sriXYZ` without the party who caused the operating costs would then be able to burn their own
the notable insolvency. `sriXYZ` without notable fees.
To describe this attack in its optimal form, assume a sole malicious block To describe this attack in its optimal form, assume a sole malicious block
producer for an external network where `max_inputs_per_tx` is 16. The malicious producer for an external network where `max_inputs_per_tx` is 16. The malicious

View File

@@ -23,7 +23,7 @@ mod plan;
pub use plan::*; pub use plan::*;
mod networks; mod networks;
use networks::{PostFeeBranch, Block, Network, get_latest_block_number, get_block}; use networks::{Block, Network, get_latest_block_number, get_block};
#[cfg(feature = "bitcoin")] #[cfg(feature = "bitcoin")]
use networks::Bitcoin; use networks::Bitcoin;
#[cfg(feature = "monero")] #[cfg(feature = "monero")]

View File

@@ -45,6 +45,7 @@ impl<N: Network, D: Db> MultisigsDb<N, D> {
key: &[u8], key: &[u8],
block_number: u64, block_number: u64,
plan: &Plan<N>, plan: &Plan<N>,
operating_costs_at_time: u64,
) { ) {
let id = plan.id(); let id = plan.id();
@@ -66,11 +67,12 @@ impl<N: Network, D: Db> MultisigsDb<N, D> {
{ {
let mut buf = block_number.to_le_bytes().to_vec(); let mut buf = block_number.to_le_bytes().to_vec();
plan.write(&mut buf).unwrap(); plan.write(&mut buf).unwrap();
buf.extend(&operating_costs_at_time.to_le_bytes());
txn.put(Self::plan_key(&id), &buf); txn.put(Self::plan_key(&id), &buf);
} }
} }
pub fn active_plans<G: Get>(getter: &G, key: &[u8]) -> Vec<(u64, Plan<N>)> { pub fn active_plans<G: Get>(getter: &G, key: &[u8]) -> Vec<(u64, Plan<N>, u64)> {
let signing = getter.get(Self::signing_key(key)).unwrap_or(vec![]); let signing = getter.get(Self::signing_key(key)).unwrap_or(vec![]);
let mut res = vec![]; let mut res = vec![];
@@ -82,12 +84,30 @@ impl<N: Network, D: Db> MultisigsDb<N, D> {
let block_number = u64::from_le_bytes(buf[.. 8].try_into().unwrap()); let block_number = u64::from_le_bytes(buf[.. 8].try_into().unwrap());
let plan = Plan::<N>::read::<&[u8]>(&mut &buf[8 ..]).unwrap(); let plan = Plan::<N>::read::<&[u8]>(&mut &buf[8 ..]).unwrap();
assert_eq!(id, &plan.id()); assert_eq!(id, &plan.id());
res.push((block_number, plan)); let operating_costs = u64::from_le_bytes(buf[(buf.len() - 8) ..].try_into().unwrap());
res.push((block_number, plan, operating_costs));
} }
res res
} }
fn operating_costs_key() -> Vec<u8> {
Self::multisigs_key(b"operating_costs", [])
}
pub fn take_operating_costs(txn: &mut D::Transaction<'_>) -> u64 {
let existing = txn
.get(Self::operating_costs_key())
.map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap()))
.unwrap_or(0);
txn.del(Self::operating_costs_key());
existing
}
pub fn set_operating_costs(txn: &mut D::Transaction<'_>, amount: u64) {
if amount != 0 {
txn.put(Self::operating_costs_key(), amount.to_le_bytes());
}
}
pub fn resolved_plan<G: Get>( pub fn resolved_plan<G: Get>(
getter: &G, getter: &G,
tx: <N::Transaction as Transaction<N>>::Id, tx: <N::Transaction as Transaction<N>>::Id,

View File

@@ -35,8 +35,10 @@ pub mod scheduler;
use scheduler::Scheduler; use scheduler::Scheduler;
use crate::{ use crate::{
Get, Db, Payment, PostFeeBranch, Plan, Get, Db, Payment, Plan,
networks::{OutputType, Output, Transaction, SignableTransaction, Block, Network, get_block}, networks::{
OutputType, Output, Transaction, SignableTransaction, Block, PreparedSend, Network, get_block,
},
}; };
// InInstructionWithBalance from an external output // InInstructionWithBalance from an external output
@@ -57,8 +59,12 @@ fn instruction_from_output<N: Network>(output: &N::Output) -> Option<InInstructi
let Ok(shorthand) = Shorthand::decode(&mut data) else { None? }; let Ok(shorthand) = Shorthand::decode(&mut data) else { None? };
let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { None? }; let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { None? };
let balance = output.balance();
// TODO: Decrease amount by
// `2 * (the estimation of an input-merging transaction fee) / max_inputs_per_tx`
// TODO2: Set instruction.origin if not set (and handle refunds in general) // TODO2: Set instruction.origin if not set (and handle refunds in general)
Some(InInstructionWithBalance { instruction: instruction.instruction, balance: output.balance() }) Some(InInstructionWithBalance { instruction: instruction.instruction, balance })
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -74,7 +80,7 @@ enum RotationStep {
ClosingExisting, ClosingExisting,
} }
async fn get_fee<N: Network>(network: &N, block_number: usize) -> N::Fee { async fn get_fee_rate<N: Network>(network: &N, block_number: usize) -> N::Fee {
// TODO2: Use an fee representative of several blocks // TODO2: Use an fee representative of several blocks
get_block(network, block_number).await.median_fee() get_block(network, block_number).await.median_fee()
} }
@@ -82,11 +88,12 @@ async fn get_fee<N: Network>(network: &N, block_number: usize) -> N::Fee {
async fn prepare_send<N: Network>( async fn prepare_send<N: Network>(
network: &N, network: &N,
block_number: usize, block_number: usize,
fee: N::Fee, fee_rate: N::Fee,
plan: Plan<N>, plan: Plan<N>,
) -> (Option<(N::SignableTransaction, N::Eventuality)>, Vec<PostFeeBranch>) { operating_costs: u64,
) -> PreparedSend<N> {
loop { loop {
match network.prepare_send(block_number, plan.clone(), fee).await { match network.prepare_send(block_number, plan.clone(), fee_rate, operating_costs).await {
Ok(prepared) => { Ok(prepared) => {
return prepared; return prepared;
} }
@@ -145,18 +152,20 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
// Load any TXs being actively signed // Load any TXs being actively signed
let key = key.to_bytes(); let key = key.to_bytes();
for (block_number, plan) in MultisigsDb::<N, D>::active_plans(raw_db, key.as_ref()) { for (block_number, plan, operating_costs) in
MultisigsDb::<N, D>::active_plans(raw_db, key.as_ref())
{
let block_number = block_number.try_into().unwrap(); let block_number = block_number.try_into().unwrap();
let fee = get_fee(network, block_number).await; let fee_rate = get_fee_rate(network, block_number).await;
let id = plan.id(); let id = plan.id();
info!("reloading plan {}: {:?}", hex::encode(id), plan); info!("reloading plan {}: {:?}", hex::encode(id), plan);
let key_bytes = plan.key.to_bytes(); let key_bytes = plan.key.to_bytes();
let (Some((tx, eventuality)), _) = let Some((tx, eventuality)) =
prepare_send(network, block_number, fee, plan.clone()).await prepare_send(network, block_number, fee_rate, plan.clone(), operating_costs).await.tx
else { else {
panic!("previously created transaction is no longer being created") panic!("previously created transaction is no longer being created")
}; };
@@ -666,7 +675,7 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
let res = { let res = {
let mut res = Vec::with_capacity(plans.len()); let mut res = Vec::with_capacity(plans.len());
let fee = get_fee(network, block_number).await; let fee_rate = get_fee_rate(network, block_number).await;
for plan in plans { for plan in plans {
let id = plan.id(); let id = plan.id();
@@ -674,18 +683,27 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
let key = plan.key; let key = plan.key;
let key_bytes = key.to_bytes(); let key_bytes = key.to_bytes();
let running_operating_costs = MultisigsDb::<N, D>::take_operating_costs(txn);
MultisigsDb::<N, D>::save_active_plan( MultisigsDb::<N, D>::save_active_plan(
txn, txn,
key_bytes.as_ref(), key_bytes.as_ref(),
block_number.try_into().unwrap(), block_number.try_into().unwrap(),
&plan, &plan,
running_operating_costs,
); );
let to_be_forwarded = forwarded_external_outputs.remove(plan.inputs[0].id().as_ref()); let to_be_forwarded = forwarded_external_outputs.remove(plan.inputs[0].id().as_ref());
if to_be_forwarded.is_some() { if to_be_forwarded.is_some() {
assert_eq!(plan.inputs.len(), 1); assert_eq!(plan.inputs.len(), 1);
} }
let (tx, branches) = prepare_send(network, block_number, fee, plan).await; let PreparedSend { tx, post_fee_branches, operating_costs } =
prepare_send(network, block_number, fee_rate, plan, running_operating_costs).await;
// 'Drop' running_operating_costs to ensure only operating_costs is used from here on out
#[allow(unused, clippy::let_unit_value)]
let running_operating_costs: () = ();
MultisigsDb::<N, D>::set_operating_costs(txn, operating_costs);
// If this is a Plan for an output we're forwarding, we need to save the InInstruction for // If this is a Plan for an output we're forwarding, we need to save the InInstruction for
// its output under the amount successfully forwarded // its output under the amount successfully forwarded
@@ -697,7 +715,7 @@ impl<D: Db, N: Network> MultisigManager<D, N> {
} }
} }
for branch in branches { for branch in post_fee_branches {
let existing = self.existing.as_mut().unwrap(); let existing = self.existing.as_mut().unwrap();
let to_use = if key == existing.key { let to_use = if key == existing.key {
existing existing

View File

@@ -322,8 +322,6 @@ impl<N: Network> Scheduler<N> {
} }
for chunk in utxo_chunks.drain(..) { for chunk in utxo_chunks.drain(..) {
// TODO: While payments have their TXs' fees deducted from themselves, that doesn't hold here
// We need the documented, but not yet implemented, virtual amount scheme to solve this
log::debug!("aggregating a chunk of {} inputs", N::MAX_INPUTS); log::debug!("aggregating a chunk of {} inputs", N::MAX_INPUTS);
plans.push(Plan { plans.push(Plan {
key: self.key, key: self.key,

View File

@@ -47,8 +47,8 @@ use crate::{
networks::{ networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
Eventuality as EventualityTrait, EventualitiesTracker, PostFeeBranch, Network, drop_branches, Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network,
amortize_fee, drop_branches, amortize_fee,
}, },
Plan, Plan,
}; };
@@ -509,8 +509,8 @@ impl Network for Bitcoin {
_: usize, _: usize,
mut plan: Plan<Self>, mut plan: Plan<Self>,
fee: Fee, fee: Fee,
) -> Result<(Option<(SignableTransaction, Self::Eventuality)>, Vec<PostFeeBranch>), NetworkError> operating_costs: u64,
{ ) -> Result<PreparedSend<Self>, NetworkError> {
let signable = |plan: &Plan<Self>, tx_fee: Option<_>| { let signable = |plan: &Plan<Self>, tx_fee: Option<_>| {
let mut payments = vec![]; let mut payments = vec![];
for payment in &plan.payments { for payment in &plan.payments {
@@ -558,23 +558,37 @@ impl Network for Bitcoin {
let tx_fee = match signable(&plan, None) { let tx_fee = match signable(&plan, None) {
Some(tx) => tx.needed_fee(), Some(tx) => tx.needed_fee(),
None => return Ok((None, drop_branches(&plan))), None => {
return Ok(PreparedSend {
tx: None,
post_fee_branches: drop_branches(&plan),
// We expected a change output of sum(inputs) - sum(outputs)
// Since we can no longer create this change output, it becomes an operating cost
operating_costs: operating_costs +
plan.inputs.iter().map(|input| input.amount()).sum::<u64>() -
plan.payments.iter().map(|payment| payment.amount).sum::<u64>(),
});
}
}; };
let branch_outputs = amortize_fee(&mut plan, tx_fee); let AmortizeFeeRes { post_fee_branches, operating_costs } =
amortize_fee(&mut plan, operating_costs, tx_fee);
let signable = signable(&plan, Some(tx_fee)).unwrap(); let signable = signable(&plan, Some(tx_fee)).unwrap();
// TODO: If the change output was dropped by Bitcoin, increase operating costs
let plan_binding_input = *plan.inputs[0].output.outpoint(); let plan_binding_input = *plan.inputs[0].output.outpoint();
let outputs = signable.outputs().to_vec(); let outputs = signable.outputs().to_vec();
Ok(( Ok(PreparedSend {
Some(( tx: Some((
SignableTransaction { transcript: plan.transcript(), actual: signable }, SignableTransaction { transcript: plan.transcript(), actual: signable },
Eventuality { plan_binding_input, outputs }, Eventuality { plan_binding_input, outputs },
)), )),
branch_outputs, post_fee_branches,
)) operating_costs,
})
} }
async fn attempt_send( async fn attempt_send(

View File

@@ -209,19 +209,47 @@ pub fn drop_branches<N: Network>(plan: &Plan<N>) -> Vec<PostFeeBranch> {
branch_outputs branch_outputs
} }
pub struct AmortizeFeeRes {
post_fee_branches: Vec<PostFeeBranch>,
operating_costs: u64,
}
// Amortize a fee over the plan's payments // Amortize a fee over the plan's payments
pub fn amortize_fee<N: Network>(plan: &mut Plan<N>, tx_fee: u64) -> Vec<PostFeeBranch> { pub fn amortize_fee<N: Network>(
// No payments to amortize over plan: &mut Plan<N>,
if plan.payments.is_empty() { operating_costs: u64,
return vec![]; tx_fee: u64,
} ) -> AmortizeFeeRes {
let total_fee = {
let mut total_fee = tx_fee;
// Since we're creating a change output, letting us recoup coins, amortize the operating costs
// as well
if plan.change.is_some() {
total_fee += operating_costs;
}
total_fee
};
let original_outputs = plan.payments.iter().map(|payment| payment.amount).sum::<u64>(); let original_outputs = plan.payments.iter().map(|payment| payment.amount).sum::<u64>();
// If this isn't enough for the total fee, drop and move on
if original_outputs < total_fee {
let mut remaining_operating_costs = operating_costs;
if plan.change.is_some() {
// Operating costs increase by the TX fee
remaining_operating_costs += tx_fee;
// Yet decrease by the payments we managed to drop
remaining_operating_costs = remaining_operating_costs.saturating_sub(original_outputs);
}
return AmortizeFeeRes {
post_fee_branches: drop_branches(plan),
operating_costs: remaining_operating_costs,
};
}
// Amortize the transaction fee across outputs // Amortize the transaction fee across outputs
let mut payments_len = u64::try_from(plan.payments.len()).unwrap(); let mut payments_len = u64::try_from(plan.payments.len()).unwrap();
// Use a formula which will round up // Use a formula which will round up
let per_output_fee = |payments| (tx_fee + (payments - 1)) / payments; let per_output_fee = |payments| (total_fee + (payments - 1)) / payments;
let post_fee = |payment: &Payment<N>, per_output_fee| { let post_fee = |payment: &Payment<N>, per_output_fee| {
let mut post_fee = payment.amount.checked_sub(per_output_fee); let mut post_fee = payment.amount.checked_sub(per_output_fee);
@@ -266,9 +294,26 @@ pub fn amortize_fee<N: Network>(plan: &mut Plan<N>, tx_fee: u64) -> Vec<PostFeeB
// Sanity check the fee wa successfully amortized // Sanity check the fee wa successfully amortized
let new_outputs = plan.payments.iter().map(|payment| payment.amount).sum::<u64>(); let new_outputs = plan.payments.iter().map(|payment| payment.amount).sum::<u64>();
assert!((new_outputs + tx_fee) <= original_outputs); assert!((new_outputs + total_fee) <= original_outputs);
branch_outputs AmortizeFeeRes {
post_fee_branches: branch_outputs,
operating_costs: if plan.change.is_none() {
// If the change is None, this had no effect on the operating costs
operating_costs
} else {
// Since the change is some, and we successfully amortized, the operating costs were recouped
0
},
}
}
pub struct PreparedSend<N: Network> {
/// None for the transaction if the SignableTransaction was dropped due to lack of value.
pub tx: Option<(N::SignableTransaction, N::Eventuality)>,
pub post_fee_branches: Vec<PostFeeBranch>,
/// The updated operating costs after preparing this transaction.
pub operating_costs: u64,
} }
#[async_trait] #[async_trait]
@@ -364,18 +409,15 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
) -> HashMap<[u8; 32], (usize, Self::Transaction)>; ) -> HashMap<[u8; 32], (usize, Self::Transaction)>;
/// Prepare a SignableTransaction for a transaction. /// Prepare a SignableTransaction for a transaction.
/// // TODO: These have common code inside them
/// Returns None for the transaction if the SignableTransaction was dropped due to lack of value. // Provide prepare_send, have coins offers prepare_send_inner
#[rustfmt::skip]
async fn prepare_send( async fn prepare_send(
&self, &self,
block_number: usize, block_number: usize,
plan: Plan<Self>, plan: Plan<Self>,
fee: Self::Fee, fee_rate: Self::Fee,
) -> Result< running_operating_costs: u64,
(Option<(Self::SignableTransaction, Self::Eventuality)>, Vec<PostFeeBranch>), ) -> Result<PreparedSend<Self>, NetworkError>;
NetworkError
>;
/// Attempt to sign a SignableTransaction. /// Attempt to sign a SignableTransaction.
async fn attempt_send( async fn attempt_send(

View File

@@ -38,8 +38,8 @@ use crate::{
networks::{ networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
Eventuality as EventualityTrait, EventualitiesTracker, PostFeeBranch, Network, drop_branches, Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network,
amortize_fee, drop_branches, amortize_fee,
}, },
}; };
@@ -399,7 +399,8 @@ impl Network for Monero {
block_number: usize, block_number: usize,
mut plan: Plan<Self>, mut plan: Plan<Self>,
fee: Fee, fee: Fee,
) -> Result<(Option<(SignableTransaction, Eventuality)>, Vec<PostFeeBranch>), NetworkError> { operating_costs: u64,
) -> Result<PreparedSend<Self>, NetworkError> {
// Sanity check this has at least one output planned // Sanity check this has at least one output planned
assert!((!plan.payments.is_empty()) || plan.change.is_some()); assert!((!plan.payments.is_empty()) || plan.change.is_some());
@@ -522,20 +523,28 @@ impl Network for Monero {
let tx_fee = match signable(plan.clone(), None)? { let tx_fee = match signable(plan.clone(), None)? {
Some(tx) => tx.fee(), Some(tx) => tx.fee(),
None => return Ok((None, drop_branches(&plan))), None => {
return Ok(PreparedSend {
tx: None,
post_fee_branches: drop_branches(&plan),
// We expected a change output of sum(inputs) - sum(outputs)
// Since we can no longer create this change output, it becomes an operating cost
operating_costs: operating_costs +
plan.inputs.iter().map(|input| input.amount()).sum::<u64>() -
plan.payments.iter().map(|payment| payment.amount).sum::<u64>(),
});
}
}; };
let branch_outputs = amortize_fee(&mut plan, tx_fee); let AmortizeFeeRes { post_fee_branches, operating_costs } =
amortize_fee(&mut plan, operating_costs, tx_fee);
let signable = SignableTransaction { // TODO: If the change output was dropped by Monero, increase operating costs
transcript,
actual: match signable(plan, Some(tx_fee))? { let signable =
Some(signable) => signable, SignableTransaction { transcript, actual: signable(plan, Some(tx_fee))?.unwrap() };
None => return Ok((None, branch_outputs)),
},
};
let eventuality = signable.actual.eventuality().unwrap(); let eventuality = signable.actual.eventuality().unwrap();
Ok((Some((signable, eventuality)), branch_outputs)) Ok(PreparedSend { tx: Some((signable, eventuality)), post_fee_branches, operating_costs })
} }
async fn attempt_send( async fn attempt_send(

View File

@@ -74,7 +74,19 @@ impl<N: Network> Payment<N> {
pub struct Plan<N: Network> { pub struct Plan<N: Network> {
pub key: <N::Curve as Ciphersuite>::G, pub key: <N::Curve as Ciphersuite>::G,
pub inputs: Vec<N::Output>, pub inputs: Vec<N::Output>,
/// The payments this Plan is inteded to create.
///
/// This should only contain payments leaving Serai. While it is acceptable for users to enter
/// Serai's address(es) as the payment address, as that'll be handled by anything which expects
/// certain properties, Serai as a system MUST NOT use payments for internal transfers. Doing
/// so will cause a reduction in their value by the TX fee/operating costs, creating an
/// incomplete transfer.
pub payments: Vec<Payment<N>>, pub payments: Vec<Payment<N>>,
/// The change this Plan should use.
///
/// This MUST contain a Serai address. Operating costs may be deducted from the payments in this
/// Plan on the premise that the change address is Serai's, and accordingly, Serai will recoup
/// the operating costs.
pub change: Option<N::Address>, pub change: Option<N::Address>,
} }
impl<N: Network> core::fmt::Debug for Plan<N> { impl<N: Network> core::fmt::Debug for Plan<N> {

View File

@@ -42,10 +42,11 @@ async fn spend<N: Network, D: Db>(
change: Some(N::change_address(key)), change: Some(N::change_address(key)),
}, },
network.get_fee().await, network.get_fee().await,
0,
) )
.await .await
.unwrap() .unwrap()
.0 .tx
.unwrap(), .unwrap(),
), ),
); );

View File

@@ -171,10 +171,11 @@ pub async fn test_signer<N: Network>(network: N) {
change: Some(N::change_address(key)), change: Some(N::change_address(key)),
}, },
fee, fee,
0,
) )
.await .await
.unwrap() .unwrap()
.0 .tx
.unwrap(); .unwrap();
eventualities.push(eventuality.clone()); eventualities.push(eventuality.clone());

View File

@@ -96,10 +96,10 @@ pub async fn test_wallet<N: Network>(network: N) {
let mut eventualities = vec![]; let mut eventualities = vec![];
for (i, keys) in keys.drain() { for (i, keys) in keys.drain() {
let (signable, eventuality) = network let (signable, eventuality) = network
.prepare_send(network.get_block_number(&block_id).await, plans[0].clone(), fee) .prepare_send(network.get_block_number(&block_id).await, plans[0].clone(), fee, 0)
.await .await
.unwrap() .unwrap()
.0 .tx
.unwrap(); .unwrap();
eventualities.push(eventuality.clone()); eventualities.push(eventuality.clone());