-
Notifications
You must be signed in to change notification settings - Fork 28
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 reward payment tx fee calculation #427
Conversation
This breaks up the monolithic testPaymentMgrPayment, extracting the bits specific to testing the applyTxFees func. The goal is to make updating the tests less cumbersome. Two new tests are introduced, one for the core applyTxFees functionality, and one for its error conditions.
The applyTxFees func previously took in a set of outputs as one param, and the total value of those outputs as a separate param. This introduced an opportunity for the function to be misued in that it could be passed a set of outputs and a total which do not match. An instance of this mistake actually existed in the tests for applyTxFees. The value of the inputs and outputs was 100 DCR, but the provided total was 5 DCR.
The test can now be run multiple times with different values so that applyFee can be tested more thoroughly. This also enables characterizing the existing behaviour of applyTxFees. Using absolute values in the tests means that assertions are more precise, and any issues are more clearly visible.
This extracts fee estimation into a func which can be reused later, as well as fixing an issue which caused inSizes and outSizes to be twice as long as intended, leading to fees being overestimated.
pool/paymentmgr.go
Outdated
@@ -456,20 +456,24 @@ func (pm *PaymentMgr) applyTxFees(inputs []chainjson.TransactionInput, outputs m | |||
} | |||
|
|||
var tOut dcrutil.Amount | |||
for _, v := range outputs { | |||
var poolFee dcrutil.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.
This section is a significant improvement as it fixes the primary issue, but it's still not handling extra atoms leftover as the result of non-even divisions properly. Moreover, the rounding behavior of MulF64
could lead to underpayment of the transaction fee because it might round down which would lead to one less atom being subtracted than is necessary thereby effectively reducing the fee by 1.
To illustrate this, consider a total fee of 41 atoms, but with a proportional split of 37.5%, 32.5%, and 30%. Using the existing code here would produce:
Output 1: round(41 * .375) = 15
Output 2: round(41 * .325) = 13
Output 3: round(41 * .3) = 12
Now, if you add those up, 15+13+12 = 40
, which is not the required 41 atoms. In other words, the tx would be rejected for not paying enough fees.
That is why the rounding behavior should be avoided completely in favor of taking the floor at all times and making up for the difference as necessary via applying the remaining additional atoms to random outputs. Alternatively, it could always take the ceiling, but that would result in overpayment in some scenarios, so I don't recommend it.
I believe the following rewritten code should handle everything properly, however, do note that the tests will need to be updated to ensure that the expected amount can either be the existing values or one less (because the fee might have been increased by one thus reducing the output amount by one):
func (pm *PaymentMgr) applyTxFees(inputs []chainjson.TransactionInput, outputs map[string]dcrutil.Amount,
feeAddr stdaddr.Address) (dcrutil.Amount, error) {
const funcName = "applyTxFees"
if len(inputs) == 0 {
desc := fmt.Sprintf("%s: cannot create a payout transaction "+
"without a tx input", funcName)
return 0, errs.PoolError(errs.TxIn, desc)
}
if len(outputs) == 0 {
desc := fmt.Sprintf("%s: cannot create a payout transaction "+
"without a tx output", funcName)
return 0, errs.PoolError(errs.TxOut, desc)
}
estTxFee := estimateTxFee(len(inputs), len(outputs))
// Determine the total output amount without the pool fee.
var outputAmountSansPoolFee dcrutil.Amount
for addr, v := range outputs {
if addr == feeAddr.String() {
continue
}
outputAmountSansPoolFee += v
}
// Determine the proportional transaction fees for each output other than
// the pool fee. Note that in order to avoid overpayment of the fee, each
// proportional fee is calculated via a floor. This means there will still
// be a few atoms leftover to account for when the fee is not evenly
// divisible by the number of outputs.
var txFeesPaid dcrutil.Amount
proportionalTxFees := make(map[string]dcrutil.Amount)
for addr, v := range outputs {
// Pool fee payments are excluded from tx fee deductions.
if addr == feeAddr.String() {
continue
}
ratio := float64(v) / float64(outputAmountSansPoolFee)
outFee := dcrutil.Amount(math.Floor(float64(estTxFee) * ratio))
proportionalTxFees[addr] = outFee
txFeesPaid += outFee
}
// Apply the fees to the outputs while accounting for any extra atoms needed
// to reach the required transaction fee by reducing random outputs
// accordingly.
remainingTxFeeAtoms := estTxFee - txFeesPaid
for addr, fee := range proportionalTxFees {
outputs[addr] -= fee
if remainingTxFeeAtoms > 0 {
outputs[addr]--
remainingTxFeeAtoms--
}
}
return estTxFee, nil
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.
Here is a diff that updates the tests per my comments and code:
diff --git a/pool/paymentmgr_test.go b/pool/paymentmgr_test.go
index f07f846..9a1706f 100644
--- a/pool/paymentmgr_test.go
+++ b/pool/paymentmgr_test.go
@@ -512,7 +512,7 @@ func testPaymentMgrApplyTxFees(t *testing.T) {
},
expectedOutputs: map[string]dcrutil.Amount{
feeAddr: dcrutil.Amount(50000000),
- "user1": dcrutil.Amount(299997853),
+ "user1": dcrutil.Amount(299997854),
"user2": dcrutil.Amount(149998927),
},
},
@@ -587,9 +587,11 @@ func testPaymentMgrApplyTxFees(t *testing.T) {
// Validate outputs have been modified as expected.
for addr, amt := range test.outputs {
- if amt != test.expectedOutputs[addr] {
- t.Fatalf("expected payment for %s to be %v, got %v",
- addr, test.expectedOutputs[addr], amt)
+ wantAmt1 := test.expectedOutputs[addr]
+ wantAmt2 := wantAmt1 - 1
+ if amt != wantAmt1 && amt != wantAmt2 {
+ t.Fatalf("expected payment for %s to be %v or %v, got %v",
+ addr, wantAmt1, wantAmt2, amt)
}
}
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.
Amazing, thanks Dave. I've replaced the last commit in the PR to include this code.
There was a couple of problems in applyTxFees which lead to fees being greatly overestimated. - The division to find the ratio of a single output to the total output amount was inverted. - The pool fee should not be subject to paying fees, however the txfee was being subtracted from the total output amount rather than the pool fee. A new assertion in the applyTxFees test ensures the deductions from output amounts matches matches the expected fee to be paid.
88955d5
to
9b692c8
Compare
User @sebitt27 reported that his pool reward payments were paying extremely large fees (~0.074 DCR/kB). The cause was several bugs in the tx construction code, specifically in the function
applyTxFees
.This PR starts by refactoring the tests for
applyTxFees
such that the existing behaviour can be characterized, and then goes on to fix the issues. The impact of the fixes (in the last two commits) can be observed by following the changes to the expected output amounts in the tests.Using this new code the fee rate of the transaction shared by @sebitt27 is reduced to 0.000106216 DCR/kB, very close to the ideal value of 0.0001 DCR/kB.