diff --git a/doc/release-notes-dogmode-policy.md b/doc/release-notes-dogmode-policy.md new file mode 100644 index 000000000000..0e4e28276b1b --- /dev/null +++ b/doc/release-notes-dogmode-policy.md @@ -0,0 +1,42 @@ +$DOG Mode policy changes +======================== + +This release relaxes several transaction relay policies, so that $DOG Mode +nodes relay and mine a wider range of transactions. Nodes with the old +policies will not relay these transactions, so $DOG Mode nodes additionally +preferentially peer with each other (see below). + +Policy +------ + +- The maximum standard transaction weight has been raised from 400,000 WU + (100 kvB) to 3,900,000 WU (975 kvB), just under the 4,000,000 WU consensus + block weight limit. Transactions larger than 975 kvB remain non-standard. + The mempool cluster size limit default (`-limitclustersize`) is raised + accordingly from 101 kvB to 976 kvB, and the maximum package weight for + package relay from 404,000 WU to 3,904,000 WU. + +- Since `MAX_OP_RETURN_RELAY` is defined as one quarter of the maximum + standard transaction weight, the default `-datacarriersize` rises from + 100,000 to 975,000 bytes. + +- The minimum `-maxmempool` value remains ~5 MB; the sanity check tying it to + the cluster size limit now requires 5x (was 40x) the cluster size limit. + +- The dust limit is lowered from 294-546 sats (depending on output type) to a + global 1 sat: only zero-value spendable outputs are now considered dust. + `-dustrelayfee` is retained, but it can only lower the dust limit further + (0 disables it entirely, as before), never raise it above 1 sat. + +P2P +--- + +- Nodes now advertise the `NODE_DOG_MODE` service flag (bit 14) and open up to + 4 extra long-lived outbound connections ("dog" connection type) to peers + advertising the same flag, so that $DOG Mode nodes preferentially peer with + each other. This is a port of Libre Relay's preferential peering (which uses + bit 29). Peers connected to via a dog connection that do not advertise + `NODE_DOG_MODE` are disconnected after the version handshake. + +- The "dog" connection type is visible in `getpeerinfo`, `bitcoin-cli + -netinfo` and the GUI peers window. diff --git a/src/bench/txorphanage.cpp b/src/bench/txorphanage.cpp index 1f94553bc4d3..0bb3cfb30c41 100644 --- a/src/bench/txorphanage.cpp +++ b/src/bench/txorphanage.cpp @@ -65,7 +65,13 @@ static void OrphanageSinglePeerEviction(benchmark::Bench& bench) for (unsigned int i{0}; i < NUM_TINY_TRANSACTIONS; ++i) { tiny_txs.emplace_back(MakeTransactionBulkedTo(1, TINY_TX_WEIGHT, det_rand)); } - auto large_tx = MakeTransactionBulkedTo(1, MAX_STANDARD_TX_WEIGHT, det_rand); + // Cap the large transaction to the global usage limit. MakeTxOrphanage permits transactions up to + // MAX_STANDARD_TX_WEIGHT, but policy may raise that limit beyond what the orphanage's reserved + // per-peer usage can hold (e.g. $DOG Mode), which would make the transaction impossible to fit and + // break the expected-eviction arithmetic below. + static constexpr node::TxOrphanage::Usage LARGE_TX_WEIGHT{ + MAX_STANDARD_TX_WEIGHT < node::DEFAULT_RESERVED_ORPHAN_WEIGHT_PER_PEER ? MAX_STANDARD_TX_WEIGHT : node::DEFAULT_RESERVED_ORPHAN_WEIGHT_PER_PEER}; + auto large_tx = MakeTransactionBulkedTo(1, LARGE_TX_WEIGHT, det_rand); assert(GetTransactionWeight(*large_tx) <= MAX_STANDARD_TX_WEIGHT); const auto orphanage{node::MakeTxOrphanage(/*max_global_latency_score=*/node::DEFAULT_MAX_ORPHANAGE_LATENCY_SCORE, /*reserved_peer_usage=*/node::DEFAULT_RESERVED_ORPHAN_WEIGHT_PER_PEER)}; @@ -93,6 +99,13 @@ static void OrphanageSinglePeerEviction(benchmark::Bench& bench) assert(orphanage->TotalLatencyScore() <= orphanage->MaxGlobalLatencyScore()); assert(orphanage->TotalOrphanUsage() + TINY_TX_WEIGHT > orphanage->MaxGlobalUsage()); + // Expected evictions: enough tiny transactions must be evicted to cover the usage deficit after + // adding the large transaction. The large transaction itself raises the announcement count by + // one, so subtract it from the expected number evicted during the trim. + const auto expected_deficit{orphanage->TotalOrphanUsage() + GetTransactionWeight(*large_tx) - orphanage->MaxGlobalUsage()}; + assert(expected_deficit > 0 && expected_deficit <= orphanage->TotalOrphanUsage()); + const auto expected_evictions{(expected_deficit + TINY_TX_WEIGHT - 1) / TINY_TX_WEIGHT - 1}; + bench.epochs(1).epochIterations(1).run([&]() NO_THREAD_SAFETY_ANALYSIS { // Lastly, add the large transaction. const auto num_announcements_before_trim{orphanage->CountAnnouncements()}; @@ -104,7 +117,7 @@ static void OrphanageSinglePeerEviction(benchmark::Bench& bench) // The number of evictions is the same regardless of the number of peers. In both cases, we can exceed the // usage limit using 1 maximally-sized transaction. - assert(num_evicted == MAX_STANDARD_TX_WEIGHT / TINY_TX_WEIGHT); + assert(num_evicted == expected_evictions); }); } static void OrphanageMultiPeerEviction(benchmark::Bench& bench) @@ -117,7 +130,7 @@ static void OrphanageMultiPeerEviction(benchmark::Bench& bench) // Subtract 4 because BulkTransaction rounds up and we must avoid going over the weight limit early. static constexpr node::TxOrphanage::Usage LARGE_TX_WEIGHT{TOTAL_USAGE_LIMIT / NUM_UNIQUE_TXNS - 4}; static_assert(LARGE_TX_WEIGHT >= TINY_TX_WEIGHT * 2, "Tx is too small, increase NUM_PEERS"); - // The orphanage does not permit any transactions larger than 400'000, so this test will not work if the large tx is much larger. + // The orphanage does not permit any transactions larger than MAX_STANDARD_TX_WEIGHT, so this test will not work if the large tx is much larger. static_assert(LARGE_TX_WEIGHT <= MAX_STANDARD_TX_WEIGHT, "Tx is too large, decrease NUM_PEERS"); FastRandomContext det_rand{true}; diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 909ed09faaca..f75c33697d25 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -383,6 +383,7 @@ class NetinfoRequestHandler : public BaseRequestHandler private: std::array, 3> m_counts{{{}}}; //!< Peer counts by (in/out/total, networks/total) uint8_t m_block_relay_peers_count{0}; + uint8_t m_dog_peers_count{0}; uint8_t m_manual_peers_count{0}; uint8_t m_details_level{0}; //!< Optional user-supplied arg to set dashboard details level bool DetailsRequested() const { return m_details_level; } @@ -452,6 +453,7 @@ class NetinfoRequestHandler : public BaseRequestHandler if (conn_type == "manual" || conn_type == "feeler") return conn_type; if (conn_type == "addr-fetch") return "addr"; if (conn_type == "private-broadcast") return "priv"; + if (conn_type == "dog") return "dog"; return ""; } std::string FormatServices(const UniValue& services) @@ -530,6 +532,7 @@ class NetinfoRequestHandler : public BaseRequestHandler ++m_counts.at(2).at(network_id); // total by network ++m_counts.at(2).at(NETWORKS.size()); // total overall if (conn_type == "block-relay-only") ++m_block_relay_peers_count; + if (conn_type == "dog") ++m_dog_peers_count; if (conn_type == "manual") ++m_manual_peers_count; if (m_outbound_only_selected && !is_outbound) continue; if (DetailsRequested()) { @@ -633,6 +636,7 @@ class NetinfoRequestHandler : public BaseRequestHandler } result += " total block"; + if (m_dog_peers_count) result += " dog"; if (m_manual_peers_count) result += " manual"; const std::array rows{"in", "out", "total"}; @@ -642,8 +646,9 @@ class NetinfoRequestHandler : public BaseRequestHandler result += strprintf("%8i", m_counts.at(i).at(n)); // network peers count } result += strprintf(" %5i", m_counts.at(i).at(NETWORKS.size())); // total peers count - if (i == 1) { // the outbound row has two extra columns for block relay and manual peer counts + if (i == 1) { // the outbound row has extra columns for block relay, dog mode and manual peer counts result += strprintf(" %5i", m_block_relay_peers_count); + if (m_dog_peers_count) result += strprintf(" %5i", m_dog_peers_count); if (m_manual_peers_count) result += strprintf(" %5i", m_manual_peers_count); } } @@ -704,6 +709,7 @@ class NetinfoRequestHandler : public BaseRequestHandler " \"feeler\" - short-lived connection for testing addresses\n" " \"addr\" - address fetch; short-lived connection for requesting addresses\n" " \"priv\" - private broadcast; short-lived connection for broadcasting our transactions\n" + " \"dog\" - $DOG Mode; long-lived connection to a $DOG Mode peer\n" " net Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", \"cjdns\", or \"npr\" (not publicly routable))\n" " serv Services offered by the peer\n" " \"n\" - NETWORK: peer can serve the full block chain\n" @@ -712,6 +718,7 @@ class NetinfoRequestHandler : public BaseRequestHandler " \"c\" - COMPACT_FILTERS: peer can handle basic block filter requests (see BIPs 157 and 158)\n" " \"l\" - NETWORK_LIMITED: peer limited to serving only the last 288 blocks (~2 days)\n" " \"2\" - P2P_V2: peer supports version 2 P2P transport protocol, as defined in BIP 324\n" + " \"d\" - DOG_MODE: $DOG Mode peer (preferential peering)\n" " \"u\" - UNKNOWN: unrecognized bit flag\n" " v Version of transport protocol used for the connection\n" " mping Minimum observed ping time, in milliseconds (ms)\n" diff --git a/src/init.cpp b/src/init.cpp index fb4b8fa9e27a..2d6915d00096 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -671,7 +671,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-acceptnonstdtxn", strprintf("Relay and mine \"non-standard\" transactions (test networks only; default: %u)", DEFAULT_ACCEPT_NON_STD_TXN), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::NODE_RELAY); argsman.AddArg("-incrementalrelayfee=", strprintf("Fee rate (in %s/kvB) used to define cost of relay, used for mempool limiting and replacement policy. (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_INCREMENTAL_RELAY_FEE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::NODE_RELAY); - argsman.AddArg("-dustrelayfee=", strprintf("Fee rate (in %s/kvB) used to define dust, the value of an output such that it will cost more than its value in fees at this fee rate to spend it. (default: %s)", CURRENCY_UNIT, FormatMoney(DUST_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-dustrelayfee=", strprintf("Fee rate (in %s/kvB) used to define dust, the value of an output such that it will cost more than its value in fees at this fee rate to spend it. The dust threshold is capped at a global limit of 1 satoshi; this option can only lower it further. (default: %s)", CURRENCY_UNIT, FormatMoney(DUST_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::NODE_RELAY); argsman.AddArg("-acceptstalefeeestimates", strprintf("Read fee estimates even if they are stale (%sdefault: %u) fee estimates are considered stale if they are %s hours old", "regtest only; ", DEFAULT_ACCEPT_STALE_FEE_ESTIMATES, Ticks(MAX_FILE_AGE)), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-bytespersigop", strprintf("Equivalent bytes per sigop in transactions for relay and mining (default: %u)", DEFAULT_BYTES_PER_SIGOP), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-datacarrier", strprintf("Relay and mine data carrier transactions (default: %u)", DEFAULT_ACCEPT_DATACARRIER), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); @@ -860,7 +860,7 @@ namespace { // Variables internal to initialization process only int nMaxConnections; int available_fds; -ServiceFlags g_local_services = ServiceFlags(NODE_NETWORK_LIMITED | NODE_WITNESS); +ServiceFlags g_local_services = ServiceFlags(NODE_NETWORK_LIMITED | NODE_WITNESS | NODE_DOG_MODE); int64_t peer_connect_timeout; std::set g_enabled_filter_types; @@ -2099,6 +2099,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) CConnman::Options connOptions; connOptions.m_local_services = g_local_services; connOptions.m_max_automatic_connections = nMaxConnections; + connOptions.m_max_outbound_dog_mode = MAX_DOG_MODE_CONNECTIONS; connOptions.uiInterface = &uiInterface; connOptions.m_banman = node.banman.get(); connOptions.m_msgproc = node.peerman.get(); diff --git a/src/net.cpp b/src/net.cpp index 5c28efade891..62cd1aa6309b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1887,6 +1887,9 @@ bool CConnman::AddConnection(const std::string& address, ConnectionType conn_typ case ConnectionType::BLOCK_RELAY: max_connections = m_max_outbound_block_relay; break; + case ConnectionType::DOG_MODE: + max_connections = m_max_outbound_dog_mode; + break; // no limit for ADDR_FETCH because -seednode has no limit either case ConnectionType::ADDR_FETCH: break; @@ -2669,6 +2672,7 @@ void CConnman::ThreadOpenConnections(const std::vector connect, std // Only connect out to one peer per ipv4/ipv6 network group (/16 for IPv4). int nOutboundFullRelay = 0; int nOutboundBlockRelay = 0; + int nOutboundDogMode = 0; int outbound_privacy_network_peers = 0; std::set> outbound_ipv46_peer_netgroups; @@ -2677,6 +2681,7 @@ void CConnman::ThreadOpenConnections(const std::vector connect, std for (const CNode* pnode : m_nodes) { if (pnode->IsFullOutboundConn()) nOutboundFullRelay++; if (pnode->IsBlockOnlyConn()) nOutboundBlockRelay++; + if (pnode->IsDogModeConn()) nOutboundDogMode++; // Make sure our persistent outbound slots to ipv4/ipv6 peers belong to different netgroups. switch (pnode->m_conn_type) { @@ -2693,6 +2698,7 @@ void CConnman::ThreadOpenConnections(const std::vector connect, std case ConnectionType::MANUAL: case ConnectionType::OUTBOUND_FULL_RELAY: case ConnectionType::BLOCK_RELAY: + case ConnectionType::DOG_MODE: const CAddress address{pnode->addr}; if (address.IsTor() || address.IsI2P() || address.IsCJDNS()) { // Since our addrman-groups for these networks are @@ -2721,6 +2727,7 @@ void CConnman::ThreadOpenConnections(const std::vector connect, std auto now = GetTime(); bool anchor = false; bool fFeeler = false; + bool dog_mode = false; std::optional preferred_net; // Determine what type of connection to open. Opening @@ -2767,6 +2774,9 @@ void CConnman::ThreadOpenConnections(const std::vector connect, std // (similar to how we deal with extra outbound peers). next_extra_block_relay = now + rng.rand_exp_duration(EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL); conn_type = ConnectionType::BLOCK_RELAY; + } else if (nOutboundDogMode < m_max_outbound_dog_mode) { + conn_type = ConnectionType::DOG_MODE; + dog_mode = true; } else if (now > next_feeler) { next_feeler = now + rng.rand_exp_duration(FEELER_INTERVAL); conn_type = ConnectionType::FEELER; @@ -2863,6 +2873,12 @@ void CConnman::ThreadOpenConnections(const std::vector connect, std continue; } + // if we would like to open a $DOG Mode connection, require the + // NODE_DOG_MODE service flag to be set + if (dog_mode && !HasDogModeServiceFlag(addr.nServices)) { + continue; + } + // for non-feelers, require all the services we'll want, // for feelers, only require they be a full node (only because most // SPV clients don't have a good address DB available) diff --git a/src/net.h b/src/net.h index 96a171736ed3..648adff71a78 100644 --- a/src/net.h +++ b/src/net.h @@ -71,6 +71,8 @@ static const int MAX_OUTBOUND_FULL_RELAY_CONNECTIONS = 8; static const int MAX_ADDNODE_CONNECTIONS = 8; /** Maximum number of block-relay-only outgoing connections */ static const int MAX_BLOCK_RELAY_ONLY_CONNECTIONS = 2; +/** Maximum number of automatic $DOG Mode peers */ +static const int MAX_DOG_MODE_CONNECTIONS = 4; /** Maximum number of feeler connections */ static const int MAX_FEELER_CONNECTIONS = 1; /** Maximum number of private broadcast connections */ @@ -777,6 +779,7 @@ class CNode switch (m_conn_type) { case ConnectionType::OUTBOUND_FULL_RELAY: case ConnectionType::BLOCK_RELAY: + case ConnectionType::DOG_MODE: return true; case ConnectionType::INBOUND: case ConnectionType::MANUAL: @@ -807,6 +810,7 @@ class CNode case ConnectionType::PRIVATE_BROADCAST: return false; case ConnectionType::OUTBOUND_FULL_RELAY: + case ConnectionType::DOG_MODE: case ConnectionType::MANUAL: return true; } // no default case, so the compiler can warn about missing cases @@ -835,6 +839,10 @@ class CNode return m_conn_type == ConnectionType::INBOUND; } + bool IsDogModeConn() const { + return m_conn_type == ConnectionType::DOG_MODE; + } + bool ExpectServicesFromConn() const { switch (m_conn_type) { case ConnectionType::INBOUND: @@ -845,6 +853,7 @@ class CNode case ConnectionType::BLOCK_RELAY: case ConnectionType::ADDR_FETCH: case ConnectionType::PRIVATE_BROADCAST: + case ConnectionType::DOG_MODE: return true; } // no default case, so the compiler can warn about missing cases @@ -1104,6 +1113,7 @@ class CConnman bool whitelist_forcerelay = DEFAULT_WHITELISTFORCERELAY; bool whitelist_relay = DEFAULT_WHITELISTRELAY; bool m_capture_messages = false; + int m_max_outbound_dog_mode = 0; }; void Init(const Options& connOptions) EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_total_bytes_sent_mutex) @@ -1114,7 +1124,12 @@ class CConnman m_max_automatic_connections = connOptions.m_max_automatic_connections; m_max_outbound_full_relay = std::min(MAX_OUTBOUND_FULL_RELAY_CONNECTIONS, m_max_automatic_connections); m_max_outbound_block_relay = std::min(MAX_BLOCK_RELAY_ONLY_CONNECTIONS, m_max_automatic_connections - m_max_outbound_full_relay); - m_max_automatic_outbound = m_max_outbound_full_relay + m_max_outbound_block_relay + m_max_feeler; + // Clamp dog mode connections against the budget remaining after the + // other outbound slots, like the block-relay counter above, instead + // of trusting the caller-provided value. + m_max_outbound_dog_mode = std::min(connOptions.m_max_outbound_dog_mode, + std::max(0, m_max_automatic_connections - m_max_outbound_full_relay - m_max_outbound_block_relay - m_max_feeler)); + m_max_automatic_outbound = m_max_outbound_full_relay + m_max_outbound_block_relay + m_max_feeler + m_max_outbound_dog_mode; m_max_inbound = std::max(0, m_max_automatic_connections - m_max_automatic_outbound); m_use_addrman_outgoing = connOptions.m_use_addrman_outgoing; m_client_interface = connOptions.uiInterface; @@ -1679,6 +1694,9 @@ class CConnman // We do not relay tx or addr messages with these peers int m_max_outbound_block_relay; + // How many $DOG Mode outbound peers we want + int m_max_outbound_dog_mode; + int m_max_addnode{MAX_ADDNODE_CONNECTIONS}; int m_max_feeler{MAX_FEELER_CONNECTIONS}; int m_max_automatic_outbound; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index e2f7126e3ce6..7a9ede5c411c 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3620,6 +3620,13 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string return; } + if (pfrom.IsDogModeConn() && !HasDogModeServiceFlag(nServices)) + { + LogDebug(BCLog::NET, "peer=%d does not offer $DOG Mode relay as expected; disconnecting\n", pfrom.GetId()); + pfrom.fDisconnect = true; + return; + } + if (nVersion < MIN_PEER_PROTO_VERSION) { // disconnect from peers older than this proto version LogDebug(BCLog::NET, "peer using obsolete version %i, %s\n", nVersion, pfrom.DisconnectMsg(fLogIPs)); diff --git a/src/node/connection_types.cpp b/src/node/connection_types.cpp index b2448352c220..6a1c9148947d 100644 --- a/src/node/connection_types.cpp +++ b/src/node/connection_types.cpp @@ -22,6 +22,8 @@ std::string ConnectionTypeAsString(ConnectionType conn_type) return "addr-fetch"; case ConnectionType::PRIVATE_BROADCAST: return "private-broadcast"; + case ConnectionType::DOG_MODE: + return "dog"; } // no default case, so the compiler can warn about missing cases assert(false); diff --git a/src/node/connection_types.h b/src/node/connection_types.h index eeb106b616d7..85626e093834 100644 --- a/src/node/connection_types.h +++ b/src/node/connection_types.h @@ -82,6 +82,14 @@ enum class ConnectionType { * our own transactions) and closed afterwards. */ PRIVATE_BROADCAST, + + /** + * Dog mode connections are long-lived connections to peers advertising + * NODE_DOG_MODE, used to relay transactions with the $DOG Mode + * standardness policies. We automatically attempt to open + * MAX_DOG_MODE_CONNECTIONS using addresses from our AddrMan. + */ + DOG_MODE, }; /** Convert ConnectionType enum to a string value */ diff --git a/src/policy/packages.h b/src/policy/packages.h index 1a7e101ef46c..cd99e810139d 100644 --- a/src/policy/packages.h +++ b/src/policy/packages.h @@ -21,7 +21,7 @@ static constexpr uint32_t MAX_PACKAGE_COUNT{25}; to allow for context-less checks. This must allow a superset of sigops weighted vsize limited transactions to not disallow transactions we would have otherwise accepted individually. */ -static constexpr uint32_t MAX_PACKAGE_WEIGHT = 404'000; +static constexpr uint32_t MAX_PACKAGE_WEIGHT = 3'904'000; static_assert(MAX_PACKAGE_WEIGHT >= MAX_STANDARD_TX_WEIGHT); // Packages are part of a single cluster, so ensure that the package limits are diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 46ec238c3b18..98f7bf23e96f 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -29,16 +29,12 @@ CAmount GetDustThreshold(const CTxOut& txout, const CFeeRate& dustRelayFeeIn) // which has units satoshis-per-kilobyte. // If you'd pay more in fees than the value of the output // to spend something, then we consider it dust. - // A typical spendable non-segwit txout is 34 bytes big, and will - // need a CTxIn of at least 148 bytes to spend: - // so dust is a spendable txout less than - // 182*dustRelayFee/1000 (in satoshis). - // 546 satoshis at the default rate of 3000 sat/kvB. - // A typical spendable segwit P2WPKH txout is 31 bytes big, and will - // need a CTxIn of at least 67 bytes to spend: - // so dust is a spendable txout less than - // 98*dustRelayFee/1000 (in satoshis). - // 294 satoshis at the default rate of 3000 sat/kvB. + // + // $DOG Mode: the dust limit is a global 1 satoshi for all spendable + // output types; only zero-value outputs are considered dust. The + // fee-rate-based computation is retained so that -dustrelayfee can only + // lower the dust limit further (0 disables it entirely), but never raise + // it above 1 satoshi. if (txout.scriptPubKey.IsUnspendable()) return 0; @@ -59,7 +55,7 @@ CAmount GetDustThreshold(const CTxOut& txout, const CFeeRate& dustRelayFeeIn) nSize += (32 + 4 + 1 + 107 + 4); // the 148 mentioned above } - return dustRelayFeeIn.GetFee(nSize); + return std::min(dustRelayFeeIn.GetFee(nSize), CAmount{1}); } bool IsDust(const CTxOut& txout, const CFeeRate& dustRelayFeeIn) diff --git a/src/policy/policy.h b/src/policy/policy.h index 0bc7b3277083..435c903e1124 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -33,8 +33,11 @@ static constexpr unsigned int DEFAULT_COINBASE_OUTPUT_MAX_ADDITIONAL_SIGOPS{400} static constexpr unsigned int MINIMUM_BLOCK_RESERVED_WEIGHT{2000}; /** Default for -blockmintxfee, which sets the minimum feerate for a transaction in blocks created by mining code **/ static constexpr unsigned int DEFAULT_BLOCK_MIN_TX_FEE{1}; -/** The maximum weight for transactions we're willing to relay/mine */ -static constexpr int32_t MAX_STANDARD_TX_WEIGHT{400000}; +/** The maximum weight for transactions we're willing to relay/mine. This is + * just under MAX_BLOCK_WEIGHT so that a maximum size transaction can still be + * mined in a block (with a small coinbase), and its stripped serialization + * always fits within the 4MB MAX_PROTOCOL_MESSAGE_LENGTH limit. */ +static constexpr int32_t MAX_STANDARD_TX_WEIGHT{3'900'000}; /** The minimum non-witness size for transactions we're willing to relay/mine: one larger than 64 */ static constexpr unsigned int MIN_STANDARD_TX_NONWITNESS_SIZE{65}; /** Maximum number of signature check operations in an IsStandard() P2SH script */ @@ -63,14 +66,18 @@ static constexpr unsigned int MAX_STANDARD_SCRIPTSIG_SIZE{1650}; * Changing the dust limit changes which transactions are * standard and should be done with care and ideally rarely. It makes sense to * only increase the dust limit after prior releases were already not creating - * outputs below the new threshold */ + * outputs below the new threshold. + * Note: $DOG Mode caps the dust threshold at a global limit of 1 satoshi, + * so this feerate can only lower the dust limit further (see GetDustThreshold). */ static constexpr unsigned int DUST_RELAY_TX_FEE{3000}; /** Default for -minrelaytxfee, minimum relay fee for transactions */ static constexpr unsigned int DEFAULT_MIN_RELAY_TX_FEE{100}; /** Maximum number of transactions per cluster (default) */ static constexpr unsigned int DEFAULT_CLUSTER_LIMIT{64}; -/** Maximum size of cluster in virtual kilobytes */ -static constexpr unsigned int DEFAULT_CLUSTER_SIZE_LIMIT_KVB{101}; +/** Maximum size of cluster in virtual kilobytes. Must be larger than + * MAX_STANDARD_TX_WEIGHT (in kvB) so that a maximum size standard transaction + * is accepted to the mempool as a cluster of one. */ +static constexpr unsigned int DEFAULT_CLUSTER_SIZE_LIMIT_KVB{976}; /** Default for -limitancestorcount, max number of in-mempool ancestors */ static constexpr unsigned int DEFAULT_ANCESTOR_LIMIT{25}; /** Default for -limitdescendantcount, max number of in-mempool descendants */ diff --git a/src/protocol.cpp b/src/protocol.cpp index 5f0b76e39449..da86b8519329 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -99,6 +99,7 @@ static std::string serviceFlagToStr(size_t bit) case NODE_COMPACT_FILTERS: return "COMPACT_FILTERS"; case NODE_NETWORK_LIMITED: return "NETWORK_LIMITED"; case NODE_P2P_V2: return "P2P_V2"; + case NODE_DOG_MODE: return "DOG_MODE"; // Not using default, so we get warned when a case is missing } diff --git a/src/protocol.h b/src/protocol.h index 8ed90dd3f8ef..c4f7b3dc1bb3 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -329,6 +329,18 @@ enum ServiceFlags : uint64_t { // NODE_P2P_V2 means the node supports BIP324 transport NODE_P2P_V2 = (1 << 11), + // NODE_DOG_MODE means the node is a $DOG Mode node, implementing $DOG Mode + // relay policies. $DOG Mode nodes preferentially peer with each other to + // form a relay subnetwork for transactions that are non-standard on the + // rest of the network. Modeled on Libre Relay's NODE_LIBRE_RELAY (bit 29). + // Bit 14 is used rather than a bit in the experimental range below: that + // range is occupied by Bitcoin Knots (bits 24, 26-29, of which 28 is + // NODE_BLAKE2B, a proof-of-work hardfork signal, since the v29.4.1 + // release candidates) and by Libre Relay (bit 29). Bit 14 is declared by + // no deployed client and was advertised by zero nodes in network crawls + // at the time of writing. + NODE_DOG_MODE = (1 << 14), + // Bits 24-31 are reserved for temporary experiments. Just pick a bit that // isn't getting used, or one not being used much, and notify the // bitcoin-development mailing list. Remember that service bits are just @@ -362,6 +374,14 @@ static inline bool MayHaveUsefulAddressDB(ServiceFlags services) return (services & NODE_NETWORK) || (services & NODE_NETWORK_LIMITED); } +/** + * Checks if a peer with the given service flags is a $DOG Mode node. + */ +static inline bool HasDogModeServiceFlag(ServiceFlags services) +{ + return (services & NODE_DOG_MODE); +} + /** A CService with information about it as peer */ class CAddress : public CService { diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 1619227a419f..2333ee44bfbf 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -724,6 +724,8 @@ QString ConnectionTypeToQString(ConnectionType conn_type, bool prepend_direction case ConnectionType::ADDR_FETCH: return prefix + QObject::tr("Address Fetch"); //: Short-lived peer connection type that is used for broadcasting privacy-sensitive data. case ConnectionType::PRIVATE_BROADCAST: return prefix + QObject::tr("Private Broadcast"); + //: Long-lived peer connection type to $DOG Mode peers. + case ConnectionType::DOG_MODE: return prefix + QObject::tr("Dog Mode"); } // no default case, so the compiler can warn about missing cases assert(false); } diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index d30a8acff623..27c3a47ed06c 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -487,7 +487,10 @@ RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformSty tr("Outbound Address Fetch: short-lived, for soliciting addresses"), /*: Explanatory text for a short-lived outbound peer connection that is used to broadcast privacy-sensitive data (like our transactions). */ - tr("Private broadcast: short-lived, for broadcasting privacy-sensitive transactions")}; + tr("Private broadcast: short-lived, for broadcasting privacy-sensitive transactions"), + /*: Explanatory text for a long-lived outbound peer connection that is used + to relay transactions with $DOG Mode peers. */ + tr("Outbound Dog Mode: long-lived, for connecting with $DOG Mode peers")}; const QString connection_types_list{"
  • " + Join(CONNECTION_TYPE_DOC, QString("
  • ")) + "
"}; ui->peerConnectionTypeLabel->setToolTip(ui->peerConnectionTypeLabel->toolTip().arg(connection_types_list)); const std::vector TRANSPORT_TYPE_DOC{ diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index e9fbd31cc3c8..e9ebc8e9b044 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -49,7 +49,8 @@ const std::vector CONNECTION_TYPE_DOC{ "manual (added via addnode RPC or -addnode/-connect configuration options)", "addr-fetch (short-lived automatic connection for soliciting addresses)", "feeler (short-lived automatic connection for testing addresses)", - "private-broadcast (short-lived automatic connection for broadcasting privacy-sensitive transactions)" + "private-broadcast (short-lived automatic connection for broadcasting privacy-sensitive transactions)", + "dog (long-lived automatic connection to $DOG Mode peers)" }; const std::vector TRANSPORT_TYPE_DOC{ @@ -391,7 +392,7 @@ static RPCHelpMan addconnection() "Open an outbound connection to a specified node. This RPC is for testing only.\n", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The IP address and port to attempt connecting to."}, - {"connection_type", RPCArg::Type::STR, RPCArg::Optional::NO, "Type of connection to open (\"outbound-full-relay\", \"block-relay-only\", \"addr-fetch\" or \"feeler\")."}, + {"connection_type", RPCArg::Type::STR, RPCArg::Optional::NO, "Type of connection to open (\"outbound-full-relay\", \"block-relay-only\", \"addr-fetch\", \"feeler\" or \"dog\")."}, {"v2transport", RPCArg::Type::BOOL, RPCArg::Optional::NO, "Attempt to connect using BIP324 v2 transport protocol"}, }, RPCResult{ @@ -421,6 +422,8 @@ static RPCHelpMan addconnection() conn_type = ConnectionType::ADDR_FETCH; } else if (conn_type_in == "feeler") { conn_type = ConnectionType::FEELER; + } else if (conn_type_in == "dog") { + conn_type = ConnectionType::DOG_MODE; } else { throw JSONRPCError(RPC_INVALID_PARAMETER, self.ToString()); } diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 32801d97b8bd..8b60a7adb62b 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -139,6 +139,89 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) BOOST_CHECK_EQUAL(pnode4->ConnectedThroughNetwork(), Network::NET_ONION); } +BOOST_AUTO_TEST_CASE(cnode_dog_mode_conn_test) +{ + NodeId id = 0; + + in_addr ipv4Addr; + ipv4Addr.s_addr = 0xa0b0c001; + + CAddress addr = CAddress(CService(ipv4Addr, 7777), NODE_NETWORK); + std::string pszDest; + + std::unique_ptr pnode = std::make_unique(id++, + /*sock=*/nullptr, + addr, + /*nKeyedNetGroupIn=*/0, + /*nLocalHostNonceIn=*/0, + CAddress(), + pszDest, + ConnectionType::DOG_MODE, + /*inbound_onion=*/false, + /*network_key=*/0); + // $DOG Mode connections are persistent outbound, transaction-relaying connections + BOOST_CHECK(pnode->IsDogModeConn()); + BOOST_CHECK(pnode->IsOutboundOrBlockRelayConn()); + BOOST_CHECK(pnode->IsManualOrFullOutboundConn()); + BOOST_CHECK(pnode->ExpectServicesFromConn()); + BOOST_CHECK(!pnode->IsFullOutboundConn()); + BOOST_CHECK(!pnode->IsBlockOnlyConn()); + BOOST_CHECK(!pnode->IsManualConn()); + BOOST_CHECK(!pnode->IsFeelerConn()); + BOOST_CHECK(!pnode->IsAddrFetchConn()); + BOOST_CHECK(!pnode->IsInboundConn()); + BOOST_CHECK(!pnode->IsPrivateBroadcastConn()); + + // The connection type string used by RPC and -netinfo + BOOST_CHECK_EQUAL(ConnectionTypeAsString(ConnectionType::DOG_MODE), "dog"); + + // The NODE_DOG_MODE service flag + BOOST_CHECK(HasDogModeServiceFlag(ServiceFlags(NODE_NETWORK | NODE_WITNESS | NODE_DOG_MODE))); + BOOST_CHECK(!HasDogModeServiceFlag(ServiceFlags(NODE_NETWORK | NODE_WITNESS))); + BOOST_CHECK(!HasDogModeServiceFlag(NODE_NONE)); + BOOST_CHECK_EQUAL(serviceFlagsToStr(NODE_DOG_MODE).front(), "DOG_MODE"); +} + +BOOST_AUTO_TEST_CASE(cconnman_dog_mode_clamp_test) +{ + // The number of $DOG Mode outbound slots is clamped against the + // connection budget remaining after the full-relay, block-relay and + // feeler slots, instead of trusting the caller-provided value. + struct Expected { + int max_connections; + int full_relay; + int block_relay; + int dog_mode; + int inbound; + }; + const std::vector tests{ + // -maxconnections=11 leaves no budget for dog mode connections, + {11, 8, 2, 0, 0}, + // they ramp up one per extra connection, + {12, 8, 2, 1, 0}, + {13, 8, 2, 2, 0}, + {14, 8, 2, 3, 0}, + // reach the maximum at 15, + {15, 8, 2, 4, 0}, + // and only above that do inbound slots appear. + {16, 8, 2, 4, 1}, + {125, 8, 2, 4, 110}, + }; + for (const auto& t : tests) { + auto connman{std::make_unique(0x1337, 0x1337, *m_node.addrman, *m_node.netgroupman, Params())}; + CConnman::Options options; + options.m_max_automatic_connections = t.max_connections; + options.m_max_outbound_dog_mode = MAX_DOG_MODE_CONNECTIONS; + connman->Init(options); + BOOST_CHECK_EQUAL(connman->GetMaxOutboundFullRelay(), t.full_relay); + BOOST_CHECK_EQUAL(connman->GetMaxOutboundBlockRelay(), t.block_relay); + BOOST_CHECK_EQUAL(connman->GetMaxOutboundDogMode(), t.dog_mode); + BOOST_CHECK_EQUAL(connman->GetMaxAutomaticOutbound(), t.full_relay + t.block_relay + MAX_FEELER_CONNECTIONS + t.dog_mode); + BOOST_CHECK_LE(connman->GetMaxAutomaticOutbound(), t.max_connections); + BOOST_CHECK_EQUAL(connman->GetMaxInbound(), t.inbound); + } +} + BOOST_AUTO_TEST_CASE(cnetaddr_basic) { CNetAddr addr; diff --git a/src/test/orphanage_tests.cpp b/src/test/orphanage_tests.cpp index 6ebfab34c52b..a1c84efca073 100644 --- a/src/test/orphanage_tests.cpp +++ b/src/test/orphanage_tests.cpp @@ -483,7 +483,9 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans) tx.vout.resize(1); tx.vout[0].nValue = 1*CENT; tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); - tx.vin.resize(2777); + // Each signed input is ~147-149 bytes (~590 WU), so 7000 inputs puts the + // transaction above MAX_STANDARD_TX_WEIGHT even with the smallest signatures. + tx.vin.resize(7000); for (unsigned int j = 0; j < tx.vin.size(); j++) { tx.vin[j].prevout.n = j; diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 916f565c04df..dcd8c3af048c 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -777,9 +777,9 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) CheckIsStandard(t); - // Check dust with default relay fee: - CAmount nDustThreshold = 182 * g_dust.GetFeePerK() / 1000; - BOOST_CHECK_EQUAL(nDustThreshold, 546); + // Check dust with default relay fee: the dust limit is a global 1 sat + CAmount nDustThreshold = 1; + BOOST_CHECK_EQUAL(GetDustThreshold(CTxOut(0, t.vout[0].scriptPubKey), g_dust), nDustThreshold); // Add dust outputs up to allowed maximum, still standard! for (size_t i{0}; i < MAX_DUST_OUTPUTS_PER_TX; ++i) { @@ -811,14 +811,18 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) t.version = 2; CheckIsStandard(t); - // Check dust with odd relay fee to verify rounding: - // nDustThreshold = 182 * 3702 / 1000 + // Check dust with a higher relay fee: the global 1 sat dust limit is a + // cap, a higher dust relay fee does not raise it g_dust = CFeeRate(3702); // dust: - t.vout[0].nValue = 674 - 1; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // not dust: - t.vout[0].nValue = 674; + t.vout[0].nValue = 1; + CheckIsStandard(t); + // A zero dust relay fee disables the dust limit entirely + g_dust = CFeeRate(0); + t.vout[0].nValue = 0; CheckIsStandard(t); g_dust = CFeeRate{DUST_RELAY_TX_FEE}; @@ -922,19 +926,19 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) // Check tx-size (non-standard if transaction weight is > MAX_STANDARD_TX_WEIGHT) t.vin.clear(); - t.vin.resize(2438); // size per input (empty scriptSig): 41 bytes - t.vout[0].scriptPubKey = CScript() << OP_RETURN << std::vector(19, 0); // output size: 30 bytes - // tx header: 12 bytes => 48 weight units - // 2438 inputs: 2438*41 = 99958 bytes => 399832 weight units - // 1 output: 30 bytes => 120 weight units - // ====================================== - // total: 400000 weight units - BOOST_CHECK_EQUAL(GetTransactionWeight(CTransaction(t)), 400000); + t.vin.resize(23779); // size per input (empty scriptSig): 41 bytes + t.vout[0].scriptPubKey = CScript() << OP_RETURN << std::vector(38, 0); // output size: 49 bytes + // tx header: 12 bytes => 48 weight units + // 23779 inputs: 23779*41 = 974939 bytes => 3899756 weight units + // 1 output: 49 bytes => 196 weight units + // ======================================== + // total: 3900000 weight units + BOOST_CHECK_EQUAL(GetTransactionWeight(CTransaction(t)), 3900000); CheckIsStandard(t); - // increase output size by one byte, so we end up with 400004 weight units - t.vout[0].scriptPubKey = CScript() << OP_RETURN << std::vector(20, 0); // output size: 31 bytes - BOOST_CHECK_EQUAL(GetTransactionWeight(CTransaction(t)), 400004); + // increase output size by one byte, so we end up with 3900004 weight units + t.vout[0].scriptPubKey = CScript() << OP_RETURN << std::vector(39, 0); // output size: 50 bytes + BOOST_CHECK_EQUAL(GetTransactionWeight(CTransaction(t)), 3900004); CheckIsNotStandard(t, "tx-size"); // Check bare multisig (standard if policy flag g_bare_multi is set) @@ -952,71 +956,74 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) assert(t.vout.size() == 1); t.vout.insert(t.vout.end(), MAX_DUST_OUTPUTS_PER_TX, {0, t.vout[0].scriptPubKey}); + // With the global 1 sat dust limit, outputs of all types are dust only + // if they carry no value at all. + // Check compressed P2PK outputs dust threshold (must have leading 02 or 03) t.vout[0].scriptPubKey = CScript() << std::vector(33, 0x02) << OP_CHECKSIG; - t.vout[0].nValue = 576; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 575; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check uncompressed P2PK outputs dust threshold (must have leading 04/06/07) t.vout[0].scriptPubKey = CScript() << std::vector(65, 0x04) << OP_CHECKSIG; - t.vout[0].nValue = 672; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 671; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check P2PKH outputs dust threshold t.vout[0].scriptPubKey = CScript() << OP_DUP << OP_HASH160 << std::vector(20, 0) << OP_EQUALVERIFY << OP_CHECKSIG; - t.vout[0].nValue = 546; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 545; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check P2SH outputs dust threshold t.vout[0].scriptPubKey = CScript() << OP_HASH160 << std::vector(20, 0) << OP_EQUAL; - t.vout[0].nValue = 540; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 539; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check P2WPKH outputs dust threshold t.vout[0].scriptPubKey = CScript() << OP_0 << std::vector(20, 0); - t.vout[0].nValue = 294; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 293; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check P2WSH outputs dust threshold t.vout[0].scriptPubKey = CScript() << OP_0 << std::vector(32, 0); - t.vout[0].nValue = 330; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 329; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check P2TR outputs dust threshold (Invalid xonly key ok!) t.vout[0].scriptPubKey = CScript() << OP_1 << std::vector(32, 0); - t.vout[0].nValue = 330; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 329; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); // Check future Witness Program versions dust threshold (non-32-byte pushes are undefined for version 1) for (int op = OP_1; op <= OP_16; op += 1) { t.vout[0].scriptPubKey = CScript() << (opcodetype)op << std::vector(2, 0); - t.vout[0].nValue = 240; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 239; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); } // Check anchor outputs t.vout[0].scriptPubKey = CScript() << OP_1 << ANCHOR_BYTES; BOOST_CHECK(t.vout[0].scriptPubKey.IsPayToAnchor()); - t.vout[0].nValue = 240; + t.vout[0].nValue = 1; CheckIsStandard(t); - t.vout[0].nValue = 239; + t.vout[0].nValue = 0; CheckIsNotStandard(t, "dust"); } diff --git a/src/test/txpackage_tests.cpp b/src/test/txpackage_tests.cpp index 06c5816b72d8..6e0fac5d2b9e 100644 --- a/src/test/txpackage_tests.cpp +++ b/src/test/txpackage_tests.cpp @@ -144,8 +144,8 @@ BOOST_AUTO_TEST_CASE(package_sanitization_tests) BOOST_CHECK_EQUAL(state_too_many.GetResult(), PackageValidationResult::PCKG_POLICY); BOOST_CHECK_EQUAL(state_too_many.GetRejectReason(), "package-too-many-transactions"); - // Packages can't have a total weight of more than 404'000WU. - CTransactionRef large_ptx = create_placeholder_tx(150, 150); + // Packages can't have a total weight of more than MAX_PACKAGE_WEIGHT. + CTransactionRef large_ptx = create_placeholder_tx(1500, 1500); Package package_too_large; auto size_large = GetTransactionWeight(*large_ptx); size_t total_weight{0}; @@ -239,8 +239,9 @@ BOOST_AUTO_TEST_CASE(package_validation_tests) BOOST_CHECK_EQUAL(it_child->second.m_wtxids_fee_calculations.value().front(), tx_child->GetWitnessHash()); } // A single, giant transaction submitted through ProcessNewPackage fails on single tx policy. - CTransactionRef giant_ptx = create_placeholder_tx(999, 999); + CTransactionRef giant_ptx = create_placeholder_tx(5400, 5400); BOOST_CHECK(GetVirtualTransactionSize(*giant_ptx) > DEFAULT_CLUSTER_SIZE_LIMIT_KVB * 1000); + BOOST_CHECK(GetTransactionWeight(*giant_ptx) > MAX_STANDARD_TX_WEIGHT); Package package_single_giant{giant_ptx}; auto result_single_large = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_single_giant, /*test_accept=*/true, /*client_maxfeerate=*/{}); if (auto err_single_large{CheckPackageMempoolAcceptResult(package_single_giant, result_single_large, /*expect_valid=*/false, nullptr)}) { diff --git a/src/test/util/net.h b/src/test/util/net.h index 247eba8ea0fd..cab4b3877c80 100644 --- a/src/test/util/net.h +++ b/src/test/util/net.h @@ -47,6 +47,13 @@ struct ConnmanTestMsg : public CConnman { m_peer_connect_timeout = timeout; } + int GetMaxOutboundFullRelay() const { return m_max_outbound_full_relay; } + int GetMaxOutboundBlockRelay() const { return m_max_outbound_block_relay; } + int GetMaxOutboundDogMode() const { return m_max_outbound_dog_mode; } + int GetMaxFeeler() const { return m_max_feeler; } + int GetMaxAutomaticOutbound() const { return m_max_automatic_outbound; } + int GetMaxInbound() const { return m_max_inbound; } + void ResetAddrCache(); void ResetMaxOutboundCycle(); /// Reset the internal state. @@ -125,6 +132,7 @@ constexpr ServiceFlags ALL_SERVICE_FLAGS[]{ NODE_COMPACT_FILTERS, NODE_NETWORK_LIMITED, NODE_P2P_V2, + NODE_DOG_MODE, }; constexpr NetPermissionFlags ALL_NET_PERMISSION_FLAGS[]{ @@ -148,6 +156,7 @@ constexpr ConnectionType ALL_CONNECTION_TYPES[]{ ConnectionType::BLOCK_RELAY, ConnectionType::ADDR_FETCH, ConnectionType::PRIVATE_BROADCAST, + ConnectionType::DOG_MODE, }; constexpr auto ALL_NETWORKS = std::array{ diff --git a/src/txmempool.cpp b/src/txmempool.cpp index bfff18168327..3c72ccf444d4 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -166,7 +166,10 @@ CTxMemPool::setEntries CTxMemPool::CalculateMemPoolAncestors(const CTxMemPoolEnt static CTxMemPool::Options&& Flatten(CTxMemPool::Options&& opts, bilingual_str& error) { opts.check_ratio = std::clamp(opts.check_ratio, 0, 1'000'000); - int64_t cluster_limit_bytes = opts.limits.cluster_size_vbytes * 40; + // The mempool must have room for at least a handful of maximum size + // clusters. 5x the (much larger) cluster size limit approximately + // preserves the historical ~5 MB minimum. + int64_t cluster_limit_bytes = opts.limits.cluster_size_vbytes * 5; if (opts.max_size_bytes < 0 || (opts.max_size_bytes > 0 && opts.max_size_bytes < cluster_limit_bytes)) { error = strprintf(_("-maxmempool must be at least %d MB"), std::ceil(cluster_limit_bytes / 1'000'000.0)); } diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index fe33e9347e78..0fd67a6b0fc1 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -1313,8 +1313,12 @@ BOOST_AUTO_TEST_CASE(check_max_selection_weight) /*tx_noinputs_size=*/10 + 34, // static header size + output size /*avoid_partial=*/false, }; + // Test against a fixed max transaction weight of 400,000 WU so that the + // scenarios below keep working at the boundary they were designed for, + // independent of the (larger) policy-wide MAX_STANDARD_TX_WEIGHT. + cs_params.m_max_tx_weight = 400'000; - int max_weight = MAX_STANDARD_TX_WEIGHT - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size); + int max_weight = 400'000 - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size); { // Scenario 1: // The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs @@ -1386,7 +1390,7 @@ BOOST_AUTO_TEST_CASE(check_max_selection_weight) // No results // 1515 inputs * 68 bytes = 103,020 bytes - // 103,020 bytes * 4 = 412,080 weight, which is above the MAX_STANDARD_TX_WEIGHT of 400,000 + // 103,020 bytes * 4 = 412,080 weight, which is above the max transaction weight of 400,000 BOOST_CHECK(!result); } } diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index f626ee517616..171f784d9eee 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -91,7 +91,7 @@ def test_netinfo(self): self.log.info("Test -netinfo local services are moved to header if details are requested") det = self.nodes[0].cli('-netinfo', '1').send_cli().splitlines() self.log.debug(f"Test -netinfo 1 header output: {det[0]}") - assert re.match(rf"^{re.escape(self.config['environment']['CLIENT_NAME'])} client.+services nwl2?$", det[0]) + assert re.match(rf"^{re.escape(self.config['environment']['CLIENT_NAME'])} client.+services nwl2?d?$", det[0]) assert not any(line.startswith("Local services:") for line in det) def test_echojson_positional_equals(self): diff --git a/test/functional/interface_usdt_net.py b/test/functional/interface_usdt_net.py index de481133aaed..3b7e01110db9 100755 --- a/test/functional/interface_usdt_net.py +++ b/test/functional/interface_usdt_net.py @@ -35,8 +35,8 @@ # from net_address.h NETWORK_TYPE_UNROUTABLE = 0 # Use in -maxconnections. Results in a maximum of 21 inbound connections -MAX_CONNECTIONS = 32 -MAX_INBOUND_CONNECTIONS = MAX_CONNECTIONS - 10 - 1 # 10 outbound and 1 feeler +MAX_CONNECTIONS = 36 +MAX_INBOUND_CONNECTIONS = MAX_CONNECTIONS - 10 - 4 - 1 # 10 outbound, 4 dog mode and 1 feeler net_tracepoints_program = """ #include diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index 5893b1cc7526..991b9f1ec0c4 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -325,7 +325,7 @@ def run_test(self): ) tx = tx_from_hex(raw_tx_reference) output_p2sh_burn = CTxOut(nValue=540, scriptPubKey=script_to_p2sh_script(b'burn')) - num_scripts = 100000 // len(output_p2sh_burn.serialize()) # Use enough outputs to make the tx too large for our policy + num_scripts = (int(MAX_STANDARD_TX_WEIGHT / 4) + 1000) // len(output_p2sh_burn.serialize()) # Use enough outputs to make the tx too large for our policy tx.vout = [output_p2sh_burn] * num_scripts self.check_mempool_result( result_expected=[{'txid': tx.txid_hex, 'allowed': False, 'reject-reason': 'tx-size'}], @@ -333,7 +333,7 @@ def run_test(self): ) tx = tx_from_hex(raw_tx_reference) tx.vout[0] = output_p2sh_burn - tx.vout[0].nValue -= 1 # Make output smaller, such that it is dust for our policy + tx.vout[0].nValue = 0 # Make output zero-valued, such that it is dust for our policy (1 sat dust limit) self.check_mempool_result( result_expected=[{'txid': tx.txid_hex, 'allowed': False, 'reject-reason': 'dust'}], rawtxs=[tx.serialize().hex()], diff --git a/test/functional/mempool_cluster.py b/test/functional/mempool_cluster.py index 7325028412a2..97c97ad70fbc 100755 --- a/test/functional/mempool_cluster.py +++ b/test/functional/mempool_cluster.py @@ -256,11 +256,14 @@ def test_cluster_merging_size(self, max_cluster_size_vbytes): assert_greater_than_or_equal(vsize_remaining, 500) # Create a transaction spending from all clusters that exceeds the cluster size limit. - tx_merger_too_big = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge, target_vsize=vsize_remaining + 4, fee_per_output=10000) + # The fee must cover the mempool's minimum relay fee (100 sat/kvB), which is + # significant for transactions approaching the ~976 kvB default cluster limit. + fee_per_output = (vsize_remaining + 4) // 10 + 1000 + tx_merger_too_big = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge, target_vsize=vsize_remaining + 4, fee_per_output=fee_per_output) assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger_too_big["hex"]) # A transaction that is slightly smaller should work. - tx_merger_small = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge[:-1], target_vsize=vsize_remaining - 4, fee_per_output=10000) + tx_merger_small = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge[:-1], target_vsize=vsize_remaining - 4, fee_per_output=fee_per_output) node.sendrawtransaction(tx_merger_small["hex"]) assert tx_merger_small["txid"] in node.getrawmempool() diff --git a/test/functional/mempool_dust.py b/test/functional/mempool_dust.py index 957cfccccf61..c1e9323d2e34 100755 --- a/test/functional/mempool_dust.py +++ b/test/functional/mempool_dust.py @@ -44,13 +44,14 @@ def set_test_params(self): def test_dust_output(self, node: TestNode, dust_relay_fee: Decimal, output_script: CScript, type_desc: str) -> None: - # determine dust threshold (see `GetDustThreshold`) + # determine dust threshold (see `GetDustThreshold`); the fee-rate-based + # threshold is capped at a global limit of 1 sat if output_script[0] == OP_RETURN: dust_threshold = 0 else: tx_size = len(CTxOut(nValue=0, scriptPubKey=output_script).serialize()) tx_size += 67 if output_script.IsWitnessProgram() else 148 - dust_threshold = int(get_fee(tx_size, dust_relay_fee) * COIN) + dust_threshold = min(int(get_fee(tx_size, dust_relay_fee) * COIN), 1) self.log.info(f"-> Test {type_desc} output (size {len(output_script)}, limit {dust_threshold})") # amount right on the dust threshold should pass @@ -94,8 +95,11 @@ def test_dustrelay(self): assert sweep_txid in mempool_entries assert_equal(len(mempool_entries), 2) - # Wipe extra arg to reset dust relay + # Wipe extra arg to reset dust relay. Under the global 1 sat dust limit + # the 1-sat outputs are not dust, so the persisted transactions are + # still valid after the restart; mine a block to clear the mempool. self.restart_node(0, extra_args=[]) + self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].getrawmempool(), []) diff --git a/test/functional/mempool_ephemeral_dust.py b/test/functional/mempool_ephemeral_dust.py index 61dd4c7102dd..9cd043127b42 100755 --- a/test/functional/mempool_ephemeral_dust.py +++ b/test/functional/mempool_ephemeral_dust.py @@ -204,7 +204,7 @@ def test_nonzero_dust(self): self.restart_node(1, extra_args=["-minrelaytxfee=0"]) self.connect_nodes(0, 1) - # 330 is dust threshold for taproot outputs + # Any non-zero value is at or above the global 1 sat dust limit for value in [1, 329, 330]: assert_equal(self.nodes[0].getrawmempool(), []) dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=value) @@ -231,7 +231,7 @@ def test_unspent_ephemeral(self): self.log.info("Test that spending from a tx with ephemeral outputs is only allowed if dust is spent as well") assert_equal(self.nodes[0].getrawmempool(), []) - dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, dust_value=329) + dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, dust_value=0) # Valid sweep we will RBF incorrectly by not spending dust as well self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]]) @@ -255,7 +255,7 @@ def test_unspent_ephemeral(self): self.generate(self.nodes[0], 1) assert_equal(self.nodes[0].getrawmempool(), []) - dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=329) + dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=0) # Spend non-dust only unspent_sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3) diff --git a/test/functional/mempool_package_limits.py b/test/functional/mempool_package_limits.py index 8c0ae3587116..a94df1d115d5 100755 --- a/test/functional/mempool_package_limits.py +++ b/test/functional/mempool_package_limits.py @@ -161,17 +161,18 @@ def test_desc_count_limits_2(self): @check_package_limits def test_desc_size_limits(self): - """Create 3 mempool transactions and 2 package transactions (21KvB each): + """Create 3 mempool transactions and 2 package transactions (196KvB each): Ma ^ ^ Mb Mc ^ ^ Pd Pe The top ancestor in the package exceeds descendant size limits but only if the in-mempool - and in-package descendants are all considered together. + and in-package descendants are all considered together. (The cluster is 5x196=980 KvB, + exceeding the 976 KvB default cluster size limit.) """ node = self.nodes[0] - target_vsize = 21_000 + target_vsize = 196_000 high_fee = 10 * target_vsize # 10 sats/vB self.log.info("Check that in-mempool and in-package descendant sizes are calculated properly in packages") # Top parent in mempool, Ma diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py index 932e045a8162..61e1c6b3490c 100755 --- a/test/functional/mempool_sigoplimit.py +++ b/test/functional/mempool_sigoplimit.py @@ -143,7 +143,7 @@ def test_sigops_limit(self, bytes_per_sigop, num_sigops): def test_sigops_package(self): self.log.info("Test a overly-large sigops-vbyte hits package limits") - # Make a 2-transaction package which fails vbyte checks even though + # Make a chain of transactions which fails vbyte checks even though # separately they would work. self.restart_node(0, extra_args=["-bytespersigop=5000","-permitbaremultisig=1"]) @@ -156,11 +156,25 @@ def create_bare_multisig_tx(utxo_to_spend=None): tx.vout.append(CTxOut(amount_for_bare, keys_to_multisig_script([pubkey], k=1))) tx.vout[0].nValue -= amount_for_bare tx_utxo["txid"] = tx.txid_hex - tx_utxo["value"] -= Decimal("0.00005000") + tx_utxo["value"] -= Decimal("0.00050000") # amount_for_bare was subtracted from the main output return (tx_utxo, tx) - tx_parent_utxo, tx_parent = create_bare_multisig_tx() - _tx_child_utxo, tx_child = create_bare_multisig_tx(tx_parent_utxo) + # Each parent transaction has a sigop-adjusted vsize of 100,000 vbytes. + # A child spending from all of them forms a cluster exceeding the + # default cluster size limit of 976,000 vbytes (the child itself is tiny). + num_parents = 10 + parent_utxos = [] + txs = [] + for _ in range(num_parents): + parent_utxo, tx = create_bare_multisig_tx() + parent_utxos.append(parent_utxo) + txs.append(tx) + tx_parent = txs[0] + + # The child spends the parents' regular outputs, so it has no sigops of + # its own and only exceeds limits in the *package* context. + tx_child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"] + txs.append(tx_child) # Separately, the parent tx is ok parent_individual_testres = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex()])[0] @@ -170,17 +184,18 @@ def create_bare_multisig_tx(utxo_to_spend=None): # But together, it's exceeding limits in the *package* context. If sigops adjusted vsize wasn't being checked # here, it would get further in validation and give too-large-cluster error instead. - packet_test = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex(), tx_child.serialize().hex()]) + packet_test = self.nodes[0].testmempoolaccept([tx.serialize().hex() for tx in txs]) expected_package_error = "too-large-cluster" - assert_equal([x["package-error"] for x in packet_test], [expected_package_error] * 2) + assert_equal([x["package-error"] for x in packet_test], [expected_package_error] * len(txs)) - # When we actually try to submit, the parent makes it into the mempool, but the child would exceed cluster vsize limits - res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()]) + # When we actually try to submit, the parents make it into the mempool, but the child would exceed cluster vsize limits + res = self.nodes[0].submitpackage([tx.serialize().hex() for tx in txs]) assert "too-large-cluster" in res["tx-results"][tx_child.wtxid_hex]["error"] - assert tx_parent.txid_hex in self.nodes[0].getrawmempool() + for tx in txs[:-1]: + assert tx.txid_hex in self.nodes[0].getrawmempool() # Transactions are tiny in weight - assert_greater_than(2000, tx_parent.get_weight() + tx_child.get_weight()) + assert_greater_than(2000 * len(txs), sum(tx.get_weight() for tx in txs)) def test_legacy_sigops_stdness(self): self.log.info("Test a transaction with too many legacy sigops in its inputs is non-standard.") diff --git a/test/functional/p2p_dogmode_peering.py b/test/functional/p2p_dogmode_peering.py new file mode 100755 index 000000000000..508b11822656 --- /dev/null +++ b/test/functional/p2p_dogmode_peering.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026-present 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 $DOG Mode preferential peering. + +$DOG Mode nodes advertise the NODE_DOG_MODE service flag and open up to +MAX_DOG_MODE_CONNECTIONS (4) extra long-lived outbound connections ("dog" +connection type) to peers advertising the same flag, so that $DOG Mode nodes +preferentially peer with each other. + +Modelled on Libre Relay's preferential peering. +""" +from test_framework.messages import NODE_DOG_MODE +from test_framework.p2p import ( + P2P_SERVICES, + P2PDataStore, + P2PInterface, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_greater_than, + assert_raises_rpc_error, +) +from test_framework.wallet import MiniWallet + +# Maximum number of automatic $DOG Mode peers (see net.h) +MAX_DOG_MODE_CONNECTIONS = 4 + +DOG_SERVICES = P2P_SERVICES | NODE_DOG_MODE + + +class DogModePeeringTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + node = self.nodes[0] + self.wallet = MiniWallet(node) + + self.log.info("Check that the node advertises the NODE_DOG_MODE service flag") + local_services = int(node.getnetworkinfo()["localservices"], 16) + assert_greater_than(local_services & NODE_DOG_MODE, 0) + + self.log.info("Open a dog connection to a peer advertising NODE_DOG_MODE") + dog_peer = node.add_outbound_p2p_connection( + P2PDataStore(), p2p_idx=0, connection_type="dog", services=DOG_SERVICES) + + # The peer must have been told our service flags, including NODE_DOG_MODE + assert_greater_than(dog_peer.nServices & NODE_DOG_MODE, 0) + + # The connection shows up as a long-lived, tx-relaying "dog" connection + peer_info = [p for p in node.getpeerinfo() if p["connection_type"] == "dog"] + assert_equal(len(peer_info), 1) + assert_equal(peer_info[0]["inbound"], False) + assert_equal(peer_info[0]["relaytxes"], True) + + self.log.info("Check that transactions relay through the dog connection") + tx = self.wallet.create_self_transfer()["tx"] + dog_peer.send_txs_and_test([tx], node) + + self.log.info("A dog connection to a peer not advertising NODE_DOG_MODE is disconnected") + node.add_outbound_p2p_connection( + P2PInterface(), p2p_idx=1, connection_type="dog", + services=P2P_SERVICES, wait_for_disconnect=True) + + self.log.info(f"Fill the remaining {MAX_DOG_MODE_CONNECTIONS - 1} dog connection slots; one more must fail") + for i in range(1, MAX_DOG_MODE_CONNECTIONS): + node.add_outbound_p2p_connection( + P2PInterface(), p2p_idx=i, connection_type="dog", services=DOG_SERVICES) + assert_equal(len([p for p in node.getpeerinfo() if p["connection_type"] == "dog"]), MAX_DOG_MODE_CONNECTIONS) + assert_raises_rpc_error( + -34, "Already at capacity for specified connection type", + node.addconnection, "127.0.0.1:18333", "dog", False) + + +if __name__ == '__main__': + DogModePeeringTest(__file__).main() diff --git a/test/functional/p2p_eviction.py b/test/functional/p2p_eviction.py index c26e46e735df..f8b5763b069d 100755 --- a/test/functional/p2p_eviction.py +++ b/test/functional/p2p_eviction.py @@ -46,10 +46,10 @@ def on_ping(self, message): class P2PEvict(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - # The choice of maxconnections=32 results in a maximum of 21 inbound connections - # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction: + # The choice of maxconnections=36 results in a maximum of 21 inbound connections + # (36 - 10 outbound - 4 dog mode - 1 feeler). 20 inbound peers are protected from eviction: # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time - self.extra_args = [['-maxconnections=32']] + self.extra_args = [['-maxconnections=36']] def run_test(self): protected_peers = set() # peers that we expect to be protected from eviction diff --git a/test/functional/p2p_node_network_limited.py b/test/functional/p2p_node_network_limited.py index 8140823f58a0..4f7c99c2028d 100755 --- a/test/functional/p2p_node_network_limited.py +++ b/test/functional/p2p_node_network_limited.py @@ -11,6 +11,7 @@ from test_framework.messages import ( CInv, MSG_BLOCK, + NODE_DOG_MODE, NODE_NETWORK_LIMITED, NODE_P2P_V2, NODE_WITNESS, @@ -118,7 +119,7 @@ def test_avoid_requesting_historical_blocks(self): def run_test(self): node = self.nodes[0].add_p2p_connection(P2PIgnoreInv()) - expected_services = NODE_WITNESS | NODE_NETWORK_LIMITED + expected_services = NODE_WITNESS | NODE_NETWORK_LIMITED | NODE_DOG_MODE if self.options.v2transport: expected_services |= NODE_P2P_V2 diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index 17f8085b44f0..64b79ea741aa 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -1260,8 +1260,10 @@ def test_tx_relay_after_segwit_activation(self): tx3.wit.vtxinwit.append(CTxInWitness()) # Add too-large for IsStandard witness and check that it does not enter reject filter + # (must exceed the 3,900,000 WU max standard tx weight while staying + # under the 4 MB p2p message limit) p2sh_script = CScript([OP_TRUE]) - witness_script2 = CScript([b'a' * 400000]) + witness_script2 = CScript([b'a' * 3910000]) tx3.vout.append(CTxOut(tx2.vout[0].nValue - 1000, script_to_p2sh_script(p2sh_script))) tx3.wit.vtxinwit[0].scriptWitness.stack = [witness_script2] diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index e1453e3a99b8..bb79e8fb8f3b 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -50,7 +50,7 @@ MAX_BLOCK_SIGOPS = 20000 MAX_BLOCK_SIGOPS_WEIGHT = MAX_BLOCK_SIGOPS * WITNESS_SCALE_FACTOR MAX_STANDARD_TX_SIGOPS = 4000 -MAX_STANDARD_TX_WEIGHT = 400000 +MAX_STANDARD_TX_WEIGHT = 3_900_000 # Genesis block time (regtest) TIME_GENESIS_BLOCK = 1296688602 diff --git a/test/functional/test_framework/mempool_util.py b/test/functional/test_framework/mempool_util.py index 2ee1f03ddc65..dca1f44fc514 100644 --- a/test/functional/test_framework/mempool_util.py +++ b/test/functional/test_framework/mempool_util.py @@ -34,7 +34,7 @@ # Default for -incrementalrelayfee in sat/kvB DEFAULT_INCREMENTAL_RELAY_FEE = 100 DEFAULT_CLUSTER_LIMIT = 64 -DEFAULT_CLUSTER_SIZE_LIMIT_KVB = 101 +DEFAULT_CLUSTER_SIZE_LIMIT_KVB = 976 TRUC_MAX_VSIZE = 10000 TRUC_CHILD_MAX_VSIZE = 1000 diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index f6dfc4618837..e6f48038d4f1 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -59,6 +59,7 @@ NODE_COMPACT_FILTERS = (1 << 6) NODE_NETWORK_LIMITED = (1 << 10) NODE_P2P_V2 = (1 << 11) +NODE_DOG_MODE = (1 << 14) MSG_TX = 1 MSG_BLOCK = 2 @@ -79,7 +80,7 @@ # Default setting for -datacarriersize. -MAX_OP_RETURN_RELAY = 100_000 +MAX_OP_RETURN_RELAY = 975_000 DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 298edf880246..e3d1e0dbd68f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -275,6 +275,8 @@ 'p2p_leak_tx.py --v2transport', 'p2p_eviction.py', 'p2p_outbound_eviction.py', + 'p2p_dogmode_peering.py --v1transport', + 'p2p_dogmode_peering.py --v2transport', 'p2p_ibd_stalling.py --v1transport', 'p2p_ibd_stalling.py --v2transport', 'p2p_net_deadlock.py --v1transport', diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index 0d435a35e919..c1ea7ff542f4 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -1010,22 +1010,24 @@ def test_transaction_too_large(self): wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) recipient = self.nodes[0].get_wallet_rpc("large") outputs = {} - rawtx = recipient.createrawtransaction([], {wallet.getnewaddress(): 147.99899260}) + rawtx = recipient.createrawtransaction([], {wallet.getnewaddress(): 289.975}) - # Make 1500 0.1 BTC outputs. The amount that we target for funding is in + # Make 14500 0.02 BTC outputs. The amount that we target for funding is in # the BnB range when these outputs are used. However if these outputs - # are selected, the transaction will end up being too large, so it - # shouldn't use BnB and instead fall back to Knapsack but that behavior - # is not implemented yet. For now we just check that we get an error. + # are selected, the transaction will end up being too large (the inputs + # weigh ~272 WU each, so 14500 of them exceed the 3,900,000 WU maximum + # standard tx weight), so it shouldn't use BnB and instead fall back to + # Knapsack but that behavior is not implemented yet. For now we just + # check that we get an error. # First, force the wallet to bulk-generate the addresses we'll need. - recipient.keypoolrefill(1500) - for _ in range(1500): - outputs[recipient.getnewaddress()] = 0.1 + recipient.keypoolrefill(14500) + for _ in range(14500): + outputs[recipient.getnewaddress()] = 0.02 wallet.sendmany("", outputs, fee_rate=self.fee_rate_sats_per_vb) self.generate(self.nodes[0], 10) assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. " "Please try sending a smaller amount or manually consolidating your wallet's UTXOs", - recipient.fundrawtransaction, rawtx) + recipient.fundrawtransaction, rawtx, fee_rate=self.fee_rate_sats_per_vb) self.nodes[0].unloadwallet("large") def test_external_inputs(self): @@ -1060,7 +1062,7 @@ def test_external_inputs(self): assert_raises_rpc_error(-8, "Invalid parameter, missing weight key", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"]}]) assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 164}]) assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be less than 165", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": -1}]) - assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be greater than", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 400001}]) + assert_raises_rpc_error(-8, "Invalid parameter, weight cannot be greater than", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "weight": 3900001}]) # But funding should work when the solving data is provided funded_tx = wallet.fundrawtransaction(raw_tx, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"]]}) @@ -1317,29 +1319,31 @@ def test_weight_limits(self): self.nodes[2].createwallet("test_weight_limits") wallet = self.nodes[2].get_wallet_rpc("test_weight_limits") + # The outputs must be segwit (bech32) so that the ~15k-output funding + # transaction stays under the per-tx sigops standardness limit. outputs = [] - for _ in range(1472): - outputs.append({wallet.getnewaddress(address_type="legacy"): 0.1}) + for _ in range(14500): + outputs.append({wallet.getnewaddress(address_type="bech32"): 0.02}) txid = self.nodes[0].send(outputs=outputs, change_position=0, fee_rate=self.fee_rate_sats_per_vb)["txid"] self.generate(self.nodes[0], 1) - # 272 WU per input (273 when high-s); picking 1471 inputs will exceed the max standard tx weight. - rawtx = wallet.createrawtransaction([], [{wallet.getnewaddress(): 0.1 * 1471}]) + # 272 WU per input (273 when high-s); picking 14499 inputs will exceed the 3,900,000 WU max standard tx weight. + rawtx = wallet.createrawtransaction([], [{wallet.getnewaddress(): 0.02 * 14499}]) - # 1) Try to fund transaction only using the preset inputs (pick all 1472 inputs to cover the fee) + # 1) Try to fund transaction only using the preset inputs (pick all 14500 inputs to cover the fee) input_weights = [] - for i in range(1, 1473): # skip first output as it is the parent tx change output + for i in range(1, 14501): # skip first output as it is the parent tx change output input_weights.append({"txid": txid, "vout": i, "weight": 273}) - assert_raises_rpc_error(-4, "Transaction too large", wallet.fundrawtransaction, hexstring=rawtx, input_weights=input_weights) + assert_raises_rpc_error(-4, "Transaction too large", wallet.fundrawtransaction, hexstring=rawtx, input_weights=input_weights, fee_rate=self.fee_rate_sats_per_vb) # 2) Let the wallet fund the transaction assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs", - wallet.fundrawtransaction, hexstring=rawtx) + wallet.fundrawtransaction, hexstring=rawtx, fee_rate=self.fee_rate_sats_per_vb) # 3) Pre-select some inputs and let the wallet fill-up the remaining amount - inputs = input_weights[0:1000] + inputs = input_weights[0:14000] assert_raises_rpc_error(-4, "The combination of the pre-selected inputs and the wallet automatic inputs selection exceeds the transaction maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs", - wallet.fundrawtransaction, hexstring=rawtx, input_weights=inputs) + wallet.fundrawtransaction, hexstring=rawtx, input_weights=inputs, fee_rate=self.fee_rate_sats_per_vb) self.nodes[2].unloadwallet("test_weight_limits") diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index a086cf02e6d5..ef44f284b0e7 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -373,6 +373,9 @@ def run_test(self): # Test we can sign for a max-size TapMiniscript. Recompute the maximum accepted size # for a TapMiniscript (see cpp file for details). Then pad a simple pubkey check up # to the maximum size. Make sure we can import and spend this script. + # Note: the policy limit this size is derived from (MAX_STANDARD_TX_WEIGHT) has since + # been raised to 3,900,000 WU, but we keep testing at the smaller legacy size of + # ~330KB as building a ~3.8MB miniscript string is impractical for this test. leeway_weight = (4 + 4 + 1 + 36 + 4 + 1 + 1 + 8 + 1 + 1 + 33) * 4 + 2 max_tapmini_size = 400_000 - 3 - (1 + 65) * 1_000 - 3 - (33 + 32 * 128) - leeway_weight - 5 padding = max_tapmini_size - 33 - 1 @@ -380,20 +383,6 @@ def run_test(self): ms = "n" * padding + ":" + ms desc = f"tr({PUBKEYS[0]},{ms})" self.signing_test(desc, None, None, 1, 3, None) - # This was really the maximum size, one more byte and we can't import it. - ms = "n" + ms - desc = f"tr({PUBKEYS[0]},{ms})" - res = self.ms_wo_wallet.importdescriptors( - [ - { - "desc": descsum_create(desc), - "active": False, - "timestamp": "now", - } - ] - )[0] - assert not res["success"] - assert "is not a valid descriptor function" in res["error"]["message"] if __name__ == "__main__": diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index ab8f74f20e8e..a691121c23e2 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -540,26 +540,30 @@ def test_weight_limits(self): wallet = self.nodes[1].get_wallet_rpc("test_weight_limits") # Generate future inputs; 272 WU per input (273 when high-s). - # Picking 1471 inputs will exceed the max standard tx weight. + # Picking 14499 inputs will exceed the 3,900,000 WU max standard tx weight. + # Use a low fee rate so the (large) fee of the resulting ~1 MvB + # transaction stays below the default -maxtxfee. + # The outputs must be segwit (bech32) so that the ~15k-output funding + # transaction stays under the per-tx sigops standardness limit. outputs = [] - for _ in range(1472): - outputs.append({wallet.getnewaddress(address_type="legacy"): 0.1}) + for _ in range(14500): + outputs.append({wallet.getnewaddress(address_type="bech32"): 0.1}) self.nodes[0].send(outputs=outputs) self.generate(self.nodes[0], 1) # 1) Try to fund transaction only using the preset inputs inputs = wallet.listunspent() assert_raises_rpc_error(-4, "Transaction too large", - wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 1471}], options={"inputs": inputs, "add_inputs": False}) + wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 14499}], options={"inputs": inputs, "add_inputs": False, "fee_rate": 1}) # 2) Let the wallet fund the transaction assert_raises_rpc_error(-4, "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs", - wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 1471}]) + wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 14499}], options={"fee_rate": 1}) # 3) Pre-select some inputs and let the wallet fill-up the remaining amount inputs = inputs[0:1000] assert_raises_rpc_error(-4, "The combination of the pre-selected inputs and the wallet automatic inputs selection exceeds the transaction maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs", - wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 1471}], options={"inputs": inputs, "add_inputs": True}) + wallet.send, outputs=[{wallet.getnewaddress(): 0.1 * 14499}], options={"inputs": inputs, "add_inputs": True, "fee_rate": 1}) self.nodes[1].unloadwallet("test_weight_limits") diff --git a/test/functional/wallet_sendall.py b/test/functional/wallet_sendall.py index 1977a1e227b3..80cffafbe392 100755 --- a/test/functional/wallet_sendall.py +++ b/test/functional/wallet_sendall.py @@ -158,11 +158,14 @@ def sendall_invalid_amounts(self): assert_raises_rpc_error(-6, "Insufficient funds for fees after creating specified outputs.", self.wallet.sendall, [{self.recipient: pre_sendall_balance}, self.remainder_target]) assert_raises_rpc_error(-8, "Specified output amount to {} is below dust threshold".format(self.recipient), - self.wallet.sendall, [{self.recipient: 0.00000001}, self.remainder_target]) + self.wallet.sendall, [{self.recipient: 0}, self.remainder_target]) assert_raises_rpc_error(-6, "Dynamically assigned remainder results in dust output.", self.wallet.sendall, [{self.recipient: pre_sendall_balance - fee}, self.remainder_target]) - assert_raises_rpc_error(-6, "Dynamically assigned remainder results in dust output.", self.wallet.sendall, - [{self.recipient: pre_sendall_balance - fee - Decimal(0.00000010)}, self.remainder_target]) + # With the global 1 sat dust limit, a remainder of 1 sat is viable and must be created + tx = self.wallet.sendall(recipients=[{self.recipient: pre_sendall_balance - fee - Decimal("0.00000001")}, self.remainder_target], add_to_wallet=False) + decoded_tx = self.wallet.decoderawtransaction(tx["hex"]) + remainder_vout = next(o for o in decoded_tx["vout"] if o["scriptPubKey"].get("address") == self.remainder_target) + assert_equal(remainder_vout["value"], Decimal("0.00000001")) # @cleanup not needed because different wallet used def sendall_negative_effective_value(self): @@ -459,18 +462,23 @@ def sendall_fails_with_transaction_too_large(self): self.log.info("Test that sendall fails if resulting transaction is too large") # Force the wallet to bulk-generate the addresses we'll need - self.wallet.keypoolrefill(1600) + self.wallet.keypoolrefill(15000) - # create many inputs - outputs = {self.wallet.getnewaddress(): 0.000025 for _ in range(1600)} + # Create enough inputs for the resulting transaction to exceed the + # 3,900,000 WU maximum standard transaction weight (each signed + # P2WPKH input weighs ~272 WU) + outputs = {self.wallet.getnewaddress(): 0.000025 for _ in range(15000)} self.def_wallet.sendmany(amounts=outputs) self.generate(self.nodes[0], 1) + # Use a low fee rate so the (large) fee stays below the default + # -maxtxfee and the transaction size limit is hit instead. assert_raises_rpc_error( -4, "Transaction too large.", self.wallet.sendall, - recipients=[self.remainder_target]) + recipients=[self.remainder_target], + options={"fee_rate": 1}) def run_test(self): self.nodes[0].createwallet("activewallet")