Eliminate TX trickle bypass, sort TX invs for privacy and priority.

Previously Bitcoin would send 1/4 of transactions out to all peers
 instantly.  This causes high overhead because it makes >80% of
 INVs size 1.  Doing so harms privacy, because it limits the
 amount of source obscurity a transaction can receive.

These randomized broadcasts also disobeyed transaction dependencies
 and required use of the orphan pool.  Because the orphan pool is
 so small this leads to poor propagation for dependent transactions.

When the bypass wasn't in effect, transactions were sent in the
 order they were received.  This avoided creating orphans but
 undermines privacy fairly significantly.

This commit:
 Eliminates the bypass. The bypass is replaced by halving the
  average delay for outbound peers.

 Sorts candidate transactions for INV by their topological
  depth then by their feerate (then hash); removing the
  information leakage and providing priority service to
  higher fee transactions.

 Limits the amount of transactions sent in a single INV to
  7tx/sec (and twice that for outbound); this limits the
  harm of low fee transaction floods, gives faster relay
  service to higher fee transactions. The 7 sounds lower
  than it really is because received advertisements need
  not be sent, and because the aggregate rate is multipled
  by the number of peers.

(cherry picked from commit f2d3ba73860e875972738d1da1507124d0971ae5)

Zcash: Candidate transactions for INV are not sorted by their
topological depth because we haven't backported bitcoin/bitcoin#6654.
This commit is contained in:
Gregory Maxwell 2016-04-04 02:36:47 +00:00 committed by Jack Grigg
parent 543c909166
commit 2a7ebb3aba
5 changed files with 71 additions and 30 deletions

View File

@ -4,8 +4,14 @@
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, initialize_chain_clean, \
start_node, connect_nodes
from test_framework.util import (
assert_equal,
connect_nodes,
initialize_chain_clean,
start_node,
sync_blocks,
sync_mempools,
)
from test_framework.mininode import COIN
import time
@ -39,7 +45,11 @@ class PrioritiseTransactionTest (BitcoinTestFramework):
for _ in range(900):
self.nodes[0].sendtoaddress(taddr, 0.1)
self.nodes[0].generate(1)
self.sync_all()
sync_blocks(self.nodes)
# With a rate of either 7tx/s or 14tx/s per peer (depending on whether
# the connection is inbound or outbound), syncing this many transactions
# could take up to 128s. So use a higher timeout on the mempool sync.
sync_mempools(self.nodes, timeout=150)
# Create tx of lower value to be prioritized on node 0
# Older transactions get mined first, so this lower value, newer tx is unlikely to be mined without prioritisation

View File

@ -7294,6 +7294,29 @@ bool ProcessMessages(const CChainParams& chainparams, CNode* pfrom)
return fOk;
}
class CompareInvMempoolOrder
{
CTxMemPool *mp;
public:
CompareInvMempoolOrder(CTxMemPool *mempool)
{
mp = mempool;
}
bool operator()(const CInv &a, const CInv &b)
{
if (a.type != MSG_TX && b.type != MSG_TX) {
return false;
} else {
if (a.type != MSG_TX) {
return true;
} else if (b.type != MSG_TX) {
return false;
}
return mp->CompareDepthAndScore(a.hash, b.hash);
}
}
};
bool SendMessages(const Consensus::Params& params, CNode* pto)
{
@ -7429,42 +7452,31 @@ bool SendMessages(const Consensus::Params& params, CNode* pto)
bool fSendTrickle = pto->fWhitelisted;
if (pto->nNextInvSend < nNow) {
fSendTrickle = true;
pto->nNextInvSend = PoissonNextSend(nNow, AVG_INVENTORY_BROADCAST_INTERVAL);
// Use half the delay for outbound peers, as their is less privacy concern for them.
pto->nNextInvSend = PoissonNextSend(nNow, INVENTORY_BROADCAST_INTERVAL >> !pto->fInbound);
}
LOCK(pto->cs_inventory);
vInv.reserve(std::min<size_t>(1000, pto->vInventoryToSend.size()));
if (fSendTrickle && pto->vInventoryToSend.size() > 1) {
// Topologically and fee-rate sort the inventory we send for privacy and priority reasons.
CompareInvMempoolOrder compareInvMempoolOrder(&mempool);
std::stable_sort(pto->vInventoryToSend.begin(), pto->vInventoryToSend.end(), compareInvMempoolOrder);
}
vInv.reserve(std::min<size_t>(INVENTORY_BROADCAST_MAX, pto->vInventoryToSend.size()));
vInvWait.reserve(pto->vInventoryToSend.size());
for (const CInv& inv : pto->vInventoryToSend)
{
if (inv.type == MSG_TX && pto->filterInventoryKnown.contains(inv.hash))
continue;
// trickle out tx inv to protect privacy
if (inv.type == MSG_TX && !fSendTrickle)
{
// 1/4 of tx invs blast to all immediately
static uint256 hashSalt;
if (hashSalt.IsNull())
hashSalt = GetRandHash();
uint256 hashRand = ArithToUint256(UintToArith256(inv.hash) ^ UintToArith256(hashSalt));
hashRand = Hash(BEGIN(hashRand), END(hashRand));
bool fTrickleWait = ((UintToArith256(hashRand) & 3) != 0);
if (fTrickleWait)
{
vInvWait.push_back(inv);
continue;
}
// No reason to drain out at many times the network's capacity,
// especially since we have many peers and some will draw much shorter delays.
if (vInv.size() >= INVENTORY_BROADCAST_MAX || (inv.type == MSG_TX && !fSendTrickle)) {
vInvWait.push_back(inv);
continue;
}
pto->filterInventoryKnown.insert(inv.hash);
vInv.push_back(inv);
if (vInv.size() >= 1000)
{
pto->PushMessage("inv", vInv);
vInv.clear();
}
}
pto->vInventoryToSend = vInvWait;
}

View File

@ -118,9 +118,12 @@ static const unsigned int MAX_REJECT_MESSAGE_LENGTH = 111;
static const unsigned int AVG_LOCAL_ADDRESS_BROADCAST_INTERVAL = 24 * 24 * 60;
/** Average delay between peer address broadcasts in seconds. */
static const unsigned int AVG_ADDRESS_BROADCAST_INTERVAL = 30;
/** Average delay between trickled inventory broadcasts in seconds.
* Blocks, whitelisted receivers, and a random 25% of transactions bypass this. */
static const unsigned int AVG_INVENTORY_BROADCAST_INTERVAL = 5;
/** Average delay between trickled inventory transmissions in seconds.
* Blocks and whitelisted receivers bypass this, outbound peers get half this delay. */
static const unsigned int INVENTORY_BROADCAST_INTERVAL = 5;
/** Maximum number of inventory items to send per transmission.
* Limits the impact of low-fee transaction floods. */
static const unsigned int INVENTORY_BROADCAST_MAX = 7 * INVENTORY_BROADCAST_INTERVAL;
static const unsigned int DEFAULT_LIMITFREERELAY = 15;
static const bool DEFAULT_RELAYPRIORITY = false;

View File

@ -674,6 +674,21 @@ void CTxMemPool::checkNullifiers(ShieldedType type) const
}
}
bool CTxMemPool::CompareDepthAndScore(const uint256& hasha, const uint256& hashb)
{
LOCK(cs);
indexed_transaction_set::const_iterator i = mapTx.find(hasha);
if (i == mapTx.end()) return false;
indexed_transaction_set::const_iterator j = mapTx.find(hashb);
if (j == mapTx.end()) return true;
// We don't actually compare by depth here because we haven't backported
// https://github.com/bitcoin/bitcoin/pull/6654
//
// But the method name is left as-is because renaming it is not worth the
// merge conflicts.
return CompareTxMemPoolEntryByScore()(*i, *j);
}
void CTxMemPool::queryHashes(std::vector<uint256>& vtxid)
{
vtxid.clear();

View File

@ -268,6 +268,7 @@ public:
void removeWithoutBranchId(uint32_t nMemPoolBranchId);
void clear();
void _clear(); // unlocked
bool CompareDepthAndScore(const uint256& hasha, const uint256& hashb);
void queryHashes(std::vector<uint256>& vtxid);
void pruneSpent(const uint256& hash, CCoins &coins);
unsigned int GetTransactionsUpdated() const;