-
Notifications
You must be signed in to change notification settings - Fork 29
Fix/wallet fee calculation #1916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
05abe2e
to
6b1fe65
Compare
- account for compact input length encoding - fix rounding error in case of individual input fee calculation vs total fee calculation using entire tx size - add missing limit for tx size/weight
6b1fe65
to
5a4271e
Compare
@@ -1447,7 +1462,12 @@ fn wallet_transaction_with_fees(#[case] seed: Seed) { | |||
.unwrap(); | |||
|
|||
let tx_size = serialization::Encode::encoded_size(&transaction); | |||
let fee = feerate.compute_fee(tx_size).unwrap(); | |||
|
|||
// 16 bytes (u128) is the max tx size diffference in the estimation, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo - diffference
let fee = feerate.compute_fee(tx_size).unwrap(); | ||
|
||
// 16 bytes (u128) is the max tx size diffference in the estimation, | ||
// because of the compact encoding for the change amount which is unknown beforhand |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beforhand -> beforehand
total_fees_not_paid = | ||
(total_fees_not_paid + cost_of_change).ok_or(WalletError::OutputAmountOverflow)?; | ||
|
||
// There can be an small rounding error of computing the fee for each individual input |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a small
let tx = SignedTransaction::new( | ||
Transaction::new(1, vec![], outputs.into()).expect("should not fail"), | ||
Transaction::new(1, vec![], vec![]).expect("should not fail"), | ||
vec![], | ||
) | ||
.expect("should not fail"); | ||
serialization::Encode::encoded_size(&tx) | ||
let size = serialization::Encode::encoded_size(&tx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put empty tx size calculation to a constant? (in a lazy_static block)
(Can be local to this function)
let output_compact_size_diff = | ||
serialization::Encode::encoded_size(&CompactSize { | ||
value: num_outputs as u64, | ||
}) - serialization::Encode::encoded_size(&CompactSize { value: 0 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plz calculate the fixed part (serialization::Encode::encoded_size(&CompactSize { value: 0 })
) only once.
Also:
- The codec used u32 for the length of the vector/slice instead of u64 - https://github.com/paritytech/parity-scale-codec/blob/0a0295a465ac40fd2d38359220fdcc3f3de94cae/src/codec.rs#L1090 (not that it makes much of a difference though).
- It may make sense to get rid of the
CompactSize
and useCompact::<u32>::compact_len
here instead (likeCompact::<u32>::compact_len(&(num_outputs as u32)) - Compact::<u32>::compact_len(&0)
).
(probably first checking that num_outputs actually fits into u32)
let selection_result = select_coins( | ||
utxos, | ||
(amount_to_be_paid_in_currency_with_fees - preselected_amount).unwrap_or(Amount::ZERO), | ||
PayFee::PayFeeWithThisCurrency, | ||
cost_of_change, | ||
selection_algo, | ||
self.chain_config.max_tx_size_for_mempool(), | ||
)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder, why do we need to call select_coins
in the case when amount_to_be_paid_in_currency_with_fees == preselected_amount
?
Also, select_coins
has this code:
if selection_target == Amount::ZERO && pay_fees == PayFee::DoNotPayFeeWithThisCurrency {
return Ok(SelectionResult::new(selection_target));
}
Why doesn't it ignore zero amount in the PayFeeWithThisCurrency
case too?
It looks like we select coins for the change even when we don't need any change.
pay_fee_with_currency, | ||
(amount_to_be_paid_in_currency_with_fees + selection_result.get_total_fees()) | ||
.ok_or(WalletError::OutputAmountOverflow)?, | ||
); | ||
selected_inputs.insert(pay_fee_with_currency, selection_result); | ||
|
||
// Check outputs against inputs and create change | ||
self.check_outputs_and_add_change( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that check_outputs_and_add_change
no longer checks outputs, so it's better to rename it.
(Or maybe remove it completely and put its body directly into select_inputs_for_send_request
. Since it's not really reusable IMO).
(inputs.amount, inputs.fee, inputs.total_input_size) | ||
}); | ||
|
||
let (size_of_change, cost_of_change) = output_change_size_and_fees( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a tight coupling between output_change_size_and_fees
and check_outputs_and_add_change
- the former accepts Option<&Address<Destination>>
and assumes PublicKeyHash
if it's None. And the latter then calls key_chain.next_unused_address
if change_addresses
doesn't contain the corresponding currency, and next_unused_address
is assumed to always return PublicKeyHash
I guess, which is not a great assumption either.
I wonder if we could refactor it in this PR. E.g. modify change_addresses
in advance, calling key_chain.next_unused_address
if necessary, so that the map already contains all possible currencies.
(If not, then it's better to at least add some comments to output_change_size_and_fees
and check_outputs_and_add_change
, mentioning this interdependency).
// increase fee and total_input_size, this is same for all inputs | ||
match preselected_inputs.entry(Currency::Coin) { | ||
Entry::Vacant(entry) => { | ||
entry.insert(PreselectedInputAmounts { | ||
amount: Amount::ZERO, | ||
fee, | ||
burn: Amount::ZERO, | ||
total_input_size, | ||
}); | ||
} | ||
Entry::Occupied(mut entry) => { | ||
let existing = entry.get_mut(); | ||
existing.fee = (existing.fee + fee).ok_or(WalletError::OutputAmountOverflow)?; | ||
existing.total_input_size += total_input_size; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks hacky.
select_inputs_for_send_request
operates onpay_fee_with_currency
, so I guess you should pass it here instead of assuming that it's always Coin.
(or if you think it's redundant, then maybeselect_inputs_for_send_request
should no longer pretend that it can handle other currencies to pay fees with).- The code in
select_inputs_for_send_request
, where it iterates over "non-pay_fee_with_currency" currencies and updates the total tx size and fee, doesn't make sense anymore, because those values will always be zeroes now.
So,
a) I wonder what was wrong with the previous approach. Why can't we return the sizes/fees on per-currency basis as before, and make the caller combine the result (like it still does)?
b) If the old approach is not good anymore, then maybe this function's return type should be changed to something else that better reflects the data that is being collected?
// 1. give X amount of coins to the wallet | ||
// 2. Try to create a transaction where the feerate is so high that the UTXO will need to pay more | ||
// fee than it has so it will return an NoUTXOs error | ||
// 3. Successfuly create a TX with many outputs and make sure the fee paid is within the range of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully
This was fixed by passing the number of inputs to the
tx_size_with_outputs
.Closes #1208