rpc/wallet: add simulaterawtransaction RPC #22751

pull kallewoof wants to merge 2 commits into bitcoin:master from kallewoof:202108-analyzerawtransaction changing 5 files +249 −0
  1. kallewoof commented at 6:11 AM on August 20, 2021: member

    (note: this was originally titled "add analyzerawtransaction RPC")

    This command iterates over the inputs and outputs of the given transactions, and tallies up the balance change for the given wallet. This can be useful e.g. when verifying that a coin join like transaction doesn't contain unexpected inputs that the wallet will then sign for unintentionally.

    I originally proposed this to Elements (https://github.com/ElementsProject/elements/pull/1016) and it was suggested that I propose this upstream.

    There is an alternative #22776 to instead add this info to getbalances when providing an optional transaction as argument.

  2. kallewoof force-pushed on Aug 20, 2021
  3. kallewoof force-pushed on Aug 20, 2021
  4. DrahtBot added the label RPC/REST/ZMQ on Aug 20, 2021
  5. DrahtBot added the label Wallet on Aug 20, 2021
  6. in test/functional/wallet_analyzerawtx.py:60 in 70620ac3c4 outdated
      55 | +
      56 | +        # node0 sees fee + 5 btc decrease
      57 | +        assert_approx(w0.analyzerawtransaction(tx), -(5.0 + bitcoin_fee))
      58 | +
      59 | +        # node1 sees same as before
      60 | +        analysis = w1.analyzerawtransaction(tx)
    


    fanquake commented at 6:40 AM on August 20, 2021:

    test/functional/wallet_analyzerawtx.py:60:9: F841 local variable 'analysis' is assigned to but never used


    kallewoof commented at 7:04 AM on August 20, 2021:

    Thanks, fixed.

  7. kallewoof force-pushed on Aug 20, 2021
  8. kallewoof force-pushed on Aug 20, 2021
  9. meshcollider commented at 12:00 AM on August 21, 2021: contributor

    Concept ACK, but I don't think the RPC name represents the functionality. Analyze implies some sort of in-depth breakdown of information. Perhaps nettransactionbalance or something to represent the balance-specific use?

  10. ghost commented at 4:05 AM on August 21, 2021: none

    but I don't think the RPC name represents the functionality. Analyze implies some sort of in-depth breakdown of information.

    Agree. We already have RPC with similar name: analyzepsbt which returns lot of things that would be helpful for someone using PSBT. Although I am still confused between decodepsbt and analyzepsbt.

    Perhaps nettransactionbalance or something to represent the balance-specific use?

    Or maybe add it in the results of any existing RPC

  11. kallewoof commented at 7:45 AM on August 21, 2021: member

    I have no strong feelings about keeping analyze but I think <verb>rawtransaction is a good name (edit: because it is sticking to the convention), where <verb> somehow indicates looking at each input and output and determining whether they belong to the wallet, and if so to add/subtract the appropriate amount.

    Edit: adding to results of existing RPC sounds good to me too, but not sure which that would be.

  12. ghost commented at 11:45 AM on August 21, 2021: none

    adding to results of existing RPC sounds good to me too, but not sure which that would be.

    Can we add this to getbalance? Will need one argument include_tx so the command would look like this:

    $ bitcoin-cli -named getbalance include_tx=hex
    
    Result:
    {                   
      "balance" : n    (numeric) The total amount in BTC received for this wallet.
    
      "change"  : n    (numeric) The wallet balance change including tx(negative means decrease).
    }
    
  13. in test/functional/wallet_analyzerawtx.py:37 in f1cc9a1be3 outdated
      32 | +        node0.createwallet(wallet_name='w0')
      33 | +        w0 = node0.get_wallet_rpc('w0')
      34 | +        node1.createwallet(wallet_name='w1')
      35 | +        w1 = node1.get_wallet_rpc('w1')
      36 | +
      37 | +        node0.generatetoaddress(101, w0.getnewaddress())
    


    jonatack commented at 4:14 PM on August 21, 2021:

    <details><summary>Some suggestions for the test</summary><p>

    --- a/test/functional/wallet_analyzerawtx.py
    +++ b/test/functional/wallet_analyzerawtx.py
    @@ -5,12 +5,14 @@
     """Test analyzerawtransaction.
     """
     
    +from test_framework.blocktools import COINBASE_MATURITY
     from test_framework.test_framework import BitcoinTestFramework
     from test_framework.util import (
         assert_approx,
         assert_equal,
     )
     
    +
     class AnalyzeTxTest(BitcoinTestFramework):
         def set_test_params(self):
             self.setup_clean_chain = True
    @@ -27,14 +29,14 @@ class AnalyzeTxTest(BitcoinTestFramework):
             node1 = self.nodes[1]
             self.connect_nodes(0, 1)
     
    -        node0.generate(1) # Leave IBD
    +        node0.generate(1)  # Leave IBD
     
             node0.createwallet(wallet_name='w0')
             w0 = node0.get_wallet_rpc('w0')
             node1.createwallet(wallet_name='w1')
             w1 = node1.get_wallet_rpc('w1')
     
    -        node0.generatetoaddress(101, w0.getnewaddress())
    +        node0.generatetoaddress(nblocks=COINBASE_MATURITY + 1, address=w0.getnewaddress())
             assert_equal(w0.getbalance(), 50.0)
             assert_equal(w1.getbalance(), 0.0)
     
    @@ -43,10 +45,10 @@ class AnalyzeTxTest(BitcoinTestFramework):
             tx = node1.createrawtransaction([], [{address1: 5.0}])
     
             # node0 should be unaffected
    -        assert_equal(w0.analyzerawtransaction(tx), 0.0)
    +        assert_equal(w0.analyzerawtransaction(tx), {"balance_change": 0.0})
     
             # node1 should see +5 balance
    -        assert_equal(w1.analyzerawtransaction(tx), 5.0)
    +        assert_equal(w1.analyzerawtransaction(tx), {"balance_change": 5.0})
     
             # w0 funds transaction; it should now see a decrease in (tx fee and payment), and w1 should see the same as above
             funding = w0.fundrawtransaction(tx)
    @@ -54,10 +56,11 @@ class AnalyzeTxTest(BitcoinTestFramework):
             bitcoin_fee = float(funding["fee"])
     
             # node0 sees fee + 5 btc decrease
    -        assert_approx(w0.analyzerawtransaction(tx), -(5.0 + bitcoin_fee))
    +        assert_approx(w0.analyzerawtransaction(tx)["balance_change"], -(5.0 + bitcoin_fee))
     
             # node1 sees same as before
    -        assert_equal(w1.analyzerawtransaction(tx), 5.0)
    +        assert_equal(w1.analyzerawtransaction(tx)["balance_change"], 5.0)
    +
     
     if __name__ == '__main__':
         AnalyzeTxTest().main()
    

    </p></details>

  14. in src/wallet/rpcwallet.cpp:4741 in f1cc9a1be3 outdated
    4737 | @@ -4685,6 +4738,7 @@ static const CRPCCommand commands[] =
    4738 |      { "wallet",             &unloadwallet,                   },
    4739 |      { "wallet",             &upgradewallet,                  },
    4740 |      { "wallet",             &walletcreatefundedpsbt,         },
    4741 | +    { "wallet",             &analyzerawtransaction,          },
    


    jonatack commented at 4:28 PM on August 21, 2021:

    <details><summary>Some suggestions for the rpc</summary><p>

    --- a/src/wallet/rpcwallet.cpp
    +++ b/src/wallet/rpcwallet.cpp
    @@ -4647,13 +4647,16 @@ static RPCHelpMan upgradewallet()
     RPCHelpMan analyzerawtransaction()
     {
         return RPCHelpMan{"analyzerawtransaction",
    -        "\nCalculate the balance change resulting in the signing and broadcasting of the given transaction.\n" +
    +        "\nCalculate the balance change that would result from the signing and broadcasting of the given transaction.\n" +
             HELP_REQUIRING_PASSPHRASE,
             {
                 {"hexstring", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction hex string"},
             },
             RPCResult{
    -            RPCResult::Type::NUM, "", "The wallet balance change (negative means decrease)."
    +            RPCResult::Type::OBJ, "", "",
    +            {
    +                {RPCResult::Type::STR_AMOUNT, "balance_change", "The wallet balance change (negative means decrease)."},
    +            }
             },
             RPCExamples{
                 HelpExampleCli("analyzerawtransaction", "\"myhex\"")
    @@ -4673,26 +4676,31 @@ RPCHelpMan analyzerawtransaction()
         // Decode the transaction
         CMutableTransaction mtx;
         if (!DecodeHexTx(mtx, request.params[0].get_str(), true, true)) {
    -        throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed. Make sure the tx has at least one input.");
    +        throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction hex string decoding failed. Make sure the transaction has at least one input.");
         }
     
         // Calculate changes
    -    CAmount changes = 0;
    +    CAmount changes{0};
     
    -    // Fetch debit; we are *spending* these; if the transaction is signed and broadcast, we will lose everything in these
    +    // Fetch debit; we are *spending* these; if the transaction is signed and
    +    // broadcast, we will lose everything in these.
         for (size_t i = 0; i < mtx.vin.size(); ++i) {
             changes -= pwallet->GetDebit(mtx.vin.at(i), ISMINE_SPENDABLE);
         }
     
    -    // Iterate over outputs; we are *receiving* these, if the wallet considers them "mine"; if the transaciton is signed
    -    // and broadcast, we will receive everything in these
    +    // Iterate over outputs; we are *receiving* these, if the wallet considers
    +    // them "mine"; if the transaction is signed and broadcast, we will receive
    +    // everything in these.
         for (size_t i = 0; i < mtx.vout.size(); ++i) {
             const CTxOut& txout = mtx.vout.at(i);
             if (!pwallet->IsMine(txout)) continue;
             changes += txout.nValue;
         }
     
    -    return ValueFromAmount(changes);
    +    UniValue result(UniValue::VOBJ);
    +    result.pushKV("balance_change", ValueFromAmount(changes));
    +
    +    return result;
     }
         };
     }
    @@ -4762,6 +4770,7 @@ static const CRPCCommand commands[] =
         { "wallet",             &abandontransaction,             },
         { "wallet",             &abortrescan,                    },
         { "wallet",             &addmultisigaddress,             },
    +    { "wallet",             &analyzerawtransaction,          },
         { "wallet",             &backupwallet,                   },
         { "wallet",             &bumpfee,                        },
         { "wallet",             &psbtbumpfee,                    },
    @@ -4816,7 +4825,6 @@ static const CRPCCommand commands[] =
         { "wallet",             &unloadwallet,                   },
         { "wallet",             &upgradewallet,                  },
         { "wallet",             &walletcreatefundedpsbt,         },
    -    { "wallet",             &analyzerawtransaction,          },
    

    </p></details>

    • return an amount? (edit: seems to give the same result)
    • ISTM returning the result as a JSON object is preferred
    • s/transaciton/transaction/ line 4610
    • sort here

    kallewoof commented at 4:27 AM on August 23, 2021:

    Thanks, addressed all. See also alt PR.

  15. jonatack commented at 5:07 PM on August 21, 2021: contributor

    Concept ACK. Maybe add to decoderawtransaction? (Ignore me if that's dumb, and feel free to ignore the more detailed review comments below until the direction is set.)

  16. kallewoof commented at 7:38 AM on August 22, 2021: member

    @prayank23

    Can we add this to getbalance?

    Adding to getbalance sounds reasonable to me. @jonatack

    Concept ACK. Maybe add to decoderawtransaction? (Ignore me if that's dumb, and feel free to ignore the more detailed review comments below until the direction is set.)

    This requires the wallet so decoderawtransaction is out, I'm afraid. :)

  17. jonatack commented at 7:45 AM on August 22, 2021: contributor

    (getbalances is the replacement for getbalance).

  18. kristapsk commented at 12:23 PM on August 22, 2021: contributor

    I think it would be better to return JSON instead of numeric, that would allow adding possible other analysis in the future without breaking backwards compatibility.

  19. in src/wallet/rpcwallet.cpp:4598 in f1cc9a1be3 outdated
    4593 | +    LOCK(pwallet->cs_wallet);
    4594 | +    EnsureWalletIsUnlocked(*pwallet);
    4595 | +
    4596 | +    // Decode the transaction
    4597 | +    CMutableTransaction mtx;
    4598 | +    if (!DecodeHexTx(mtx, request.params[0].get_str(), true, true)) {
    


    kiminuo commented at 3:18 PM on August 22, 2021:

    Nit: Consider please:

        if (!DecodeHexTx(mtx, request.params[0].get_str(), /* try_no_witness */ true, /* try_witness */ true)) {
    
  20. in src/wallet/rpcwallet.cpp:4590 in f1cc9a1be3 outdated
    4582 | +            HelpExampleCli("analyzerawtransaction", "\"myhex\"")
    4583 | +            + HelpExampleRpc("analyzerawtransaction", "\"myhex\"")
    4584 | +        },
    4585 | +    [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
    4586 | +{
    4587 | +    std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
    


    kiminuo commented at 3:20 PM on August 22, 2021:

    Nit: People typically place consts before types in this codebase, AFAIK. If I'm wrong, please correct me.


    kallewoof commented at 4:25 AM on August 23, 2021:

    Looking around in the code, the way I wrote it seems to be the way it's written elsewhere, so keeping as is.


    kiminuo commented at 10:32 AM on August 23, 2021:

    Two lines above the const modifier is before type. Anyway, it's not important so I don't want to waste your time on this any further.


    kallewoof commented at 11:10 AM on August 23, 2021:

    That would actually make the shared pointer a constant, which points to a non-constant CWallet instance.

    See e.g. https://stackoverflow.com/questions/7526152/easy-rule-to-read-complicated-const-declarations


    kallewoof commented at 11:12 AM on August 23, 2021:

    Also try modifying my PR to this, and try compiling:

        const std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
        wallet->ResendWalletTransactions(); // <- this is not const
    

    (it works, even though ResendWalletTransactions() is NOT const-declared)


    kallewoof commented at 11:33 AM on August 23, 2021:

    Actually, I'm mistaken, and the two versions are identical. (ResendWalletTransactions() compiles for both cases.) I actually think this is a mistake, and could just as well be

    std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
    

    but I'm just going to keep the const where it is, as that's the case for other RPC calls. Sorry for the misinformation!

    To clarify, I mis-remembered const as applying to the thing directly to the right of it, but it's the opposite; const as the starting keyword is simply a remnant from before the const rule was solidified, and is an exception that says "usually we apply to the token to our left, but when const is preceded by nothing at all, it applies to the thing right after it". (That stack overflow post explains it.)

    So

    const std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
    std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
    

    all lead to a wallet whose non-const methods can be called, and the const applies to the std::shared_ptr in both cases (it stops us from changing wallet to, say, some other CWallet somewhere).

    What we really want here is actually

    std::shared_ptr<CWallet const> const wallet = GetWalletForJSOMRPCRequest(request);
    

    kallewoof commented at 4:29 AM on August 24, 2021:

    I opened a separate pull request on this subject. Thanks for the nudge. https://github.com/bitcoin/bitcoin/pull/22787

  21. in src/wallet/rpcwallet.cpp:4606 in f1cc9a1be3 outdated
    4601 | +
    4602 | +    // Calculate changes
    4603 | +    CAmount changes = 0;
    4604 | +
    4605 | +    // Fetch debit; we are *spending* these; if the transaction is signed and broadcast, we will lose everything in these
    4606 | +    for (size_t i = 0; i < mtx.vin.size(); ++i) {
    


    kiminuo commented at 3:23 PM on August 22, 2021:

    Would it be more idiomatic to use for (CTxIn& txin : mtx.vin) {?


    kallewoof commented at 4:25 AM on August 23, 2021:

    Done.

  22. in src/wallet/rpcwallet.cpp:4594 in f1cc9a1be3 outdated
    4586 | +{
    4587 | +    std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
    4588 | +    if (!wallet) return NullUniValue;
    4589 | +    CWallet* const pwallet = wallet.get();
    4590 | +
    4591 | +    RPCTypeCheck(request.params, {UniValue::VSTR}, true);
    


    kiminuo commented at 3:30 PM on August 22, 2021:
        RPCTypeCheck(request.params, {UniValue::VSTR}, /* fAllowNull */ true);
    

    kiminuo commented at 3:32 PM on August 22, 2021:

    If you allow null, then L4598 will lead to a crash, would it not?


    kallewoof commented at 4:19 AM on August 23, 2021:

    The RPCHelpMan framework will not execute the code, since the first param is non-optional.


    kiminuo commented at 1:51 PM on August 24, 2021:

    Would RPCTypeCheck(request.params, {UniValue::VSTR}) work too then?

  23. kallewoof commented at 3:58 AM on August 23, 2021: member

    Thanks for all the feedback. I'm rewriting this as a separate pull request that adds the feature to getbalances. I'll potentially keep updating this PR as well, in case somebody prefers this variant.

  24. kallewoof force-pushed on Aug 23, 2021
  25. kallewoof commented at 4:20 AM on August 23, 2021: member

    Opened alternative #22776 and updated this PR.

  26. kallewoof force-pushed on Aug 23, 2021
  27. kallewoof renamed this:
    wallet/rpc: add analyzerawtransaction RPC
    rpc/wallet: add analyzerawtransaction RPC
    on Aug 23, 2021
  28. kallewoof force-pushed on Aug 23, 2021
  29. kallewoof force-pushed on Aug 23, 2021
  30. kallewoof force-pushed on Aug 23, 2021
  31. kristapsk approved
  32. kristapsk commented at 9:26 AM on August 23, 2021: contributor

    ACK f19676bf02cb9cc279e2439c53b1f54ff53328fa

  33. DrahtBot commented at 2:13 PM on August 23, 2021: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #25621 (rpc/wallet: Add details and duplicate section for simulaterawtransaction by anibilthare)

    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.

  34. kallewoof force-pushed on Aug 24, 2021
  35. kallewoof force-pushed on Aug 24, 2021
  36. kristapsk approved
  37. kristapsk commented at 1:36 PM on August 24, 2021: contributor

    re-ACK c3628daf58bbe8d745cef08db8cee5fffd95167e

  38. in test/functional/wallet_analyzerawtx.py:55 in c3628daf58 outdated
      50 | +        assert_equal(w1.analyzerawtransaction(tx)["balance_change"], 5.0)
      51 | +
      52 | +        # w0 funds transaction; it should now see a decrease in (tx fee and payment), and w1 should see the same as above
      53 | +        funding = w0.fundrawtransaction(tx)
      54 | +        tx = funding["hex"]
      55 | +        bitcoin_fee = float(funding["fee"])
    


    kiminuo commented at 1:51 PM on August 24, 2021:

    Would it be better to use Decimal here?


    kallewoof commented at 10:11 AM on August 25, 2021:

    Could, but would be another import just for this one place, and the code should be fine (i.e. no rounding errors) due to the assert_approx.

  39. meshcollider commented at 4:32 AM on August 25, 2021: contributor

    @kallewoof do you mind adding mention of #22776 to the OP so people looking at this are clear of the alternative

  40. kallewoof commented at 10:12 AM on August 25, 2021: member

    @kallewoof do you mind adding mention of #22776 to the OP so people looking at this are clear of the alternative

    Good point, done.

  41. kallewoof force-pushed on Aug 25, 2021
  42. kallewoof commented at 10:13 AM on August 25, 2021: member

    ~Apologies, I switched to a constant CWallet so had to re-push.~

    Reverted change.

  43. kallewoof force-pushed on Aug 25, 2021
  44. achow101 commented at 11:49 PM on August 25, 2021: member

    This seems useful, but I would like to see its functionality be a bit broader, and the name changed.

    On the topic of functionality, I do see how it would be useful to see what changes a transaction would make without actually broadcasting it. We currently have testmempoolaccept which simulates sendrawtransaction. What if we had this RPC be testwalletaccept where we can not only observe how the balances change, but also which transaction(s) it conflicts with/replaces in the wallet. It could also be useful to have such an RPC indicate whether it would even be tracked by the wallet, and what metadata the wallet would assign to each output (e.g. mine, change, etc.). However such changes would likely need more invasive changes to the wallet itself before the RPC could be implemented (e.g. refactors to allow checks without actually adding to the wallet).

    On the topic of naming, I would prefer that we don't use analyzerawtransaction unless it is doing something equivalent to analyzepsbt. Currently the PSBT and raw transaction RPCs have similar names to indicate that they do equivalent things but with different data structures. This proposed analyzerawtransaction is not at all equivalent to the existing analyzepsbt - the equivalent thing would be to say what steps need to be done next to the raw transaction in order to sign it. I would rather this be something like testwalletaccept or perhaps simulaterawtransaction.

  45. kallewoof commented at 3:15 AM on August 26, 2021: member

    @achow101

    Thanks for the feedback. Great suggestions!

    I think it's agreed that this should be renamed if we do not go with the alternative proposal to add this to getbalances (see #22776); I'll rename to simulaterawtransaction for now. It seems like your additional functionality may come as follow-up PR(s), too.

  46. kallewoof force-pushed on Aug 26, 2021
  47. kallewoof renamed this:
    rpc/wallet: add analyzerawtransaction RPC
    rpc/wallet: add simulaterawtransaction RPC
    on Aug 26, 2021
  48. kallewoof force-pushed on Aug 26, 2021
  49. kallewoof force-pushed on Aug 26, 2021
  50. kallewoof commented at 8:05 AM on August 26, 2021: member

    Updated to take array of transactions instead of just one.

  51. kallewoof force-pushed on Aug 26, 2021
  52. laanwj commented at 6:18 PM on September 16, 2021: member

    Concept ACK, interesting feature

  53. laanwj added the label Feature on Sep 16, 2021
  54. in src/wallet/rpcwallet.cpp:4732 in 4d6c1a0153 outdated
    4615 | +        for (const auto& txin : mtx.vin) {
    4616 | +            if (spent.count(txin.prevout)) {
    4617 | +                throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction(s) are spending the same output more than once");
    4618 | +            }
    4619 | +            spent.insert(txin.prevout);
    4620 | +            changes -= pwallet->GetDebit(txin, ISMINE_SPENDABLE);
    


    achow101 commented at 6:56 PM on September 16, 2021:

    In 4d6c1a01532fa189c44714fae31916d758f3a574 "rpc/wallet: add simulaterawtransaction RPC"

    The isminetype here needs to match the isminetype below as otherwise we could get incorrect balance calculations. This either needs to be changed to ISMINE_ALL or the calculation in the outputs needs to check ISMINE_SPENDABLE too.

    I think it would be better to use ISMINE_ALL so that the balance computation includes watchonly things as those are part of the wallet balance as well.

    Or there could be an include_watchonly option that would change the IsMine filter from ISMINE_SPENDABLE to ISMINE_ALL. This would be the most inline with the rest of the RPCs.


    kallewoof commented at 4:58 AM on September 21, 2021:

    Sounds good - adding an include_watchonly option.

  55. in test/functional/wallet_simulaterawtx.py:19 in 4d6c1a0153 outdated
      14 | +)
      15 | +
      16 | +class SimulateTxTest(BitcoinTestFramework):
      17 | +    def set_test_params(self):
      18 | +        self.setup_clean_chain = True
      19 | +        self.num_nodes = 2
    


    achow101 commented at 7:02 PM on September 16, 2021:

    In 4d6c1a01532fa189c44714fae31916d758f3a574 "rpc/wallet: add simulaterawtransaction RPC"

    I would prefer using one node with 2 wallets. Having multiple nodes, with one wallet on each, is how we used to do things before multiwallet. Given that the test already makes testing wallets (one on each node), it's unnecessary to have multiple nodes.


    kallewoof commented at 7:22 AM on September 21, 2021:

    That is obviously the right way to do it. Thanks, fixed.

  56. kallewoof force-pushed on Sep 21, 2021
  57. kallewoof force-pushed on Sep 21, 2021
  58. kallewoof force-pushed on Sep 21, 2021
  59. kallewoof force-pushed on Sep 21, 2021
  60. kallewoof commented at 10:53 AM on September 21, 2021: member
    • added include_watchonly option
    • updated tests to use one node with two wallets
    • added include_watchonly tests
  61. ghost commented at 2:57 PM on September 21, 2021: none

    Approach NACK

    I prefer #22776

    Reasons:

    1. No need to create another RPC and remember another RPC.
    2. Other PR keeps things simple. This is basically returning balances but based on some conditions. It makes sense to add conditions (transactions in this case) as an argument in getbalances.
    3. Same approach can be followed in future for other RPCs if required. Example: What will be output for getmempoolinfo if a list of transactions are broadcasted: getmempoolinfo "[Tx1, Tx2.. Txn]"
  62. kallewoof commented at 4:29 AM on September 22, 2021: member

    FWIW, @apoelstra suggests this is modified to take PSBT(s) instead of raw transactions, stating that the latter is more or less made obsolete by the former, and in multi-party transactions people will most likely be using PSBT.

  63. in src/wallet/rpcwallet.cpp:4699 in 40944a573a outdated
    4694 | +    const CWallet* pwallet = wallet.get();
    4695 | +
    4696 | +    RPCTypeCheck(request.params, {UniValue::VARR, UniValue::VOBJ}, true);
    4697 | +
    4698 | +    LOCK(pwallet->cs_wallet);
    4699 | +    EnsureWalletIsUnlocked(*pwallet);
    


    achow101 commented at 8:25 PM on September 24, 2021:

    In 40944a573a21c225662ce0c8ba7f64a8427ca22d "rpc/wallet: add simulaterawtransaction RPC"

    This is unnecessary since no key access is needed.


    kallewoof commented at 5:18 AM on September 25, 2021:

    Do the vin/vout checks not access keys to determine if the wallet owns them?


    achow101 commented at 7:02 AM on September 25, 2021:

    It only uses pubkeys.


    kallewoof commented at 7:12 AM on September 25, 2021:

    OK, you're talking about the EnsureWalletIsUnlocked line. I thought you were also talking about L4698. Fixed.

  64. kallewoof force-pushed on Sep 25, 2021
  65. luke-jr referenced this in commit 5600f6febd on Oct 10, 2021
  66. in test/functional/wallet_simulaterawtx.py:37 in b269f1bb0d outdated
      32 | +        node.createwallet(wallet_name='w0')
      33 | +        node.createwallet(wallet_name='w1')
      34 | +        w0 = node.get_wallet_rpc('w0')
      35 | +        w1 = node.get_wallet_rpc('w1')
      36 | +
      37 | +        node.generatetoaddress(nblocks=COINBASE_MATURITY + 1, address=w0.getnewaddress())
    


    MarcoFalke commented at 12:01 PM on November 9, 2021:

    Needs to use self.gener... after a rebase


    kallewoof commented at 11:52 AM on November 14, 2021:

    Thanks, updated.

  67. kallewoof force-pushed on Nov 14, 2021
  68. DrahtBot added the label Needs rebase on Dec 8, 2021
  69. kallewoof force-pushed on Dec 9, 2021
  70. DrahtBot removed the label Needs rebase on Dec 9, 2021
  71. kallewoof commented at 8:10 AM on December 9, 2021: member

    Rebased. I don't even see an error in the two failing tests.

  72. laanwj commented at 3:04 PM on December 17, 2021: member

    For what it's worth I prefer this as a separate RPC over #22776. Trying to squash this into getbalances seems a bit weird to me and I agree with the reasoning in #22751 (comment) that this can do more, this way.

    Code + test review ACK 9cc3c4db5c4fdd200e64415fa3184d1b530581dc

  73. in test/functional/wallet_simulaterawtx.py:68 in 9cc3c4db5c outdated
      63 | +        assert_equal(w1.simulaterawtransaction([tx1, tx2])["balance_change"], 15.0)
      64 | +
      65 | +        # w0 funds transaction; it should now see a decrease in (tx fee and payment), and w1 should see the same as above
      66 | +        funding = w0.fundrawtransaction(tx1)
      67 | +        tx1 = funding["hex"]
      68 | +        bitcoin_fee = float(funding["fee"])
    


    laanwj commented at 3:41 PM on December 17, 2021:

    Would be better to use Decimal instead of float for monetary amounts.

  74. kallewoof force-pushed on Dec 19, 2021
  75. kallewoof commented at 5:50 AM on December 19, 2021: member

    Switched to using Decimal. Also added support for mixing floats and Decimals to assert_approx as a separate commit.

  76. kallewoof force-pushed on Dec 19, 2021
  77. kallewoof commented at 9:10 AM on December 19, 2021: member

    So if I understand the CI error right, it's no longer supported to import pubkeys to wallets that have privkeys (but nodes can?) so I should revert back to two nodes with separate wallets I assume? Sounds like a regression, but if it's the way it is, I'm fine doing that.

  78. in test/functional/wallet_simulaterawtx.py:47 in ae8564c198 outdated
      42 | +
      43 | +        address1 = w1.getnewaddress()
      44 | +        address2 = w1.getnewaddress()
      45 | +
      46 | +        # Make address1 watch-only in w0
      47 | +        w0.importpubkey(pubkey=w1.getaddressinfo(address1)["pubkey"])
    


    MarcoFalke commented at 8:44 AM on December 20, 2021:

    importpubkey does not exist for descriptor wallets (sqlite).

    test_framework.authproxy.JSONRPCException: Cannot import descriptor without private keys to a wallet with private keys enabled (-4)


    kallewoof commented at 11:11 AM on December 20, 2021:

    Maybe I should rethink the usage of importpubkey completely. It seems to be not very well supported anymore.

  79. achow101 commented at 8:10 PM on December 20, 2021: member

    So if I understand the CI error right, it's no longer supported to import pubkeys to wallets that have privkeys (but nodes can?) so I should revert back to two nodes with separate wallets I assume? Sounds like a regression, but if it's the way it is, I'm fine doing that.

    Yes. Part of the descriptor wallet changes is to have a clear separation of what is watch-only and what is spendable. This is achieved by having watch-only wallets which do not have private keys, and disallowing descriptor wallets with private keys to include watch-only things. So you should avoid continuing to use the legacy wallet behavior of mixing spendable and watch-only things in one wallet.

  80. in src/wallet/rpc/wallet.cpp:589 in ae8564c198 outdated
     581 | @@ -582,6 +582,94 @@ static RPCHelpMan upgradewallet()
     582 |      };
     583 |  }
     584 |  
     585 | +RPCHelpMan simulaterawtransaction()
     586 | +{
     587 | +    return RPCHelpMan{"simulaterawtransaction",
     588 | +        "\nCalculate the balance change resulting in the signing and broadcasting of the given transaction(s).\n" +
     589 | +        HELP_REQUIRING_PASSPHRASE,
    


    achow101 commented at 8:10 PM on December 20, 2021:

    In ae8564c198a3f9c57cc896489afcb78d6b947b45 "rpc/wallet: add simulaterawtransaction RPC"

    The wallet does not need to be unlocked, so this help text is not needed.


    kallewoof commented at 5:23 AM on December 21, 2021:

    Good point, removed.

  81. in test/functional/wallet_simulaterawtx.py:93 in ae8564c198 outdated
      87 | +        funding = w0.fundrawtransaction(tx2)
      88 | +        tx2 = funding["hex"]
      89 | +        bitcoin_fee2 = Decimal(funding["fee"])
      90 | +
      91 | +        # w0 sees fees + 15 btc decrease, or fees + 10 btc decrease for watch only case
      92 | +        assert_approx(w0.simulaterawtransaction([tx1, tx2])["balance_change"], -(Decimal("5") + bitcoin_fee + Decimal("10") + bitcoin_fee2))
    


    achow101 commented at 8:12 PM on December 20, 2021:

    In ae8564c198a3f9c57cc896489afcb78d6b947b45 "rpc/wallet: add simulaterawtransaction RPC"

    Since tx1 is already sent, it will include inputs that are already spent. It seems like we should disallow transactions that spend already spent inputs, at least until we implement conflict checking in this RPC.


    kallewoof commented at 6:50 AM on December 21, 2021:

    Added fetch/check of coins for transactions and RPC asserts when coins are missing or spent.

  82. JeremyRubin commented at 8:33 PM on December 20, 2021: contributor

    Approach wise, i think a better API might be filterrawtxformine, where it returns a list of inputs and outputs that are IsMine.

    Although: I think this can already be done with a batch of gettxout and getaddressinfo. Why the need for a new API?

    edit: pseudocode

    fn simulate(t: Transaction, info: SomeAPI) {
      let results = vec![];
      for out in t.ins.iter().map(|i| info.gettxout(i.hash, i.index)) {
        for address in out.scriptPubKey.addresses {
           if info.getaddressinfo(adress).ismine {
               resuts.push((address, out.value));
               break;
           }
        }
      }
      let outs =  t.outs.iter().filter_map(|v| { let a = info.getaddressinfo(a); if v.ismine {Some((a.address, v.value))} else {None} });
    }
    

    This should be reordable into 2 batches: [all gettxouts + outs gettaddressinfo, inputs getaddressinfo].

    It could be one if we added a is_mine filter to gettxout so the behavior doesn't change based on wallet presence.

  83. achow101 commented at 12:23 AM on December 21, 2021: member

    Although: I think this can already be done with a batch of gettxout and getaddressinfo. Why the need for a new API?

    For users who don't know the various ismine things and how the wallet actually calculates balances, it is preferable to have a built in RPC that does all that for them instead of hoping that what they think the wallet does is actually what it will do.

    Additionally, I would like to see this RPC be expanded to be more than just balance calculation (in a future PR). I would like to have it check for conflicts, and that would require much more work if implemented externally.

  84. JeremyRubin commented at 12:56 AM on December 21, 2021: contributor

    that seems like a recursive problem. A new wallet rpc will probably not do what we think a user should do?

    Maybe it's more a matter of cleaning up / augmenting the behavior of things like ismine so that the behavior is clearer for API consumers?

  85. JeremyRubin commented at 12:58 AM on December 21, 2021: contributor

    that said I don't think there's a terrible cost to a new RPC if it's useful, but I think we should have a concrete justification comparing it to the equivalent userland code. E.g., there have been cases where you can do it in userland but it is very very inefficient.

  86. kallewoof force-pushed on Dec 21, 2021
  87. kallewoof force-pushed on Dec 21, 2021
  88. kallewoof force-pushed on Dec 21, 2021
  89. kallewoof commented at 6:56 AM on December 21, 2021: member

    Yes. Part of the descriptor wallet changes is to have a clear separation of what is watch-only and what is spendable. This is achieved by having watch-only wallets which do not have private keys, and disallowing descriptor wallets with private keys to include watch-only things. So you should avoid continuing to use the legacy wallet behavior of mixing spendable and watch-only things in one wallet.

    Understood! I've added a third watch-only wallet and moved the watch-only checks there.

  90. kallewoof force-pushed on Dec 21, 2021
  91. kallewoof force-pushed on Dec 21, 2021
  92. in test/functional/wallet_simulaterawtx.py:117 in aaa078857a outdated
      84 | +        self.generate(node, 1, sync_fun=self.no_op) # Confirm tx to trigger error below
      85 | +        self.sync_all()
      86 | +
      87 | +        # w0 funds transaction 2; it should now see a decrease in (tx fee and payment), and w1 should see the same as above
      88 | +        funding = w0.fundrawtransaction(tx2)
      89 | +        tx2 = funding["hex"]
    


    achow101 commented at 8:40 PM on December 21, 2021:

    In aaa078857aa0bd4d881f781992dfc7c11aef0451 "rpc/wallet: add simulaterawtransaction RPC"

    I think this section isn't doing anything now.


    kallewoof commented at 4:17 AM on December 22, 2021:

    Re-added tests for tx2 only.

  93. in src/wallet/rpc/wallet.cpp:661 in aaa078857a outdated
     656 | +        // broadcast, we will lose everything in these
     657 | +        for (const auto& txin : mtx.vin) {
     658 | +            if (spent.count(txin.prevout)) {
     659 | +                throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction(s) are spending the same output more than once");
     660 | +            }
     661 | +            if (!coins.count(txin.prevout)) {
    


    achow101 commented at 8:45 PM on December 21, 2021:

    In aaa078857aa0bd4d881f781992dfc7c11aef0451 "rpc/wallet: add simulaterawtransaction RPC"

    If the UTXO points to one of the transactions in the array, then this would fail. I think it is possible someone would do this, especially with package relay on the horizon.

    We could have coins outside of the txs loop, add the outputs of each tx to coins, and also remove coins as they are processed in this loop.


    kallewoof commented at 4:55 AM on December 22, 2021:

    To avoid things like tx1 spending tx2 and tx2 spending tx1, I think we may want to strictly require that transactions are in order. If we do, I think we can just iterate one at a time and just append new outputs as we go, without the extra round.

    I updated the code to reflect this suggestion.

    Edit:re-reading your suggestion, I think maybe you suggested the same thing I am saying..

  94. kallewoof force-pushed on Dec 22, 2021
  95. kallewoof force-pushed on Dec 22, 2021
  96. kallewoof force-pushed on Dec 22, 2021
  97. ghost commented at 8:04 AM on December 22, 2021: none

    A new wallet rpc will probably not do what we think a user should do? @JeremyRubin

    There is an alternative solution in #22776 which does not add any new RPC command

  98. JeremyRubin commented at 9:03 AM on December 22, 2021: contributor

    @prayank23 my point was more that no code changes are required to use a constant number of RPC calls to answer this question. This technique might not have occurred to Kalle, so it was worth sharing.

    Andrew's response ~was that "the already wallet does not do what you think it does" and that future changes might extend this RPC further.

    I think adding an RPC makes more sense than changing and old one and both make less sense than not adding anything new unless there is a tangible issue with the mock algorithm i provided. A tangible issue could be "It's slow" or "race conditions between batches". I don't think we should have that issue here. Another compelling answer is "well it should eventually check for conflicts too, which we can't do well via just RPCs". In this case I would be curious to know if the check for conflict RPC should really be it's own RPC, and if we might benefit from thinking that through before pushing this.

    I'm also worried that if there are defects in existing RPCs that make them unreliable somehow that we might fix those too. I am curious -- for my benefit to learn -- why Andrew thinks the ismine field is not safe to use for this.

    All that said, an RPC isn't giving anyone cancer so by all means, merge if it's useful.

  99. kallewoof commented at 4:44 AM on December 23, 2021: member

    @JeremyRubin Primarily, my reason for wanting this is because I want a clear-cut way to say that the wallet changes I expect are happening. Yes I could manually write code to use getaddressinfo to do parts of this, but I might get it wrong. (Note that the command also includes new UTXO balance changes, which your pseudo-code does not, I think?)

  100. JeremyRubin commented at 7:22 AM on December 23, 2021: contributor

    Yeah you should be able to add it onto the code i provided by just inspecting the inputs/outputs fetched with something like:

    outs.iter().map(|v| v.1).sum()  - results.iter().map(|v| v.1).sum() 
    

    i don't have a problem with this RPC existing, it does seem useful.

    But I want to point out some other problematic behavior you might not have considered:

    suppose I have a wallet that has an address X that ismine but has the script/descriptor: and(older(2 years), key(mine))

    Suppose my wallet has outputs J under address A and K under address X that has matured.

    then i ask my node to analyze a transaction that looks like:

    Inputs: (J, 2.0) (K, 1.0), .... Outputs: (A, 2.0), (X, 1.0), ....

    where I was expecting the J -> A as my coinjoin, but K -> X was unexpected. Now my funds got automatically locked for another 2 years and this was not caught by this logic because the balance seemed fine.


    IDK if we can have CSV scripts in descriptors today, but it's something to note that APIs like this are incredibly tricks.

    I think the better version would be something like "I expect to care about input J and output A" and no other effect.

  101. kallewoof commented at 10:35 AM on December 23, 2021: member

    But I want to point out some other problematic behavior you might not have considered:

    Those to me sound like good reasons to have a command like this, as it seems like a natural expansion to add feedback related to time locks in the response.

  102. JeremyRubin commented at 3:04 AM on December 28, 2021: contributor

    i don't have a great answer for all this, it does seem to me that the best case is to make sure the only things matches are the ones you expect to be there, and that you only sign those.

    Anything else could be a vuln.

  103. DrahtBot added the label Needs rebase on Jan 3, 2022
  104. kallewoof force-pushed on Jan 4, 2022
  105. DrahtBot removed the label Needs rebase on Jan 4, 2022
  106. in src/wallet/rpc/wallet.cpp:678 in 84a745eed2 outdated
     673 | +                changes -= pwallet->GetDebit(txin, filter);
     674 | +            }
     675 | +            spent.insert(txin.prevout);
     676 | +        }
     677 | +
     678 | +        for (size_t i = 0; i < txs.size(); ++i) {
    


    achow101 commented at 10:42 PM on January 11, 2022:

    In 84a745eed211b597a89f3bbc3da73b0ac2cb457d "rpc/wallet: add simulaterawtransaction RPC"

    This loop doesn't make sense here. It shouldn't be necessary to iterate txs at the same time we are already iterating txs.

    At best, this could be moved outside of the first txs loop as a preprocessing step to extract all of the UTXOs created, but I don't think that would be a good idea. That would allow for transactions that have circular dependencies and those would be invalid. It is fine to have an order requirement as that is what package validation requires.


    kallewoof commented at 1:35 AM on January 12, 2022:

    Yeah, this was clearly a mistake. I removed the loop and kept the inner block only.

  107. in src/wallet/rpc/wallet.cpp:682 in 84a745eed2 outdated
     677 | +
     678 | +        for (size_t i = 0; i < txs.size(); ++i) {
     679 | +            // Populate new outputs
     680 | +            const auto hash = mtx.GetHash();
     681 | +            for (size_t i = 0; i < mtx.vout.size(); ++i) {
     682 | +                new_utxos[COutPoint(hash, i)] = (pwallet->IsMine(mtx.vout[i]) & filter) ? mtx.vout[i].nValue : 0;
    


    achow101 commented at 10:42 PM on January 11, 2022:

    In 84a745eed211b597a89f3bbc3da73b0ac2cb457d "rpc/wallet: add simulaterawtransaction RPC"

    This can be moved into the loop below that iterates the outputs.


    kallewoof commented at 1:37 AM on January 12, 2022:

    Moved.

  108. in test/functional/test_runner.py:261 in 84a745eed2 outdated
     257 | @@ -258,6 +258,7 @@
     258 |      'wallet_implicitsegwit.py --legacy-wallet',
     259 |      'rpc_named_arguments.py',
     260 |      'feature_startupnotify.py',
     261 | +    'wallet_simulaterawtx.py',
    


    achow101 commented at 10:45 PM on January 11, 2022:

    In 84a745eed211b597a89f3bbc3da73b0ac2cb457d "rpc/wallet: add simulaterawtransaction RPC"

    Wallet functional tests should appear in test_runner.py twice, once with --legacy-wallet and once with --descriptors.


    kallewoof commented at 1:38 AM on January 12, 2022:

    Got it. Done.

  109. kallewoof force-pushed on Jan 12, 2022
  110. kallewoof force-pushed on Jan 12, 2022
  111. kallewoof force-pushed on Jan 12, 2022
  112. kallewoof force-pushed on Jan 12, 2022
  113. in src/wallet/rpc/wallet.cpp:692 in aff96844f7 outdated
     683 | +
     684 | +        const auto hash = mtx.GetHash();
     685 | +        for (size_t i = 0; i < mtx.vout.size(); ++i) {
     686 | +            const auto& txout = mtx.vout[i];
     687 | +            bool is_mine = 0 < (pwallet->IsMine(txout) & filter);
     688 | +            changes += new_utxos[COutPoint(hash, i)] = is_mine ? txout.nValue : 0;
    


    kallewoof commented at 4:03 AM on January 12, 2022:

    I hope this line isn't overly confusing.

  114. achow101 commented at 7:53 PM on January 12, 2022: member

    ACK aff96844f7086458a414df9bb5cf9327a7266f2d

  115. achow101 commented at 1:18 PM on March 8, 2022: member
  116. DrahtBot added the label Needs rebase on Mar 30, 2022
  117. kallewoof force-pushed on Apr 4, 2022
  118. DrahtBot removed the label Needs rebase on Apr 4, 2022
  119. achow101 commented at 3:00 PM on April 25, 2022: member

    re-ACK bd520345f7a5076e5e1eb3877f6330a9e5816267

  120. in test/functional/test_framework/util.py:35 in bd520345f7 outdated
      27 | @@ -28,6 +28,10 @@
      28 |  
      29 |  def assert_approx(v, vexp, vspan=0.00001):
      30 |      """Assert that `v` is within `vspan` of `vexp`"""
      31 | +    if isinstance(v, Decimal) or isinstance(vexp, Decimal):
      32 | +        v=Decimal(v)
      33 | +        vexp=Decimal(vexp)
      34 | +        vspan=Decimal(vspan)
    


    luke-jr commented at 8:15 PM on April 25, 2022:

    This results in Decimal('0.000010000000000000000818030539140313095458623138256371021270751953125')

    Better to just insist v and vexp have compatible types, and default vspan sanely?


    kallewoof commented at 5:11 AM on April 26, 2022:

    The idea is to let the caller not worry too much about converting stuff to/from Decimal instances. Is there an issue due to vspan having some noise somewhere?

  121. DrahtBot added the label Needs rebase on Apr 26, 2022
  122. luke-jr referenced this in commit 2744179c4e on May 21, 2022
  123. luke-jr referenced this in commit e2165e800c on May 21, 2022
  124. kallewoof force-pushed on Jul 15, 2022
  125. DrahtBot removed the label Needs rebase on Jul 15, 2022
  126. anibilthare commented at 10:41 AM on July 25, 2022: none

    Ack. Tested manually. Works fine.

    I have also created a follow up PR which extends the functionality of this RPC.

  127. achow101 commented at 6:03 PM on July 26, 2022: member

    ACK b557f53eac39b92bfc4922729e94087556de749a

  128. in src/wallet/rpc/wallet.cpp:662 in b557f53eac outdated
     657 | +        // Fetch previous transactions (inputs)
     658 | +        std::map<COutPoint, Coin> coins;
     659 | +        for (const CTxIn& txin : mtx.vin) {
     660 | +            coins[txin.prevout]; // Create empty map entry keyed by prevout.
     661 | +        }
     662 | +        pwallet->chain().findCoins(coins);
    


    furszy commented at 7:14 PM on July 26, 2022:

    Based on the command description, why this is needed?

    Shouldn't simulaterawtransaction only care about how the wallet behaves if a certain transaction gets relayed to the network?.

    In other words, doesn't seems to be ok to fetch inputs that are not owned/watched by this wallet as them don't mean anything for the wallet.

    We could replace the input spendability check that is below using wallet.IsSpent(outpoint).


    kallewoof commented at 5:01 AM on July 27, 2022:

    I'm not sure what you're proposing. Mind writing a patch that describes your suggestion?


    furszy commented at 4:46 PM on August 1, 2022:

    sure, it's merely about removing the coins fetching from chain and using the wallet information to answer the spendability question.


    furszy commented at 5:00 PM on August 1, 2022:

    Note: haven't tested it but check it out:

    diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp
    --- a/src/wallet/rpc/wallet.cpp	(revision b557f53eac39b92bfc4922729e94087556de749a)
    +++ b/src/wallet/rpc/wallet.cpp	(date 1659372880572)
    @@ -654,13 +654,6 @@
                 throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction hex string decoding failure.");
             }
     
    -        // Fetch previous transactions (inputs)
    -        std::map<COutPoint, Coin> coins;
    -        for (const CTxIn& txin : mtx.vin) {
    -            coins[txin.prevout]; // Create empty map entry keyed by prevout.
    -        }
    -        pwallet->chain().findCoins(coins);
    -
             // Fetch debit; we are *spending* these; if the transaction is signed and
             // broadcast, we will lose everything in these
             for (const auto& txin : mtx.vin) {
    @@ -668,19 +661,21 @@
                 if (spent.count(op)) {
                     throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction(s) are spending the same output more than once");
                 }
    +            spent.insert(op); // mark it as spent
                 if (new_utxos.count(op)) {
                     changes -= new_utxos.at(op);
                     new_utxos.erase(op);
                 } else {
    -                if (!coins.count(op)) {
    -                    throw JSONRPCError(RPC_INVALID_PARAMETER, "One or more transaction inputs are missing");
    -                }
    -                if (coins.at(op).IsSpent()) {
    +                const auto* input_wtx = pwallet->GetWalletTx(txin.prevout.hash);
    +                if (!input_wtx) continue;
    +                if (input_wtx->tx->vout.size() >= txin.prevout.n) continue; // todo: this should throw an error
    +
    +                if (!(pwallet->IsMine(input_wtx->tx->vout[txin.prevout.n]) & filter)) continue;
    +                if (pwallet->IsSpent(txin.prevout)) {
                         throw JSONRPCError(RPC_INVALID_PARAMETER, "One or more transaction inputs have been spent already");
                     }
                     changes -= pwallet->GetDebit(txin, filter);
                 }
    -            spent.insert(op);
             }
     
             // Iterate over outputs; we are *receiving* these, if the wallet considers
    

    As said in the comment, it's about only using the wallet information to check the transaction and not the chain information. Mainly because, in this command, we only care about the wallet state.


    kallewoof commented at 12:59 AM on August 2, 2022:

    I'm not sure that's an improvement. The current implementation seems to be a bit more straightforward, even if it ends up pulling in a few extra inputs. If the difference in performance is significant it might be worth it though, but I'm dubious. :)

  129. in test/functional/wallet_simulaterawtx.py:95 in b557f53eac outdated
      90 | +
      91 | +        # on their own, both should fail due to missing input(s)
      92 | +        assert_raises_rpc_error(-8, "One or more transaction inputs have been spent already", w0.simulaterawtransaction, [tx3])
      93 | +        assert_raises_rpc_error(-8, "One or more transaction inputs have been spent already", w1.simulaterawtransaction, [tx3])
      94 | +        assert_raises_rpc_error(-8, "One or more transaction inputs have been spent already", w0.simulaterawtransaction, [tx4])
      95 | +        assert_raises_rpc_error(-8, "One or more transaction inputs have been spent already", w1.simulaterawtransaction, [tx4])
    


    w0xlt commented at 7:52 PM on July 27, 2022:

    Shouldn't these 4 cases raise the exception One or more transaction inputs are missing ?

    Anyway, this exception is not being tested.


    kallewoof commented at 1:07 AM on August 2, 2022:

    You're right. I don't think that exception will ever be triggered, because coins is always populated with all the outputs in mtx.vin in the earlier block. I think the best approach is to simply reword this exception to say "spent or missing" and drop the other one.

  130. in src/wallet/rpc/wallet.cpp:667 in b557f53eac outdated
     662 | +        pwallet->chain().findCoins(coins);
     663 | +
     664 | +        // Fetch debit; we are *spending* these; if the transaction is signed and
     665 | +        // broadcast, we will lose everything in these
     666 | +        for (const auto& txin : mtx.vin) {
     667 | +            auto& op = txin.prevout;
    


    w0xlt commented at 7:54 PM on July 27, 2022:

    nit: perhaps the op variable can be renamed to outpoint for the sake of clarity.


    kallewoof commented at 1:10 AM on August 2, 2022:

    Sure

  131. w0xlt approved
  132. w0xlt commented at 7:56 PM on July 27, 2022: contributor

    tACK https://github.com/bitcoin/bitcoin/pull/22751/commits/b557f53eac39b92bfc4922729e94087556de749a

    But as mentioned below, I think a test coverage for the exception One or more transaction inputs are missing can be added.

  133. kallewoof force-pushed on Aug 2, 2022
  134. test: add support for Decimal to assert_approx 701a64f548
  135. kallewoof force-pushed on Aug 2, 2022
  136. achow101 commented at 5:49 PM on August 4, 2022: member

    re-ACK ced8a3e77d0dde428360f2250576582b624c013c

  137. in src/wallet/rpc/wallet.cpp:667 in ced8a3e77d outdated
     662 | +        pwallet->chain().findCoins(coins);
     663 | +
     664 | +        // Fetch debit; we are *spending* these; if the transaction is signed and
     665 | +        // broadcast, we will lose everything in these
     666 | +        for (const auto& txin : mtx.vin) {
     667 | +            auto& outpoint = txin.prevout;
    


    jonatack commented at 7:43 PM on August 4, 2022:

    ced8a3e outpoint isn't mutated

                const auto& outpoint = txin.prevout;
    
  138. in src/wallet/rpc/wallet.cpp:688 in ced8a3e77d outdated
     683 | +        // Iterate over outputs; we are *receiving* these, if the wallet considers
     684 | +        // them "mine"; if the transaction is signed and broadcast, we will receive
     685 | +        // everything in these
     686 | +        // Also populate new_utxos in case these are spent in later transactions
     687 | +
     688 | +        const auto hash = mtx.GetHash();
    


    jonatack commented at 7:43 PM on August 4, 2022:

    ced8a3e IIRC GetHash() is uint256, which is worth not copying (unsure if compilers do automatic copy elision here)

            const uint256 hash{mtx.GetHash()};
    

    or

            const uint256& hash = mtx.GetHash();
    

    kallewoof commented at 12:41 AM on August 5, 2022:

    Does that differ from just doing const auto& hash = ? I assumed it wouldn't, but you're explicitly switching to uint256 so now I'm unsure.


    jonatack commented at 12:39 PM on August 5, 2022:

    No difference between auto and uint256, just highlighting the return type as auto isn't really shorter or simpler in this case than just stating the type.

  139. in src/wallet/rpc/wallet.cpp:605 in ced8a3e77d outdated
     600 | +                    {"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
     601 | +                },
     602 | +            },
     603 | +            {"options", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED_NAMED_ARG, "Options",
     604 | +                {
     605 | +                    {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
    


    jonatack commented at 8:07 PM on August 4, 2022:

    https://github.com/bitcoin/bitcoin/commit/ced8a3e77d0dde428360f2250576582b624c013c

                        {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see RPC importaddress)"},
    
  140. in src/wallet/rpc/wallet.cpp:623 in ced8a3e77d outdated
     618 | +        },
     619 | +    [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
     620 | +{
     621 | +    std::shared_ptr<const CWallet> const wallet = GetWalletForJSONRPCRequest(request);
     622 | +    if (!wallet) return NullUniValue;
     623 | +    const CWallet* pwallet = wallet.get();
    


    jonatack commented at 8:18 PM on August 4, 2022:

    https://github.com/bitcoin/bitcoin/commit/ced8a3e77d0dde428360f2250576582b624c013c my understanding was that a reference is preferred? (see RPCHelpMan getbalances for an example)

        const CWallet& wallet = *wallet;
    
  141. jonatack commented at 8:20 PM on August 4, 2022: contributor

    Light ACK ced8a3e77d0dde428360f2250576582b624c013c light review, rebased to current master, debug build, checked RPC help and new test

    A few suggestions.

  142. rpc/wallet: add simulaterawtransaction RPC
    This command iterates over the inputs and outputs of the given transactions, and tallies up the balance change for the given wallet. This can be useful e.g. when verifying that a coin join like transaction doesn't contain unexpected inputs that the wallet will then sign for unintentionally.
    db10cf8ae3
  143. kallewoof force-pushed on Aug 5, 2022
  144. kallewoof commented at 12:48 AM on August 5, 2022: member

    Applied suggestions by @jonatack. Thanks for the feedback!

  145. jonatack commented at 1:25 PM on August 5, 2022: contributor

    ACK db10cf8ae36693cb4d3ed1b47b84709cf9c0d849

    <details><summary>A few minor ideas if you retouch (clang-tidy named args format, a const ref, a test)</summary><p>

    --- a/src/wallet/rpc/wallet.cpp
    +++ b/src/wallet/rpc/wallet.cpp
    @@ -628,12 +628,12 @@ RPCHelpMan simulaterawtransaction()
     
         UniValue include_watchonly(UniValue::VNULL);
         if (request.params[1].isObject()) {
    -        UniValue options = request.params[1];
    +        const UniValue& options = request.params[1];
             RPCTypeCheckObj(options,
                 {
                     {"include_watchonly", UniValueType(UniValue::VBOOL)},
                 },
    -            true, true);
    +            /*fAllowNull=*/true, /*fStrict=*/true);
     
             include_watchonly = options["include_watchonly"];
         }
    @@ -650,7 +650,7 @@ RPCHelpMan simulaterawtransaction()
     
         for (size_t i = 0; i < txs.size(); ++i) {
             CMutableTransaction mtx;
    -        if (!DecodeHexTx(mtx, txs[i].get_str(), /* try_no_witness */ true, /* try_witness */ true)) {
    +        if (!DecodeHexTx(mtx, txs[i].get_str(), /*try_no_witness=*/true, /*try_witness=*/true)) {
                 throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction hex string decoding failure.");
             }
     
    @@ -685,7 +685,7 @@ RPCHelpMan simulaterawtransaction()
             // everything in these
             // Also populate new_utxos in case these are spent in later transactions
     
    -        const auto& hash = mtx.GetHash(); // auto isn't shorter or simpler here so maybe be explicit about the type 
    +        const uint256& hash = mtx.GetHash();
             for (size_t i = 0; i < mtx.vout.size(); ++i) {
                 const auto& txout = mtx.vout[i];
    
    --- a/test/functional/wallet_simulaterawtx.py
    +++ b/test/functional/wallet_simulaterawtx.py
    @@ -50,6 +50,9 @@ class SimulateTxTest(BitcoinTestFramework):
             tx1 = node.createrawtransaction([], [{address1: 5.0}])
             tx2 = node.createrawtransaction([], [{address2: 10.0}])
     
    +        self.log.info("Test simulaterawtransaction result if no txns provided")  # drop this line if no other test logging
    +        assert_equal(w0.simulaterawtransaction([]), {'balance_change': Decimal('0E-8')})
    +
             # w0 should be unaffected, w2 should see +5 for tx1
             assert_equal(w0.simulaterawtransaction([tx1])["balance_change"], 0.0)
    

    </p></details>

  146. achow101 commented at 7:08 PM on August 5, 2022: member

    re-ACK db10cf8ae36693cb4d3ed1b47b84709cf9c0d849

  147. achow101 merged this on Aug 5, 2022
  148. achow101 closed this on Aug 5, 2022

  149. sidhujag referenced this in commit 3cdbdce312 on Aug 6, 2022
  150. kallewoof deleted the branch on Aug 6, 2022
  151. in src/wallet/rpc/wallet.cpp:598 in db10cf8ae3
     589 | @@ -590,6 +590,117 @@ static RPCHelpMan upgradewallet()
     590 |      };
     591 |  }
     592 |  
     593 | +RPCHelpMan simulaterawtransaction()
     594 | +{
     595 | +    return RPCHelpMan{"simulaterawtransaction",
     596 | +        "\nCalculate the balance change resulting in the signing and broadcasting of the given transaction(s).\n",
     597 | +        {
     598 | +            {"rawtxs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "An array of hex strings of raw transactions.\n",
    


    MarcoFalke commented at 1:39 PM on August 29, 2022:

    Pretty sure this is not optional (should be NO, instead of OMITTED_NAMED_ARG)


    MarcoFalke commented at 1:40 PM on August 29, 2022:

    Also, the newline seems spurious?

  152. in src/wallet/rpc/wallet.cpp:596 in db10cf8ae3
     589 | @@ -590,6 +590,117 @@ static RPCHelpMan upgradewallet()
     590 |      };
     591 |  }
     592 |  
     593 | +RPCHelpMan simulaterawtransaction()
     594 | +{
     595 | +    return RPCHelpMan{"simulaterawtransaction",
     596 | +        "\nCalculate the balance change resulting in the signing and broadcasting of the given transaction(s).\n",
    


    MarcoFalke commented at 1:40 PM on August 29, 2022:

    style nit: \n are not needed, as they don't change the help output

  153. in src/wallet/rpc/wallet.cpp:653 in db10cf8ae3
     648 | +    std::map<COutPoint, CAmount> new_utxos; // UTXO:s that were made available in transaction array
     649 | +    std::set<COutPoint> spent;
     650 | +
     651 | +    for (size_t i = 0; i < txs.size(); ++i) {
     652 | +        CMutableTransaction mtx;
     653 | +        if (!DecodeHexTx(mtx, txs[i].get_str(), /* try_no_witness */ true, /* try_witness */ true)) {
    


    MarcoFalke commented at 1:44 PM on August 29, 2022:

    Why are you trying no witness? It is impossible for a valid transaction to fail witness deserialization. See also git grep "Make sure the tx has at least one input"


    MarcoFalke commented at 1:51 PM on August 29, 2022:

    I see that the tests are relying on no-inputs txs, but I wonder if there is a use case to run this on transactions that can never be valid on the network, even if properly signed.

  154. in test/functional/wallet_simulaterawtx.py:25 in db10cf8ae3
      20 | +        self.num_nodes = 1
      21 | +
      22 | +    def skip_test_if_missing_module(self):
      23 | +        self.skip_if_no_wallet()
      24 | +
      25 | +    def setup_network(self, split=False):
    


    MarcoFalke commented at 1:45 PM on August 29, 2022:

    What is split?


    MarcoFalke commented at 1:45 PM on August 29, 2022:

    Why is this function needed in the first place?

  155. in test/functional/wallet_simulaterawtx.py:31 in db10cf8ae3
      26 | +        self.setup_nodes()
      27 | +
      28 | +    def run_test(self):
      29 | +        node = self.nodes[0]
      30 | +
      31 | +        self.generate(node, 1, sync_fun=self.no_op) # Leave IBD
    


    MarcoFalke commented at 1:46 PM on August 29, 2022:

    No need to skip the sync_fun. With one node, it will be a no-op anyway

  156. in test/functional/wallet_simulaterawtx.py:112 in db10cf8ae3
     107 | +        assert_raises_rpc_error(-8, "Transaction(s) are spending the same output more than once", w0.simulaterawtransaction, [tx1, tx3, tx4])
     108 | +        assert_raises_rpc_error(-8, "Transaction(s) are spending the same output more than once", w1.simulaterawtransaction, [tx1, tx3, tx4])
     109 | +
     110 | +        # send tx1 to avoid reusing same UTXO below
     111 | +        node.sendrawtransaction(w0.signrawtransactionwithwallet(tx1)["hex"])
     112 | +        self.generate(node, 1, sync_fun=self.no_op) # Confirm tx to trigger error below
    


    MarcoFalke commented at 1:47 PM on August 29, 2022:

    Same

  157. in test/functional/wallet_simulaterawtx.py:113 in db10cf8ae3
     108 | +        assert_raises_rpc_error(-8, "Transaction(s) are spending the same output more than once", w1.simulaterawtransaction, [tx1, tx3, tx4])
     109 | +
     110 | +        # send tx1 to avoid reusing same UTXO below
     111 | +        node.sendrawtransaction(w0.signrawtransactionwithwallet(tx1)["hex"])
     112 | +        self.generate(node, 1, sync_fun=self.no_op) # Confirm tx to trigger error below
     113 | +        self.sync_all()
    


    MarcoFalke commented at 1:47 PM on August 29, 2022:

    Can be removed

  158. bitcoin locked this on Aug 29, 2023

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-13 15:14 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me