diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -3,3 +3,6 @@ This release includes the following features and fixes: + - Update fee calculation to add 179 effective bytes per transaction output in excess of inputs. + Refund 179 bytes worth of minimum fee per input in excess of outputs to a minimum of + 10 + 34 * (number of utxos) diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -312,6 +312,10 @@ // size) unsigned int CalculateModifiedSize(unsigned int nTxSize = 0) const; + // Computes an adjusted tx size so that the UTXIs are billed partially + // upfront. + size_t GetBillableSize() const; + /** * Get the total transaction size in bytes. * @return Total transaction size in bytes diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -109,6 +109,24 @@ return nTxSize; } +size_t CTransaction::GetBillableSize() const { + size_t nTxSize = GetTotalSize(), inputs = vin.size(), outputs = vout.size(); + + // 179 bytes is the minimum size it would take to spend any outputs which + // are created. We want to change in advance of spending them to + // incentivize keeping your UTXO set reasonbly sized. + int64_t modSize = + int64_t(nTxSize) + (int64_t(outputs) - int64_t(inputs)) * 179; + + // Note: It is impossible to generate a negative number above in any real + // world situation. This is because the inputs have a least 179 byte + // each. However, it is possible to have shorter scriptSigs than 179 + // bytes. Therefore, we include a minimum of 10 bytes + 34 * vouts. + nTxSize = std::max(int64_t(outputs * 34 + 10), modSize); + + return nTxSize; +} + unsigned int CTransaction::GetTotalSize() const { return ::GetSerializeSize(*this, SER_NETWORK, PROTOCOL_VERSION); } diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -757,4 +757,24 @@ BOOST_CHECK(!IsStandardTx(CTransaction(t), reason)); } +BOOST_AUTO_TEST_CASE(tx_transaction_fee) { + std::vector sizes = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512}; + for (size_t inputs : sizes) { + for (size_t outputs : sizes) { + CMutableTransaction mtx; + mtx.vin.resize(inputs); + mtx.vout.resize(outputs); + CTransaction tx(mtx); + auto bs = tx.GetBillableSize(); + auto s = tx.GetTotalSize(); + BOOST_CHECK(bs > 0); + if (inputs > outputs) { + BOOST_CHECK(bs < s); + } else { + BOOST_CHECK(bs >= s); + } + } + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -909,6 +909,7 @@ chainActive.Height(), inChainInputValue, fSpendsCoinbase, nSigOpsCount, lp); unsigned int nSize = entry.GetTxSize(); + size_t feeSize = tx.GetBillableSize(); // Check that the transaction doesn't have an excessive number of // sigops, making it impossible to mine. Since the coinbase transaction @@ -926,7 +927,7 @@ pool.GetMinFee( gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000) - .GetFee(nSize); + .GetFee(feeSize); if (mempoolRejectFee > Amount(0) && nModifiedFees < mempoolRejectFee) { return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "mempool min fee not met", false, @@ -934,7 +935,7 @@ } if (gArgs.GetBoolArg("-relaypriority", DEFAULT_RELAYPRIORITY) && - nModifiedFees < minRelayTxFee.GetFee(nSize) && + nModifiedFees < minRelayTxFee.GetFee(feeSize) && !AllowFree(entry.GetPriority(chainActive.Height() + 1))) { // Require that free transactions have sufficient priority to be // mined in the next block. @@ -946,7 +947,7 @@ // This mitigates 'penny-flooding' -- sending thousands of free // transactions just to be annoying or make others' transactions take // longer to confirm. - if (fLimitFree && nModifiedFees < minRelayTxFee.GetFee(nSize)) { + if (fLimitFree && nModifiedFees < minRelayTxFee.GetFee(feeSize)) { static CCriticalSection csFreeLimiter; static double dFreeCount; static int64_t nLastTime; @@ -959,6 +960,9 @@ nLastTime = nNow; // -limitfreerelay unit is thousand-bytes-per-minute // At default rate it would take over a month to fill 1GB + + // NOTE: Use the actual size here, and not the fee size since this + // is counting real size for the rate limiter. if (dFreeCount + nSize >= gArgs.GetArg("-limitfreerelay", DEFAULT_LIMITFREERELAY) * 10 * 1000) { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2916,6 +2916,12 @@ CTransaction txNewConst(txNew); unsigned int nBytes = txNewConst.GetTotalSize(); + + // Note: The relaying code has been changed to charge upfront for + // the minimum required bytes to spend a UTXO. This means that + // we need to calculate possible fees based that size. + size_t feeBytes = txNewConst.GetBillableSize(); + dPriority = txNewConst.ComputePriority(dPriority, nBytes); // Remove scriptSigs to eliminate the fee calculation dummy @@ -2932,20 +2938,20 @@ } Amount nFeeNeeded = - GetMinimumFee(nBytes, currentConfirmationTarget, mempool); + GetMinimumFee(feeBytes, currentConfirmationTarget, mempool); if (coinControl && nFeeNeeded > Amount(0) && coinControl->nMinimumTotalFee > nFeeNeeded) { nFeeNeeded = coinControl->nMinimumTotalFee; } if (coinControl && coinControl->fOverrideFeeRate) { - nFeeNeeded = coinControl->nFeeRate.GetFee(nBytes); + nFeeNeeded = coinControl->nFeeRate.GetFee(feeBytes); } // If we made it here and we aren't even able to meet the relay fee // on the next pass, give up because we must be at the maximum // allowed fee. - Amount minFee = GetConfig().GetMinFeePerKB().GetFee(nBytes); + Amount minFee = GetConfig().GetMinFeePerKB().GetFee(feeBytes); if (nFeeNeeded < minFee) { strFailReason = _("Transaction too large for fee policy"); return false; diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -21,7 +21,7 @@ relayfee = self.nodes[0].getnetworkinfo()['relayfee'] txids = [] - utxos = create_confirmed_utxos(relayfee, self.nodes[0], 91) + utxos = create_confirmed_utxos(relayfee, self.nodes[0], 121) # create a mempool tx that will be evicted us0 = utxos.pop() @@ -38,7 +38,7 @@ relayfee = self.nodes[0].getnetworkinfo()['relayfee'] base_fee = relayfee * 100 - for i in range(3): + for i in range(4): txids.append([]) txids[i] = create_lots_of_big_transactions( self.nodes[0], txouts, utxos[30 * i:30 * i + 30], 30, (i + 1) * base_fee) diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -402,6 +402,10 @@ self.sha256 = None self.hash = None + def billable_size(self): + tx_size = len(ToHex(self)) + len(self.vout)*179 - len(self.vin)*179 + return max(tx_size, 10 + 34 * len(self.vout)) + def serialize(self): r = b"" r += struct.pack("