Add opt-in full-RBF to mempool
Replaces transactions already in the mempool if a new transaction seen with a higher fee, specifically both a higher fee per KB and a higher absolute fee. Children are evaluateed for replacement as well, using the mempool package tracking to calculate replaced fees/size. Transactions can opt-out of transaction replacement by setting nSequence >= maxint-1 on all inputs. (which all wallets do already)
This commit is contained in:
parent
de7d4591a7
commit
5891f870d6
1 changed files with 121 additions and 5 deletions
124
src/main.cpp
124
src/main.cpp
|
@ -831,15 +831,42 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
|
||||||
return state.Invalid(false, REJECT_ALREADY_KNOWN, "txn-already-in-mempool");
|
return state.Invalid(false, REJECT_ALREADY_KNOWN, "txn-already-in-mempool");
|
||||||
|
|
||||||
// Check for conflicts with in-memory transactions
|
// Check for conflicts with in-memory transactions
|
||||||
|
set<uint256> setConflicts;
|
||||||
{
|
{
|
||||||
LOCK(pool.cs); // protect pool.mapNextTx
|
LOCK(pool.cs); // protect pool.mapNextTx
|
||||||
for (unsigned int i = 0; i < tx.vin.size(); i++)
|
BOOST_FOREACH(const CTxIn &txin, tx.vin)
|
||||||
{
|
{
|
||||||
COutPoint outpoint = tx.vin[i].prevout;
|
if (pool.mapNextTx.count(txin.prevout))
|
||||||
if (pool.mapNextTx.count(outpoint))
|
|
||||||
{
|
{
|
||||||
// Disable replacement feature for now
|
const CTransaction *ptxConflicting = pool.mapNextTx[txin.prevout].ptx;
|
||||||
|
if (!setConflicts.count(ptxConflicting->GetHash()))
|
||||||
|
{
|
||||||
|
// Allow opt-out of transaction replacement by setting
|
||||||
|
// nSequence >= maxint-1 on all inputs.
|
||||||
|
//
|
||||||
|
// maxint-1 is picked to still allow use of nLockTime by
|
||||||
|
// non-replacable transactions. All inputs rather than just one
|
||||||
|
// is for the sake of multi-party protocols, where we don't
|
||||||
|
// want a single party to be able to disable replacement.
|
||||||
|
//
|
||||||
|
// The opt-out ignores descendants as anyone relying on
|
||||||
|
// first-seen mempool behavior should be checking all
|
||||||
|
// unconfirmed ancestors anyway; doing otherwise is hopelessly
|
||||||
|
// insecure.
|
||||||
|
bool fReplacementOptOut = true;
|
||||||
|
BOOST_FOREACH(const CTxIn &txin, ptxConflicting->vin)
|
||||||
|
{
|
||||||
|
if (txin.nSequence < std::numeric_limits<unsigned int>::max()-1)
|
||||||
|
{
|
||||||
|
fReplacementOptOut = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fReplacementOptOut)
|
||||||
return state.Invalid(false, REJECT_CONFLICT, "txn-mempool-conflict");
|
return state.Invalid(false, REJECT_CONFLICT, "txn-mempool-conflict");
|
||||||
|
|
||||||
|
setConflicts.insert(ptxConflicting->GetHash());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -957,6 +984,82 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
|
||||||
return state.DoS(0, false, REJECT_NONSTANDARD, "too-long-mempool-chain", false, errString);
|
return state.DoS(0, false, REJECT_NONSTANDARD, "too-long-mempool-chain", false, errString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A transaction that spends outputs that would be replaced by it is invalid. Now
|
||||||
|
// that we have the set of all ancestors we can detect this
|
||||||
|
// pathological case by making sure setConflicts and setAncestors don't
|
||||||
|
// intersect.
|
||||||
|
BOOST_FOREACH(CTxMemPool::txiter ancestorIt, setAncestors)
|
||||||
|
{
|
||||||
|
const uint256 &hashAncestor = ancestorIt->GetTx().GetHash();
|
||||||
|
if (setConflicts.count(hashAncestor))
|
||||||
|
{
|
||||||
|
return state.DoS(10, error("AcceptToMemoryPool: %s spends conflicting transaction %s",
|
||||||
|
hash.ToString(),
|
||||||
|
hashAncestor.ToString()),
|
||||||
|
REJECT_INVALID, "bad-txns-spends-conflicting-tx");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's economically rational to mine this transaction rather
|
||||||
|
// than the ones it replaces.
|
||||||
|
CAmount nConflictingFees = 0;
|
||||||
|
size_t nConflictingSize = 0;
|
||||||
|
if (setConflicts.size())
|
||||||
|
{
|
||||||
|
LOCK(pool.cs);
|
||||||
|
|
||||||
|
// For efficiency we simply sum up the pre-calculated
|
||||||
|
// fees/size-with-descendants values from the mempool package
|
||||||
|
// tracking; this does mean the pathological case of diamond tx
|
||||||
|
// graphs will be overcounted.
|
||||||
|
BOOST_FOREACH(const uint256 hashConflicting, setConflicts)
|
||||||
|
{
|
||||||
|
CTxMemPool::txiter mi = pool.mapTx.find(hashConflicting);
|
||||||
|
if (mi == pool.mapTx.end())
|
||||||
|
continue;
|
||||||
|
nConflictingFees += mi->GetFeesWithDescendants();
|
||||||
|
nConflictingSize += mi->GetSizeWithDescendants();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First of all we can't allow a replacement unless it pays greater
|
||||||
|
// fees than the transactions it conflicts with - if we did the
|
||||||
|
// bandwidth used by those conflicting transactions would not be
|
||||||
|
// paid for
|
||||||
|
if (nFees < nConflictingFees)
|
||||||
|
{
|
||||||
|
return state.DoS(0, error("AcceptToMemoryPool: rejecting replacement %s, less fees than conflicting txs; %s < %s",
|
||||||
|
hash.ToString(), FormatMoney(nFees), FormatMoney(nConflictingFees)),
|
||||||
|
REJECT_INSUFFICIENTFEE, "insufficient fee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondly in addition to paying more fees than the conflicts the
|
||||||
|
// new transaction must additionally pay for its own bandwidth.
|
||||||
|
CAmount nDeltaFees = nFees - nConflictingFees;
|
||||||
|
if (nDeltaFees < ::minRelayTxFee.GetFee(nSize))
|
||||||
|
{
|
||||||
|
return state.DoS(0,
|
||||||
|
error("AcceptToMemoryPool: rejecting replacement %s, not enough additional fees to relay; %s < %s",
|
||||||
|
hash.ToString(),
|
||||||
|
FormatMoney(nDeltaFees),
|
||||||
|
FormatMoney(::minRelayTxFee.GetFee(nSize))),
|
||||||
|
REJECT_INSUFFICIENTFEE, "insufficient fee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally replace only if we end up with a larger fees-per-kb than
|
||||||
|
// the replacements.
|
||||||
|
CFeeRate oldFeeRate(nConflictingFees, nConflictingSize);
|
||||||
|
CFeeRate newFeeRate(nFees, nSize);
|
||||||
|
if (newFeeRate <= oldFeeRate)
|
||||||
|
{
|
||||||
|
return state.DoS(0,
|
||||||
|
error("AcceptToMemoryPool: rejecting replacement %s; new feerate %s <= old feerate %s",
|
||||||
|
hash.ToString(),
|
||||||
|
newFeeRate.ToString(),
|
||||||
|
oldFeeRate.ToString()),
|
||||||
|
REJECT_INSUFFICIENTFEE, "insufficient fee");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check against previous transactions
|
// Check against previous transactions
|
||||||
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
|
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
|
||||||
if (!CheckInputs(tx, state, view, true, STANDARD_SCRIPT_VERIFY_FLAGS, true))
|
if (!CheckInputs(tx, state, view, true, STANDARD_SCRIPT_VERIFY_FLAGS, true))
|
||||||
|
@ -977,6 +1080,19 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
|
||||||
__func__, hash.ToString(), FormatStateMessage(state));
|
__func__, hash.ToString(), FormatStateMessage(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove conflicting transactions from the mempool
|
||||||
|
list<CTransaction> ltxConflicted;
|
||||||
|
pool.removeConflicts(tx, ltxConflicted);
|
||||||
|
|
||||||
|
BOOST_FOREACH(const CTransaction &txConflicted, ltxConflicted)
|
||||||
|
{
|
||||||
|
LogPrint("mempool", "replacing tx %s with %s for %s BTC additional fees, %d delta bytes\n",
|
||||||
|
txConflicted.GetHash().ToString(),
|
||||||
|
hash.ToString(),
|
||||||
|
FormatMoney(nFees - nConflictingFees),
|
||||||
|
(int)nSize - (int)nConflictingSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Store transaction in memory
|
// Store transaction in memory
|
||||||
pool.addUnchecked(hash, entry, setAncestors, !IsInitialBlockDownload());
|
pool.addUnchecked(hash, entry, setAncestors, !IsInitialBlockDownload());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue