From e915ab520d6d748a1eedcc4b4ba65a5d0dc94987 Mon Sep 17 00:00:00 2001 From: merge-script Date: Sat, 31 Oct 2026 23:15:25 +0700 Subject: [PATCH 01/11] Merge dashpay/dash#7990: fix(wallet): pay for 3 byte input and output counts so 253+ input sends work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7f7b34ff40cdb610f5a31205bd92eec7a5e9bdae fix(wallet): pay for 3 byte input and output counts (pasta) Pull request description: ## Issue being fixed or feature implemented Since v24 (the bitcoin#25647 backport in #7550), sending fails with `Internal bug detected: Fee needed > fee paid` whenever the transaction spends 253 or more inputs and has a change output. A wallet with many small UTXOs can't pay an amount that needs 253+ of them. The same error happens when paying exactly 252 recipients with change (253 outputs). CoinJoin's `CTransactionBuilder` (up to 500 outputs, preselected inputs) can hit both cases. v23 is not affected. Before #25647 the change amount was set from the measured signed size, so estimation errors were absorbed. Since then it comes from the coin selection estimate (`SelectionResult::GetChange()`). That estimate assumes a 1 byte input count, and `change_output_size` ignores that adding change can grow the output count. Why upstream Bitcoin Core does not trip on it. Upstream makes exactly the same two assumptions, and even documents one of them in the GetChange() comment ("input counter size is always assumed to be 1vbyte"). It survives only because of segwit-related overestimates that Dash removed along with segwit: - Upstream sizes every input with tx_is_segwit=true hardcoded in CalculateMaximumSignedInputSize, so each legacy input gets a phantom witness-stack byte. That rounds up to a full extra vbyte per input. - Per-input weight is converted to vsize with ceil, so each P2WPKH or P2TR input overestimates by 0.25 vbyte. - The fixed header counts 1 vbyte for marker and flag, which actually cost 0.5. At 253 inputs that slack is dozens of vbytes, far more than the 2 missing for the CompactSize. Dash sizes byte-exact, so the shortfall lands exactly on the 2 bytes PR 7990 describes. ## What was done? - `tx_noinputs_size` sizes the input count CompactSize for every coin selection could pick (preselected + available). This is an upper bound, so the selection target can only overestimate. With change, the existing "increase the change to match the fee needed" step returns the difference. The `not_input_fees`/`selection_target` computation moves below the coin fetch for this; error precedence is unchanged. - `change_output_size` includes the output count CompactSize growth when change is added (252 -> 253 outputs). - Updated the `GetChange()` doc comment, which listed segwit factors that don't apply to Dash. One side effect: in wallets with 253+ spendable coins, changeless transactions pay `2 bytes * feerate` more (2 duffs at the default feerate). Transactions with change still pay the exact fee. ## How Has This Been Tested? Added `test_compact_size_counts` to `wallet_create_tx.py`: - Spend 253+ of 260 small UTXOs with change. - Pay 252 recipients plus change. Both check that the fee covers the transaction size. On `develop` and `v24.0.x` without the fix, both cases fail with `Fee needed > fee paid`, with `--descriptors` and `--legacy-wallet`. Both pass with the fix. The commit also cherry-picks cleanly onto `v24.0.x`, and the test passes there. Also ran the `coinselector_tests`, `spend_tests` and `wallet_tests` unit tests, plus the wallet, `rpc_fundrawtransaction`, `rpc_psbt`, `rpc_rawtransaction`, `mempool_*` and fee functional tests: 117 passed, 2 skipped (`mempool_compatibility.py` and `wallet_upgradewallet.py` need previous release binaries). ## Breaking Changes None. Targeted for v24.0.0 final. This is a user-visible regression from v23 in the core send path, and it will be backported to `v24.0.x` once merged here (label: `backport-candidate-24.0.x`). ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(if you have permission to set one)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 041b6a6dd965f5d4f1546fd9cdd9f629c31ab1e6e92c0b27fa386c1a7f34aba3ab65f3f5935ce6e2779d51942f7e826d47863d2434143104a1df49284756cee6 (cherry picked from commit 0ad28ad3ed273411212c10358be59ee3e41a3257) --- src/wallet/coinselection.h | 4 +--- src/wallet/spend.cpp | 32 +++++++++++++++---------- test/functional/wallet_create_tx.py | 37 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 64a6784ad8dc..91617630b1be 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -371,10 +371,8 @@ struct SelectionResult * When there is a discrepancy, most of the time the final change would be slightly bigger than estimated. * * Following are the possible factors of discrepancy: - * + non-input fees always include segwit flags - * + input fee estimation always include segwit stack size * + input fees are rounded individually and not collectively, which leads to small rounding errors - * - input counter size is always assumed to be 1vbyte + * + input counter is sized for all coins available for selection, not only the selected ones * * @param[in] min_viable_change Minimum amount for change output, if change would be less then we forgo change * @param[in] change_fee Fees to include change output in the tx diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index e3721a2c8238..3546bc1997fe 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -882,7 +882,9 @@ static util::Result CreateTransactionInternal( CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty()); } CTxOut change_prototype_txout(0, scriptChange); - coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout); + // Adding the change output may also grow the output count CompactSize + coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout) + + GetSizeOfCompactSize(vecSend.size() + 1) - GetSizeOfCompactSize(vecSend.size()); // Get size of spending the change output int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &wallet); @@ -927,8 +929,8 @@ static util::Result CreateTransactionInternal( const auto change_spend_fee = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size); coin_selection_params.min_viable_change = std::max(change_spend_fee + 1, dust); - // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count - coin_selection_params.tx_noinputs_size = 9 + GetSizeOfCompactSize(vecSend.size()) + extra_payload_bytes; // bytes for output count + // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime (input count is added once the coins are known) + coin_selection_params.tx_noinputs_size = 8 + GetSizeOfCompactSize(vecSend.size()) + extra_payload_bytes; // bytes for output count // vouts to the payees for (const auto& recipient : vecSend) @@ -944,16 +946,6 @@ static util::Result CreateTransactionInternal( txNew.vout.push_back(txout); } - // Include the fees for things that aren't inputs, excluding the change output - const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.m_subtract_fee_outputs ? 0 : coin_selection_params.tx_noinputs_size); - CAmount selection_target = recipients_sum + not_input_fees; - - // This can only happen if feerate is 0, and requested destinations are value of 0 (e.g. OP_RETURN) - // and no pre-selected inputs. This will result in 0-input transaction, which is consensus-invalid anyways - if (selection_target == 0 && !coin_control.HasSelected()) { - return util::Error{_("Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input")}; - } - // Fetch manually selected coins PreSelectedInputs preset_inputs; if (coin_control.HasSelected()) { @@ -969,6 +961,20 @@ static util::Result CreateTransactionInternal( available_coins = AvailableCoins(wallet, &coin_control, coin_selection_params.m_effective_feerate); } + // Coin selection can't know in advance how many inputs it will pick, so size the input count for every coin + // it could pick. Otherwise transactions with 253+ inputs underpay the fee for the 3 byte CompactSize. + coin_selection_params.tx_noinputs_size += GetSizeOfCompactSize(preset_inputs.coins.size() + available_coins.Size()); + + // Include the fees for things that aren't inputs, excluding the change output + const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.m_subtract_fee_outputs ? 0 : coin_selection_params.tx_noinputs_size); + CAmount selection_target = recipients_sum + not_input_fees; + + // This can only happen if feerate is 0, and requested destinations are value of 0 (e.g. OP_RETURN) + // and no pre-selected inputs. This will result in 0-input transaction, which is consensus-invalid anyways + if (selection_target == 0 && !coin_control.HasSelected()) { + return util::Error{_("Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input")}; + } + // Choose coins to use auto select_coins_res = SelectCoins(wallet, available_coins, preset_inputs, /*nTargetValue=*/selection_target, coin_control, coin_selection_params); if (!select_coins_res) { diff --git a/test/functional/wallet_create_tx.py b/test/functional/wallet_create_tx.py index c7c33b93d178..994ed3d011bf 100755 --- a/test/functional/wallet_create_tx.py +++ b/test/functional/wallet_create_tx.py @@ -3,9 +3,11 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +from test_framework.messages import COIN from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, assert_raises_rpc_error, ) @@ -23,6 +25,7 @@ def skip_test_if_missing_module(self): def run_test(self): self.test_anti_fee_sniping() self.test_tx_size_too_large() + self.test_compact_size_counts() def test_anti_fee_sniping(self): self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled') @@ -72,6 +75,40 @@ def test_tx_size_too_large(self): ) self.nodes[0].settxfee(0) + def test_compact_size_counts(self): + self.log.info('Check that the input and output counts are fully paid for once they need a 3 byte CompactSize') + node = self.nodes[0] + node.createwallet(wallet_name='compact_size', descriptors=self.options.descriptors) + wallet = node.get_wallet_rpc('compact_size') + default_wallet = node.get_wallet_rpc(self.default_wallet_name) + + def check_fee(txid, num_inputs, num_outputs): + tx = wallet.gettransaction(txid=txid, verbose=True) + vin, size = tx['decoded']['vin'], tx['decoded']['size'] + assert_greater_than_or_equal(len(vin), num_inputs) + assert_equal(len(tx['decoded']['vout']), num_outputs) + # The default feerate is 1 duff/byte. The fee is estimated with maximum size signatures, so it + # may exceed the size by a couple of bytes for each signature that came out shorter. + fee = -tx['fee'] * COIN + assert_greater_than_or_equal(fee, size) + assert_greater_than_or_equal(size + 2 * len(vin), fee) + + # Spend at least 253 inputs and create change + num_utxos = 260 + default_wallet.sendmany(dummy="", amounts={wallet.getnewaddress(): 0.01 for _ in range(num_utxos)}) + self.generate(node, 1) + txid = wallet.sendtoaddress(address=default_wallet.getnewaddress(), amount=2.55) + check_fee(txid, num_inputs=253, num_outputs=2) + self.generate(node, 1) + + # Pay 252 recipients, so that adding change makes it 253 outputs + default_wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1) + self.generate(node, 1) + txid = wallet.sendmany(dummy="", amounts={default_wallet.getnewaddress(): 0.001 for _ in range(252)}) + check_fee(txid, num_inputs=1, num_outputs=253) + self.generate(node, 1) + wallet.unloadwallet() + if __name__ == '__main__': CreateTxWalletTest().main() From 9091f0b12587e75a1bb4d535ff42185997fbd179 Mon Sep 17 00:00:00 2001 From: merge-script Date: Sat, 31 Oct 2026 23:14:41 -0500 Subject: [PATCH 02/11] Merge dashpay/dash#8005: backport: bitcoin#27114, bitcoin#33395 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ddcfbfc6d3c6b1993b18db06b6a983fb29125f3e test: check that -whitelist does not apply to inbound onion connections (pasta) 3b4c5abba244f63adf67bb7fb2f12b6e3a5116f5 Merge bitcoin/bitcoin#33395: net: do not apply whitelist permissions to onion inbounds (pasta) d91e48b4c9b229034d5387f1a480a62c706366e4 Merge bitcoin/bitcoin#27114: p2p: Allow whitelisting manual connections (pasta) Pull request description: ## Issue being fixed or feature implemented Peers that reach us through our onion service are forwarded by the local Tor daemon, so their source address is Tor's, usually `127.0.0.1`. Today `-whitelist` is matched against that address. With a common entry like `-whitelist=noban@127.0.0.1`, every Tor peer gets `noban` (and whatever other permissions the entry grants). This has been the case since onion binds were introduced, v23 included. This complements #8003. #8003 makes sure Tor only ever forwards to a listener that tags its peers as onion. This PR then stops address-based `-whitelist` permissions from applying to those peers. `-whitebind` is unaffected because it is per listener, and #8003 already avoids pointing Tor at our own `-whitebind`. ## What was done? Backports, in upstream order: - **bitcoin#27114** (prerequisite): `-whitelist` accepts `in` and `out` flags, so permissions can also apply to manual outbound connections (`-addnode`, `-connect`, `addnode onetry`); the default stays incoming only. Whitelist ranges are split into incoming and outgoing lists, and `-whitelistrelay`/`-whitelistforcerelay` are stored in `CConnman`. Tests that whitelisted peers to speed up relay use the new `noban_tx_relay` framework option. - Dash adaptations: test hunks for files Dash doesn't have (`p2p_feefilter.py`, `p2p_segwit.py`, `wallet_address_types.py`, `wallet_avoid_mixing_output_types.py`, `wallet_bumpfee.py`) are dropped; converted tests keep their Dash-specific arguments. Masternode, probe and CoinJoin connections are not `MANUAL`, so they never pick up the outgoing whitelist. - **bitcoin#33395**: `CreateNodeFromAcceptedSocket` determines `inbound_onion` first and applies no address-based whitelist permissions to onion inbounds. Applies 1:1 on top of #27114. - A Dash test commit adds a check to `p2p_permissions.py`, since upstream has no test for #33395. ## How Has This Been Tested? - New check in `p2p_permissions.py`: with `-whitelist=noban@127.0.0.1` and `-bind=127.0.0.1:=onion`, a peer on the clearnet bind gets `noban, download` and a peer on the onion bind (`network=onion`) gets no permissions. Without the #33395 commit it fails with the onion peer holding `noban, download`. - Full build including the fuzz harness; `test_dash --run_test=netbase_tests,net_tests,net_peer_eviction_tests`. - Every functional test touched by #27114 (both wallet variants where applicable, `interface_zmq.py` included), plus `rpc_net.py`, `p2p_eviction.py`, `feature_proxy.py`, `feature_bind_extra.py`, `p2p_addr_relay.py`, pass on Linux x86_64. - Merges cleanly with #8003. ## Breaking Changes - Inbound onion peers no longer receive permissions from `-whitelist`, even if an entry matches the Tor daemon's address. Address-based whitelisting can't identify Tor peers, so this is intended. - `-whitelist=in,out@` without any permission is now a startup error (from #27114). ## Checklist: - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [x] I have added or updated relevant unit/integration/functional/e2e tests - [x] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) ACKs for top commit: PastaPastaPasta: utACK ddcfbfc6d3c6b1993b18db06b6a983fb29125f3e Tree-SHA512: c4d5fe9a03e49d556110efff5dc8c84cf7dbd13410ee842cfa87bac3685c5ac539a7eedd5768dafb213f9168e777c8431996a1e46979d32316fe7993e90bd2dd [v24.0.x: wallet_import_rescan.py now restarts its pruned nodes with their real -prune arguments instead of only -whitelist. On this branch, which lacks #7860, pruned nodes still print the governance-disabled warning to stderr, so the test stops them with EXPECTED_STDERR_NO_GOV_PRUNE at the end of run_test.] (cherry picked from commit 6c9bfcade97ed5c2b8dcbdee42fe15b61b74e434) --- doc/release-notes-27114.md | 2 + src/init.cpp | 24 ++++++++--- src/net.cpp | 43 +++++++++++-------- src/net.h | 33 ++++++++++---- src/net_permissions.cpp | 28 ++++++++++-- src/net_permissions.h | 8 +++- src/net_processing.cpp | 5 +++ src/test/fuzz/net_permissions.cpp | 4 +- src/test/netbase_tests.cpp | 18 ++++++-- test/functional/feature_cltv.py | 3 +- test/functional/feature_csv_activation.py | 3 +- test/functional/feature_dersig.py | 3 +- test/functional/feature_fee_estimation.py | 9 ++-- test/functional/interface_rest.py | 3 +- test/functional/interface_zmq.py | 8 ++-- test/functional/mempool_packages.py | 3 +- test/functional/p2p_filter.py | 3 +- test/functional/p2p_invalid_block.py | 3 +- test/functional/p2p_permissions.py | 20 ++++++++- test/functional/rpc_packages.py | 3 +- test/functional/rpc_rawtransaction.py | 5 +-- .../test_framework/test_framework.py | 5 +++ test/functional/wallet_abandonconflict.py | 3 +- test/functional/wallet_avoidreuse.py | 5 +-- test/functional/wallet_backup.py | 13 +++--- test/functional/wallet_balance.py | 5 +-- test/functional/wallet_basic.py | 6 ++- test/functional/wallet_fundrawtransaction.py | 6 +-- test/functional/wallet_groups.py | 3 +- test/functional/wallet_hd.py | 3 +- test/functional/wallet_import_rescan.py | 9 +++- test/functional/wallet_importdescriptors.py | 5 +-- test/functional/wallet_listreceivedby.py | 2 +- test/functional/wallet_listsinceblock.py | 2 +- test/functional/wallet_listtransactions.py | 5 +-- test/functional/wallet_send.py | 7 +-- 36 files changed, 209 insertions(+), 101 deletions(-) create mode 100644 doc/release-notes-27114.md diff --git a/doc/release-notes-27114.md b/doc/release-notes-27114.md new file mode 100644 index 000000000000..980ffd78a44f --- /dev/null +++ b/doc/release-notes-27114.md @@ -0,0 +1,2 @@ +- Additional flags "in" and "out" have been added to `-whitelist` to control whether + permissions apply to incoming connections and/or manual (default: incoming only). \ No newline at end of file diff --git a/src/init.cpp b/src/init.cpp index 170936cbf62f..72cae9865ad2 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -603,7 +603,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-blocknotify=", "Execute command when the best block changes (%s in cmd is replaced by block hash)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #endif argsman.AddArg("-blockreconstructionextratxn=", strprintf("Extra transactions to keep in memory for compact block reconstructions (default: %u)", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); - argsman.AddArg("-blocksonly", strprintf("Whether to reject transactions from network peers. Automatic broadcast and rebroadcast of any transactions from inbound peers is disabled, unless the peer has the 'forcerelay' permission. RPC transactions are not affected. (default: %u)", DEFAULT_BLOCKSONLY), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-blocksonly", strprintf("Whether to reject transactions from network peers. Disables automatic broadcast and rebroadcast of transactions, unless the source peer has the 'forcerelay' permission. RPC transactions are not affected. (default: %u)", DEFAULT_BLOCKSONLY), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #if HAVE_SYSTEM argsman.AddArg("-chainlocknotify=", "Execute command when the best chainlock changes (%s in cmd is replaced by chainlocked block hash)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #endif @@ -712,9 +712,11 @@ void SetupServerArgs(ArgsManager& argsman) "Use [host]:port notation for IPv6. Allowed permissions: " + Join(NET_PERMISSIONS_DOC, ", ") + ". " "Specify multiple permissions separated by commas (default: download,noban,mempool,relay). Can be specified multiple times.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); - argsman.AddArg("-whitelist=<[permissions@]IP address or network>", "Add permission flags to the peers connecting from the given IP address (e.g. 1.2.3.4) or " + argsman.AddArg("-whitelist=<[permissions@]IP address or network>", "Add permission flags to the peers using the given IP address (e.g. 1.2.3.4) or " "CIDR-notated network (e.g. 1.2.3.0/24). Uses the same permissions as " - "-whitebind. Can be specified multiple times." , ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); + "-whitebind. " + "Additional flags \"in\" and \"out\" control whether permissions apply to incoming connections and/or manual (default: incoming only). " + "Can be specified multiple times.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); g_wallet_init_interface.AddWalletOptions(argsman); @@ -849,8 +851,8 @@ void SetupServerArgs(ArgsManager& argsman) OptionsCategory::NODE_RELAY); argsman.AddArg("-minrelaytxfee=", strprintf("Fees (in %s/kB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); - argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted inbound peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); - argsman.AddArg("-whitelistrelay", strprintf("Add 'relay' permission to whitelisted inbound peers with default permissions. This will accept relayed transactions even when not relaying transactions (default: %d)", DEFAULT_WHITELISTRELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); + argsman.AddArg("-whitelistrelay", strprintf("Add 'relay' permission to whitelisted peers with default permissions. This will accept relayed transactions even when not relaying transactions (default: %d)", DEFAULT_WHITELISTRELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY); argsman.AddArg("-blockmaxsize=", strprintf("Set maximum block size in bytes (default: %d)", DEFAULT_BLOCK_MAX_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-blockmintxfee=", strprintf("Set lowest fee rate (in %s/kB) for transactions to be included in block creation. (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_BLOCK_MIN_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); @@ -2624,6 +2626,8 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // wallets may mix with CoinJoin, which connects to the mixing masternode connOptions.m_masternode_connections = node.active_ctx != nullptr || quorums_watch || node.wallet_loader != nullptr; + connOptions.whitelist_forcerelay = args.GetBoolArg("-whitelistforcerelay", DEFAULT_WHITELISTFORCERELAY); + connOptions.whitelist_relay = args.GetBoolArg("-whitelistrelay", DEFAULT_WHITELISTRELAY); // Port to bind to if `-bind=addr` is provided without a `:port` suffix. const uint16_t default_bind_port = @@ -2711,9 +2715,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) for (const auto& net : args.GetArgs("-whitelist")) { NetWhitelistPermissions subnet; + ConnectionDirection connection_direction; bilingual_str error; - if (!NetWhitelistPermissions::TryParse(net, subnet, error)) return InitError(error); - connOptions.vWhitelistedRange.push_back(subnet); + if (!NetWhitelistPermissions::TryParse(net, subnet, connection_direction, error)) return InitError(error); + if (connection_direction & ConnectionDirection::In) { + connOptions.vWhitelistedRangeIncoming.push_back(subnet); + } + if (connection_direction & ConnectionDirection::Out) { + connOptions.vWhitelistedRangeOutgoing.push_back(subnet); + } } connOptions.vSeedNodes = args.GetArgs("-seednode"); diff --git a/src/net.cpp b/src/net.cpp index cf1ad1ca52a5..463988392edf 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -487,6 +487,10 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo return nullptr; } + NetPermissionFlags permission_flags = NetPermissionFlags::None; + std::vector whitelist_permissions = conn_type == ConnectionType::MANUAL ? vWhitelistedRangeOutgoing : std::vector{}; + AddWhitelistPermissionFlags(permission_flags, addrConnect, whitelist_permissions); + // Add node NodeId id = GetNewNodeId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); @@ -503,6 +507,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo conn_type, /*inbound_onion=*/false, CNodeOptions{ + .permission_flags = permission_flags, .i2p_sam_session = std::move(i2p_transient_session), .recv_flood_size = nReceiveFloodSize, .use_v2transport = use_v2transport, @@ -545,9 +550,18 @@ void CNode::CloseSocketDisconnect(CConnman* connman) ::g_stats_client->inc("peers.disconnect", 1.0f); } -void CConnman::AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const { - for (const auto& subnet : vWhitelistedRange) { - if (subnet.m_subnet.Match(addr)) NetPermissions::AddFlag(flags, subnet.m_flags); +void CConnman::AddWhitelistPermissionFlags(NetPermissionFlags& flags, std::optional addr, const std::vector& ranges) const { + for (const auto& subnet : ranges) { + if (addr.has_value() && subnet.m_subnet.Match(addr.value())) { + NetPermissions::AddFlag(flags, subnet.m_flags); + } + } + if (NetPermissions::HasFlag(flags, NetPermissionFlags::Implicit)) { + NetPermissions::ClearFlag(flags, NetPermissionFlags::Implicit); + if (whitelist_forcerelay) NetPermissions::AddFlag(flags, NetPermissionFlags::ForceRelay); + if (whitelist_relay) NetPermissions::AddFlag(flags, NetPermissionFlags::Relay); + NetPermissions::AddFlag(flags, NetPermissionFlags::Mempool); + NetPermissions::AddFlag(flags, NetPermissionFlags::NoBan); } } @@ -1886,14 +1900,11 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, int nVerifiedInboundMasternodes = 0; int nMaxInbound = nMaxConnections - m_max_outbound; - AddWhitelistPermissionFlags(permission_flags, addr); - if (NetPermissions::HasFlag(permission_flags, NetPermissionFlags::Implicit)) { - NetPermissions::ClearFlag(permission_flags, NetPermissionFlags::Implicit); - if (gArgs.GetBoolArg("-whitelistforcerelay", DEFAULT_WHITELISTFORCERELAY)) NetPermissions::AddFlag(permission_flags, NetPermissionFlags::ForceRelay); - if (gArgs.GetBoolArg("-whitelistrelay", DEFAULT_WHITELISTRELAY)) NetPermissions::AddFlag(permission_flags, NetPermissionFlags::Relay); - NetPermissions::AddFlag(permission_flags, NetPermissionFlags::Mempool); - NetPermissions::AddFlag(permission_flags, NetPermissionFlags::NoBan); - } + const bool inbound_onion = std::find(m_onion_binds.begin(), m_onion_binds.end(), addr_bind) != m_onion_binds.end(); + + // Tor inbound connections do not reveal the peer's actual network address. + // Therefore do not apply address-based whitelist permissions to them. + AddWhitelistPermissionFlags(permission_flags, inbound_onion ? std::optional{} : addr, vWhitelistedRangeIncoming); { @@ -1973,15 +1984,9 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, NodeId id = GetNewNodeId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); - ServiceFlags nodeServices = nLocalServices; - if (NetPermissions::HasFlag(permission_flags, NetPermissionFlags::BloomFilter)) { - nodeServices = static_cast(nodeServices | NODE_BLOOM); - } - - const bool inbound_onion = std::find(m_onion_binds.begin(), m_onion_binds.end(), addr_bind) != m_onion_binds.end(); // The V2Transport transparently falls back to V1 behavior when an incoming V1 connection is // detected, so use it whenever we signal NODE_P2P_V2. - const bool use_v2transport(nodeServices & NODE_P2P_V2); + const bool use_v2transport(nLocalServices & NODE_P2P_V2); CNode* pnode = new CNode(id, std::move(sock), @@ -1999,7 +2004,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, .use_v2transport = use_v2transport, }); pnode->AddRef(); - m_msgproc->InitializeNode(*pnode, nodeServices); + m_msgproc->InitializeNode(*pnode, nLocalServices); { LOCK(pnode->m_sock_mutex); diff --git a/src/net.h b/src/net.h index c23545887849..59463b88af6e 100644 --- a/src/net.h +++ b/src/net.h @@ -63,11 +63,6 @@ class CScheduler; struct bilingual_str; struct NodeEvictionCandidate; -/** Default for -whitelistrelay. */ -static const bool DEFAULT_WHITELISTRELAY = true; -/** Default for -whitelistforcerelay. */ -static const bool DEFAULT_WHITELISTFORCERELAY = false; - /** Time after which to disconnect, after waiting for a ping response (or inactivity). */ static constexpr std::chrono::minutes TIMEOUT_INTERVAL{20}; /** Time to wait since m_connected before disconnecting a probe node. */ @@ -1238,7 +1233,8 @@ friend class CNode; uint64_t nMaxOutboundLimit = 0; int64_t m_peer_connect_timeout = DEFAULT_PEER_CONNECT_TIMEOUT; std::vector vSeedNodes; - std::vector vWhitelistedRange; + std::vector vWhitelistedRangeIncoming; + std::vector vWhitelistedRangeOutgoing; std::vector vWhiteBinds; std::vector vBinds; std::vector onion_binds; @@ -1254,6 +1250,8 @@ friend class CNode; //! Run the thread that opens connections to masternodes; only masternodes, quorum watchers and //! CoinJoin mixing ever request them. bool m_masternode_connections = true; + bool whitelist_forcerelay = DEFAULT_WHITELISTFORCERELAY; + bool whitelist_relay = DEFAULT_WHITELISTRELAY; }; void Init(const Options& connOptions) EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_total_bytes_sent_mutex) @@ -1279,7 +1277,8 @@ friend class CNode; LOCK(m_total_bytes_sent_mutex); nMaxOutboundLimit = connOptions.nMaxOutboundLimit; } - vWhitelistedRange = connOptions.vWhitelistedRange; + vWhitelistedRangeIncoming = connOptions.vWhitelistedRangeIncoming; + vWhitelistedRangeOutgoing = connOptions.vWhitelistedRangeOutgoing; { LOCK(m_added_nodes_mutex); // Attempt v2 connection if we support v2 - we'll reconnect with v1 if our @@ -1293,6 +1292,8 @@ friend class CNode; m_onion_binds = connOptions.onion_binds; m_active_masternode = connOptions.m_active_masternode; m_masternode_connections = connOptions.m_masternode_connections; + whitelist_forcerelay = connOptions.whitelist_forcerelay; + whitelist_relay = connOptions.whitelist_relay; } CConnman(uint64_t seed0, uint64_t seed1, AddrMan& addrman, const NetGroupManager& netgroupman, @@ -1749,7 +1750,7 @@ friend class CNode; std::vector GetEvictionCandidates() const EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); bool AttemptToEvictConnection() EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type, bool use_v2transport) EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_unused_i2p_sessions_mutex); - void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const; + void AddWhitelistPermissionFlags(NetPermissionFlags& flags, std::optional addr, const std::vector& ranges) const; void DeleteNode(CNode* pnode); @@ -1807,7 +1808,9 @@ friend class CNode; // Whitelisted ranges. Any node connecting from these is automatically // whitelisted (as well as those connecting to whitelisted binds). - std::vector vWhitelistedRange; + std::vector vWhitelistedRangeIncoming; + // Whitelisted ranges for outgoing connections. + std::vector vWhitelistedRangeOutgoing; unsigned int nSendBufferMaxSize{0}; unsigned int nReceiveFloodSize{0}; @@ -1990,6 +1993,18 @@ friend class CNode; */ std::vector m_onion_binds; + /** + * flag for adding 'forcerelay' permission to whitelisted inbound + * and manual peers with default permissions. + */ + bool whitelist_forcerelay; + + /** + * flag for adding 'relay' permission to whitelisted inbound + * and manual peers with default permissions. + */ + bool whitelist_relay; + /** * Mutex protecting m_i2p_sam_sessions. */ diff --git a/src/net_permissions.cpp b/src/net_permissions.cpp index 7ec2e6dc8b79..9cb2e35ac705 100644 --- a/src/net_permissions.cpp +++ b/src/net_permissions.cpp @@ -21,9 +21,10 @@ const std::vector NET_PERMISSIONS_DOC{ namespace { // Parse the following format: "perm1,perm2@xxxxxx" -bool TryParsePermissionFlags(const std::string& str, NetPermissionFlags& output, size_t& readen, bilingual_str& error) +static bool TryParsePermissionFlags(const std::string& str, NetPermissionFlags& output, ConnectionDirection* output_connection_direction, size_t& readen, bilingual_str& error) { NetPermissionFlags flags = NetPermissionFlags::None; + ConnectionDirection connection_direction = ConnectionDirection::None; const auto atSeparator = str.find('@'); // if '@' is not found (ie, "xxxxx"), the caller should apply implicit permissions @@ -52,6 +53,15 @@ bool TryParsePermissionFlags(const std::string& str, NetPermissionFlags& output, else if (permission == "all") NetPermissions::AddFlag(flags, NetPermissionFlags::All); else if (permission == "relay") NetPermissions::AddFlag(flags, NetPermissionFlags::Relay); else if (permission == "addr") NetPermissions::AddFlag(flags, NetPermissionFlags::Addr); + else if (permission == "in") connection_direction |= ConnectionDirection::In; + else if (permission == "out") { + if (output_connection_direction == nullptr) { + // Only NetWhitebindPermissions() should pass a nullptr. + error = _("whitebind may only be used for incoming connections (\"out\" was passed)"); + return false; + } + connection_direction |= ConnectionDirection::Out; + } else if (permission.length() == 0); // Allow empty entries else { error = strprintf(_("Invalid P2P permission: '%s'"), permission); @@ -61,7 +71,16 @@ bool TryParsePermissionFlags(const std::string& str, NetPermissionFlags& output, readen++; } + // By default, whitelist only applies to incoming connections + if (connection_direction == ConnectionDirection::None) { + connection_direction = ConnectionDirection::In; + } else if (flags == NetPermissionFlags::None) { + error = strprintf(_("Only direction was set, no permissions: '%s'"), str); + return false; + } + output = flags; + if (output_connection_direction) *output_connection_direction = connection_direction; error = Untranslated(""); return true; } @@ -85,7 +104,7 @@ bool NetWhitebindPermissions::TryParse(const std::string& str, NetWhitebindPermi { NetPermissionFlags flags; size_t offset; - if (!TryParsePermissionFlags(str, flags, offset, error)) return false; + if (!TryParsePermissionFlags(str, flags, /*output_connection_direction=*/nullptr, offset, error)) return false; const std::string strBind = str.substr(offset); const std::optional addrBind{Lookup(strBind, 0, false)}; @@ -104,11 +123,12 @@ bool NetWhitebindPermissions::TryParse(const std::string& str, NetWhitebindPermi return true; } -bool NetWhitelistPermissions::TryParse(const std::string& str, NetWhitelistPermissions& output, bilingual_str& error) +bool NetWhitelistPermissions::TryParse(const std::string& str, NetWhitelistPermissions& output, ConnectionDirection& output_connection_direction, bilingual_str& error) { NetPermissionFlags flags; size_t offset; - if (!TryParsePermissionFlags(str, flags, offset, error)) return false; + // Only NetWhitebindPermissions should pass a nullptr for output_connection_direction. + if (!TryParsePermissionFlags(str, flags, &output_connection_direction, offset, error)) return false; const std::string net = str.substr(offset); const CSubNet subnet{LookupSubNet(net)}; diff --git a/src/net_permissions.h b/src/net_permissions.h index 0c5388de5986..f61b946f6d5d 100644 --- a/src/net_permissions.h +++ b/src/net_permissions.h @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include @@ -15,6 +16,11 @@ struct bilingual_str; extern const std::vector NET_PERMISSIONS_DOC; +/** Default for -whitelistrelay. */ +constexpr bool DEFAULT_WHITELISTRELAY = true; +/** Default for -whitelistforcerelay. */ +constexpr bool DEFAULT_WHITELISTFORCERELAY = false; + enum class NetPermissionFlags : uint32_t { None = 0, // Can query bloomfilter even if -peerbloomfilters is false @@ -82,7 +88,7 @@ class NetWhitebindPermissions : public NetPermissions class NetWhitelistPermissions : public NetPermissions { public: - static bool TryParse(const std::string& str, NetWhitelistPermissions& output, bilingual_str& error); + static bool TryParse(const std::string& str, NetWhitelistPermissions& output, ConnectionDirection& output_connection_direction, bilingual_str& error); CSubNet m_subnet; }; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 26550568f8f0..460c0aed9f30 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1749,6 +1749,11 @@ void PeerManagerImpl::InitializeNode(CNode& node, ServiceFlags our_services) { m_node_states.emplace_hint(m_node_states.end(), std::piecewise_construct, std::forward_as_tuple(nodeid), std::forward_as_tuple(node.IsInboundConn())); assert(WITH_LOCK(m_object_request_mutex, return m_object_request.Count(nodeid)) == 0); } + + if (NetPermissions::HasFlag(node.m_permission_flags, NetPermissionFlags::BloomFilter)) { + our_services = static_cast(our_services | NODE_BLOOM); + } + PeerRef peer = std::make_shared(nodeid, our_services); { LOCK(m_peer_mutex); diff --git a/src/test/fuzz/net_permissions.cpp b/src/test/fuzz/net_permissions.cpp index 6ea2139c4606..811c0de4b92c 100644 --- a/src/test/fuzz/net_permissions.cpp +++ b/src/test/fuzz/net_permissions.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include @@ -31,8 +32,9 @@ FUZZ_TARGET(net_permissions) } NetWhitelistPermissions net_whitelist_permissions; + ConnectionDirection connection_direction; bilingual_str error_net_whitelist_permissions; - if (NetWhitelistPermissions::TryParse(s, net_whitelist_permissions, error_net_whitelist_permissions)) { + if (NetWhitelistPermissions::TryParse(s, net_whitelist_permissions, connection_direction, error_net_whitelist_permissions)) { (void)NetPermissions::ToStrings(net_whitelist_permissions.m_flags); (void)NetPermissions::AddFlag(net_whitelist_permissions.m_flags, net_permission_flags); assert(NetPermissions::HasFlag(net_whitelist_permissions.m_flags, net_permission_flags)); diff --git a/src/test/netbase_tests.cpp b/src/test/netbase_tests.cpp index 30a085c26699..7d9e6860d071 100644 --- a/src/test/netbase_tests.cpp +++ b/src/test/netbase_tests.cpp @@ -500,6 +500,7 @@ BOOST_AUTO_TEST_CASE(netpermissions_test) bilingual_str error; NetWhitebindPermissions whitebindPermissions; NetWhitelistPermissions whitelistPermissions; + ConnectionDirection connection_direction; // Detect invalid white bind BOOST_CHECK(!NetWhitebindPermissions::TryParse("", whitebindPermissions, error)); @@ -569,24 +570,33 @@ BOOST_AUTO_TEST_CASE(netpermissions_test) BOOST_CHECK(NetWhitebindPermissions::TryParse(",,@1.2.3.4:32", whitebindPermissions, error)); BOOST_CHECK_EQUAL(whitebindPermissions.m_flags, NetPermissionFlags::None); + BOOST_CHECK(!NetWhitebindPermissions::TryParse("out,forcerelay@1.2.3.4:32", whitebindPermissions, error)); + BOOST_CHECK(error.original.find("whitebind may only be used for incoming connections (\"out\" was passed)") != std::string::npos); + // Detect invalid flag BOOST_CHECK(!NetWhitebindPermissions::TryParse("bloom,forcerelay,oopsie@1.2.3.4:32", whitebindPermissions, error)); BOOST_CHECK(error.original.find("Invalid P2P permission") != std::string::npos); // Check netmask error - BOOST_CHECK(!NetWhitelistPermissions::TryParse("bloom,forcerelay,noban@1.2.3.4:32", whitelistPermissions, error)); + BOOST_CHECK(!NetWhitelistPermissions::TryParse("bloom,forcerelay,noban@1.2.3.4:32", whitelistPermissions, connection_direction, error)); BOOST_CHECK(error.original.find("Invalid netmask specified in -whitelist") != std::string::npos); // Happy path for whitelist parsing - BOOST_CHECK(NetWhitelistPermissions::TryParse("noban@1.2.3.4", whitelistPermissions, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("noban@1.2.3.4", whitelistPermissions, connection_direction, error)); BOOST_CHECK_EQUAL(whitelistPermissions.m_flags, NetPermissionFlags::NoBan); BOOST_CHECK(NetPermissions::HasFlag(whitelistPermissions.m_flags, NetPermissionFlags::NoBan)); - BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay@1.2.3.4/32", whitelistPermissions, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay@1.2.3.4/32", whitelistPermissions, connection_direction, error)); BOOST_CHECK_EQUAL(whitelistPermissions.m_flags, NetPermissionFlags::BloomFilter | NetPermissionFlags::ForceRelay | NetPermissionFlags::NoBan | NetPermissionFlags::Relay); BOOST_CHECK(error.empty()); BOOST_CHECK_EQUAL(whitelistPermissions.m_subnet.ToString(), "1.2.3.4/32"); - BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay,mempool@1.2.3.4/32", whitelistPermissions, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay,mempool@1.2.3.4/32", whitelistPermissions, connection_direction, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("in,relay@1.2.3.4", whitelistPermissions, connection_direction, error)); + BOOST_CHECK_EQUAL(connection_direction, ConnectionDirection::In); + BOOST_CHECK(NetWhitelistPermissions::TryParse("out,bloom@1.2.3.4", whitelistPermissions, connection_direction, error)); + BOOST_CHECK_EQUAL(connection_direction, ConnectionDirection::Out); + BOOST_CHECK(NetWhitelistPermissions::TryParse("in,out,bloom@1.2.3.4", whitelistPermissions, connection_direction, error)); + BOOST_CHECK_EQUAL(connection_direction, ConnectionDirection::Both); const auto strings = NetPermissions::ToStrings(NetPermissionFlags::All); BOOST_CHECK_EQUAL(strings.size(), 7U); diff --git a/test/functional/feature_cltv.py b/test/functional/feature_cltv.py index ca725722632a..15b5fd799f6e 100755 --- a/test/functional/feature_cltv.py +++ b/test/functional/feature_cltv.py @@ -86,9 +86,10 @@ def cltv_validate(tx, height): class BIP65Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [[ f'-testactivationheight=cltv@{CLTV_HEIGHT}', - '-whitelist=noban@127.0.0.1', '-par=1', # Use only one script thread to get the exact reject reason for testing '-acceptnonstdtxn=1', # cltv_invalidate is nonstandard '-dip3params=9000:9000', diff --git a/test/functional/feature_csv_activation.py b/test/functional/feature_csv_activation.py index 9c7f6e6ca647..6c96175fe1a3 100755 --- a/test/functional/feature_csv_activation.py +++ b/test/functional/feature_csv_activation.py @@ -95,10 +95,11 @@ class BIP68_112_113Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True # Must set '-dip3params=2000:2000' to create pre-dip3 blocks only self.extra_args = [[ '-peertimeout=999999', # bump because mocktime might cause a disconnect otherwise - '-whitelist=noban@127.0.0.1', f'-testactivationheight=csv@{CSV_ACTIVATION_HEIGHT}', '-par=1', # Use only one script thread to get the exact reject reason for testing '-dip3params=2000:2000', diff --git a/test/functional/feature_dersig.py b/test/functional/feature_dersig.py index 44428a02fcb0..42fc282b5709 100755 --- a/test/functional/feature_dersig.py +++ b/test/functional/feature_dersig.py @@ -48,9 +48,10 @@ def unDERify(tx): class BIP66Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [[ f'-testactivationheight=dersig@{DERSIG_HEIGHT}', - '-whitelist=noban@127.0.0.1', '-par=1', # Use only one script thread to get the exact log msg for testing '-dip3params=9000:9000', '-testactivationheight=v20@9000', # due to changes in CbTx diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index a37668e89136..377430cc7e3f 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -124,11 +124,12 @@ def check_estimates(node, fees_seen): class EstimateFeeTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 - # Force fSendTrickle to true (via whitelist) + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [ - ["-whitelist=noban@127.0.0.1"], - ["-whitelist=noban@127.0.0.1", "-blockmaxsize=17000"], - ["-whitelist=noban@127.0.0.1", "-blockmaxsize=8000"] + [], + ["-blockmaxsize=17000"], + ["-blockmaxsize=8000"], ] def setup_network(self): diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 7436f36fa1b9..0e381fdd4dab 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -52,8 +52,7 @@ def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-rest", "-blockfilterindex=1"], []] # whitelist peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") + self.noban_tx_relay = True self.supports_cli = False def test_rest_request( diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index 69e1c6e8399f..a14089fda676 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -103,9 +103,8 @@ class ZMQTest (BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.disable_mocktime = True - # This test isn't testing txn relay/timing, so set whitelist on the - # peers for instant txn relay. This speeds up the test run time 2-3x. - self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.zmq_port_base = p2p_port(self.num_nodes + 1) def skip_test_if_missing_module(self): @@ -143,8 +142,7 @@ def setup_zmq_test(self, services, *, recv_timeout=60, sync_blocks=True, ipv6=Fa socket.setsockopt(zmq.IPV6, 1) subscribers.append(ZMQSubscriber(socket, topic.encode())) - self.restart_node(0, [f"-zmqpub{topic}={address.replace('ipc://', 'unix:')}" for topic, address in services] + - self.extra_args[0]) + self.restart_node(0, [f"-zmqpub{topic}={address.replace('ipc://', 'unix:')}" for topic, address in services]) for i, sub in enumerate(subscribers): sub.socket.connect(services[i][1]) diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index cb2207999f8e..1cf2c6945e99 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -31,10 +31,11 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 2 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [ [ "-maxorphantxsize=1000", - "-whitelist=noban@127.0.0.1", # immediate tx relay ], [ "-maxorphantxsize=1000", diff --git a/test/functional/p2p_filter.py b/test/functional/p2p_filter.py index 02950e30cabb..61676b45d7e6 100755 --- a/test/functional/p2p_filter.py +++ b/test/functional/p2p_filter.py @@ -99,9 +99,10 @@ def merkleblock_received(self, value): class FilterTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [[ '-peerbloomfilters', - '-whitelist=noban@127.0.0.1', # immediate tx relay ]] def generatetoscriptpubkey(self, scriptpubkey): diff --git a/test/functional/p2p_invalid_block.py b/test/functional/p2p_invalid_block.py index 1d8217b40289..cf5f108fe3c9 100755 --- a/test/functional/p2p_invalid_block.py +++ b/test/functional/p2p_invalid_block.py @@ -31,7 +31,8 @@ class InvalidBlockRequestTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [["-whitelist=noban@127.0.0.1"]] + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True def run_test(self): # Add p2p connection to node0 diff --git a/test/functional/p2p_permissions.py b/test/functional/p2p_permissions.py index 2452e307c4de..0bddec876e11 100755 --- a/test/functional/p2p_permissions.py +++ b/test/functional/p2p_permissions.py @@ -10,12 +10,16 @@ from test_framework.messages import ( SEQUENCE_FINAL, ) -from test_framework.p2p import P2PDataStore +from test_framework.p2p import ( + P2PDataStore, + P2PInterface, +) from test_framework.test_node import ErrorMatch from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, p2p_port, + tor_port, ) from test_framework.wallet import MiniWallet @@ -83,7 +87,21 @@ def run_test(self): ["-whitelist=all@127.0.0.1"], ["forcerelay", "noban", "mempool", "bloomfilter", "relay", "download", "addr"]) + for flag, permissions in [(["-whitelist=noban,out@127.0.0.1"], ["noban", "download"]), (["-whitelist=noban@127.0.0.1"], [])]: + self.restart_node(0, flag) + self.connect_nodes(0, 1) + peerinfo = self.nodes[0].getpeerinfo()[0] + assert_equal(peerinfo['permissions'], permissions) + + self.log.info("Check that -whitelist does not apply to inbound onion connections") + self.restart_node(1, ["-whitelist=noban@127.0.0.1", f"-bind=127.0.0.1:{tor_port(1)}=onion"]) + self.nodes[1].add_p2p_connection(P2PInterface()) + self.nodes[1].add_p2p_connection(P2PInterface(), dstport=tor_port(1)) + permissions = {peer["network"]: peer["permissions"] for peer in self.nodes[1].getpeerinfo()} + assert_equal(permissions, {"not_publicly_routable": ["noban", "download"], "onion": []}) + self.stop_node(1) + self.nodes[1].assert_start_raises_init_error(["-whitelist=in,out@127.0.0.1"], "Only direction was set, no permissions", match=ErrorMatch.PARTIAL_REGEX) self.nodes[1].assert_start_raises_init_error(["-whitelist=oopsie@127.0.0.1"], "Invalid P2P permission", match=ErrorMatch.PARTIAL_REGEX) self.nodes[1].assert_start_raises_init_error(["-whitelist=noban@127.0.0.1:230"], "Invalid netmask specified in", match=ErrorMatch.PARTIAL_REGEX) self.nodes[1].assert_start_raises_init_error(["-whitebind=noban@127.0.0.1/10"], "Cannot resolve -whitebind address", match=ErrorMatch.PARTIAL_REGEX) diff --git a/test/functional/rpc_packages.py b/test/functional/rpc_packages.py index cdc189a5e81e..ff4fa01c931b 100755 --- a/test/functional/rpc_packages.py +++ b/test/functional/rpc_packages.py @@ -29,7 +29,8 @@ class RPCPackagesTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [["-whitelist=noban@127.0.0.1"]] # noban speeds up tx relay + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True def assert_testres_equal(self, package_hex, testres_expected): """Shuffle package_hex and assert that the testmempoolaccept result matches testres_expected. This should only diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 04aa11023052..917206836823 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -65,9 +65,8 @@ def set_test_params(self): ["-txindex"], ["-notxindex", "-fastprune", "-prune=1"], ] - # whitelist all peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.supports_cli = False def setup_network(self): diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 0c679c574995..20f9e5783722 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -122,6 +122,7 @@ def __init__(self): self.chain: str = 'regtest' self.setup_clean_chain: bool = False self.disable_mocktime: bool = False + self.noban_tx_relay: bool = False self.nodes: List[TestNode] = [] self.extra_args = None self.network_thread = None @@ -547,6 +548,10 @@ def get_bin_from_version(version, bin_name, bin_default): extra_confs = [[]] * num_nodes if extra_args is None: extra_args = [[]] * num_nodes + # Whitelist peers to speed up tx relay / mempool sync. Don't use it if testing tx relay or timing. + if self.noban_tx_relay: + for i in range(len(extra_args)): + extra_args[i] = extra_args[i] + ["-whitelist=noban,in,out@127.0.0.1"] if versions is None: versions = [None] * num_nodes if binary is None: diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index f72f6d1f7fdb..8a589f1aefd8 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -28,8 +28,7 @@ def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-minrelaytxfee=0.00001"], []] # whitelist peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") + self.noban_tx_relay = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py index 1d8bb663f6e6..5dca235a24b8 100755 --- a/test/functional/wallet_avoidreuse.py +++ b/test/functional/wallet_avoidreuse.py @@ -68,9 +68,8 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 2 - # This test isn't testing txn relay/timing, so set whitelist on the - # peers for instant txn relay. This speeds up the test run time 2-3x. - self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index b137f34f460c..ba7007bee964 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -46,13 +46,14 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True - # nodes 1, 2,3 are spenders, let's give them a keypool=100 - # whitelist all peers to speed up tx relay / mempool sync + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True + # nodes 1, 2, 3 are spenders, let's give them a keypool=100 self.extra_args = [ - ["-whitelist=noban@127.0.0.1", "-keypool=100"], - ["-whitelist=noban@127.0.0.1", "-keypool=100"], - ["-whitelist=noban@127.0.0.1", "-keypool=100"], - ["-whitelist=noban@127.0.0.1"], + ["-keypool=100"], + ["-keypool=100"], + ["-keypool=100"], + [], ] self.rpc_timeout = 120 diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 8a9752bf158f..81252afffb9a 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -53,15 +53,14 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [ # Limit mempool descendants as a hack to have wallet txs rejected from the mempool. # Set walletrejectlongchains=0 so the wallet still creates the transactions. ['-limitdescendantcount=3', '-walletrejectlongchains=0'], [], ] - # whitelist peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 62e9c26f6de0..37c62f796a29 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -30,13 +30,15 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 4 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True if self.options.descriptors: self.extra_args = [[ - "-dustrelayfee=0", "-walletrejectlongchains=0", "-whitelist=noban@127.0.0.1" + "-dustrelayfee=0", "-walletrejectlongchains=0" ] for i in range(self.num_nodes)] else: self.extra_args = [[ - "-dustrelayfee=0", "-walletrejectlongchains=0", "-whitelist=noban@127.0.0.1", + "-dustrelayfee=0", "-walletrejectlongchains=0", '-usehd={:d}'.format(i%2==0) ] for i in range(self.num_nodes)] self.setup_clean_chain = True diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py index e024cc455efe..e2324f4dcbb0 100755 --- a/test/functional/wallet_fundrawtransaction.py +++ b/test/functional/wallet_fundrawtransaction.py @@ -38,9 +38,9 @@ class RawTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True - # This test isn't testing tx relay. Set whitelist on the peers for - # instant tx relay. - self.extra_args = [[f'-usehd={not self.options.nohd}', '-whitelist=noban@127.0.0.1']] * self.num_nodes + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True + self.extra_args = [[f'-usehd={not self.options.nohd}']] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py index f57d9a6d47e0..a4cb6e98b255 100755 --- a/test/functional/wallet_groups.py +++ b/test/functional/wallet_groups.py @@ -21,6 +21,8 @@ def add_options(self, parser): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 5 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [ [], [], @@ -30,7 +32,6 @@ def set_test_params(self): ] for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") # whitelist peers to speed up tx relay / mempool sync args.append(f"-paytxfee={20 * 1e3 / 1e8}") # apply feerate of 20 sats/vB across all nodes self.rpc_timeout = 480 diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index 41871483b44f..87da58189f93 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -23,8 +23,7 @@ def set_test_params(self): self.num_nodes = 2 self.extra_args = [['-usehd=0'], ['-usehd=1', '-keypool=0']] # whitelist peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") + self.noban_tx_relay = True def setup_network(self): self.add_nodes(self.num_nodes, self.extra_args) diff --git a/test/functional/wallet_import_rescan.py b/test/functional/wallet_import_rescan.py index d469a679748a..8ce08d1d01a7 100755 --- a/test/functional/wallet_import_rescan.py +++ b/test/functional/wallet_import_rescan.py @@ -19,6 +19,7 @@ happened previously. """ +from test_framework.governance import EXPECTED_STDERR_NO_GOV_PRUNE from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -143,6 +144,8 @@ def set_test_params(self): self.num_nodes = 2 + len(IMPORT_NODES) self.supports_cli = False self.rpc_timeout = 120 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -161,7 +164,7 @@ def setup_network(self): self.import_deterministic_coinbase_privkeys() self.stop_nodes() - self.start_nodes(extra_args=[["-whitelist=noban@127.0.0.1"]] * self.num_nodes) + self.start_nodes() for i in range(1, self.num_nodes): self.connect_nodes(i, 0) @@ -277,6 +280,10 @@ def run_test(self): variant.expected_txs = 0 variant.check() + for i, import_node in enumerate(IMPORT_NODES, 2): + if import_node.prune: + self.stop_node(i, expected_stderr=EXPECTED_STDERR_NO_GOV_PRUNE) + if __name__ == "__main__": ImportRescanTest().main() diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index ccf303dbc229..b0ec564dcbaf 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -37,12 +37,11 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 2 + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [[], ["-keypool=5"] ] - # whitelist peers to speed up tx relay / mempool sync - for args in self.extra_args: - args.append("-whitelist=noban@127.0.0.1") self.setup_clean_chain = True self.wallet_names = [] diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py index 825420c1b769..156db5d1abcc 100755 --- a/test/functional/wallet_listreceivedby.py +++ b/test/functional/wallet_listreceivedby.py @@ -22,7 +22,7 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 2 # whitelist peers to speed up tx relay / mempool sync - self.extra_args = [["-whitelist=noban@127.0.0.1"], ["-whitelist=noban@127.0.0.1"]] + self.noban_tx_relay = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index 8ca295cd8c52..9a263ed81d52 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -27,7 +27,7 @@ def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True # whitelist peers to speed up tx relay / mempool sync - self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes + self.noban_tx_relay = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index 4c6cc43f35b2..73c0fb8f82c1 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -22,9 +22,8 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 3 - # This test isn't testing txn relay/timing, so set whitelist on the - # peers for instant txn relay. This speeds up the test run time 2-3x. - self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 6012f1549cc0..0768c630dbb4 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -25,10 +25,11 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 2 - # whitelist all peers to speed up tx relay / mempool sync + # whitelist peers to speed up tx relay / mempool sync + self.noban_tx_relay = True self.extra_args = [ - ["-whitelist=127.0.0.1"], - ["-whitelist=127.0.0.1"], + [], + [], ] getcontext().prec = 8 # Satoshi precision for Decimal From 349859f413579576d90bd287a26041036d0d173f Mon Sep 17 00:00:00 2001 From: merge-script Date: Sun, 1 Nov 2026 11:45:37 -0600 Subject: [PATCH 03/11] Merge dashpay/dash#7957: fix(qt): validate shared-masternode shares with the consensus rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit f916c5d3738ee6fb2223aa2a45d2c0ee6ba5e137 refactor: check share amounts with one consensus helper in Qt and validation (pasta) 474d4da35c6a793579a47efddb3b551ba8c10998 fix(qt): validate shared-masternode shares with the consensus rules (pasta) Pull request description: ## Issue being fixed or feature implemented The shared-masternode Qt editor kept its own hand-written copy of the consensus share-list rules (`IsShareListTriviallyValid`), and the copies had drifted. Consensus rejects a non-zero early-exit penalty when there is no early period (`bad-protx-shares-penalty`), but the GUI accepted it. `lockTerms` relies only on the GUI check, and a reopened session skips `validatePage`. So the user only found out from the node's rejection, after the co-owners had already coordinated. ## What was done? The GUI no longer carries its own copy of those rules. It calls the consensus function and maps the reject reason to a translated message. - `src/evo/providertx.cpp`: the penalty rules move into `IsEarlyPenaltyValid`, which `IsShareListTriviallyValid` calls as its last step. The count, per-share bound and sum rules move into `AreShareAmountsValid`, which `IsShareListTriviallyValid` calls first. Which lists are valid is unchanged. A list that breaks several rules at once can report an amount reason where it used to report the early-period or a key or script reason. - `MnShareSession::validateShares` now only parses the address strings, which consensus cannot see, and reports those errors per share. It then calls `IsShareListTriviallyValid` and turns its reject reason into a message (`ShareListErrorText`). The hand-written checks are gone. - The exit-terms page uses `IsEarlyPenaltyValid`, and the participants page, where no addresses exist yet, uses `AreShareAmountsValid`. Trade-off: a consensus rejection is now one message without the share number, where the old copy said which share was wrong. Parse errors still name the share. Out of scope, left as a follow-up: the reward-address-reuse check in the contribution page of `sharedmncreatedialog.cpp` and in `sharedmndialogs.cpp`. ## How Has This Been Tested? Built on Linux with Qt (`--enable-debug`). Ran `test_dash-qt` (`QT_QPA_PLATFORM=minimal`, all passing) and `test_dash --run_test='evo_*'`. `feature_masternode_shares.py` passes. `MnShareSessionTests::validateSharesMirror` gains rows for a penalty with no early period, for `CheckEarlyPenalty`, and for `CheckShareAmounts`. They check that validation fails or passes and do not pin message text. I disabled the `early_period_blocks == 0` rule in `IsEarlyPenaltyValid` and confirmed the new penalty row fails; with the rule restored it passes. ## Breaking Changes None. Which transactions are valid is unchanged; only GUI validation messages change, plus the reject reason for lists that break several rules at once. ## Checklist: - [ ] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: cbce4d93d2e2b2ce373bcb34cba2cb1b3c04767ea8098c16940c70d003563da85f0dde656c5390feb374a6f47924537d734838ac05dc85c61e1039d398373894 (cherry picked from commit 608e98f88148a6aee19be39dc9fdb0989bf741a8) --- src/evo/providertx.cpp | 68 ++++++---- src/evo/providertx.h | 6 + src/qt/mnsharesession.cpp | 192 ++++++++++++++-------------- src/qt/mnsharesession.h | 17 ++- src/qt/sharedmncreatedialog.cpp | 39 ++---- src/qt/test/mnsharesessiontests.cpp | 18 +++ 6 files changed, 184 insertions(+), 156 deletions(-) diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index c6a5014b9c0d..f20a981ca3ff 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -15,6 +15,7 @@ #include