diff --git a/src/Makefile.am b/src/Makefile.am index 38f51fada8f3a..e3a135b66ff28 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -104,6 +104,7 @@ noinst_LTLIBRARIES = bin_PROGRAMS = noinst_PROGRAMS = +check_PROGRAMS = TESTS = BENCHMARKS = @@ -258,7 +259,6 @@ BITCOIN_CORE_H = \ memusage.h \ merkleblock.h \ messagesigner.h \ - minisketchwrapper.h \ net.h \ net_permissions.h \ net_processing.h \ @@ -275,8 +275,10 @@ BITCOIN_CORE_H = \ node/context.h \ node/eviction.h \ node/miner.h \ + node/minisketchwrapper.h \ node/psbt.h \ node/transaction.h \ + node/txreconciliation.h \ node/ui_interface.h \ node/utxo_snapshot.h \ noui.h \ @@ -494,7 +496,6 @@ libbitcoin_server_a_SOURCES = \ masternode/payments.cpp \ masternode/sync.cpp \ masternode/utils.cpp \ - minisketchwrapper.cpp \ net.cpp \ netfulfilledman.cpp \ netgroup.cpp \ @@ -507,8 +508,10 @@ libbitcoin_server_a_SOURCES = \ node/eviction.cpp \ node/interfaces.cpp \ node/miner.cpp \ + node/minisketchwrapper.cpp \ node/psbt.cpp \ node/transaction.cpp \ + node/txreconciliation.cpp \ node/ui_interface.cpp \ noui.cpp \ policy/fees.cpp \ diff --git a/src/Makefile.minisketch.include b/src/Makefile.minisketch.include index b337f483498ed..1363bec34eac2 100644 --- a/src/Makefile.minisketch.include +++ b/src/Makefile.minisketch.include @@ -31,7 +31,7 @@ if ENABLE_TESTS if !ENABLE_FUZZ MINISKETCH_TEST = minisketch/test TESTS += $(MINISKETCH_TEST) -noinst_PROGRAMS += $(MINISKETCH_TEST) +check_PROGRAMS += $(MINISKETCH_TEST) minisketch_test_SOURCES = $(MINISKETCH_TEST_SOURCES_INT) minisketch_test_CPPFLAGS = $(AM_CPPFLAGS) $(LIBMINISKETCH_CPPFLAGS) diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 1a3f5381181b9..cb8b740404649 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -57,6 +57,7 @@ FUZZ_SUITE_LD_COMMON = \ $(LIBLEVELDB_SSE42) \ $(LIBMEMENV) \ $(LIBSECP256K1) \ + $(MINISKETCH_LIBS) \ $(EVENT_LIBS) \ $(EVENT_PTHREADS_LIBS) \ $(GMP_LIBS) \ @@ -173,6 +174,7 @@ BITCOIN_TESTS =\ test/torcontrol_tests.cpp \ test/transaction_tests.cpp \ test/txindex_tests.cpp \ + test/txreconciliation_tests.cpp \ test/txvalidation_tests.cpp \ test/txvalidationcache_tests.cpp \ test/uint256_tests.cpp \ @@ -289,6 +291,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/locale.cpp \ test/fuzz/merkleblock.cpp \ test/fuzz/message.cpp \ + test/fuzz/minisketch.cpp \ test/fuzz/muhash.cpp \ test/fuzz/multiplication_overflow.cpp \ test/fuzz/net.cpp \ diff --git a/src/hash.cpp b/src/hash.cpp index 29384b8c86cf1..650d531bd43f9 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -7,6 +7,7 @@ #include #include +#include inline uint32_t ROTL32(uint32_t x, int8_t r) { @@ -84,3 +85,12 @@ uint256 SHA256Uint256(const uint256& input) CSHA256().Write(input.begin(), 32).Finalize(result.begin()); return result; } + +CHashWriter TaggedHash(const std::string& tag) +{ + CHashWriter writer(SER_GETHASH, 0); + uint256 taghash; + CSHA256().Write((const unsigned char*)tag.data(), tag.size()).Finalize(taghash.begin()); + writer << taghash << taghash; + return writer; +} diff --git a/src/hash.h b/src/hash.h index 4a13da8f1f6e3..5dad7c72bcf77 100644 --- a/src/hash.h +++ b/src/hash.h @@ -16,6 +16,7 @@ #include #include +#include #include typedef uint256 ChainCode; @@ -241,4 +242,12 @@ unsigned int MurmurHash3(unsigned int nHashSeed, Span vData void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char header, const unsigned char data[32], unsigned char output[64]); +/** Return a CHashWriter primed for tagged hashes (as specified in BIP 340). + * + * The returned object will have SHA256(tag) written to it twice (= 64 bytes). + * A tagged hash can be computed by feeding the message into this object, and + * then calling CHashWriter::GetSHA256(). + */ +CHashWriter TaggedHash(const std::string& tag); + #endif // BITCOIN_HASH_H diff --git a/src/init.cpp b/src/init.cpp index 4524a33d84615..a7d9467381e8f 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -578,6 +579,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-v2transport", strprintf("Support v2 transport (default: %u)", DEFAULT_V2_TRANSPORT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-peerblockfilters", strprintf("Serve compact block filters to peers per BIP 157 (default: %u)", DEFAULT_PEERBLOCKFILTERS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-peerbloomfilters", strprintf("Support filtering of blocks and transaction with bloom filters (default: %u)", DEFAULT_PEERBLOOMFILTERS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); + argsman.AddArg("-txreconciliation", strprintf("Enable transaction reconciliations per BIP 330 (default: %d)", DEFAULT_TXRECONCILIATION_ENABLE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-peertimeout=", strprintf("Specify a p2p connection timeout delay in seconds. After connecting to a peer, wait this amount of time before considering disconnection based on inactivity (minimum: 1, default: %d)", DEFAULT_PEER_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-permitbaremultisig", strprintf("Relay non-P2SH multisig (default: %u)", DEFAULT_PERMIT_BAREMULTISIG), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-port=", strprintf("Listen for connections on . Nodes not using the default ports (default: %u, testnet: %u, regtest: %u) are unlikely to get incoming connections. Not relevant for I2P (see doc/i2p.md).", defaultChainParams->GetDefaultPort(), testnetChainParams->GetDefaultPort(), regtestChainParams->GetDefaultPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); diff --git a/src/logging.cpp b/src/logging.cpp index 967b247ec734f..dbbff8b92f1d4 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -161,6 +161,7 @@ const CLogCategoryDesc LogCategories[] = {BCLog::I2P, "i2p"}, {BCLog::IPC, "ipc"}, {BCLog::LOCK, "lock"}, + {BCLog::TXRECONCILIATION, "txreconciliation"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, diff --git a/src/logging.h b/src/logging.h index c57b3221649bc..a534512049513 100644 --- a/src/logging.h +++ b/src/logging.h @@ -62,6 +62,7 @@ namespace BCLog { I2P = (1 << 22), IPC = (1 << 23), LOCK = (1 << 24), + TXRECONCILIATION = (1 << 27), //Start Dash CHAINLOCKS = ((uint64_t)1 << 32), diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 0ac9e317f4558..d621b1008fcb6 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -730,6 +731,7 @@ class PeerManagerImpl final : public PeerManager BanMan* const m_banman; ChainstateManager& m_chainman; CTxMemPool& m_mempool; + std::unique_ptr m_txreconciliation; const std::unique_ptr& m_dmnman; const std::unique_ptr& m_cj_ctx; const std::unique_ptr& m_llmq_ctx; @@ -1633,6 +1635,7 @@ void PeerManagerImpl::FinalizeNode(const CNode& node) { mapBlocksInFlight.erase(entry.pindex->GetBlockHash()); } WITH_LOCK(g_cs_orphans, m_orphanage.EraseForPeer(nodeid)); + if (m_txreconciliation) m_txreconciliation->ForgetPeer(nodeid); m_num_preferred_download_peers -= state->fPreferredDownload; m_peers_downloading_from -= (state->nBlocksInFlight != 0); assert(m_peers_downloading_from >= 0); @@ -1935,6 +1938,11 @@ PeerManagerImpl::PeerManagerImpl(const CChainParams& chainparams, CConnman& conn m_mn_activeman(mn_activeman), m_ignore_incoming_txs(ignore_incoming_txs) { + // While Erlay support is incomplete, it must be enabled explicitly via -txreconciliation. + // This argument can go away after Erlay support is complete. + if (gArgs.GetBoolArg("-txreconciliation", DEFAULT_TXRECONCILIATION_ENABLE)) { + m_txreconciliation = std::make_unique(TXRECONCILIATION_VERSION); + } } void PeerManagerImpl::StartScheduledTasks(CScheduler& scheduler) @@ -3509,8 +3517,6 @@ void PeerManagerImpl::ProcessMessage( m_connman.PushMessage(&pfrom, msg_maker.Make(NetMsgType::SENDADDRV2)); } - m_connman.PushMessage(&pfrom, msg_maker.Make(NetMsgType::VERACK)); - pfrom.m_has_all_wanted_services = HasAllDesirableServiceFlags(nServices); peer->m_their_services = nServices; pfrom.SetAddrLocal(addrMe); @@ -3536,6 +3542,22 @@ void PeerManagerImpl::ProcessMessage( if (fRelay) pfrom.m_relays_txs = true; } + if (greatest_common_version >= INCREASE_MAX_HEADERS2_VERSION && m_txreconciliation) { + // Per BIP-330, we announce txreconciliation support if: + // - protocol version per the peer's VERSION message supports INCREASE_MAX_HEADERS2_VERSION; + // - transaction relay is supported per the peer's VERSION message (see m_relays_txs); + // - this is not a block-relay-only connection and not a feeler (see m_relays_txs); + // - this is not an addr fetch connection; + // - we are not in -blocksonly mode. + if (pfrom.m_relays_txs && !pfrom.IsAddrFetchConn() && !m_ignore_incoming_txs) { + const uint64_t recon_salt = m_txreconciliation->PreRegisterPeer(pfrom.GetId()); + m_connman.PushMessage(&pfrom, msg_maker.Make(NetMsgType::SENDTXRCNCL, + TXRECONCILIATION_VERSION, recon_salt)); + } + } + + m_connman.PushMessage(&pfrom, msg_maker.Make(NetMsgType::VERACK)); + // Potentially mark this peer as a preferred download peer. { LOCK(cs_main); @@ -3677,6 +3699,15 @@ void PeerManagerImpl::ProcessMessage( } } + if (m_txreconciliation) { + if (pfrom.nVersion < INCREASE_MAX_HEADERS2_VERSION || !m_txreconciliation->IsPeerRegistered(pfrom.GetId())) { + // We could have optimistically pre-registered/registered the peer. In that case, + // we should forget about the reconciliation state here if the node version is below + // our minimum supported version. + m_txreconciliation->ForgetPeer(pfrom.GetId()); + } + } + pfrom.fSuccessfullyConnected = true; return; } @@ -3727,6 +3758,61 @@ void PeerManagerImpl::ProcessMessage( return; } + // Received from a peer demonstrating readiness to announce transactions via reconciliations. + // This feature negotiation must happen between VERSION and VERACK to avoid relay problems + // from switching announcement protocols after the connection is up. + if (msg_type == NetMsgType::SENDTXRCNCL) { + if (!m_txreconciliation) { + LogPrint(BCLog::NET, "sendtxrcncl from peer=%d ignored, as our node does not have txreconciliation enabled\n", pfrom.GetId()); + return; + } + + if (pfrom.fSuccessfullyConnected) { + LogPrint(BCLog::NET, "sendtxrcncl received after verack from peer=%d; disconnecting\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + } + + // Peer must not offer us reconciliations if we specified no tx relay support in VERSION. + if (RejectIncomingTxs(pfrom)) { + LogPrint(BCLog::NET, "sendtxrcncl received from peer=%d to which we indicated no tx relay; disconnecting\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + } + + // Peer must not offer us reconciliations if they specified no tx relay support in VERSION. + // This flag might also be false in other cases, but the RejectIncomingTxs check above + // eliminates them, so that this flag fully represents what we are looking for. + if (!pfrom.m_relays_txs) { + LogPrint(BCLog::NET, "sendtxrcncl received from peer=%d which indicated no tx relay to us; disconnecting\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + } + + uint32_t peer_txreconcl_version; + uint64_t remote_salt; + vRecv >> peer_txreconcl_version >> remote_salt; + + const ReconciliationRegisterResult result = m_txreconciliation->RegisterPeer(pfrom.GetId(), pfrom.IsInboundConn(), + peer_txreconcl_version, remote_salt); + switch (result) { + case ReconciliationRegisterResult::NOT_FOUND: + LogPrint(BCLog::NET, "Ignore unexpected txreconciliation signal from peer=%d\n", pfrom.GetId()); + break; + case ReconciliationRegisterResult::SUCCESS: + break; + case ReconciliationRegisterResult::ALREADY_REGISTERED: + LogPrint(BCLog::NET, "txreconciliation protocol violation from peer=%d (sendtxrcncl received from already registered peer); disconnecting\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + case ReconciliationRegisterResult::PROTOCOL_VIOLATION: + LogPrint(BCLog::NET, "txreconciliation protocol violation from peer=%d; disconnecting\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + } + return; + } + if (!pfrom.fSuccessfullyConnected) { LogPrint(BCLog::NET, "Unsupported message \"%s\" prior to verack from peer=%d\n", SanitizeString(msg_type), pfrom.GetId()); return; diff --git a/src/minisketchwrapper.cpp b/src/node/minisketchwrapper.cpp similarity index 98% rename from src/minisketchwrapper.cpp rename to src/node/minisketchwrapper.cpp index fb176fb1533de..572df63463abb 100644 --- a/src/minisketchwrapper.cpp +++ b/src/node/minisketchwrapper.cpp @@ -2,7 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include +#include #include #include diff --git a/src/minisketchwrapper.h b/src/node/minisketchwrapper.h similarity index 79% rename from src/minisketchwrapper.h rename to src/node/minisketchwrapper.h index 409221de79a5d..426781d50869d 100644 --- a/src/minisketchwrapper.h +++ b/src/node/minisketchwrapper.h @@ -2,8 +2,8 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#ifndef BITCOIN_MINISKETCHWRAPPER_H -#define BITCOIN_MINISKETCHWRAPPER_H +#ifndef BITCOIN_NODE_MINISKETCHWRAPPER_H +#define BITCOIN_NODE_MINISKETCHWRAPPER_H #include #include @@ -14,4 +14,4 @@ Minisketch MakeMinisketch32(size_t capacity); /** Wrapper around Minisketch::CreateFP. */ Minisketch MakeMinisketch32FP(size_t max_elements, uint32_t fpbits); -#endif // BITCOIN_DBWRAPPER_H +#endif // BITCOIN_NODE_MINISKETCHWRAPPER_H diff --git a/src/node/txreconciliation.cpp b/src/node/txreconciliation.cpp new file mode 100644 index 0000000000000..8d23c85a9a8a5 --- /dev/null +++ b/src/node/txreconciliation.cpp @@ -0,0 +1,168 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +#include +#include + + +namespace { + +/** Static salt component used to compute short txids for sketch construction, see BIP-330. */ +const std::string RECON_STATIC_SALT = "Tx Relay Salting"; +const CHashWriter RECON_SALT_HASHER = TaggedHash(RECON_STATIC_SALT); + +/** + * Salt (specified by BIP-330) constructed from contributions from both peers. It is used + * to compute transaction short IDs, which are then used to construct a sketch representing a set + * of transactions we want to announce to the peer. + */ +uint256 ComputeSalt(uint64_t salt1, uint64_t salt2) +{ + // According to BIP-330, salts should be combined in ascending order. + return (HashWriter(RECON_SALT_HASHER) << std::min(salt1, salt2) << std::max(salt1, salt2)).GetSHA256(); +} + +/** + * Keeps track of txreconciliation-related per-peer state. + */ +class TxReconciliationState +{ +public: + /** + * TODO: This field is public to ignore -Wunused-private-field. Make private once used in + * the following commits. + * + * Reconciliation protocol assumes using one role consistently: either a reconciliation + * initiator (requesting sketches), or responder (sending sketches). This defines our role, + * based on the direction of the p2p connection. + * + */ + bool m_we_initiate; + + /** + * TODO: These fields are public to ignore -Wunused-private-field. Make private once used in + * the following commits. + * + * These values are used to salt short IDs, which is necessary for transaction reconciliations. + */ + uint64_t m_k0, m_k1; + + TxReconciliationState(bool we_initiate, uint64_t k0, uint64_t k1) : m_we_initiate(we_initiate), m_k0(k0), m_k1(k1) {} +}; + +} // namespace + +/** Actual implementation for TxReconciliationTracker's data structure. */ +class TxReconciliationTracker::Impl +{ +private: + mutable Mutex m_txreconciliation_mutex; + + // Local protocol version + uint32_t m_recon_version; + + /** + * Keeps track of txreconciliation states of eligible peers. + * For pre-registered peers, the locally generated salt is stored. + * For registered peers, the locally generated salt is forgotten, and the state (including + * "full" salt) is stored instead. + */ + std::unordered_map> m_states GUARDED_BY(m_txreconciliation_mutex); + +public: + explicit Impl(uint32_t recon_version) : m_recon_version(recon_version) {} + + uint64_t PreRegisterPeer(NodeId peer_id) EXCLUSIVE_LOCKS_REQUIRED(!m_txreconciliation_mutex) + { + AssertLockNotHeld(m_txreconciliation_mutex); + LOCK(m_txreconciliation_mutex); + + LogPrint(BCLog::TXRECONCILIATION, "Pre-register peer=%d\n", peer_id); + const uint64_t local_salt{GetRand(UINT64_MAX)}; + + // We do this exactly once per peer (which are unique by NodeId, see GetNewNodeId) so it's + // safe to assume we don't have this record yet. + Assume(m_states.emplace(peer_id, local_salt).second); + return local_salt; + } + + ReconciliationRegisterResult RegisterPeer(NodeId peer_id, bool is_peer_inbound, uint32_t peer_recon_version, + uint64_t remote_salt) EXCLUSIVE_LOCKS_REQUIRED(!m_txreconciliation_mutex) + { + AssertLockNotHeld(m_txreconciliation_mutex); + LOCK(m_txreconciliation_mutex); + auto recon_state = m_states.find(peer_id); + + if (recon_state == m_states.end()) return ReconciliationRegisterResult::NOT_FOUND; + + if (std::holds_alternative(recon_state->second)) { + return ReconciliationRegisterResult::ALREADY_REGISTERED; + } + + uint64_t local_salt = *std::get_if(&recon_state->second); + + // If the peer supports the version which is lower than ours, we downgrade to the version + // it supports. For now, this only guarantees that nodes with future reconciliation + // versions have the choice of reconciling with this current version. However, they also + // have the choice to refuse supporting reconciliations if the common version is not + // satisfactory (e.g. too low). + const uint32_t recon_version{std::min(peer_recon_version, m_recon_version)}; + // v1 is the lowest version, so suggesting something below must be a protocol violation. + if (recon_version < 1) return ReconciliationRegisterResult::PROTOCOL_VIOLATION; + + LogPrint(BCLog::TXRECONCILIATION, "Register peer=%d (inbound=%i)\n", peer_id, is_peer_inbound); + + const uint256 full_salt{ComputeSalt(local_salt, remote_salt)}; + recon_state->second = TxReconciliationState(!is_peer_inbound, full_salt.GetUint64(0), full_salt.GetUint64(1)); + return ReconciliationRegisterResult::SUCCESS; + } + + void ForgetPeer(NodeId peer_id) EXCLUSIVE_LOCKS_REQUIRED(!m_txreconciliation_mutex) + { + AssertLockNotHeld(m_txreconciliation_mutex); + LOCK(m_txreconciliation_mutex); + if (m_states.erase(peer_id)) { + LogPrint(BCLog::TXRECONCILIATION, "Forget txreconciliation state of peer=%d\n", peer_id); + } + } + + bool IsPeerRegistered(NodeId peer_id) const EXCLUSIVE_LOCKS_REQUIRED(!m_txreconciliation_mutex) + { + AssertLockNotHeld(m_txreconciliation_mutex); + LOCK(m_txreconciliation_mutex); + auto recon_state = m_states.find(peer_id); + return (recon_state != m_states.end() && + std::holds_alternative(recon_state->second)); + } +}; + +TxReconciliationTracker::TxReconciliationTracker(uint32_t recon_version) : m_impl{std::make_unique(recon_version)} {} + +TxReconciliationTracker::~TxReconciliationTracker() = default; + +uint64_t TxReconciliationTracker::PreRegisterPeer(NodeId peer_id) +{ + return m_impl->PreRegisterPeer(peer_id); +} + +ReconciliationRegisterResult TxReconciliationTracker::RegisterPeer(NodeId peer_id, bool is_peer_inbound, + uint32_t peer_recon_version, uint64_t remote_salt) +{ + return m_impl->RegisterPeer(peer_id, is_peer_inbound, peer_recon_version, remote_salt); +} + +void TxReconciliationTracker::ForgetPeer(NodeId peer_id) +{ + m_impl->ForgetPeer(peer_id); +} + +bool TxReconciliationTracker::IsPeerRegistered(NodeId peer_id) const +{ + return m_impl->IsPeerRegistered(peer_id); +} diff --git a/src/node/txreconciliation.h b/src/node/txreconciliation.h new file mode 100644 index 0000000000000..4591dd5df7c9f --- /dev/null +++ b/src/node/txreconciliation.h @@ -0,0 +1,91 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_NODE_TXRECONCILIATION_H +#define BITCOIN_NODE_TXRECONCILIATION_H + +#include +#include + +#include +#include + +/** Whether transaction reconciliation protocol should be enabled by default. */ +static constexpr bool DEFAULT_TXRECONCILIATION_ENABLE{false}; +/** Supported transaction reconciliation protocol version */ +static constexpr uint32_t TXRECONCILIATION_VERSION{1}; + +enum class ReconciliationRegisterResult { + NOT_FOUND, + SUCCESS, + ALREADY_REGISTERED, + PROTOCOL_VIOLATION, +}; + +/** + * Transaction reconciliation is a way for nodes to efficiently announce transactions. + * This object keeps track of all txreconciliation-related communications with the peers. + * The high-level protocol is: + * 0. Txreconciliation protocol handshake. + * 1. Once we receive a new transaction, add it to the set instead of announcing immediately. + * 2. At regular intervals, a txreconciliation initiator requests a sketch from a peer, where a + * sketch is a compressed representation of short form IDs of the transactions in their set. + * 3. Once the initiator received a sketch from the peer, the initiator computes a local sketch, + * and combines the two sketches to attempt finding the difference in *sets*. + * 4a. If the difference was not larger than estimated, see SUCCESS below. + * 4b. If the difference was larger than estimated, initial txreconciliation fails. The initiator + * requests a larger sketch via an extension round (allowed only once). + * - If extension succeeds (a larger sketch is sufficient), see SUCCESS below. + * - If extension fails (a larger sketch is insufficient), see FAILURE below. + * + * SUCCESS. The initiator knows full symmetrical difference and can request what the initiator is + * missing and announce to the peer what the peer is missing. + * + * FAILURE. The initiator notifies the peer about the failure and announces all transactions from + * the corresponding set. Once the peer received the failure notification, the peer + * announces all transactions from their set. + + * This is a modification of the Erlay protocol (https://arxiv.org/abs/1905.10518) with two + * changes (sketch extensions instead of bisections, and an extra INV exchange round), both + * are motivated in BIP-330. + */ +class TxReconciliationTracker +{ +private: + class Impl; + const std::unique_ptr m_impl; + +public: + explicit TxReconciliationTracker(uint32_t recon_version); + ~TxReconciliationTracker(); + + /** + * Step 0. Generates initial part of the state (salt) required to reconcile txs with the peer. + * The salt is used for short ID computation required for txreconciliation. + * The function returns the salt. + * A peer can't participate in future txreconciliations without this call. + * This function must be called only once per peer. + */ + uint64_t PreRegisterPeer(NodeId peer_id); + + /** + * Step 0. Once the peer agreed to reconcile txs with us, generate the state required to track + * ongoing reconciliations. Must be called only after pre-registering the peer and only once. + */ + ReconciliationRegisterResult RegisterPeer(NodeId peer_id, bool is_peer_inbound, + uint32_t peer_recon_version, uint64_t remote_salt); + + /** + * Attempts to forget txreconciliation-related state of the peer (if we previously stored any). + * After this, we won't be able to reconcile transactions with the peer. + */ + void ForgetPeer(NodeId peer_id); + + /** + * Check if a peer is registered to reconcile transactions with us. + */ + bool IsPeerRegistered(NodeId peer_id) const; +}; + +#endif // BITCOIN_NODE_TXRECONCILIATION_H diff --git a/src/protocol.cpp b/src/protocol.cpp index e3499646b10b9..ad97fef7ebf28 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -48,6 +48,7 @@ MAKE_MSG(GETCFHEADERS, "getcfheaders"); MAKE_MSG(CFHEADERS, "cfheaders"); MAKE_MSG(GETCFCHECKPT, "getcfcheckpt"); MAKE_MSG(CFCHECKPT, "cfcheckpt"); +MAKE_MSG(SENDTXRCNCL, "sendtxrcncl"); // Dash message types MAKE_MSG(SPORK, "spork"); MAKE_MSG(GETSPORKS, "getsporks"); @@ -127,6 +128,7 @@ const static std::string allNetMessageTypes[] = { NetMsgType::CFHEADERS, NetMsgType::GETCFCHECKPT, NetMsgType::CFCHECKPT, + NetMsgType::SENDTXRCNCL, // Dash message types // NOTE: do NOT include non-implmented here, we want them to be "Unknown command" in ProcessMessage() NetMsgType::SPORK, diff --git a/src/protocol.h b/src/protocol.h index 44930e750da3f..9a9f1cb4ca360 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -254,6 +254,12 @@ extern const char* GETCFCHECKPT; * evenly spaced filter headers for blocks on the requested chain. */ extern const char* CFCHECKPT; +/** + * Contains a 4-byte version number and an 8-byte salt. + * The salt is used to compute short txids needed for efficient + * txreconciliation, as described by BIP 330. + */ +extern const char* SENDTXRCNCL; // Dash message types // NOTE: do NOT declare non-implmented here, we don't want them to be exposed to the outside diff --git a/src/test/fuzz/minisketch.cpp b/src/test/fuzz/minisketch.cpp new file mode 100644 index 0000000000000..93954bd3cf242 --- /dev/null +++ b/src/test/fuzz/minisketch.cpp @@ -0,0 +1,64 @@ +// Copyright (c) 2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include + +#include +#include + +FUZZ_TARGET(minisketch) +{ + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + const auto capacity{fuzzed_data_provider.ConsumeIntegralInRange(1, 200)}; + Minisketch sketch_a{Assert(MakeMinisketch32(capacity))}; + Minisketch sketch_b{Assert(MakeMinisketch32(capacity))}; + + // Fill two sets and keep the difference in a map + std::map diff; + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) + { + const auto entry{fuzzed_data_provider.ConsumeIntegralInRange(1, std::numeric_limits::max() - 1)}; + const auto KeepDiff{[&] { + bool& mut{diff[entry]}; + mut = !mut; + }}; + CallOneOf( + fuzzed_data_provider, + [&] { + sketch_a.Add(entry); + KeepDiff(); + }, + [&] { + sketch_b.Add(entry); + KeepDiff(); + }, + [&] { + sketch_a.Add(entry); + sketch_b.Add(entry); + }); + } + const auto num_diff{std::accumulate(diff.begin(), diff.end(), size_t{0}, [](auto n, const auto& e) { return n + e.second; })}; + + Minisketch sketch_ar{MakeMinisketch32(capacity)}; + Minisketch sketch_br{MakeMinisketch32(capacity)}; + sketch_ar.Deserialize(sketch_a.Serialize()); + sketch_br.Deserialize(sketch_b.Serialize()); + + Minisketch sketch_diff{std::move(fuzzed_data_provider.ConsumeBool() ? sketch_a : sketch_ar)}; + sketch_diff.Merge(fuzzed_data_provider.ConsumeBool() ? sketch_b : sketch_br); + + if (capacity >= num_diff) { + const auto max_elements{fuzzed_data_provider.ConsumeIntegralInRange(num_diff, capacity)}; + const auto dec{*Assert(sketch_diff.Decode(max_elements))}; + Assert(dec.size() == num_diff); + for (auto d : dec) { + Assert(diff.at(d)); + } + } +} diff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp index a5a59e3154ecb..132fb71f4f35c 100644 --- a/src/test/fuzz/process_message.cpp +++ b/src/test/fuzz/process_message.cpp @@ -64,7 +64,9 @@ void initialize_process_message() { Assert(GetNumMsgTypes() == getAllNetMessageTypes().size()); // If this fails, add or remove the message type below - static const auto testing_setup = MakeNoLogFileContext(); + static const auto testing_setup = MakeNoLogFileContext( + /*chain_name=*/CBaseChainParams::REGTEST, + /*extra_args=*/{"-txreconciliation"}); g_setup = testing_setup.get(); for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { MineBlock(g_setup->m_node, CScript() << OP_TRUE); @@ -174,6 +176,7 @@ FUZZ_TARGET_MSG(sendcmpct); FUZZ_TARGET_MSG(senddsq); FUZZ_TARGET_MSG(sendheaders); FUZZ_TARGET_MSG(sendheaders2); +FUZZ_TARGET_MSG(sendtxrcncl); FUZZ_TARGET_MSG(spork); FUZZ_TARGET_MSG(ssc); FUZZ_TARGET_MSG(tx); diff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp index 5eb6e149253eb..df3bbb6c7a3ba 100644 --- a/src/test/fuzz/process_messages.cpp +++ b/src/test/fuzz/process_messages.cpp @@ -23,7 +23,9 @@ const TestingSetup* g_setup; void initialize_process_messages() { - static const auto testing_setup = MakeNoLogFileContext(); + static const auto testing_setup = MakeNoLogFileContext( + /*chain_name=*/CBaseChainParams::REGTEST, + /*extra_args=*/{"-txreconciliation"}); g_setup = testing_setup.get(); for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { MineBlock(g_setup->m_node, CScript() << OP_TRUE); diff --git a/src/test/minisketch_tests.cpp b/src/test/minisketch_tests.cpp index 6798331936d8f..d07924d18b8e7 100644 --- a/src/test/minisketch_tests.cpp +++ b/src/test/minisketch_tests.cpp @@ -3,7 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include -#include +#include #include #include @@ -38,7 +38,7 @@ BOOST_AUTO_TEST_CASE(minisketch_test) Minisketch sketch_c = std::move(sketch_ar); sketch_c.Merge(sketch_br); auto dec = sketch_c.Decode(errors); - BOOST_CHECK(dec.has_value()); + BOOST_REQUIRE(dec.has_value()); auto sols = std::move(*dec); std::sort(sols.begin(), sols.end()); for (uint32_t i = 0; i < a_not_b; ++i) BOOST_CHECK_EQUAL(sols[i], start_a + i); diff --git a/src/test/txreconciliation_tests.cpp b/src/test/txreconciliation_tests.cpp new file mode 100644 index 0000000000000..b018629e76aec --- /dev/null +++ b/src/test/txreconciliation_tests.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2021 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(txreconciliation_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(RegisterPeerTest) +{ + TxReconciliationTracker tracker(TXRECONCILIATION_VERSION); + const uint64_t salt = 0; + + // Prepare a peer for reconciliation. + tracker.PreRegisterPeer(0); + + // Invalid version. + BOOST_CHECK_EQUAL(tracker.RegisterPeer(/*peer_id=*/0, /*is_peer_inbound=*/true, + /*peer_recon_version=*/0, salt), + ReconciliationRegisterResult::PROTOCOL_VIOLATION); + + // Valid registration (inbound and outbound peers). + BOOST_REQUIRE(!tracker.IsPeerRegistered(0)); + BOOST_REQUIRE_EQUAL(tracker.RegisterPeer(0, true, 1, salt), ReconciliationRegisterResult::SUCCESS); + BOOST_CHECK(tracker.IsPeerRegistered(0)); + BOOST_REQUIRE(!tracker.IsPeerRegistered(1)); + tracker.PreRegisterPeer(1); + BOOST_REQUIRE(tracker.RegisterPeer(1, false, 1, salt) == ReconciliationRegisterResult::SUCCESS); + BOOST_CHECK(tracker.IsPeerRegistered(1)); + + // Reconciliation version is higher than ours, should be able to register. + BOOST_REQUIRE(!tracker.IsPeerRegistered(2)); + tracker.PreRegisterPeer(2); + BOOST_REQUIRE(tracker.RegisterPeer(2, true, 2, salt) == ReconciliationRegisterResult::SUCCESS); + BOOST_CHECK(tracker.IsPeerRegistered(2)); + + // Try registering for the second time. + BOOST_REQUIRE(tracker.RegisterPeer(1, false, 1, salt) == ReconciliationRegisterResult::ALREADY_REGISTERED); + + // Do not register if there were no pre-registration for the peer. + BOOST_REQUIRE_EQUAL(tracker.RegisterPeer(100, true, 1, salt), ReconciliationRegisterResult::NOT_FOUND); + BOOST_CHECK(!tracker.IsPeerRegistered(100)); +} + +BOOST_AUTO_TEST_CASE(ForgetPeerTest) +{ + TxReconciliationTracker tracker(TXRECONCILIATION_VERSION); + NodeId peer_id0 = 0; + + // Removing peer after pre-registring works and does not let to register the peer. + tracker.PreRegisterPeer(peer_id0); + tracker.ForgetPeer(peer_id0); + BOOST_CHECK_EQUAL(tracker.RegisterPeer(peer_id0, true, 1, 1), ReconciliationRegisterResult::NOT_FOUND); + + // Removing peer after it is registered works. + tracker.PreRegisterPeer(peer_id0); + BOOST_REQUIRE(!tracker.IsPeerRegistered(peer_id0)); + BOOST_REQUIRE_EQUAL(tracker.RegisterPeer(peer_id0, true, 1, 1), ReconciliationRegisterResult::SUCCESS); + BOOST_CHECK(tracker.IsPeerRegistered(peer_id0)); + tracker.ForgetPeer(peer_id0); + BOOST_CHECK(!tracker.IsPeerRegistered(peer_id0)); +} + +BOOST_AUTO_TEST_CASE(IsPeerRegisteredTest) +{ + TxReconciliationTracker tracker(TXRECONCILIATION_VERSION); + NodeId peer_id0 = 0; + + BOOST_REQUIRE(!tracker.IsPeerRegistered(peer_id0)); + tracker.PreRegisterPeer(peer_id0); + BOOST_REQUIRE(!tracker.IsPeerRegistered(peer_id0)); + + BOOST_REQUIRE_EQUAL(tracker.RegisterPeer(peer_id0, true, 1, 1), ReconciliationRegisterResult::SUCCESS); + BOOST_CHECK(tracker.IsPeerRegistered(peer_id0)); + + tracker.ForgetPeer(peer_id0); + BOOST_CHECK(!tracker.IsPeerRegistered(peer_id0)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/p2p_sendtxrcncl.py b/test/functional/p2p_sendtxrcncl.py new file mode 100755 index 0000000000000..ac5fc40ee00b1 --- /dev/null +++ b/test/functional/p2p_sendtxrcncl.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test SENDTXRCNCL message +""" + +from test_framework.messages import ( + msg_sendtxrcncl, + msg_version, + NODE_BLOOM, +) +from test_framework.p2p import ( + P2PInterface, + P2P_SERVICES, + P2P_SUBVERSION, + P2P_VERSION, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +class PeerNoVerack(P2PInterface): + def __init__(self,): + super().__init__() + + def on_version(self, message): + # Avoid sending verack in response to version. + pass + +class SendTxrcnclReceiver(P2PInterface): + def __init__(self): + super().__init__() + self.sendtxrcncl_msg_received = None + + def on_sendtxrcncl(self, message): + self.sendtxrcncl_msg_received = message + + +class P2PFeelerReceiver(SendTxrcnclReceiver): + def on_version(self, message): + pass # feeler connections can not send any message other than their own version + + +class PeerTrackMsgOrder(P2PInterface): + def __init__(self): + super().__init__() + self.messages = [] + + def on_message(self, message): + super().on_message(message) + self.messages.append(message) + +def create_sendtxrcncl_msg(): + sendtxrcncl_msg = msg_sendtxrcncl() + sendtxrcncl_msg.version = 1 + sendtxrcncl_msg.salt = 2 + return sendtxrcncl_msg + +class SendTxRcnclTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [['-txreconciliation']] + + def run_test(self): + # Check everything concerning *sending* SENDTXRCNCL + # First, *sending* to *inbound*. + self.log.info('SENDTXRCNCL sent to an inbound') + peer = self.nodes[0].add_p2p_connection(SendTxrcnclReceiver(), send_version=True, wait_for_verack=True) + assert peer.sendtxrcncl_msg_received + assert_equal(peer.sendtxrcncl_msg_received.version, 1) + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL should be sent before VERACK') + peer = self.nodes[0].add_p2p_connection(PeerTrackMsgOrder(), send_version=True, wait_for_verack=True) + peer.wait_for_verack() + verack_index = [i for i, msg in enumerate(peer.messages) if msg.msgtype == b'verack'][0] + sendtxrcncl_index = [i for i, msg in enumerate(peer.messages) if msg.msgtype == b'sendtxrcncl'][0] + assert sendtxrcncl_index < verack_index + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL on pre-v22 version should not be sent') + peer = self.nodes[0].add_p2p_connection(SendTxrcnclReceiver(), send_version=False, wait_for_verack=False) + pre_v22_version_msg = msg_version() + pre_v22_version_msg.nVersion = 70234 + pre_v22_version_msg.strSubVer = P2P_SUBVERSION + pre_v22_version_msg.nServices = P2P_SERVICES + pre_v22_version_msg.relay = 1 + peer.send_message(pre_v22_version_msg) + peer.wait_for_verack() + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL for fRelay=false should not be sent') + peer = self.nodes[0].add_p2p_connection(SendTxrcnclReceiver(), send_version=False, wait_for_verack=False) + no_txrelay_version_msg = msg_version() + no_txrelay_version_msg.nVersion = P2P_VERSION + no_txrelay_version_msg.strSubVer = P2P_SUBVERSION + no_txrelay_version_msg.nServices = P2P_SERVICES + no_txrelay_version_msg.relay = 0 + peer.send_message(no_txrelay_version_msg) + peer.wait_for_verack() + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL for fRelay=false should not be sent (with NODE_BLOOM offered)') + self.restart_node(0, ["-peerbloomfilters", "-txreconciliation"]) + peer = self.nodes[0].add_p2p_connection(SendTxrcnclReceiver(), send_version=False, wait_for_verack=False) + no_txrelay_version_msg = msg_version() + no_txrelay_version_msg.nVersion = P2P_VERSION + no_txrelay_version_msg.strSubVer = P2P_SUBVERSION + no_txrelay_version_msg.nServices = P2P_SERVICES + no_txrelay_version_msg.relay = 0 + peer.send_message(no_txrelay_version_msg) + peer.wait_for_verack() + assert peer.nServices & NODE_BLOOM != 0 + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + # Now, *sending* to *outbound*. + self.log.info('SENDTXRCNCL sent to an outbound') + peer = self.nodes[0].add_outbound_p2p_connection( + SendTxrcnclReceiver(), wait_for_verack=True, p2p_idx=0, connection_type="outbound-full-relay") + assert peer.sendtxrcncl_msg_received + assert_equal(peer.sendtxrcncl_msg_received.version, 1) + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL should not be sent if block-relay-only') + peer = self.nodes[0].add_outbound_p2p_connection( + SendTxrcnclReceiver(), wait_for_verack=True, p2p_idx=0, connection_type="block-relay-only") + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + self.log.info("SENDTXRCNCL should not be sent if feeler") + peer = self.nodes[0].add_outbound_p2p_connection(P2PFeelerReceiver(), p2p_idx=0, connection_type="feeler") + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + self.log.info("SENDTXRCNCL should not be sent if addrfetch") + peer = self.nodes[0].add_outbound_p2p_connection( + SendTxrcnclReceiver(), wait_for_verack=True, p2p_idx=0, connection_type="addr-fetch") + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL not sent if -txreconciliation flag is not set') + self.restart_node(0, []) + peer = self.nodes[0].add_p2p_connection(SendTxrcnclReceiver(), send_version=True, wait_for_verack=True) + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + self.log.info('SENDTXRCNCL not sent if blocksonly is set') + self.restart_node(0, ["-txreconciliation", "-blocksonly"]) + peer = self.nodes[0].add_p2p_connection(SendTxrcnclReceiver(), send_version=True, wait_for_verack=True) + assert not peer.sendtxrcncl_msg_received + self.nodes[0].disconnect_p2ps() + + # Check everything concerning *receiving* SENDTXRCNCL + # First, receiving from *inbound*. + self.restart_node(0, ["-txreconciliation"]) + self.log.info('valid SENDTXRCNCL received') + peer = self.nodes[0].add_p2p_connection(PeerNoVerack(), send_version=True, wait_for_verack=False) + with self.nodes[0].assert_debug_log(["received: sendtxrcncl"]): + peer.send_message(create_sendtxrcncl_msg()) + self.log.info('second SENDTXRCNCL triggers a disconnect') + with self.nodes[0].assert_debug_log(["(sendtxrcncl received from already registered peer); disconnecting"]): + peer.send_message(create_sendtxrcncl_msg()) + peer.wait_for_disconnect() + + self.restart_node(0, []) + self.log.info('SENDTXRCNCL if no txreconciliation supported is ignored') + peer = self.nodes[0].add_p2p_connection(PeerNoVerack(), send_version=True, wait_for_verack=False) + with self.nodes[0].assert_debug_log(['ignored, as our node does not have txreconciliation enabled']): + peer.send_message(create_sendtxrcncl_msg()) + self.nodes[0].disconnect_p2ps() + + self.restart_node(0, ["-txreconciliation"]) + + self.log.info('SENDTXRCNCL with version=0 triggers a disconnect') + sendtxrcncl_low_version = create_sendtxrcncl_msg() + sendtxrcncl_low_version.version = 0 + peer = self.nodes[0].add_p2p_connection(PeerNoVerack(), send_version=True, wait_for_verack=False) + with self.nodes[0].assert_debug_log(["txreconciliation protocol violation"]): + peer.send_message(sendtxrcncl_low_version) + peer.wait_for_disconnect() + + self.log.info('SENDTXRCNCL with version=2 is valid') + sendtxrcncl_higher_version = create_sendtxrcncl_msg() + sendtxrcncl_higher_version.version = 2 + peer = self.nodes[0].add_p2p_connection(PeerNoVerack(), send_version=True, wait_for_verack=False) + with self.nodes[0].assert_debug_log(['Register peer=1']): + peer.send_message(sendtxrcncl_higher_version) + self.nodes[0].disconnect_p2ps() + + self.log.info('unexpected SENDTXRCNCL is ignored') + peer = self.nodes[0].add_p2p_connection(PeerNoVerack(), send_version=False, wait_for_verack=False) + old_version_msg = msg_version() + old_version_msg.nVersion = 70234 + old_version_msg.strSubVer = P2P_SUBVERSION + old_version_msg.nServices = P2P_SERVICES + old_version_msg.relay = 1 + peer.send_message(old_version_msg) + with self.nodes[0].assert_debug_log(['Ignore unexpected txreconciliation signal']): + peer.send_message(create_sendtxrcncl_msg()) + self.nodes[0].disconnect_p2ps() + + self.log.info('sending SENDTXRCNCL after sending VERACK triggers a disconnect') + peer = self.nodes[0].add_p2p_connection(P2PInterface()) + with self.nodes[0].assert_debug_log(["sendtxrcncl received after verack"]): + peer.send_message(create_sendtxrcncl_msg()) + peer.wait_for_disconnect() + + # Now, *receiving* from *outbound*. + self.log.info('SENDTXRCNCL if block-relay-only triggers a disconnect') + peer = self.nodes[0].add_outbound_p2p_connection( + PeerNoVerack(), wait_for_verack=False, p2p_idx=0, connection_type="block-relay-only") + with self.nodes[0].assert_debug_log(["we indicated no tx relay; disconnecting"]): + peer.send_message(create_sendtxrcncl_msg()) + peer.wait_for_disconnect() + + +if __name__ == '__main__': + SendTxRcnclTest().main() diff --git a/test/functional/rpc_misc.py b/test/functional/rpc_misc.py index 9e2663349f42d..ca3901dd6d32f 100755 --- a/test/functional/rpc_misc.py +++ b/test/functional/rpc_misc.py @@ -57,7 +57,7 @@ def run_test(self): self.log.info("test logging rpc and help") # Test logging RPC returns the expected number of logging categories. - assert_equal(len(node.logging()), 38) + assert_equal(len(node.logging()), 39) # Test toggling a logging category on/off/on with the logging RPC. assert_equal(node.logging()['qt'], True) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 945f9713ce4a4..d2f96e1efd8fc 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -2558,3 +2558,25 @@ def serialize(self): def __repr__(self): return "msg_cfcheckpt(filter_type={:#x}, stop_hash={:x})".format( self.filter_type, self.stop_hash) + +class msg_sendtxrcncl: + __slots__ = ("version", "salt") + msgtype = b"sendtxrcncl" + + def __init__(self): + self.version = 0 + self.salt = 0 + + def deserialize(self, f): + self.version = struct.unpack("