diff --git a/ci/test/00_setup_env_native_fuzz_with_valgrind.sh b/ci/test/00_setup_env_native_fuzz_with_valgrind.sh index db5e6a53d80d..9c276881eb83 100755 --- a/ci/test/00_setup_env_native_fuzz_with_valgrind.sh +++ b/ci/test/00_setup_env_native_fuzz_with_valgrind.sh @@ -15,5 +15,5 @@ export RUN_FUZZ_TESTS=true export FUZZ_TESTS_CONFIG="--valgrind" export GOAL="install" # Temporarily pin dwarf 4, until valgrind can understand clang's dwarf 5 -export BITCOIN_CONFIG="--enable-fuzz --with-sanitizers=fuzzer CC=clang-19 CXX=clang++-19 CFLAGS='-gdwarf-4' CXXFLAGS='-gdwarf-4'" +export BITCOIN_CONFIG="--enable-fuzz --with-sanitizers=fuzzer CC='clang-19 -gdwarf-4' CXX='clang++-19 -gdwarf-4'" export CCACHE_MAXSIZE=200M diff --git a/ci/test/00_setup_env_native_tsan.sh b/ci/test/00_setup_env_native_tsan.sh index c9f051d98627..f52c1e756534 100755 --- a/ci/test/00_setup_env_native_tsan.sh +++ b/ci/test/00_setup_env_native_tsan.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Copyright (c) 2019-2022 The Bitcoin Core developers +# Copyright (c) 2019-2023 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. diff --git a/ci/test/00_setup_env_native_valgrind.sh b/ci/test/00_setup_env_native_valgrind.sh index ecc1a3288193..80e956798433 100755 --- a/ci/test/00_setup_env_native_valgrind.sh +++ b/ci/test/00_setup_env_native_valgrind.sh @@ -12,4 +12,4 @@ export NO_DEPENDS=1 export TEST_RUNNER_EXTRA="--exclude feature_init,rpc_bind,feature_bind_extra --timeout-factor=4" # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547 export GOAL="install" # Temporarily pin dwarf 4, until valgrind can understand clang's dwarf 5 -export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=no CC=clang-19 CXX=clang++-19 CFLAGS='-gdwarf-4' CXXFLAGS='-gdwarf-4'" # TODO enable GUI +export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=no CC='clang-19 -gdwarf-4' CXX='clang++-19 -gdwarf-4'" # TODO enable GUI diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 6bccb711b2ba..f32c491ccf2c 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -839,6 +839,8 @@ class PeerManagerImpl final : public PeerManager void RelayAddress(NodeId originator, const CAddress& addr, bool fReachable) EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, g_msgproc_mutex); + FastRandomContext m_rng GUARDED_BY(NetEventsInterface::g_msgproc_mutex); + const CChainParams& m_chainparams; CConnman& m_connman; AddrMan& m_addrman; @@ -974,7 +976,7 @@ class PeerManagerImpl final : public PeerManager bool SetupAddressRelay(const CNode& node, Peer& peer) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); void AddAddressKnown(Peer& peer, const CAddress& addr) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); - void PushAddress(Peer& peer, const CAddress& addr, FastRandomContext& insecure_rand) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); + void PushAddress(Peer& peer, const CAddress& addr) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); /** Refill the peer's GETMNLISTDIFF/GETQUORUMROTATIONINFO token bucket and take a token if one is available. */ bool ConsumeMNListRequestToken(const CNode& node, Peer& peer) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); @@ -1185,7 +1187,7 @@ void PeerManagerImpl::AddAddressKnown(Peer& peer, const CAddress& addr) peer.m_addr_known->insert(addr.GetKey()); } -void PeerManagerImpl::PushAddress(Peer& peer, const CAddress& addr, FastRandomContext& insecure_rand) +void PeerManagerImpl::PushAddress(Peer& peer, const CAddress& addr) { // Known checking here is only to save space from duplicates. // Before sending, we'll filter it again for known addresses that were @@ -1193,7 +1195,7 @@ void PeerManagerImpl::PushAddress(Peer& peer, const CAddress& addr, FastRandomCo assert(peer.m_addr_known); if (addr.IsValid() && !peer.m_addr_known->contains(addr.GetKey()) && IsAddrCompatible(peer, addr)) { if (peer.m_addrs_to_send.size() >= MAX_ADDR_TO_SEND) { - peer.m_addrs_to_send[insecure_rand.randrange(peer.m_addrs_to_send.size())] = addr; + peer.m_addrs_to_send[m_rng.randrange(peer.m_addrs_to_send.size())] = addr; } else { peer.m_addrs_to_send.push_back(addr); } @@ -2133,7 +2135,8 @@ PeerManagerImpl::PeerManagerImpl(CConnman& connman, AddrMan& addrman, BanMan* ba CJWalletManager* cj_walletman, llmq::CInstantSendManager& isman, LLMQContext& llmq_ctx, Options opts) - : m_chainparams(chainman.GetParams()), + : m_rng{opts.deterministic_rng}, + m_chainparams(chainman.GetParams()), m_connman(connman), m_addrman(addrman), m_banman(banman), @@ -2725,7 +2728,6 @@ void PeerManagerImpl::RelayAddress(NodeId originator, const CSipHasher hasher{m_connman.GetDeterministicRandomizer(RANDOMIZER_ID_ADDRESS_RELAY) .Write(hash_addr) .Write(time_addr)}; - FastRandomContext insecure_rand; // Relay reachable addresses to 2 peers. Unreachable addresses are relayed randomly to 1 or 2 peers. unsigned int nRelayNodes = (fReachable || (hasher.Finalize() & 1)) ? 2 : 1; @@ -2749,7 +2751,7 @@ void PeerManagerImpl::RelayAddress(NodeId originator, }; for (unsigned int i = 0; i < nRelayNodes && best[i].first != 0; i++) { - PushAddress(*best[i].second, addr, insecure_rand); + PushAddress(*best[i].second, addr); } } @@ -4464,7 +4466,7 @@ void PeerManagerImpl::ProcessMessage( const bool rate_limited = !pfrom.HasPermission(NetPermissionFlags::Addr); uint64_t num_proc = 0; uint64_t num_rate_limit = 0; - Shuffle(vAddr.begin(), vAddr.end(), FastRandomContext()); + Shuffle(vAddr.begin(), vAddr.end(), m_rng); for (CAddress& addr : vAddr) { if (interruptMsgProc) @@ -5453,9 +5455,8 @@ void PeerManagerImpl::ProcessMessage( } else { vAddr = m_connman.GetAddresses(pfrom, MAX_ADDR_TO_SEND, MAX_PCT_ADDR_TO_SEND); } - FastRandomContext insecure_rand; for (const CAddress &addr : vAddr) { - PushAddress(*peer, addr, insecure_rand); + PushAddress(*peer, addr); } return; } @@ -6266,8 +6267,7 @@ void PeerManagerImpl::MaybeSendAddr(CNode& node, Peer& peer, std::chrono::micros } if (std::optional local_service = GetLocalAddrForPeer(node)) { CAddress local_addr{*local_service, peer.m_our_services, Now()}; - FastRandomContext insecure_rand; - PushAddress(peer, local_addr, insecure_rand); + PushAddress(peer, local_addr); } peer.m_next_local_addr_send = GetExponentialRand(current_time, AVG_LOCAL_ADDRESS_BROADCAST_INTERVAL); } diff --git a/src/net_processing.h b/src/net_processing.h index fbb4ae8eb50a..fb3543c1ec91 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -159,6 +159,9 @@ class PeerManager : public CValidationInterface, public NetEventsInterface, publ uint32_t max_orphan_txs_size{DEFAULT_MAX_ORPHAN_TRANSACTIONS_SIZE * 1000000}; size_t max_extra_txs{DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN}; bool capture_messages{false}; + //! Whether or not the internal RNG behaves deterministically (this is + //! a test-only option). + bool deterministic_rng{false}; }; /** diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index a48562906521..a733a3d29be7 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -433,6 +433,7 @@ TestingSetup::TestingSetup( m_node.banman = std::make_unique(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME); PeerManager::Options peerman_opts; ApplyArgsManOptions(*m_node.args, peerman_opts); + peerman_opts.deterministic_rng = true; m_node.peerman = MakePeerManager(*m_node.connman, m_node, m_node.banman.get(), peerman_opts); { diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py new file mode 100755 index 000000000000..3b4bfb45bde9 --- /dev/null +++ b/test/functional/mempool_sigoplimit.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 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 sigop limit mempool policy (`-bytespersigop` parameter)""" +from math import ceil + +from test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxOut, +) +from test_framework.script import ( + CScript, + OP_CHECKMULTISIG, + OP_CHECKSIG, + OP_ENDIF, + OP_FALSE, + OP_IF, + OP_RETURN, + OP_TRUE, +) +from test_framework.script_util import ( + script_to_p2sh_script, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_greater_than_or_equal, +) +from test_framework.wallet import MiniWallet + + +DEFAULT_BYTES_PER_SIGOP = 20 # default setting +MAX_STANDARD_TX_SIZE = 100000 # Dash consensus tx size limit (after DIP0001) + + +class BytesPerSigOpTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + # allow large datacarrier output to pad transactions + # Dash has no segwit, so P2SH is used instead of P2WSH; allow non-standard + # txs to bypass the MAX_P2SH_SIGOPS (15) standardness limit + self.extra_args = [['-datacarriersize=100000', '-acceptnonstdtxn=1']] + + def create_p2sh_spending_tx(self, redeem_script, output_script): + """Create a 1-input-1-output P2SH spending transaction with only the + redeem script in the scriptSig and the given output script.""" + # create P2SH address and fund it via MiniWallet first + res = self.wallet.send_to( + from_node=self.nodes[0], + scriptPubKey=script_to_p2sh_script(redeem_script), + amount=1000000, + ) + + # create spending transaction + tx = CTransaction() + tx.vin = [CTxIn(COutPoint(int(res["txid"], 16), res["sent_vout"]), CScript([bytes(redeem_script)]))] + tx.vout = [CTxOut(500000, output_script)] + return tx + + def test_sigops_limit(self, bytes_per_sigop, num_sigops): + sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop) + self.log.info(f"- {num_sigops} sigops (equivalent size of {sigop_equivalent_vsize} vbytes)") + + # create a template tx with the specified sigop cost in the redeem script + # (note that the sigops count even though being in a branch that's not executed) + num_multisigops = num_sigops // 20 + num_singlesigops = num_sigops % 20 + redeem_script = CScript( + [OP_FALSE, OP_IF] + + [OP_CHECKMULTISIG]*num_multisigops + + [OP_CHECKSIG]*num_singlesigops + + [OP_ENDIF, OP_TRUE] + ) + # use a 256-byte data-push as lower bound in the output script, in order + # to avoid having to compensate for tx size changes caused by varying + # length serialization sizes (both for scriptPubKey and data-push lengths) + tx = self.create_p2sh_spending_tx(redeem_script, CScript([OP_RETURN, b'X'*256])) + + # bump the tx to reach the sigop-limit equivalent size by padding the datacarrier output + assert_greater_than_or_equal(sigop_equivalent_vsize, tx.get_vsize()) + vsize_to_pad = sigop_equivalent_vsize - tx.get_vsize() + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)]) + # without witness discount (Dash has no segwit) the padding can exceed + # 65535 bytes, growing the push/script length serialization; compensate + vsize_to_pad -= tx.get_vsize() - sigop_equivalent_vsize + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)]) + assert_equal(sigop_equivalent_vsize, tx.get_vsize()) + + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize) + + # increase the tx's vsize to be right above the sigop-limit equivalent size + # => tx's vsize in mempool should also grow accordingly + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad+1)]) + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize+1) + + # decrease the tx's vsize to be right below the sigop-limit equivalent size + # => tx's vsize in mempool should stick at the sigop-limit equivalent + # bytes level, as it is higher than the tx's serialized vsize + # (the maximum of both is taken) + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad-1)]) + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize) + + def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) + + for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072): + if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP: + self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...") + else: + bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}" + self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...") + self.restart_node(0, extra_args=[bytespersigop_parameter] + self.extra_args[0]) + + for num_sigops in (69, 101, 142, 183, 222): + # without the witness discount, larger combinations exceed Dash's tx size limit + if num_sigops * bytes_per_sigop >= MAX_STANDARD_TX_SIZE: + continue + self.test_sigops_limit(bytes_per_sigop, num_sigops) + + +if __name__ == '__main__': + BytesPerSigOpTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 77ee529f6a78..6c8c8de4647f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -399,6 +399,7 @@ 'mempool_unbroadcast.py', 'mempool_compatibility.py', 'mempool_dust.py', + 'mempool_sigoplimit.py', 'rpc_deriveaddresses.py', 'rpc_deriveaddresses.py --usecli', 'p2p_ping.py',