Skip to content

Conversation

@furszy
Copy link
Member

@furszy furszy commented Jun 27, 2024

Fix #30309 (comment) inconsistency.

Currently, the test is passing due to a mistake in the test inputs
selection process. We are selecting the parent transaction change
output as one of the inputs of the transaction to fund, which
helps to surpass the target amount when it shouldn't due to the
fee reduction.

The failure arises when the test behaves as intended by its coder;
that is, when it does not select the change output. In this case,
the pre-selected inputs aren't enough to cover the target amount.

Fix this by excluding the parent transaction's change output from
the inputs selection and including an extra input to cover the tx
fee.

The CI failure can be replicated with the following patch in master:

diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py
--- a/test/functional/wallet_fundrawtransaction.py	(revision 9b480f7a25a737c9c4ebc33401e94d66c2da9ec3)
+++ b/test/functional/wallet_fundrawtransaction.py	(date 1720652934739)
@@ -1322,7 +1322,7 @@
         outputs = []
         for _ in range(1472):
             outputs.append({wallet.getnewaddress(address_type="legacy"): 0.1})
-        txid = self.nodes[0].send(outputs=outputs)["txid"]
+        txid = self.nodes[0].send(outputs=outputs, change_position=0)["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.
@@ -1330,7 +1330,7 @@
 
         # 1) Try to fund transaction only using the preset inputs
         input_weights = []
-        for i in range(1471):
+        for i in range(1, 1472):  # 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)

@DrahtBot
Copy link
Contributor

DrahtBot commented Jun 27, 2024

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK ismaelsadeeq, achow101
Stale ACK rkrux, fjahr

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #27286 (wallet: Keep track of the wallet's own transaction outputs in memory by achow101)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@DrahtBot DrahtBot added the Tests label Jun 27, 2024
@maflcko maflcko added this to the 28.0 milestone Jun 27, 2024
Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK 4f571fb

make, test/functional are successful.

Verified that the default value of add_inputs is True in fundrawtransaction that might have caused the inconsistency in the CI: https://github.com/bitcoin/bitcoin/blob/master/src/wallet/rpc/spend.cpp#L751

Also verified that the corresponding scenario in wallet_send.py has add_inputs: False passed already here: https://github.com/bitcoin/bitcoin/blob/master/test/functional/wallet_send.py#L600

@fjahr
Copy link
Contributor

fjahr commented Jun 30, 2024

Code review ACK 4f571fb73526b55a83af3c04fbc662ccdd1307ae

This looks like the right fix for the observed test failure to me.

@maflcko maflcko added the Wallet label Jul 9, 2024
Copy link
Member

@ismaelsadeeq ismaelsadeeq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

I could not recreate this failure locally. Looking closely at the code, 4f571fb73526b55a83af3c04fbc662ccdd1307ae might not solve the issue.
For us to load coins from AutomaticCoinSelection and reach the failure in the C.I.
selection_target in SelectCoins must be greater than 0.

But when selection_target is > 0 and we prevent adding input with m_allow_other_inputs=False, just as it's done in 4f571fb73526b55a83af3c04fbc662ccdd1307ae we will encounter another issue:

if (!coin_control.m_allow_other_inputs && selection_target > 0) {
return util::Error{_("The preselected coins total amount does not cover the transaction target. "
"Please allow other inputs to be automatically selected or include more coins manually")};

I wonder why selection_target is not even positive in this test and cause failure like it did in the C.I because;
The selected inputs values should be 14710000000 sats.
Output value is also same 14710000000 sats.
nTargetValue (output value + fees) should be greater than the selected inputs values making selection_target positive?


I think to fix to prevent this failure, we should add more inputs to the input_weight to cover for the fees and ensure the selection_target is negative.

diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py
index 3c1b2deb1d7..fdc7d8e80a2 100755
--- a/test/functional/wallet_fundrawtransaction.py
+++ b/test/functional/wallet_fundrawtransaction.py
@@ -1326,11 +1326,12 @@ class RawTransactionsTest(BitcoinTestFramework):
         self.generate(self.nodes[0], 1)
 
         # 272 WU per input (273 when high-s); picking 1471 inputs will exceed the max standard tx weight.
+        # Add 1 input to cover for fees
         rawtx = wallet.createrawtransaction([], [{wallet.getnewaddress(): 0.1 * 1471}])
 
         # 1) Try to fund transaction only using the preset inputs
         input_weights = []
-        for i in range(1471):
+        for i in range 1472):
             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)

Or even better, we should fail early without needing to perform all the redundant operations whenever pre-selected inputs weight alone exceed the maximum standard weight.

diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
index ac2a4826f05..d3c1349553e 100644
--- a/src/wallet/rpc/spend.cpp
+++ b/src/wallet/rpc/spend.cpp
@@ -670,6 +670,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
         }
     }
 
+    int total_inputs_weight{0};
     if (options.exists("input_weights")) {
         for (const UniValue& input : options["input_weights"].get_array().getValues()) {
             Txid txid = Txid::FromUint256(ParseHashO(input, "txid"));
@@ -696,10 +697,13 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
             if (weight > MAX_STANDARD_TX_WEIGHT) {
                 throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, weight cannot be greater than the maximum standard tx weight of %d", MAX_STANDARD_TX_WEIGHT));
             }
-
+            total_inputs_weight += weight;
             coinControl.SetInputWeight(COutPoint(txid, vout), weight);
         }
     }
+    if (total_inputs_weight > MAX_STANDARD_TX_WEIGHT) {
+        throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Selected inputs weight cannot be greater than the maximum standard tx weight of %d", MAX_STANDARD_TX_WEIGHT));
+    }
 
     if (recipients.empty())
         throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py
index 3c1b2deb1d7..727451fb79e 100755
--- a/test/functional/wallet_fundrawtransaction.py
+++ b/test/functional/wallet_fundrawtransaction.py
@@ -1332,7 +1332,7 @@ class RawTransactionsTest(BitcoinTestFramework):
         input_weights = []
         for i in range 1471):
             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(-8, "Selected inputs weight cannot be greater than the maximum standard tx weight of 400000", wallet.fundrawtransaction, hexstring=rawtx, input_weights=input_weights)
 
         # 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",

Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice writeup @ismaelsadeeq!. Thanks for the push, just understood the reason behind it.

The issue is in the inputs selection. They are manually selected from the parent tx without excluding the change output. This output is not from the wallet and helps to surpass the selection amount (otherwise yes, the test would be failing all the time). So, the CI failure arises when the test excludes the change output which only occurs when the change output is at the end of the tx outputs vector.. whose probability is very low.

This also presents an issue inside the wallet. Because it is not detecting that the pre-selected input is not solvable when the user provides the inputs weight. And this has an edge case explanation too.. it is because the wallet can know the not-owned change output internally (which is not so common) when it is an output of a transaction the wallet stores -> because another output(s) of this tx is owned by the wallet. Extra note: This is not a problem for fundrawtransaction() because the command does not sign the transaction. Only for send().

Will fix the test and also this last issue.

Currently, the test is passing due to a mistake in the test inputs
selection process. We are selecting the parent transaction change
output as one of the inputs of the transaction to fund, which
helps to surpass the target amount when it shouldn't due to the
fee reduction.

The failure arises when the test behaves as intended by its coder;
that is, when it does not select the change output. In this case,
the pre-selected inputs aren't enough to cover the target amount.

Fix this by excluding the parent transaction's change output from
the inputs selection and including an extra input to cover the tx
fee.
@furszy furszy force-pushed the 2024_test_fix_max_weight_test branch from 4f571fb to 00b8e26 Compare July 10, 2024 23:05
Copy link
Member Author

@furszy furszy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated per feedback.

The CI failure that originated this PR (#30309 (comment)) can be replicated with the following patch in master:

diff --git a/test/functional/wallet_fundrawtransaction.py b/test/functional/wallet_fundrawtransaction.py
--- a/test/functional/wallet_fundrawtransaction.py	(revision 9b480f7a25a737c9c4ebc33401e94d66c2da9ec3)
+++ b/test/functional/wallet_fundrawtransaction.py	(date 1720652934739)
@@ -1322,7 +1322,7 @@
         outputs = []
         for _ in range(1472):
             outputs.append({wallet.getnewaddress(address_type="legacy"): 0.1})
-        txid = self.nodes[0].send(outputs=outputs)["txid"]
+        txid = self.nodes[0].send(outputs=outputs, change_position=0)["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.
@@ -1330,7 +1330,7 @@
 
         # 1) Try to fund transaction only using the preset inputs
         input_weights = []
-        for i in range(1471):
+        for i in range(1, 1472):  # 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)

@maflcko
Copy link
Member

maflcko commented Jul 11, 2024

The test failure looks unrelated:

2024-07-10T23:36:07.2558178Z test_framework.authproxy.JSONRPCException: 'stop' RPC took longer than 1200.000000 seconds. Consider using larger timeout for calls that take longer to return. (-344)

https://github.com/bitcoin/bitcoin/actions/runs/9882674449/job/27296008093?pr=30353

2024-07-10T23:36:12.6566166Z  test  2024-07-10T23:16:06.381000Z TestFramework (INFO): Stopping nodes 
2024-07-10T23:36:12.6566491Z  test  2024-07-10T23:16:06.381000Z TestFramework.node0 (DEBUG): Stopping node 
2024-07-10T23:36:12.6567186Z  node0 2024-07-10T23:16:06.382752Z [http] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:305] [http_request_cb] [http] Received a POST request for / from 127.0.0.1:61436 
2024-07-10T23:36:12.6567862Z  node0 2024-07-10T23:16:06.382862Z [httpworker.3] [D:\a\bitcoin\bitcoin\src\rpc\request.cpp:232] [parse] [rpc] ThreadRPCServer method=stop user=__cookie__ 
2024-07-10T23:36:12.6568479Z  node0 2024-07-10T23:16:06.382950Z [init] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:498] [InterruptHTTPServer] [http] Interrupting HTTP server 
2024-07-10T23:36:12.6569093Z  node0 2024-07-10T23:16:06.382994Z [init] [D:\a\bitcoin\bitcoin\src\httprpc.cpp:378] [InterruptHTTPRPC] [rpc] Interrupting HTTP RPC server 
2024-07-10T23:36:12.6569629Z  node0 2024-07-10T23:16:06.383015Z [init] [D:\a\bitcoin\bitcoin\src\rpc\server.cpp:308] [operator ()] [rpc] Interrupting RPC 
2024-07-10T23:36:12.6570126Z  node0 2024-07-10T23:16:06.383041Z [init] [D:\a\bitcoin\bitcoin\src\init.cpp:273] [Shutdown] Shutdown: In progress... 
2024-07-10T23:36:12.6570693Z  node0 2024-07-10T23:16:06.383060Z [shutoff] [D:\a\bitcoin\bitcoin\src\httprpc.cpp:383] [StopHTTPRPC] [rpc] Stopping HTTP RPC server 
2024-07-10T23:36:12.6571484Z  node0 2024-07-10T23:16:06.383086Z [shutoff] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:760] [UnregisterHTTPHandler] [http] Unregistering HTTP handler for / (exactmatch 1) 
2024-07-10T23:36:12.6572255Z  node0 2024-07-10T23:16:06.383112Z [shutoff] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:760] [UnregisterHTTPHandler] [http] Unregistering HTTP handler for /wallet/ (exactmatch 0) 
2024-07-10T23:36:12.6572774Z  node0 2024-07-10T23:16:06.383131Z [shutoff] [D:\a\bitcoin\bitcoin\src\rpc\server.cpp:320] [operator ()] [rpc] Stopping RPC 
2024-07-10T23:36:12.6573277Z  node0 2024-07-10T23:16:06.383334Z [shutoff] [D:\a\bitcoin\bitcoin\src\init.cpp:440] [OnRPCStopped] [rpc] RPC stopped. 
2024-07-10T23:36:12.6573860Z  node0 2024-07-10T23:16:06.383370Z [shutoff] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:510] [StopHTTPServer] [http] Stopping HTTP server 
2024-07-10T23:36:12.6574524Z  node0 2024-07-10T23:16:06.383391Z [shutoff] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:512] [StopHTTPServer] [http] Waiting for HTTP worker threads to exit 
2024-07-10T23:36:12.6575470Z  node0 2024-07-10T23:16:06.383437Z [addcon] [D:\a\bitcoin\bitcoin\src\util\thread.cpp:22] [TraceThread] addcon thread exit 
2024-07-10T23:36:12.6576169Z  node0 2024-07-10T23:16:06.383557Z [shutoff] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:526] [StopHTTPServer] [http] Waiting for 1 connections to stop HTTP server 
2024-07-10T23:36:12.6576732Z  node0 2024-07-10T23:16:06.383658Z [http] [D:\a\bitcoin\bitcoin\src\httpserver.cpp:354] [ThreadHTTP] [http] Exited http event loop 
2024-07-10T23:36:12.6577232Z  node0 2024-07-10T23:16:06.404069Z [net] [D:\a\bitcoin\bitcoin\src\util\thread.cpp:22] [TraceThread] net thread exit 
2024-07-10T23:36:12.6577770Z  node0 2024-07-10T23:16:06.419772Z [msghand] [D:\a\bitcoin\bitcoin\src\util\thread.cpp:22] [TraceThread] msghand thread exit 
2024-07-10T23:36:12.6578383Z  node0 2024-07-10T23:16:08.104417Z [scheduler] [D:\a\bitcoin\bitcoin\src\wallet\bdb.cpp:663] [PeriodicFlush] [walletdb] Flushing wallet.dat 
2024-07-10T23:36:12.6579062Z  node0 2024-07-10T23:16:08.106925Z [scheduler] [D:\a\bitcoin\bitcoin\src\wallet\bdb.cpp:671] [PeriodicFlush] [walletdb] Flushed wallet.dat 2ms 
2024-07-10T23:36:12.6579686Z  node0 2024-07-10T23:16:08.106958Z [scheduler] [D:\a\bitcoin\bitcoin\src\wallet\bdb.cpp:663] [PeriodicFlush] [walletdb] Flushing wallet.dat 
2024-07-10T23:36:12.6580288Z  node0 2024-07-10T23:16:08.108697Z [scheduler] [D:\a\bitcoin\bitcoin\src\wallet\bdb.cpp:671] [PeriodicFlush] [walletdb] Flushed wallet.dat 1ms 
2024-07-10T23:36:12.6580980Z  node1 2024-07-10T23:16:45.638808Z [scheduler] [D:\a\bitcoin\bitcoin\src\net.cpp:2369] [StartExtraBlockRelayPeers] [net] enabling extra block-relay-only peers 
2024-07-10T23:36:12.6581662Z  node2 2024-07-10T23:16:46.802204Z [scheduler] [D:\a\bitcoin\bitcoin\src\net.cpp:2369] [StartExtraBlockRelayPeers] [net] enabling extra block-relay-only peers 
2024-07-10T23:36:12.6582341Z  node0 2024-07-10T23:16:51.102814Z [scheduler] [D:\a\bitcoin\bitcoin\src\net.cpp:2369] [StartExtraBlockRelayPeers] [net] enabling extra block-relay-only peers 
2024-07-10T23:36:12.6583076Z  node1 2024-07-10T23:17:00.635058Z [scheduler] [D:\a\bitcoin\bitcoin\src\validation.cpp:2015] [IsInitialBlockDownload] Leaving InitialBlockDownload (latching to false) 
2024-07-10T23:36:12.6583697Z  node1 2024-07-10T23:31:00.645441Z [scheduler] [D:\a\bitcoin\bitcoin\src\net.cpp:2332] [DumpAddresses] [net] Flushed 0 addresses to peers.dat  2ms 
2024-07-10T23:36:12.6584292Z  node2 2024-07-10T23:31:01.787885Z [scheduler] [D:\a\bitcoin\bitcoin\src\net.cpp:2332] [DumpAddresses] [net] Flushed 0 addresses to peers.dat  1ms 
2024-07-10T23:36:12.6584894Z  node0 2024-07-10T23:31:06.111388Z [scheduler] [D:\a\bitcoin\bitcoin\src\net.cpp:2332] [DumpAddresses] [net] Flushed 0 addresses to peers.dat  1ms 
2024-07-10T23:36:12.6584902Z 
2024-07-10T23:36:12.6584907Z 

Copy link
Member

@ismaelsadeeq ismaelsadeeq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review and Tested ACK 00b8e26

I was able to recreate the failure with the diff in the OP. I also decoded the transaction and confirmed that it's selecting the change output of the parent transaction with a value of 2.79949465 BTC. That's why the test was not failing.

The commit 00b8e26 fixes this issue.

This is not a problem for fundrawtransaction()

Yes, it is not. The call will succeed, but when attempting to sign the transaction with signrawtransactionwithkey, the RPC will return an error: "Unable to sign input, invalid stack size (possibly missing key)".

Only for send()

How is this an issue for send()?

Will fix the test and also this last issue.

In this PR? Also, should we consider returning this error early as I suggested?

@achow101
Copy link
Member

achow101 commented Jul 11, 2024

ACK 00b8e26

Ran into a similar failure in #27286 which I fixed slightly differently. However this fix would work just as well, and aligns with my understanding of both failures.

@achow101 achow101 merged commit 33af14e into bitcoin:master Jul 11, 2024
@furszy furszy deleted the 2024_test_fix_max_weight_test branch July 11, 2024 20:04
@furszy
Copy link
Member Author

furszy commented Jul 11, 2024

A bit late but here @ismaelsadeeq.

Only for send()

How is this an issue for send()?

Mainly because of what users would expect from the send() command. Which, due to its description, is to actually craft a sendable transaction. As is now, if this happens, the command will return the incomplete transaction without telling the user what happened. But.. I don't think will follow-up this thought. No one complained about it so far.

should we consider returning this error early as I suggested?

Yes sure. thats a possibility too. It does not catch all the scenarios (e.g. max weight can be exceeded with the sum of inputs and outputs sizes) but at least will promptly alert users when they set high custom inputs weights.

@bitcoin bitcoin locked and limited conversation to collaborators Jul 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants