Skip to content

Commit

Permalink
Merge bitcoin#18044: Use wtxid for transaction relay
Browse files Browse the repository at this point in the history
0a4f142 Further improve comments around recentRejects (Suhas Daftuar)
0e20cfe Disconnect peers sending wtxidrelay message after VERACK (Suhas Daftuar)
cacd852 test: Use wtxid relay generally in functional tests (Fabian Jahr)
8d8099e test: Add tests for wtxid tx relay in segwit test (Fabian Jahr)
9a5392f test: Update test framework p2p protocol version to 70016 (Fabian Jahr)
dd78d1d Rename AddInventoryKnown() to AddKnownTx() (Suhas Daftuar)
4eb5155 Make TX_WITNESS_STRIPPED its own rejection reason (Suhas Daftuar)
97141ca Delay getdata requests from peers using txid-based relay (Suhas Daftuar)
46d78d4 Add p2p message "wtxidrelay" (Suhas Daftuar)
2d282e0 ignore non-wtxidrelay compliant invs (Anthony Towns)
ac88e2e Add support for tx-relay via wtxid (Suhas Daftuar)
8e68fc2 Add wtxids to recentRejects instead of txids (Suhas Daftuar)
144c385 Add wtxids of confirmed transactions to bloom filter (Suhas Daftuar)
85c78d5 Add wtxid-index to orphan map (Suhas Daftuar)
08b3995 Add a wtxid-index to mapRelay (Suhas Daftuar)
60f0acd Just pass a hash to AddInventoryKnown (Suhas Daftuar)
c7eb6b4 Add wtxid to mempool unbroadcast tracking (Amiti Uttarwar)
2b4b90a Add a wtxid-index to the mempool (Suhas Daftuar)

Pull request description:

  Using txids (a transaction's hash, without witness) for transaction relay is problematic, post-segwit -- if a peer gives us a segwit transaction that fails policy checks, it could be because the txid associated with the transaction is definitely unacceptable to our node (regardless of the witness), or it could be that the transaction was malleated and with a different witness, the txid could be accepted to our mempool.

  We have a bloom filter of recently rejected transactions, whose purpose is to help us avoid redownloading and revalidating transactions that fail to be accepted, but because of this potential for witness malleability to interfere with relay of valid transactions, we do not use the filter for segwit transactions.  This issue is discussed at some length in bitcoin#8279.  The effect of this is that whenever a segwit transaction that fails policy checks is relayed, a node would download that transaction from every peer announcing it, because it has no way presently to cache failure.  Historically this hasn't been a big problem, but if/when policy for accepting segwit transactions were to change (eg taproot, or any other change), we could expect older nodes talking to newer nodes to be wasting bandwidth because of this.

  As discussed in that issue, switching to wtxid-based relay solves this problem -- by using an identifier for a transaction that commits to all the data in our relay protocol, we can be certain if a transaction that a peer is announcing is one that we've already tried to process, or if it's something new.  This PR introduces support for wtxid-based relay with peers that support it (and remains backwards compatible with peers that use txids for relay, of course).

  Apart from code correctness, one issue to be aware of is that by downloading from old and new peers alike, we should expect there to be some bandwidth wasted, because sometimes we might download the same transaction via txid-relay as well as wtxid-relay.  The last commit in this PR implements a heuristic I want to analyze, which is to just delay relay from txid-relay peers by 2 seconds, if we have at least 1 wtxid-based peer.  I've just started running a couple nodes with this heuristic so I can measure how well it works, but I'm open to other ideas for minimizing that issue.  In the long run, I think this will be essentially a non-issue, so I don't think it's too big a concern, we just need to bite the bullet and deal with it during upgrade.

  Finally, this proposal would need a simple BIP describing the changes, which I haven't yet drafted.  However, review and testing of this code in the interim would be welcome.

  To do items:
  - [x] Write BIP explaining the spec here (1 new p2p message for negotiating wtxid-based relay, along with a new INV type)
  - [ ] Measure and evaluate a heuristic for minimizing how often a node downloads the same transaction twice, when connected to old and new nodes.

ACKs for top commit:
  naumenkogs:
    utACK 0a4f142
  laanwj:
    utACK 0a4f142

Tree-SHA512: d8eb8f0688cf0cbe9507bf738e143edab1f595551fdfeddc2b6734686ea26e7f156b6bfde38bad8bbbe8bec1857c7223e1687f8f018de7463dde8ecaa8f450df
  • Loading branch information
laanwj authored and sidhujag committed Jul 24, 2020
1 parent b81b14c commit bdb2597
Show file tree
Hide file tree
Showing 18 changed files with 444 additions and 111 deletions.
6 changes: 5 additions & 1 deletion src/consensus/validation.h
Expand Up @@ -30,11 +30,15 @@ enum class TxValidationResult {
TX_MISSING_INPUTS, //!< transaction was missing some of its inputs
TX_PREMATURE_SPEND, //!< transaction spends a coinbase too early, or violates locktime/sequence locks
/**
* Transaction might be missing a witness, have a witness prior to SegWit
* Transaction might have a witness prior to SegWit
* activation, or witness may have been malleated (which includes
* non-standard witnesses).
*/
TX_WITNESS_MUTATED,
/**
* Transaction is missing a witness.
*/
TX_WITNESS_STRIPPED,
/**
* Tx already in mempool or conflicts with a tx in the chain
* (if it conflicts with another tx in mempool, we use MEMPOOL_POLICY as it failed to reach the RBF threshold)
Expand Down
4 changes: 2 additions & 2 deletions src/net.h
Expand Up @@ -1115,11 +1115,11 @@ class CNode
}


void AddInventoryKnown(const CInv& inv)
void AddKnownTx(const uint256& hash)
{
if (m_tx_relay != nullptr) {
LOCK(m_tx_relay->cs_tx_inventory);
m_tx_relay->filterInventoryKnown.insert(inv.hash);
m_tx_relay->filterInventoryKnown.insert(hash);
}
}

Expand Down
268 changes: 212 additions & 56 deletions src/net_processing.cpp

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/net_processing.h
Expand Up @@ -107,6 +107,6 @@ void Misbehaving(NodeId nodeid, int howmuch, const std::string& message="") EXCL
void EraseTxRequest(NodeId nodeId, const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
size_t GetRequestedTxCount(NodeId nodeId) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
/** Relay transaction to every node */
void RelayTransaction(const uint256&, const CConnman& connman);
void RelayTransaction(const uint256& txid, const uint256& wtxid, const CConnman& connman) EXCLUSIVE_LOCKS_REQUIRED(cs_main);

#endif // SYSCOIN_NET_PROCESSING_H
5 changes: 3 additions & 2 deletions src/node/transaction.cpp
Expand Up @@ -80,9 +80,10 @@ TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef t
if (relay) {
// the mempool tracks locally submitted transactions to make a
// best-effort of initial broadcast
node.mempool->AddUnbroadcastTx(hashTx);
node.mempool->AddUnbroadcastTx(hashTx, tx->GetWitnessHash());

RelayTransaction(hashTx, *node.connman);
LOCK(cs_main);
RelayTransaction(hashTx, tx->GetWitnessHash(), *node.connman);
}

return TransactionError::OK;
Expand Down
7 changes: 4 additions & 3 deletions src/protocol.cpp
Expand Up @@ -46,6 +46,7 @@ const char *GETCFHEADERS="getcfheaders";
const char *CFHEADERS="cfheaders";
const char *GETCFCHECKPT="getcfcheckpt";
const char *CFCHECKPT="cfcheckpt";
const char *WTXIDRELAY="wtxidrelay";
// Syscoin message types
const char *SPORK="spork";
const char *GETSPORKS="getsporks";
Expand All @@ -69,7 +70,6 @@ const char *QBSIGSHARES="qbsigs";
const char *QSIGREC="qsigrec";
const char *QSIGSHARE="qsigshare";
const char *MNAUTH="mnauth";

} // namespace NetMsgType

/** All known message types. Keep this in the same order as the list of
Expand Down Expand Up @@ -131,8 +131,7 @@ const static std::string allNetMessageTypes[] = {
NetMsgType::CFHEADERS,
NetMsgType::GETCFCHECKPT,
NetMsgType::CFCHECKPT,
NetMsgType::GETCFHEADERS,
NetMsgType::CFHEADERS,
NetMsgType::WTXIDRELAY,
};
const static std::vector<std::string> allNetMessageTypesVec(allNetMessageTypes, allNetMessageTypes+ARRAYLEN(allNetMessageTypes));

Expand Down Expand Up @@ -228,6 +227,8 @@ std::string CInv::GetCommand() const
switch (masked)
{
case MSG_TX: return cmd.append(NetMsgType::TX);
// WTX is not a message type, just an inv type
case MSG_WTX: return cmd.append("wtx");
case MSG_BLOCK: return cmd.append(NetMsgType::BLOCK);
case MSG_FILTERED_BLOCK: return cmd.append(NetMsgType::MERKLEBLOCK);
case MSG_CMPCT_BLOCK: return cmd.append(NetMsgType::CMPCTBLOCK);
Expand Down
11 changes: 9 additions & 2 deletions src/protocol.h
Expand Up @@ -284,6 +284,12 @@ extern const char* GETCFCHECKPT;
* evenly spaced filter headers for blocks on the requested chain.
*/
extern const char* CFCHECKPT;
/**
* Indicates that a node prefers to relay transactions via wtxid, rather than
* txid.
* @since protocol version 70016 as described by BIP 339.
*/
extern const char *WTXIDRELAY;
}; // namespace NetMsgType

/* Get a vector of all valid message types (see above) */
Expand Down Expand Up @@ -420,11 +426,12 @@ const uint32_t MSG_TYPE_MASK = 0xffffffff >> 2;
* These numbers are defined by the protocol. When adding a new value, be sure
* to mention it in the respective BIP.
*/
enum GetDataMsg {
enum GetDataMsg : uint32_t {
UNDEFINED = 0,
MSG_TX = 1,
MSG_BLOCK = 2,
// The following can only occur in getdata. Invs always use TX or BLOCK.
MSG_WTX = 5, //!< Defined in BIP 339
// The following can only occur in getdata. Invs always use TX/WTX or BLOCK.
MSG_FILTERED_BLOCK = 3, //!< Defined in BIP37
MSG_CMPCT_BLOCK = 4, //!< Defined in BIP152
// SYSCOIN message types
Expand Down
14 changes: 7 additions & 7 deletions src/txmempool.cpp
Expand Up @@ -1089,12 +1089,12 @@ void CTxMemPool::check(const CCoinsViewCache *pcoins) const
assert(innerUsage == cachedInnerUsage);
}

bool CTxMemPool::CompareDepthAndScore(const uint256& hasha, const uint256& hashb)
bool CTxMemPool::CompareDepthAndScore(const uint256& hasha, const uint256& hashb, bool wtxid)
{
LOCK(cs);
indexed_transaction_set::const_iterator i = mapTx.find(hasha);
indexed_transaction_set::const_iterator i = wtxid ? get_iter_from_wtxid(hasha) : mapTx.find(hasha);
if (i == mapTx.end()) return false;
indexed_transaction_set::const_iterator j = mapTx.find(hashb);
indexed_transaction_set::const_iterator j = wtxid ? get_iter_from_wtxid(hashb) : mapTx.find(hashb);
if (j == mapTx.end()) return true;
uint64_t counta = i->GetCountWithAncestors();
uint64_t countb = j->GetCountWithAncestors();
Expand Down Expand Up @@ -1174,10 +1174,10 @@ CTransactionRef CTxMemPool::get(const uint256& hash) const
return i->GetSharedTx();
}

TxMempoolInfo CTxMemPool::info(const uint256& hash) const
TxMempoolInfo CTxMemPool::info(const uint256& hash, bool wtxid) const
{
LOCK(cs);
indexed_transaction_set::const_iterator i = mapTx.find(hash);
indexed_transaction_set::const_iterator i = (wtxid ? get_iter_from_wtxid(hash) : mapTx.find(hash));
if (i == mapTx.end())
return TxMempoolInfo();
return GetInfo(i);
Expand Down Expand Up @@ -1369,8 +1369,8 @@ bool CCoinsViewMemPool::GetCoin(const COutPoint &outpoint, Coin &coin) const {

size_t CTxMemPool::DynamicMemoryUsage() const {
LOCK(cs);
// Estimate the overhead of mapTx to be 12 pointers + an allocation, as no exact formula for boost::multi_index_contained is implemented.
return memusage::MallocUsage(sizeof(CTxMemPoolEntry) + 12 * sizeof(void*)) * mapTx.size() + memusage::DynamicUsage(mapNextTx) + memusage::DynamicUsage(mapDeltas) + memusage::DynamicUsage(mapLinks) + memusage::DynamicUsage(vTxHashes) + cachedInnerUsage;
// Estimate the overhead of mapTx to be 15 pointers + an allocation, as no exact formula for boost::multi_index_contained is implemented.
return memusage::MallocUsage(sizeof(CTxMemPoolEntry) + 15 * sizeof(void*)) * mapTx.size() + memusage::DynamicUsage(mapNextTx) + memusage::DynamicUsage(mapDeltas) + memusage::DynamicUsage(mapLinks) + memusage::DynamicUsage(vTxHashes) + cachedInnerUsage;
}

void CTxMemPool::RemoveUnbroadcastTx(const uint256& txid, const bool unchecked) {
Expand Down
55 changes: 45 additions & 10 deletions src/txmempool.h
Expand Up @@ -206,6 +206,22 @@ struct mempoolentry_txid
}
};

// extracts a transaction witness-hash from CTxMemPoolEntry or CTransactionRef
struct mempoolentry_wtxid
{
typedef uint256 result_type;
result_type operator() (const CTxMemPoolEntry &entry) const
{
return entry.GetTx().GetWitnessHash();
}

result_type operator() (const CTransactionRef& tx) const
{
return tx->GetWitnessHash();
}
};


/** \class CompareTxMemPoolEntryByDescendantScore
*
* Sort an entry by max(score/size of entry's tx, score/size with all descendants).
Expand Down Expand Up @@ -326,6 +342,7 @@ class CompareTxMemPoolEntryByAncestorFee
struct descendant_score {};
struct entry_time {};
struct ancestor_score {};
struct index_by_wtxid {};

class CBlockPolicyEstimator;

Expand Down Expand Up @@ -394,8 +411,9 @@ class SaltedTxidHasher
*
* CTxMemPool::mapTx, and CTxMemPoolEntry bookkeeping:
*
* mapTx is a boost::multi_index that sorts the mempool on 4 criteria:
* - transaction hash
* mapTx is a boost::multi_index that sorts the mempool on 5 criteria:
* - transaction hash (txid)
* - witness-transaction hash (wtxid)
* - descendant feerate [we use max(feerate of tx, feerate of tx with all descendants)]
* - time in mempool
* - ancestor feerate [we use min(feerate of tx, feerate of tx with all unconfirmed ancestors)]
Expand Down Expand Up @@ -480,6 +498,12 @@ class CTxMemPool
boost::multi_index::indexed_by<
// sorted by txid
boost::multi_index::hashed_unique<mempoolentry_txid, SaltedTxidHasher>,
// sorted by wtxid
boost::multi_index::hashed_unique<
boost::multi_index::tag<index_by_wtxid>,
mempoolentry_wtxid,
SaltedTxidHasher
>,
// sorted by fee rate
boost::multi_index::ordered_non_unique<
boost::multi_index::tag<descendant_score>,
Expand Down Expand Up @@ -569,8 +593,11 @@ class CTxMemPool

std::vector<indexed_transaction_set::const_iterator> GetSortedDepthAndScore() const EXCLUSIVE_LOCKS_REQUIRED(cs);

/** track locally submitted transactions to periodically retry initial broadcast */
std::set<uint256> m_unbroadcast_txids GUARDED_BY(cs);
/**
* track locally submitted transactions to periodically retry initial broadcast
* map of txid -> wtxid
*/
std::map<uint256, uint256> m_unbroadcast_txids GUARDED_BY(cs);
// SYSCOIN
std::multimap<uint256, uint256> mapProTxRefs; // proTxHash -> transaction (all TXs that refer to an existing proTx)
std::map<CService, uint256> mapProTxAddresses;
Expand Down Expand Up @@ -620,7 +647,7 @@ class CTxMemPool
void removeForBlock(const std::vector<CTransactionRef>& vtx, unsigned int nBlockHeight) EXCLUSIVE_LOCKS_REQUIRED(cs);
void clear();
void _clear() EXCLUSIVE_LOCKS_REQUIRED(cs); //lock free
bool CompareDepthAndScore(const uint256& hasha, const uint256& hashb);
bool CompareDepthAndScore(const uint256& hasha, const uint256& hashb, bool wtxid=false);
void queryHashes(std::vector<uint256>& vtxid) const;
bool isSpent(const COutPoint& outpoint) const;
unsigned int GetTransactionsUpdated() const;
Expand Down Expand Up @@ -723,33 +750,41 @@ class CTxMemPool
return totalTxSize;
}

bool exists(const uint256& hash) const
bool exists(const uint256& hash, bool wtxid=false) const
{
LOCK(cs);
if (wtxid) {
return (mapTx.get<index_by_wtxid>().count(hash) != 0);
}
return (mapTx.count(hash) != 0);
}

CTransactionRef get(const uint256& hash) const;
TxMempoolInfo info(const uint256& hash) const;
txiter get_iter_from_wtxid(const uint256& wtxid) const EXCLUSIVE_LOCKS_REQUIRED(cs)
{
AssertLockHeld(cs);
return mapTx.project<0>(mapTx.get<index_by_wtxid>().find(wtxid));
}
TxMempoolInfo info(const uint256& hash, bool wtxid=false) const;
std::vector<TxMempoolInfo> infoAll() const;
// SYSCOIN
bool existsProviderTxConflict(const CTransaction &tx) const;
size_t DynamicMemoryUsage() const;

/** Adds a transaction to the unbroadcast set */
void AddUnbroadcastTx(const uint256& txid) {
void AddUnbroadcastTx(const uint256& txid, const uint256& wtxid) {
LOCK(cs);
// Sanity Check: the transaction should also be in the mempool
if (exists(txid)) {
m_unbroadcast_txids.insert(txid);
m_unbroadcast_txids[txid] = wtxid;
}
}

/** Removes a transaction from the unbroadcast set */
void RemoveUnbroadcastTx(const uint256& txid, const bool unchecked = false);

/** Returns transactions in unbroadcast set */
std::set<uint256> GetUnbroadcastTxs() const {
std::map<uint256, uint256> GetUnbroadcastTxs() const {
LOCK(cs);
return m_unbroadcast_txids;
}
Expand Down
19 changes: 11 additions & 8 deletions src/validation.cpp
Expand Up @@ -1040,7 +1040,7 @@ bool MemPoolAccept::PolicyScriptChecks(ATMPArgs& args, Workspace& ws, Precompute
if (!tx.HasWitness() && CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, txdata) &&
!CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, txdata)) {
// Only the witness is missing, so the transaction itself may be fine.
state.Invalid(TxValidationResult::TX_WITNESS_MUTATED,
state.Invalid(TxValidationResult::TX_WITNESS_STRIPPED,
state.GetRejectReason(), state.GetDebugMessage());
}
return false; // state filled in by CheckInputScripts
Expand Down Expand Up @@ -5424,19 +5424,22 @@ bool LoadMempool(CTxMemPool& pool)
}

// TODO: remove this try except in v0.22
std::map<uint256, uint256> unbroadcast_txids;
try {
std::set<uint256> unbroadcast_txids;
file >> unbroadcast_txids;
unbroadcast = unbroadcast_txids.size();

for (const auto& txid : unbroadcast_txids) {
pool.AddUnbroadcastTx(txid);
}
} catch (const std::exception&) {
// mempool.dat files created prior to v0.21 will not have an
// unbroadcast set. No need to log a failure if parsing fails here.
}

for (const auto& elem : unbroadcast_txids) {
// Don't add unbroadcast transactions that didn't get back into the
// mempool.
const CTransactionRef& added_tx = pool.get(elem.first);
if (added_tx != nullptr) {
pool.AddUnbroadcastTx(elem.first, added_tx->GetWitnessHash());
}
}
} catch (const std::exception& e) {
LogPrintf("Failed to deserialize mempool data on disk: %s. Continuing anyway.\n", e.what());
return false;
Expand All @@ -5452,7 +5455,7 @@ bool DumpMempool(const CTxMemPool& pool)

std::map<uint256, CAmount> mapDeltas;
std::vector<TxMempoolInfo> vinfo;
std::set<uint256> unbroadcast_txids;
std::map<uint256, uint256> unbroadcast_txids;

static Mutex dump_mutex;
LOCK(dump_mutex);
Expand Down
5 changes: 4 additions & 1 deletion src/version.h
Expand Up @@ -9,7 +9,7 @@
* network protocol versioning
*/

static const int PROTOCOL_VERSION = 70015;
static const int PROTOCOL_VERSION = 70016;

//! Version when we switched to a size-based "headers" limit.
static const int SIZE_HEADERS_LIMIT_VERSION = 70015;
Expand Down Expand Up @@ -43,4 +43,7 @@ static const int SHORT_IDS_BLOCKS_VERSION = 70014;
//! not banning for invalid compact blocks starts with this version
static const int INVALID_CB_NO_BAN_VERSION = 70015;

//! "wtxidrelay" command for wtxid-based relay starts with this version
static const int WTXID_RELAY_VERSION = 70016;

#endif // SYSCOIN_VERSION_H
7 changes: 6 additions & 1 deletion test/functional/mempool_packages.py
Expand Up @@ -69,14 +69,19 @@ def run_test(self):
fee = Decimal("0.0001")
# MAX_ANCESTORS transactions off a confirmed tx should be fine
chain = []
witness_chain = []
for i in range(MAX_ANCESTORS):
(txid, sent_value) = self.chain_transaction(self.nodes[0], txid, 0, value, fee, 1)
value = sent_value
chain.append(txid)
# We need the wtxids to check P2P announcements
fulltx = self.nodes[0].getrawtransaction(txid)
witnesstx = self.nodes[0].decoderawtransaction(fulltx, True)
witness_chain.append(witnesstx['hash'])

# Wait until mempool transactions have passed initial broadcast (sent inv and received getdata)
# Otherwise, getrawmempool may be inconsistent with getmempoolentry if unbroadcast changes in between
self.nodes[0].p2p.wait_for_broadcast(chain)
self.nodes[0].p2p.wait_for_broadcast(witness_chain)

# Check mempool has MAX_ANCESTORS transactions in it, and descendant and ancestor
# count and fees should look correct
Expand Down
2 changes: 1 addition & 1 deletion test/functional/p2p_blocksonly.py
Expand Up @@ -52,7 +52,7 @@ def run_test(self):
self.log.info('Check that txs from rpc are not rejected and relayed to other peers')
assert_equal(self.nodes[0].getpeerinfo()[0]['relaytxes'], True)
txid = self.nodes[0].testmempoolaccept([sigtx])[0]['txid']
with self.nodes[0].assert_debug_log(['received getdata for: tx {} peer=1'.format(txid)]):
with self.nodes[0].assert_debug_log(['received getdata for: wtx {} peer=1'.format(txid)]):
self.nodes[0].sendrawtransaction(sigtx)
self.nodes[0].p2p.wait_for_tx(txid)
assert_equal(self.nodes[0].getmempoolinfo()['size'], 1)
Expand Down
4 changes: 2 additions & 2 deletions test/functional/p2p_feefilter.py
Expand Up @@ -7,7 +7,7 @@
from decimal import Decimal
import time

from test_framework.messages import MSG_TX, msg_feefilter
from test_framework.messages import MSG_TX, MSG_WTX, msg_feefilter
from test_framework.mininode import mininode_lock, P2PInterface
from test_framework.test_framework import SyscoinTestFramework
from test_framework.util import assert_equal
Expand Down Expand Up @@ -45,7 +45,7 @@ def __init__(self):

def on_inv(self, message):
for i in message.inv:
if (i.type == MSG_TX):
if (i.type == MSG_TX) or (i.type == MSG_WTX):
self.txinvs.append(hashToHex(i.hash))

def clear_invs(self):
Expand Down

0 comments on commit bdb2597

Please sign in to comment.