-
Notifications
You must be signed in to change notification settings - Fork 98
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
fix(UTXO): calc of txfee with change, min relay fee #2316
base: dev
Are you sure you want to change the base?
Conversation
fix use gas_fee in build(); fix min_relay_tx_fee calc; refactor break build() into functions rename fn get_tx_fee to get_fee_per_kb; fix utxos tests for recalculated tx fee
fix refactored UtxoTxBuilder::build(): return only txfee (w/o gas fee) with tx as it used to be
Is this ready for review? |
I am doing final checks and will change the status for ready after that |
Thank you for this PR. Covers most of what I was working on here #2083. I will close mine when this is approved. |
mm2src/coins/utxo/utxo_common.rs
Outdated
let total_fee = if tx.outputs.len() == outputs_count { | ||
// take into account the change output | ||
data.fee_amount + (dynamic_fee * P2PKH_OUTPUT_LEN) / KILO_BYTE | ||
data.fee_amount + actual_tx_fee.get_tx_fee_for_change(None) |
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.
data.fee_amount + actual_tx_fee.get_tx_fee_for_change(None) | |
data.fee_amount + actual_tx_fee.get_tx_fee_for_change(0) |
mm2src/coins/utxo/utxo_common.rs
Outdated
data.fee_amount | ||
} | ||
// take into account the change output | ||
data.fee_amount + fee_per_kb.get_tx_fee_for_change(Some(tx_bytes.len() as u64)) |
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.
data.fee_amount + fee_per_kb.get_tx_fee_for_change(Some(tx_bytes.len() as u64)) | |
data.fee_amount + fee_per_kb.get_tx_fee_for_change(tx_bytes.len() as u64) |
mm2src/coins/utxo.rs
Outdated
} | ||
|
||
/// Return extra tx fee for the change output as p2pkh | ||
fn get_tx_fee_for_change(&self, tx_size: Option<u64>) -> u64 { |
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 see that tx_size
can be used as u64
(see the other suggestions)
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.
removed Option from get_tx_fee_for_change in 3f709fc
mm2src/coins/utxo/utxo_common.rs
Outdated
for utxo in self.required_inputs.clone() { | ||
self.tx.inputs.push(UnsignedTransactionInput { | ||
previous_output: utxo.outpoint, | ||
prev_script: utxo.script, | ||
sequence: SEQUENCE_FINAL, | ||
amount: utxo.value, | ||
}); | ||
total += utxo.value; | ||
} | ||
|
||
for utxo in self.available_inputs.clone() { | ||
if total >= amount { | ||
break; | ||
} | ||
self.tx.inputs.push(UnsignedTransactionInput { | ||
previous_output: utxo.outpoint, | ||
prev_script: utxo.script, | ||
sequence: SEQUENCE_FINAL, | ||
amount: utxo.value, | ||
}); | ||
total += utxo.value; | ||
} |
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 should be slightly cheaper:
for utxo in self.required_inputs.clone() { | |
self.tx.inputs.push(UnsignedTransactionInput { | |
previous_output: utxo.outpoint, | |
prev_script: utxo.script, | |
sequence: SEQUENCE_FINAL, | |
amount: utxo.value, | |
}); | |
total += utxo.value; | |
} | |
for utxo in self.available_inputs.clone() { | |
if total >= amount { | |
break; | |
} | |
self.tx.inputs.push(UnsignedTransactionInput { | |
previous_output: utxo.outpoint, | |
prev_script: utxo.script, | |
sequence: SEQUENCE_FINAL, | |
amount: utxo.value, | |
}); | |
total += utxo.value; | |
} | |
for utxo in &self.required_inputs { | |
self.tx.inputs.push(UnsignedTransactionInput { | |
previous_output: utxo.outpoint, | |
prev_script: utxo.script.clone(), | |
sequence: SEQUENCE_FINAL, | |
amount: utxo.value, | |
}); | |
total += utxo.value; | |
} | |
for utxo in &self.available_inputs { | |
if total >= amount { | |
break; | |
} | |
self.tx.inputs.push(UnsignedTransactionInput { | |
previous_output: utxo.outpoint, | |
prev_script: utxo.script.clone(), | |
sequence: SEQUENCE_FINAL, | |
amount: utxo.value, | |
}); | |
total += utxo.value; | |
} |
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.
done in 50a38f8
if total >= amount { | ||
break; | ||
} |
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.
We should be able to add this check above the loop, so we don't start iterating it for no reason if it's already true
.
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.
fixed in 50a38f8
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.
Thanks! 1st iteration.
went over all the changes but skipped most of my comments on the tx builder stuff since i couldn't fully comprehend it and don't wanna cause confusion.
will need a couple more iters as this part of the code is a lil risky and confusing.
@@ -279,11 +279,43 @@ pub enum TxFee { | |||
pub enum ActualTxFee { | |||
/// fee amount per Kbyte received from coin RPC | |||
Dynamic(u64), | |||
/// Use specified amount per each 1 kb of transaction and also per each output less than amount. | |||
/// Use specified fee amount per each 1 kb of transaction and also per each output less than the fee amount. |
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.
could you explain more about this fee scheme. the per each output part isn't really clear (understood it as 1 fee_rate per 1 output, which i doubt is a correct understanding).
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.
Hmm, actually I have the same question:
this fee calc is not fully clear to me. The comment refers to DOGE, I took a look at its daemon code but did not find where this logic is.
mm2src/coins/utxo.rs
Outdated
impl ActualTxFee { | ||
fn get_tx_fee(&self, tx_size: u64) -> u64 { | ||
match self { | ||
ActualTxFee::Dynamic(fee_per_kb) => (fee_per_kb * tx_size) / KILO_BYTE, |
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.
couldn't this wrongfully floor to zero?
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 guess it could.
If it goes too low we would use min relay fee (if this is set though)
mm2src/coins/utxo.rs
Outdated
@@ -279,11 +279,43 @@ pub enum TxFee { | |||
pub enum ActualTxFee { |
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.
since we corrected the confusing namings, we can fix this one as well to TxFeeRate
.
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.
renamed vars in the whole code to use always 'FeeRate' or 'fee_rate' 3f709fc
mm2src/coins/utxo/utxo_common.rs
Outdated
return_err_if_false!( | ||
!self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), | ||
GenerateTxError::EmptyUtxoSet { | ||
required: self.sum_outputs_value | ||
required: self.required_amount() | ||
} | ||
); |
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.
what if we refactor this macro to make it return_err_if!
. i find it a very challenging mind game to compute the value in there and negate it then. esp that the main action is erroring while the passive one is doing nothing.
return_err_if!(self.available.is_empty() && self.tx.is_empty())
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.
refactored in 615ff3e
mm2src/coins/utxo/utxo_common.rs
Outdated
if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { | ||
break; | ||
} | ||
let mut unused_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.
looks very unusual. let's init the value?
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.
yes I agree,
but when I added init to 0u64 clippy issues a 'value is never read' warning. I guess I'd better add init and disable the warning here.
(done)
if self.sum_inputs >= self.sum_outputs + self.total_tx_fee() { | ||
break; | ||
} |
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.
shouldn't this be exactly equal (or equal + unused 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 believe, not.
At this step tx_fee has been updated by the tx_size * fee_rate formula (not like difference of inputs - outputs). So if we have added sufficient inputs, we are finished
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.
and the change will have been adjusted to inputs - outputs - fees
, which brings the equation above to equality.
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 think this is not necessarily so.
Within an iteration, we are getting next input set and change for the total outputs + previous-txfee.
But due to the input selection algo, we may get this input set with the length less than it was at the previous step (rare case, ofc). So the updated txfee may be less than the previous-txfee (but we want to quit now as we have added enough inputs to cover the txfee. As I can see, kmd works like that: it quits the loop when nFeeRet >= nFeeNeeded)
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.
So the updated txfee may be less than the previous-txfee (but we want to quit now as we have added enough inputs to cover the txfee. As I can see, kmd works like that: it quits the loop when nFeeRet >= nFeeNeeded)
I am not following here. Do you mean that the new tx fee is less than the previous iteration but we willingly and intentionally don't update/reduce it? why?
or we can make this argument simpler: if sum_inputs > sum_outputs + fees + unused_change
where do these wasted sats (extra fees?) go? and what is the higher bound on the amount of these wasted sats?
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.
Do you mean that the new tx fee is less than the previous iteration but we willingly and intentionally don't update/reduce it? why?
I could imagine an example:
Previous txfee was calculated with presence of the change output, but on the next iteration we select inputs in a way that no change is required. So the updated txfee would be of lesser value. But it's better to quit here, because if we try to do a new iteration for new txfee we may get a new input set with the change required (and bigger txfee). This may lead to endless loop.
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.
aha gotcha. so we can sometimes over spend on the fees but that's bounded to a really small number. that number is the difference of fee for this tx assuming there is change and there is no change (same tx same inputs same outputs, just playing with the change). that's indeed a pretty small number.
BUT: isn't this number represented (or supposed to be represented) by the unused_change? and then if we plug that in we can check for equality rather than >=
.
my issue with a >=
is that if we have a bug in tx creation code, we might end up creating a tx burning a lot of money.
if >=
is necessary then we can check that the difference sum_inputs - (sum_outputs + total_tx_fee)
is bounded to a small number as a last validation check before returning such a tx.
let data = AdditionalTxData { | ||
fee_amount: self.tx_fee, | ||
received_by_me, | ||
fee_amount: self.tx_fee, // we return only txfee here (w/o gas_fee) | ||
received_by_me: self.sum_received_by_me(&change_script_pubkey), | ||
spent_by_me: self.sum_inputs, | ||
unused_change, | ||
// will be changed if the ticker is KMD |
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.
Q: why don't we add this unused/dust change to the fee amount? i mean it's part of the fee at the end of the day.
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.
As I can see in the code, unused_change is reserved until the kmd interest is calculated (for the kmd coin). And if unused_change + interest becomes greater than dust the change output would be added.
let mut one_time_fee_update = false; | ||
loop { | ||
let required_amount_0 = self.required_amount(); |
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.
im not really sure about the convergence/termination of this loop. it's not trivial to see.
we can do a 10 rounds or something using a for loop and give up.
and maybe use this for counter to simulate one_time_fee_update
.
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 logic I believe matches to the loop daemon wallet code.
I think it should work like that:
- pass 1: no change yet, we add inputs w/o it. We add change and txfee increases. If inputs not enough we go to pass 2.
- pass 2: we add inputs for the increased txfee. New inputs set may be bigger (if we added small input(s) to cover new txfee) and txfee again increases. Again, if inputs not enough go to pass 3.
- pass 3: same logic.
and so on.
I guess, if we have many inputs of small enough value, to be added on each pass to cover the previous txfee (but insufficient to cover the next txfee), increasing the input set so adding more value to next txfee, then we may have many iterations.
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.
thx so much for the explanation..that helps a lot.
I was suggesting a for loop because the logic inside is complicated and one can't trivially see that it is guaranteed to terminate (either by erroring or successfully matching inputs and outputs).
when I have more confidence and understanding of the code flow inside the loop and the edge cases all being covered I will probably be for a loop
instead of for
for simplicity 😂
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.
Really great work! Some minor notes from my side
refactor (borngraced): add_change simplified, remove tx_fee mut, use checked_sub
Fixes for utxo tx fee calculation in UtxoTxBuilder::build() fn:
UtxoTxBuilder::build() code was refactored to implement fixed txfee calc: made it similar to daemon code (with a loop), also broke it into functions.
Existing tests updated for the fixed txfee.
Fixes issue: #2313 (a test added to validate txfee from the issue).
Should also fix #1567 issue.
TODO: fix input size for segwit
@cipig