-
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. |
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) |
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) |
} | ||
|
||
/// 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)
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; | |
} |
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.
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).
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?
@@ -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
.
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())
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?
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)?
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.
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.
Really great work! Some minor notes from my side
fn validate_outputs(&self) -> MmResult<(), GenerateTxError> { | ||
for output in self.outputs.iter() { | ||
let script: Script = output.script_pubkey.clone().into(); | ||
if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { | ||
return_err_if_false!(output.value >= self.dust(), GenerateTxError::OutputValueLessThanDust { | ||
value: output.value, | ||
dust: self.dust() | ||
}); | ||
} | ||
} | ||
Ok(()) |
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 really mean to return Ok if there's no output to validate?
change | ||
}; | ||
|
||
self.sum_outputs -= self.subtract_outputs_by_txfee()?; |
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 we use checked_sub
here since we didn't validate possible overflowing.
https://doc.rust-lang.org/src/core/num/mod.rs.html#1223-1241
let mut tx_fee = fee_per_kb.get_tx_fee(tx_size); | ||
if coin.as_ref().conf.force_min_relay_fee { | ||
let relay_fee = coin.as_ref().rpc_client.get_relay_fee().compat().await?; | ||
let relay_fee_sat = sat_from_big_decimal(&relay_fee, coin.as_ref().decimals)?; | ||
if fee < relay_fee_sat { | ||
fee = relay_fee_sat; | ||
let min_relay_fee_per_kb = coin.as_ref().rpc_client.get_relay_fee().compat().await?; | ||
let min_relay_fee_per_kb = sat_from_big_decimal(&min_relay_fee_per_kb, coin.as_ref().decimals)?; | ||
let min_relay_dynamic_fee = ActualTxFee::Dynamic(min_relay_fee_per_kb); | ||
let min_relay_tx_fee = min_relay_dynamic_fee.get_tx_fee(tx_size); | ||
if tx_fee < min_relay_tx_fee { | ||
tx_fee = min_relay_tx_fee; | ||
} | ||
} | ||
Ok(fee) | ||
Ok(tx_fee) |
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 can avoid mutation.
let tx_fee = fee_per_kb.get_tx_fee(tx_size);
if coin.as_ref().conf.force_min_relay_fee {
let min_relay_fee_per_kb = coin.as_ref().rpc_client.get_relay_fee().compat().await?;
let min_relay_fee_per_kb = sat_from_big_decimal(&min_relay_fee_per_kb, coin.as_ref().decimals)?;
let min_relay_dynamic_fee = ActualTxFee::Dynamic(min_relay_fee_per_kb);
let min_relay_tx_fee = min_relay_dynamic_fee.get_tx_fee(tx_size);
if tx_fee < min_relay_tx_fee {
return Ok(min_relay_tx_fee);
}
}
Ok(tx_fee)
if self.sum_inputs > self.sum_outputs + self.total_tx_fee() { | ||
let change = self.sum_inputs - (self.sum_outputs + self.total_tx_fee()); | ||
if change > self.dust() { | ||
self.tx.outputs.push({ | ||
TransactionOutput { | ||
value: change, | ||
script_pubkey: change_script_pubkey.clone(), | ||
} | ||
true | ||
} else { | ||
false | ||
} | ||
}); | ||
(change, 0u64) | ||
} else { | ||
(0u64, change) | ||
} | ||
} else { | ||
(0u64, 0u64) | ||
} | ||
} |
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 about ?
fn add_change(&mut self, change_script_pubkey: &Bytes) -> (u64, u64) {
let sum_output_with_fee = self.sum_outputs + self.total_tx_fee();
if self.sum_inputs < sum_output_with_fee {
return (0u64, 0u64);
}
let change = self.sum_inputs - sum_output_with_fee;
if change < self.dust() {
return (0u64, change)
};
self.tx.outputs.push({
TransactionOutput {
value: change,
script_pubkey: change_script_pubkey.clone(),
}
});
(change, 0u64)
}
@@ -74,7 +74,7 @@ pub const DEFAULT_SWAP_VOUT: usize = 0; | |||
pub const DEFAULT_SWAP_VIN: usize = 0; | |||
const MIN_BTC_TRADING_VOL: &str = "0.00777"; | |||
|
|||
macro_rules! true_or { | |||
macro_rules! return_err_if_false { |
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 true_or_err
is a better name
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.
@cipig