Browse Source

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)
0.13
Peter Todd 9 years ago
parent
commit
5891f870d6
  1. 126
      src/main.cpp

126
src/main.cpp

@ -831,15 +831,42 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa @@ -831,15 +831,42 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
return state.Invalid(false, REJECT_ALREADY_KNOWN, "txn-already-in-mempool");
// Check for conflicts with in-memory transactions
set<uint256> setConflicts;
{
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(outpoint))
if (pool.mapNextTx.count(txin.prevout))
{
// Disable replacement feature for now
return state.Invalid(false, REJECT_CONFLICT, "txn-mempool-conflict");
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");
setConflicts.insert(ptxConflicting->GetHash());
}
}
}
}
@ -957,6 +984,82 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa @@ -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);
}
// 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
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
if (!CheckInputs(tx, state, view, true, STANDARD_SCRIPT_VERIFY_FLAGS, true))
@ -977,6 +1080,19 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa @@ -977,6 +1080,19 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
__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
pool.addUnchecked(hash, entry, setAncestors, !IsInitialBlockDownload());

Loading…
Cancel
Save