refactor: introduce generic ‘Result’ class and connect it to CreateTransaction and GetNewDestination #25218

pull furszy wants to merge 4 commits into bitcoin:master from furszy:2022_generic_result changing 21 files +201 −193
  1. furszy commented at 10:17 pm on May 25, 2022: member

    Based on a common function signature pattern that we have all around the sources:

    0bool doSomething(arg1, arg2, arg3, arg4, &result_obj, &error_string) {
    1    // do something...
    2    if (error) {
    3        error_string = "something bad happened";
    4        return false;
    5    }
    6
    7    result = goodResult;
    8    return true;
    9}
    

    Introduced a generic class BResult that encapsulate the function boolean result, the result object (in case of having it) and, in case of failure, the string error reason.

    Obtaining in this way cleaner function signatures and removing boilerplate code:

    0BResult<Obj> doSomething(arg1, arg2, arg3, arg4) {
    1    // do something...
    2    if (error) return "something bad happened";
    3
    4    return goodResult;
    5}
    

    Same cleanup applies equally to the function callers’ side as well. There is no longer need to add the error string and the result object declarations before calling the function:

    Before:

    0Obj result_obj;
    1std::string error_string;
    2if (!doSomething(arg1, arg2, arg3, arg4, result_obj, error_string)) {
    3    LogPrintf("Error: %s", error_string);
    4}
    5return result_obj;
    

    Now:

    0BResult<Obj> op_res = doSomething(arg1, arg2, arg3, arg4);
    1if (!op_res) {
    2    LogPrintf("Error: %s", op_res.GetError());
    3}
    4return op_res.GetObjResult();
    

    Initial Implementation:

    Have connected this new concept to two different flows for now:

    1. The CreateTransaction flow. –> 7ba2b87c
    2. The GetNewDestination flow. –> bcee0912

    Happy note: even when introduced a new class into the sources, the amount of lines removed is almost equal to added ones :).

    Extra note: this work is an extended version (and a decoupling) of the work that is inside #24845 (which does not contain the GetNewDestination changes nor the inclusion of the FeeCalculation field inside CreatedTransactionResult).

  2. furszy renamed this:
    wallet: introduce generic 'Result' classes and connect it to `CreateTransaction` and `GetNewDestination`
    wallet: introduce generic 'Result' classes and connect it to CreateTransaction and GetNewDestination
    on May 25, 2022
  3. furszy force-pushed on May 25, 2022
  4. DrahtBot added the label Refactoring on May 25, 2022
  5. furszy renamed this:
    wallet: introduce generic 'Result' classes and connect it to CreateTransaction and GetNewDestination
    refactor: introduce generic 'Result' classes and connect it to CreateTransaction and GetNewDestination
    on May 26, 2022
  6. theStack commented at 0:46 am on May 26, 2022: contributor
    Concept ACK
  7. DrahtBot commented at 9:02 am on May 26, 2022: contributor

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

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #25526 (wallet: avoid double keypool TopUp() calls on descriptor wallets by furszy)
    • #25297 (WIP, wallet: speedup transactions sync, rescan and load not flushing to db constantly by furszy)
    • #25273 (wallet: Pass through transaction locktime and preset input sequences and scripts to CreateTransaction by achow101)
    • #25269 (wallet: re-activate the not triggered “AmountWithFeeExceedsBalance” error by furszy)
    • #25234 (bench: add benchmark for wallet ‘AvailableCoins’ function. by furszy)
    • #24897 ([Draft / POC] Silent Payments by w0xlt)
    • #23444 (fuzz: Add regression test for wallet crash by MarcoFalke)
    • #19602 (wallet: Migrate legacy wallets to descriptor wallets 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.

  8. DrahtBot added the label Needs rebase on May 26, 2022
  9. furszy force-pushed on May 27, 2022
  10. furszy commented at 12:57 pm on May 27, 2022: member
    rebased, conflicts solved.
  11. furszy force-pushed on May 27, 2022
  12. DrahtBot removed the label Needs rebase on May 27, 2022
  13. furszy renamed this:
    refactor: introduce generic 'Result' classes and connect it to CreateTransaction and GetNewDestination
    refactor: introduce generic 'Result' classes and connect them to CreateTransaction and GetNewDestination
    on May 28, 2022
  14. w0xlt commented at 1:07 am on June 11, 2022: contributor
    Approach ACK
  15. ryanofsky commented at 7:37 pm on June 16, 2022: contributor

    This basically seems like a good design.

    In the simple case, you call a function and it can either succeed or fail, and if it fails it will provide an error string. In this case, the function returns OperationResult which contains the information packaged up in a standard format.

    In the slightly more complicated case, the function has a return value. Maybe it’s an informational return value, or maybe it’s a return value with details about errors if the caller needs to handle different errors in different ways. Either way, CallResult<T> can package up the result type with the error string, and return the information a standard format.

    Some design questions / things I’m not sure about:

    • Can OperationResult bool m_res member be dropped? It seems like just having a single std::optional<bilingual_str> m_error member without the bool would provide a more minimal and unambiguous version of that class. Treating nullopt case as success, and non-nullopt case as error would avoid the need for the bool.

    • Maybe OperationResult type should be replaced with CallResult<void>, just have one result class instead of two classes

    • Maybe result classes should provide a standard way to chain errors. If some inner function returns an error string, the caller might want to add its own error string placing the error in context. I think we also have some wallet code that returns lists of warnings and lists of errors, so it’s not clear if this result classes here would want to expand to handle this kind of thing, or they should just focus on simple cases.

    No need to resolve these questions to merge the PR, I think. Can add result classes now, and experiment with using them, and add/remove features as needed.

  16. DrahtBot added the label Needs rebase on Jun 16, 2022
  17. laanwj commented at 10:10 pm on June 16, 2022: member
    Concept ACK. I like this rust-inspired idea. One additional idea might be to parametrize Result on both the result and error type (in case it is not always a string).
  18. maflcko commented at 6:30 am on June 17, 2022: member

    I also wonder why this needs three members and 6 constructors. Can this be implemented in less code by just using a std::variant with two types?

    Going to the extreme, with just two lines of code:

    0template <typename Obj, typename Err>
    1using CallResult = std::variant<Obj, Err>;
    
  19. achow101 referenced this in commit 8be652e439 on Jun 17, 2022
  20. DrahtBot removed the label Needs rebase on Jun 20, 2022
  21. maflcko closed this on Jun 21, 2022

  22. maflcko reopened this on Jun 21, 2022

  23. maflcko commented at 5:29 am on June 21, 2022: member
    Not sure what is wrong with GitHub. This absolutely has a merge conflict, but it is not detected by GitHub.
  24. maflcko commented at 5:31 am on June 21, 2022: member
    Closing for now due to the GitHub bug. You can open a new pull after you rebased it, or ask us to reopen this one.
  25. maflcko closed this on Jun 21, 2022

  26. furszy commented at 12:38 pm on June 21, 2022: member
    hmm, @MarcoFalke I just rebased it locally and didn’t have any merge conflict.
  27. maflcko commented at 12:52 pm on June 21, 2022: member
    It probably depends on the version of git, as they regularly change the merge algorithm.
  28. maflcko reopened this on Jun 21, 2022

  29. maflcko added the label Needs rebase on Jun 21, 2022
  30. furszy commented at 12:57 pm on June 21, 2022: member
    k np, thanks for re-opening it. I actually have been working on your std::variant comment the past days (great point), wanted to finish wrapping up the idea properly before rebase this branch.
  31. furszy force-pushed on Jun 21, 2022
  32. ryanofsky commented at 2:43 pm on June 21, 2022: contributor

    FWIW, I do not think the std::variant / rust comments are a good idea. I see the main benefit of this PR not as providing a way of returning structured results and errors, but as a way of returning displayable and loggable error strings in a convenient way, and a way of replacing ad-hoc bilingual_str& error and std::vector<bilingual_str>& warnings output parameters with a more consistent and streamlined return mechanism.

    I don’t think it is a good idea for the internal bilingual_str error type to be customizable, or for it to be directly exposed to callers. I think the internal bilingual_str error field that is private and has convenient constructors and accessors in 9ba14ec845a364396b2a364c02dd17043bf5a678 is a good starting design. In the future, the internal type could become more complex if we decide to add support for chaining errors, or passing warnings along with errors.

    Rust-style std::variant style result types that allow returning different success and error states with data attached are great, but they are already compatible with this PR in its current form. If you want a std::variant return type, you can use CallResult<std::variant<...>>. There should be no need or benefit to making CallResult into a std::variant

  33. DrahtBot removed the label Needs rebase on Jun 21, 2022
  34. maflcko commented at 2:55 pm on June 21, 2022: member

    don’t think it is a good idea for the internal bilingual_str error type to be customizable, or for it to be directly exposed to callers. I think the internal bilingual_str error field that is private and has convenient constructors and accessors in https://github.com/bitcoin/bitcoin/commit/9ba14ec845a364396b2a364c02dd17043bf5a678 is a good starting design. In the future, the internal type could become more complex if we decide to add support for chaining errors, or passing warnings along with errors.

    How would you achieve this without providing the option to <template> on the error type. Currently in code, some places use an error string only, some use error+warnings. If the class here only accommodates one type, it can not be used in the other place.

    Rust-style std::variant style result types that allow returning different success and error states with data attached are great, but they are already compatible with this PR in its current form. If you want a std::variant return type, you can use CallResult<std::variant<…». There should be no need or benefit to making CallResult into a std::variant

    Not sure how this would work if I wanted to write a method that calculates the sum, unless it is not possible, in which case it should provide the reason.

    Currently I could write:

    0enum class ArithError{ OVERFLOW, UNDERFLOW};
    1std::variant<int, ArithErr> sum(int, int);
    

    However, the downside is that the calling code would need to unwrap the variant verbosely.

    I don’t see how

    0CallResult<std::variant<int,ArithErr>> sum(int,int);
    

    makes any sense for the caller

  35. ryanofsky commented at 3:23 pm on June 21, 2022: contributor

    I don’t think this result class is helpful (or hurtful) for low level level functions that compute sums and need to return structured result data to callers. I think this result class is more useful for high level functions that load wallets and create transactions and refill keypools and need to return human readable results and diagnostics to end-users.

    In the future, the internal type could become more complex if we decide to add support for chaining errors, or passing warnings along with errors.

    How would you achieve this without providing the option to <template> on the error type. Currently in code, some places use an error string only, some use error+warnings. If the class here only accommodates one type, it can not be used in the other place.

    The type can change in the future and the API can degrade gracefully. Version 1 of this class in this PR just lets function return simple error strings and lets callers display or log them or use them in RPC results. Version 2 of this class in a future PR might add support for multiple errors or chained errors. Callers of version 2 result class could update their code to handle multiple errors in separate strings, or they could continue asking for a single error and get multiple errors in a single string. Version 3 of this class in a future-future PR might add support for warnings. Callers of version 3 result class could ask for the warning separately, or they could keep just asking for errors, and get the warnings returned as errors.

    The class just gets rid of bilingual_str& error output parameters for now, but can grow and become more full featured in the future.

    Rust-style std::variant style result types that allow returning different success and error states with data attached are great, but they are already compatible with this PR in its current form. If you want a std::variant return type, you can use CallResult<std::variant<…». There should be no need or benefit to making CallResult into a std::variant

    Not sure how this would work if I wanted to write a method that calculates the sum, unless it is not possible, in which case it should provide the reason.

    I don’t think you would use this result class in a method that calculates a sum, unless you want that method to return loggable and displayable error strings as well as sums.

    Currently I could write:

    0enum class ArithError{ OVERFLOW, UNDERFLOW};
    1std::variant<int, ArithErr> sum(int, int);
    

    However, the downside is that the calling code would need to unwrap the variant verbosely.

    Right. I don’t think think there will be a good solution to verbosity until pattern matching features are added to c++. I just think this is orthogonal to anything in this PR. It is certainly orthogonal to anything in 9ba14ec845a364396b2a364c02dd17043bf5a678. If you want your sum function to return overflow/underflow/int, std::variant is your best bet probably, and I don’t think this PR or another PR is going to give you something better.

  36. maflcko commented at 3:35 pm on June 21, 2022: member

    I don’t think this PR or another PR is going to give you something better.

    I’d guess that pattern matching (or emulating it verbosely) might be overkill to handle two cases. Though, if this is ever needed, it can be implemented in the future and this pull can be used as-is for now.

  37. ryanofsky commented at 3:46 pm on June 21, 2022: contributor

    There might be some tricks that give a limited form of pattern matching. For example could replace enum in previous example with:

    0struct OverflowError {};
    1struct UnderflowError {};
    2std::variant<int, OverflowError, UnderflowError> sum(int, int);
    

    and use std::visit + util::Overloadedlambdas trick to handle the different cases.

  38. maflcko commented at 3:59 pm on June 21, 2022: member
    Yeah, but that’s ugly because the lambdas must have the same return type
  39. ryanofsky commented at 4:20 pm on June 21, 2022: contributor

    Yeah, but that’s ugly because the lambdas must have the same return type

    Yes probably the lambdas would return void and just execute code and modify local variables, like a switch statement. But like you said this is overkill for two cases. std::variant would be useful when there are more cases that need to be handled in many different ways.

  40. furszy commented at 6:17 pm on June 24, 2022: member

    A bit late but here.

    and use std::visit + util::Overloadedlambdas trick to handle the different cases.

    I was actually experimenting with that (and few more things). Essentially, was making a generic wrapper class for std::variant to eliminate the extra verbosity that comes with it + adding the ability to catch, at compile-time, any not handled function outcome/s.

    But.. haven’t finished the experiment yet (been jumping across other findings).

    From my side, if you guys are happy with the current PR state, we could move forward with it. At the end, it cleans up a lot of boilerplate code, can easily be used in a lot of places right away, and encapsulates the result object mechanics into its own class (which, while the class interface is good, as ryanofsky said, it can be upgraded/changed in the future without any trouble). Note: decision will not stop me from continuing with the experiment.

  41. ryanofsky commented at 6:53 pm on June 24, 2022: contributor
    My main suggestions for improvement are here: #25218 (comment). I think bool m_res member should be dropped and m_errormember being non-empty should be the only condition that determines whether the result is an error. Also just IMO but I think naming could be improved. I’d change CallResult<T> to util::Result<T> and OperationResult to util::Result<void>. I think a short name for the class is better so code is concise is and easy to read. Also I don’t think when you are writing code you should need to think about vocabulary and whether a function that returns a value is considered an “operation” or an “call”.
  42. furszy commented at 8:00 pm on June 24, 2022: member

    My main suggestions for improvement are here: #25218 (comment). I think bool m_res member should be dropped and m_error member being non-empty should be the only condition that determines whether the result is an error.

    Yeah ok 👌🏼 .

    Wouldn’t be bad to think further about the “result code” notion there (where any <=0 value means “failed”) to not make the optional string value a requirement (depends on where the function that uses the Result class is located, layer’s wise, we might don’t want to involve strings in the result and just error codes so then the upper layers can translate the error accordantly to a more informational string). –> Note: and I’m not targeting to low-level arithmetic functions, I’m thinking about middleware functions that are called from different places and mean a slightly different thing depending from where they are called.

    But.. I’m just thinking out loud.. this could be implemented in the future using a generic error type as well (if needed).

    Also just IMO but I think naming could be improved. I’d change CallResult to util::Result and OperationResult to util::Result. I think a short name for the class is better so code is concise is and easy to read. Also I don’t think when you are writing code you should need to think about vocabulary and whether a function that returns a value is considered an “operation” or an “call”.

    Yeah agree, I didn’t use the “Result” term before because it’s being used in other places and didn’t want to clash with them: feebumper.h has a Result enum and nanobench.h has a Result class. But.. just saw that those are under different namespaces, so all good. Still, I personally don’t like much having the util:: namespace prefix at the beginning of every function signature.

  43. furszy force-pushed on Jun 25, 2022
  44. furszy commented at 0:40 am on June 26, 2022: member

    Based on all the great feedback, pushed the following changes:

    1. Removed the OperationResult class. It wasn’t being used here (can be introduced in a follow-up PR when we connect it to some process).


    2. Internally, the result class now uses an std::variant instead of two optional values. This is mainly because the result object behaves as an OR operation, not as an AND (it contains one value or the other, not both).
 Which let us gain the std::variant benefits without introducing the ugly get_if and holds_alternative by type, or by index, verbosity that comes with it. Quick example:

      a) Using a plain std::variant:

      0std::variant<CreatedTransactionResult, bilingual_str> result = createTx(arg1, arg2, arg3);
      1if (std::holds_alternative<bilingual_str>(result)) {
      2    const auto& error = std::get<bilingual_str>(result);
      3    // error out..
      4}
      5const auto& txr = std::get<CreatedTransactionResult>(result);
      6// continue doing something..
      

      b) Using the introduced BResult class:

      0BResult<CreatedTransactionResult> result = createTx(arg1, arg2, arg3);
      1if (!result) {
      2    const auto& error = result.GetError();
      3    // error out..
      4}
      5const auto& txr = result.GetObj();
      6// continue doing something..
      
    3. Renamed CallResult to BResult. Some rationale for selecting this name and not the suggested util::Result:


      a) I think that defining it as “Result” alone will, sooner or later, cause issues with other library/module class. It’s a pretty common name.
 b) Function signatures readability wise, I don’t think that declare it under the util namespace helps much. At least for me, the util:: prefix usage in a return object looks odd.

  45. in src/result.h:27 in 89207d52a8 outdated
    22+    BResult() : m_variant(Untranslated("")) {}
    23+    BResult(const T& _obj) : m_variant(_obj) {}
    24+    BResult(const bilingual_str& error) : m_variant(error) {}
    25+
    26+    /* In case of success, the result object */
    27+    const T* GetObj() const { return std::get_if<T>(&m_variant); }
    


    maflcko commented at 6:48 am on June 26, 2022:
    Any reason to not call this just “get”, like https://en.cppreference.com/w/cpp/memory/unique_ptr/get

    furszy commented at 3:42 pm on June 26, 2022:

    std::get on a variant throws std::bad_variant_access on errors (if the variant isn’t storing the type) –> https://en.cppreference.com/w/cpp/utility/variant/get. std::get_if just return a nullptr.

    Ideally, I think that an assertion here would be good, so we can directly return the const reference instead of the pointer (which will force the callers to always check if the result succeeded or not before getting the type). But.. I didn’t add it here to not make this PR too broad, it was needing further restructuring in some of the function’s callers (add a new plug-and-play result class is one thing, restructure how each of the callers treats the result is another one).


    maflcko commented at 5:34 pm on June 26, 2022:
    I meant renaming the function name from GetObj to Get

    furszy commented at 8:01 pm on June 26, 2022:
    Ah ok, I tried to distinguish clearly at the interface level the two possible outcomes GetError()/GetObj() (It could had been GetRes() as well). Mainly to prevent this class from be blindly used as a smart pointer, forgetting the error existence. But if you have a strong predilection for the Get wording, could change it. Nothing is set on stone hehe.
  46. in src/result.h:29 in 89207d52a8 outdated
    24+    BResult(const bilingual_str& error) : m_variant(error) {}
    25+
    26+    /* In case of success, the result object */
    27+    const T* GetObj() const { return std::get_if<T>(&m_variant); }
    28+    /* Whether the function succeeded or not */
    29+    bool GetRes() const { return !std::holds_alternative<bilingual_str>(m_variant); }
    


    maflcko commented at 6:52 am on June 26, 2022:

    maybe get -> has , like https://en.cppreference.com/w/cpp/utility/optional/operator_bool

    and !std::holds_alternative<bilingual_str> -> std::holds_alternative<T>?


    furszy commented at 3:43 pm on June 26, 2022:
    done
  47. in src/result.h:8 in 89207d52a8 outdated
    0@@ -0,0 +1,39 @@
    1+// Copyright (c) 2022 The Bitcoin Core developers
    2+// Distributed under the MIT software license, see the accompanying
    3+// file COPYING or https://www.opensource.org/licenses/mit-license.php.
    4+
    5+#ifndef BITCOIN_RESULT_H
    6+#define BITCOIN_RESULT_H
    7+
    8+#include <optional>
    


    maflcko commented at 7:24 am on June 26, 2022:
    unused?

    furszy commented at 3:43 pm on June 26, 2022:
    👍🏼 removed.
  48. in src/result.h:6 in 89207d52a8 outdated
    0@@ -0,0 +1,39 @@
    1+// Copyright (c) 2022 The Bitcoin Core developers
    2+// Distributed under the MIT software license, see the accompanying
    3+// file COPYING or https://www.opensource.org/licenses/mit-license.php.
    4+
    5+#ifndef BITCOIN_RESULT_H
    6+#define BITCOIN_RESULT_H
    


    maflcko commented at 7:25 am on June 26, 2022:
    I think regardless of the namespace, this should be placed in the util folder

    furszy commented at 3:43 pm on June 26, 2022:
    moved 👍🏼
  49. in src/result.h:36 in 89207d52a8 outdated
    31+    bilingual_str GetError() const {
    32+        auto error = std::get_if<bilingual_str>(&m_variant);
    33+        return (error ? *error : bilingual_str());
    34+    }
    35+
    36+    explicit operator bool() const { return GetRes(); }
    


    maflcko commented at 7:26 am on June 26, 2022:

    I wonder if operator* should be added, like https://en.cppreference.com/w/cpp/memory/unique_ptr/operator*

    but instead of UB, it runs into an assertion failure?


    furszy commented at 9:26 pm on June 26, 2022:

    Hmm, it could be handy but might end up with users trying to access the object without checking the result existence first (which, as not every error path is covered by tests, could end up badly).

    I would rather walk into adding a generic std::visit direction, so users are always forced to provide handlers for both outcomes. Which would avoid the pointers need at all and make the code less error prone.

  50. maflcko approved
  51. maflcko commented at 7:26 am on June 26, 2022: member
    ack
  52. maflcko commented at 7:49 am on June 26, 2022: member
    pull request description needs to be adjusted?
  53. furszy renamed this:
    refactor: introduce generic 'Result' classes and connect them to CreateTransaction and GetNewDestination
    refactor: introduce generic 'Result' class and connect it to CreateTransaction and GetNewDestination
    on Jun 26, 2022
  54. furszy force-pushed on Jun 26, 2022
  55. furszy commented at 3:52 pm on June 26, 2022: member

    Thanks for the feedback Marco!

    PR description updated + applied the following changes:

    1. result.h file moved to util directory.
    2. Renamed GetRes for HasRes.
    3. Removed unused include.
  56. in src/qt/addresstablemodel.cpp:376 in efbeafaa1d outdated
    376+        auto op_dest = walletModel->wallet().getNewDestination(address_type, strLabel);
    377+        if (!op_dest) {
    378             WalletModel::UnlockContext ctx(walletModel->requestUnlock());
    379-            if(!ctx.isValid())
    380-            {
    381+            if(!ctx.isValid()) {
    


    theStack commented at 2:11 pm on June 27, 2022:

    spacing nit:

    0            if (!ctx.isValid()) {
    

    furszy commented at 3:23 pm on June 27, 2022:
    done
  57. in src/wallet/scriptpubkeyman.cpp:36 in efbeafaa1d outdated
    37     // Generate a new key that is added to wallet
    38     CPubKey new_key;
    39     if (!GetKeyFromPool(new_key, type)) {
    40-        error = _("Error: Keypool ran out, please call keypoolrefill first");
    41-        return false;
    42+        return _("Error: Keypool ran out, please call keypoolrefill first");;
    


    theStack commented at 2:13 pm on June 27, 2022:

    redundant nit:

    0        return _("Error: Keypool ran out, please call keypoolrefill first");
    

    furszy commented at 3:23 pm on June 27, 2022:
    done
  58. in src/wallet/test/coinselector_tests.cpp:78 in efbeafaa1d outdated
    78-        bilingual_str error;
    79-        const bool destination_ok = wallet.GetNewDestination(OutputType::BECH32, "", dest, error);
    80-        assert(destination_ok);
    81-        tx.vout[nInput].scriptPubKey = GetScriptForDestination(dest);
    82+        auto op_dest = wallet.GetNewDestination(OutputType::BECH32, "");
    83+        assert(op_dest);
    


    theStack commented at 2:22 pm on June 27, 2022:

    nit: probably be explicit here, same as in the fuzz test?

    0        assert(op_dest.HasRes());
    

    furszy commented at 3:23 pm on June 27, 2022:
    done
  59. theStack approved
  60. theStack commented at 2:26 pm on June 27, 2022: contributor

    Code-review ACK efbeafaa1d537db171016a785da4647fde7db5cb

    Back then on PR #20640, which was the first step of getting rid of out parameters for the functions CreateTransaction{Internal}, the over-use of the auto keyword was questioned (see #20640 (review)) due to obscuring the data type and hence lower readability, as it’s not immediately visible what the return type is. Probably at some places explicitly using the BResult<T> would also be better. Also I’m not personally not a fan of the implicit bool operator use (e.g. for std::optional I always preferred the implicit has_value() method), but no hard feelings here – that’s probably largely a matter of taste. Left also some (non-blocking) nits below.

  61. furszy force-pushed on Jun 27, 2022
  62. furszy force-pushed on Jun 27, 2022
  63. furszy commented at 3:46 pm on June 27, 2022: member

    Thanks for the review theStack!

    Pushed the three nits.

    Back then on PR #20640, which was the first step of getting rid of out parameters for the functions CreateTransaction{Internal}, the over-use of the auto keyword was questioned (see #20640 (review)) due to obscuring the data type and hence lower readability, as it’s not immediately visible what the return type is. Probably at some places explicitly using the BResult would also be better

    I think that at least on this, non-consensus, code is “ok” to use the auto keyword on the result. Mainly because in order to get the returned value you still have to call GetObj() (where, probably, we could clarify the type there).

    And I think that something like the following example is a bit too much for a single line of code:

    0const BResult<CreatedTransactionResult>& create_tx_result = CreateTransactionInternal(wallet, vecSend, change_pos, coin_control, sign);
    

    But.. open for comments. If more want it, will make the changes. (might be good to add a section into the developer-notes talking about this)

  64. in src/util/result.h:26 in 755cb5606d outdated
    21+    BResult() : m_variant(Untranslated("")) {}
    22+    BResult(const T& _obj) : m_variant(_obj) {}
    23+    BResult(const bilingual_str& error) : m_variant(error) {}
    24+
    25+    /* In case of success, the result object */
    26+    const T* GetObj() const { return std::get_if<T>(&m_variant); }
    


    achow101 commented at 6:36 pm on June 27, 2022:

    In 755cb5606de0b63e0027ab11840bb5e4c3f000aa “Introduce generic ‘Result’ class”

    I would prefer if we used std::get instead of get_if so that the callers don’t have to deal with pointers. This could assert(HasRes()) to avoid the exception. Then the return value would be T&.

    Same for GetError below.


    furszy commented at 8:39 pm on June 27, 2022:

    I would prefer it too but.. the rationale to not implement it was #25218 (review).

    Let me see how large the changes to make this happen are. Probably the only refactoring will be inside CreateTransaction for all the tracepoints.

  65. in src/util/result.h:30 in 755cb5606d outdated
    25+    /* In case of success, the result object */
    26+    const T* GetObj() const { return std::get_if<T>(&m_variant); }
    27+    /* Whether the function succeeded or not */
    28+    bool HasRes() const { return std::holds_alternative<T>(m_variant); }
    29+    /* In case of failure, the error cause */
    30+    bilingual_str GetError() const {
    


    achow101 commented at 6:41 pm on June 27, 2022:

    In 755cb5606de0b63e0027ab11840bb5e4c3f000aa “Introduce generic ‘Result’ class”

    0    const bilingual_str& GetError() const {
    

    furszy commented at 9:30 pm on June 27, 2022:
    done
  66. in src/wallet/spend.cpp:898 in 4e1353016d outdated
    894@@ -904,7 +895,7 @@ static std::optional<CreatedTransactionResult> CreateTransactionInternal(
    895                     } else {
    896                         error = _("The transaction amount is too small to send after the fee has been deducted");
    897                     }
    898-                    return std::nullopt;
    899+                    return error;
    


    achow101 commented at 6:56 pm on June 27, 2022:

    In 4e1353016dabcf645b22fb7c196f62d8f465df39 “send: refactor CreateTransaction flow to return a BResult

    The if above could just return the error strings directly as you have done elsewhere, there’s no need to be returning error after setting those strings.


    furszy commented at 9:30 pm on June 27, 2022:
    done
  67. in src/test/util/wallet.cpp:24 in 9a0a4024f0 outdated
    19@@ -20,11 +20,10 @@ const std::string ADDRESS_BCRT1_UNSPENDABLE = "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqq
    20 std::string getnewaddress(CWallet& w)
    21 {
    22     constexpr auto output_type = OutputType::BECH32;
    23-    CTxDestination dest;
    24-    bilingual_str error;
    25-    if (!w.GetNewDestination(output_type, "", dest, error)) assert(false);
    26+    auto op_dest = w.GetNewDestination(output_type, "");
    27+    if (!op_dest) assert(false);
    


    achow101 commented at 7:02 pm on June 27, 2022:

    In 9a0a4024f0fdd2ec598613fa8ea55683115a88f5 “wallet: refactor GetNewDestination, use BResult”

    0    assert(op_dest.HasRes());
    

    furszy commented at 9:30 pm on June 27, 2022:
    done
  68. in src/wallet/scriptpubkeyman.cpp:1763 in 9a0a4024f0 outdated
    1762@@ -1769,9 +1763,13 @@ bool DescriptorScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, Walle
    1763 bool DescriptorScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool, bilingual_str& error)
    


    achow101 commented at 7:06 pm on June 27, 2022:

    In 9a0a4024f0fdd2ec598613fa8ea55683115a88f5 “wallet: refactor GetNewDestination, use BResult”

    It would be nice if this function got the same treatment too. But that can also be done in a followup to avoid expanding the scope of this PR too far.


    furszy commented at 8:44 pm on June 27, 2022:
    yeah, better to leave it for a follow-up PR. Once we get this one merged, other PRs will be able to use the new class in different workflows independently.
  69. in src/wallet/spend.cpp:969 in 4e1353016d outdated
    974-    std::optional<CreatedTransactionResult> txr_ungrouped = CreateTransactionInternal(wallet, vecSend, change_pos, error, coin_control, sign);
    975-    TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), txr_ungrouped.has_value(),
    976-           txr_ungrouped.has_value() ? txr_ungrouped->fee : 0, txr_ungrouped.has_value() ? txr_ungrouped->change_pos : 0);
    977-    if (!txr_ungrouped) return std::nullopt;
    978+    auto res = CreateTransactionInternal(wallet, vecSend, change_pos, coin_control, sign);
    979+    TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), res,
    


    achow101 commented at 7:20 pm on June 27, 2022:

    In 4e1353016dabcf645b22fb7c196f62d8f465df39 “send: refactor CreateTransaction flow to return a BResult

    res is not being evaluated as a bool here, so the tracepoint does not work as expected.

    0    TRACE4(coin_selection, normal_create_tx_internal, wallet.GetName().c_str(), res.HasRes(),
    

    furszy commented at 9:30 pm on June 27, 2022:
    done
  70. furszy force-pushed on Jun 27, 2022
  71. furszy force-pushed on Jun 27, 2022
  72. furszy commented at 9:42 pm on June 27, 2022: member

    Updated per achow101’s feedback.

    Main modification was around the result getObj and GetError methods, which now return const references instead of pointers. This will force users to check whether the result succeeded or not before attempt to access the value.

  73. achow101 commented at 6:20 pm on June 30, 2022: member
    ACK 26288acf5c8e5cb55cf6b1829ead883289fce633
  74. theStack approved
  75. theStack commented at 11:55 am on July 6, 2022: contributor

    re-ACK 26288acf5c8e5cb55cf6b1829ead883289fce633 🌊

    Verified that since my previous ACK

    • BResult<T>::GetObj(...) interface changed to return reference T& instead of pointer T*
    • BResult<T>::GetError(...) interface changed to return reference bilingual_str& instead of bare bilingual_str
    • various nits from achow101 and me were addressed
    • bug in coin_selection tracepoint parameter evaluation was fixed (https://github.com/bitcoin/bitcoin/pull/25218#discussion_r907721005)
  76. w0xlt approved
  77. w0xlt commented at 3:09 am on July 8, 2022: contributor

    ACK https://github.com/bitcoin/bitcoin/pull/25218/commits/26288acf5c8e5cb55cf6b1829ead883289fce633

    It’s a good improvement on the codebase. Instead of returning boolean and the result object (or the error message) as an output parameter, the method can now return an single object that encapsulates them. This opens up several possibilities for refactoring.

    The CI has an error, but the error message does not match the current PR code. wallet.GetNewDestination(OutputType::BECH32, "", destination, error); was replaced in 26288ac

  78. furszy commented at 12:27 pm on July 8, 2022: member

    Thanks @w0xlt for the review!

    The CI has an error, but the error message does not match the current PR code. wallet.GetNewDestination(OutputType::BECH32, “”, destination, error); was replaced in https://github.com/bitcoin/bitcoin/commit/26288acf5c8e5cb55cf6b1829ead883289fce633

    Good eye. Seems that it’s a silent conflict with master due #24924 merge. Going to rebase it once more.

  79. furszy force-pushed on Jul 8, 2022
  80. furszy commented at 1:56 pm on July 8, 2022: member
    All green now, ready to go.
  81. Introduce generic 'Result' class
    Useful to encapsulate the function result object (in case of having it) or, in case of failure, the failure reason.
    
    This let us clean lot of boilerplate code, as now instead of returning a boolean and having to add a ref arg for the
    return object and another ref for the error string. We can simply return a 'BResult<Obj>'.
    
    Example of what we currently have:
    ```
    bool doSomething(arg1, arg2, arg3, arg4, &result, &error_string) {
        do something...
        if (error) {
            error_string = "something bad happened";
            return false;
        }
    
        result = goodResult;
        return true;
    }
    ```
    
    Example of what we will get with this commit:
    ```
    BResult<Obj> doSomething(arg1, arg2, arg3, arg4) {
        do something...
        if (error) return {"something happened"};
    
        // good
        return {goodResult};
    }
    ```
    
    This allows a similar boilerplate cleanup on the function callers side as well. They don't have to add the extra
    pre-function-call error string and result object declarations to pass the references to the function.
    7a45c33d1f
  82. wallet: refactor, include 'FeeCalculation' inside 'CreatedTransactionResult' 198fcca162
  83. send: refactor CreateTransaction flow to return a BResult<CTransactionRef> 22351725bc
  84. wallet: refactor GetNewDestination, use BResult 111ea3ab71
  85. furszy force-pushed on Jul 8, 2022
  86. furszy commented at 2:24 pm on July 8, 2022: member
    Rebased once more to include the recently merged #25337 here as well.
  87. w0xlt approved
  88. theStack approved
  89. theStack commented at 4:16 pm on July 9, 2022: contributor
    re-ACK 111ea3ab711414236f8678566a7884d48619b2d8
  90. achow101 commented at 5:44 pm on July 11, 2022: member
    ACK 111ea3ab711414236f8678566a7884d48619b2d8
  91. in src/util/result.h:9 in 7a45c33d1f outdated
    0@@ -0,0 +1,43 @@
    1+// Copyright (c) 2022 The Bitcoin Core developers
    2+// Distributed under the MIT software license, see the accompanying
    3+// file COPYING or https://www.opensource.org/licenses/mit-license.php.
    4+
    5+#ifndef BITCOIN_UTIL_RESULT_H
    6+#define BITCOIN_UTIL_RESULT_H
    7+
    8+#include <util/translation.h>
    9+#include <variant>
    


    maflcko commented at 10:51 am on July 12, 2022:
    Missing newline?

    maflcko commented at 1:40 pm on July 12, 2022:
  92. in src/util/result.h:22 in 7a45c33d1f outdated
    17+private:
    18+    std::variant<bilingual_str, T> m_variant;
    19+
    20+public:
    21+    BResult() : m_variant(Untranslated("")) {}
    22+    BResult(const T& _obj) : m_variant(_obj) {}
    


    maflcko commented at 10:53 am on July 12, 2022:
    I wonder if this is better written as BResult(T obj) : m_variant{std::move(obj)} {}. Otherwise it seems impossible to use on non-copyable types

    maflcko commented at 1:40 pm on July 12, 2022:
  93. in src/util/result.h:29 in 7a45c33d1f outdated
    24+
    25+    /* Whether the function succeeded or not */
    26+    bool HasRes() const { return std::holds_alternative<T>(m_variant); }
    27+
    28+    /* In case of success, the result object */
    29+    const T& GetObj() const {
    


    maflcko commented at 11:02 am on July 12, 2022:

    might be good to add a mutable getter, so that the result can be de-capsulated by the caller

    Edit: Fixed in https://github.com/bitcoin/bitcoin/pull/25594


    maflcko commented at 11:05 am on July 12, 2022:
    Also LIFETIMEBOUND?
  94. in src/util/result.h:35 in 7a45c33d1f outdated
    30+        assert(HasRes());
    31+        return std::get<T>(m_variant);
    32+    }
    33+
    34+    /* In case of failure, the error cause */
    35+    const bilingual_str& GetError() const {
    


    maflcko commented at 11:03 am on July 12, 2022:
    Shouldn’t this be annotated with LIFETIMEBOUND as it is returning a pointer to internal memory?

    maflcko commented at 12:47 pm on July 12, 2022:

    Diff to reproduce use-after-free with asan:

     0diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
     1index fda56ccff7..2abf2db298 100644
     2--- a/src/test/util_tests.cpp
     3+++ b/src/test/util_tests.cpp
     4@@ -4,6 +4,7 @@
     5 
     6 #include <util/system.h>
     7 
     8+#include <util/result.h>
     9 #include <clientversion.h>
    10 #include <fs.h>
    11 #include <hash.h> // For Hash()
    12@@ -2659,6 +2660,13 @@ BOOST_AUTO_TEST_CASE(message_hash)
    13     BOOST_CHECK_NE(message_hash1, signature_hash);
    14 }
    15 
    16+BOOST_AUTO_TEST_CASE(bresult)
    17+{
    18+  const auto& long_str{"Looooooooooooooooooooooooooooooooooooooooooooooooong string"};
    19+  const auto& err{BResult<int>{Untranslated(long_str)}.GetError()};
    20+  BOOST_CHECK_EQUAL(err.original, long_str);
    21+}
    22+
    23 BOOST_AUTO_TEST_CASE(remove_prefix)
    24 {
    25     BOOST_CHECK_EQUAL(RemovePrefix("./util/system.h", "./"), "util/system.h");
    

    With the warning added:

     0diff --git a/src/util/result.h b/src/util/result.h
     1index dcf5edaa5b..18364b3c6e 100644
     2--- a/src/util/result.h
     3+++ b/src/util/result.h
     4@@ -6,6 +6,8 @@
     5 #define BITCOIN_UTIL_RESULT_H
     6 
     7 #include <util/translation.h>
     8+#include <attributes.h>
     9+
    10 #include <variant>
    11 
    12 /*
    13@@ -32,7 +34,7 @@ public:
    14     }
    15 
    16     /* In case of failure, the error cause */
    17-    const bilingual_str& GetError() const {
    18+    const bilingual_str& GetError() const LIFETIMEBOUND {
    19         assert(!HasRes());
    20         return std::get<bilingual_str>(m_variant);
    21     }
    

    Results in:

    0test/util_tests.cpp:2666:19: warning: temporary bound to local reference 'err' will be destroyed at the end of the full-expression [-Wdangling]
    1  const auto& err{BResult<int>{Untranslated(long_str)}.GetError()};
    2                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    31 warning generated.
    
  95. in src/wallet/spend.h:9 in 198fcca162 outdated
    5@@ -6,6 +6,7 @@
    6 #define BITCOIN_WALLET_SPEND_H
    7 
    8 #include <consensus/amount.h>
    9+#include <policy/fees.h> // for FeeCalculation
    


    maflcko commented at 11:07 am on July 12, 2022:
    we don’t add those comments, as they are impossible to maintain

    furszy commented at 12:26 pm on July 15, 2022:
    k, got it. Thanks.
  96. in src/wallet/spend.cpp:965 in 198fcca162 outdated
    961@@ -964,7 +962,7 @@ static std::optional<CreatedTransactionResult> CreateTransactionInternal(
    962               feeCalc.est.fail.start, feeCalc.est.fail.end,
    963               (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0,
    964               feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool);
    965-    return CreatedTransactionResult(tx, nFeeRet, nChangePosInOut);
    966+    return CreatedTransactionResult(tx, nFeeRet, nChangePosInOut, feeCalc);
    


    maflcko commented at 11:10 am on July 12, 2022:
    Could use {} to clarify that this is not a function and to enable narrowing warnings for the int types.

    Riahiamirreza commented at 6:20 pm on July 14, 2022:
    @MarcoFalke Use {} where? You mean CreatedTransactionResult{tx, nFeeRet, nChangePosInOut, feeCalc} instead of current version?
  97. in src/qt/walletmodel.cpp:209 in 22351725bc outdated
    203@@ -204,10 +204,10 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
    204     {
    205         CAmount nFeeRequired = 0;
    206         int nChangePosRet = -1;
    207-        bilingual_str error;
    208 
    209         auto& newTx = transaction.getWtx();
    210-        newTx = m_wallet->createTransaction(vecSend, coinControl, !wallet().privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired, error);
    211+        const auto& res = m_wallet->createTransaction(vecSend, coinControl, !wallet().privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired);
    


    maflcko commented at 11:16 am on July 12, 2022:
    while touching this line: clang-tidy named args are of the format /*sign=*/
  98. in src/wallet/spend.cpp:983 in 22351725bc outdated
    991         tmp_cc.m_avoid_partial_spends = true;
    992-        bilingual_str error2; // fired and forgotten; if an error occurs, we discard the results
    993-        std::optional<CreatedTransactionResult> txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, error2, tmp_cc, sign);
    994+        auto res_tx_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, tmp_cc, sign);
    995+        // Helper optional class for now
    996+        std::optional<CreatedTransactionResult> txr_grouped{res_tx_grouped.HasRes() ? std::make_optional(res_tx_grouped.GetObj()) : std::nullopt};
    


    maflcko commented at 11:34 am on July 12, 2022:
    nit could use std::move here, if the mutable BResult interface is implemented
  99. in src/bench/wallet_loading.cpp:51 in 111ea3ab71
    46@@ -47,12 +47,11 @@ static void BenchUnloadWallet(std::shared_ptr<CWallet>&& wallet)
    47 
    48 static void AddTx(CWallet& wallet)
    49 {
    50-    bilingual_str error;
    51-    CTxDestination dest;
    52-    wallet.GetNewDestination(OutputType::BECH32, "", dest, error);
    53+    const auto& dest = wallet.GetNewDestination(OutputType::BECH32, "");
    54+    assert(dest.HasRes());
    


    maflcko commented at 11:39 am on July 12, 2022:
    nit: Could write shorter as const CTxDestination& dest{Assert(wallet.GetNewDestination(OutputType::BECH32, "")).GetObj()};

    maflcko commented at 11:40 am on July 12, 2022:
    Actually nvm. This would likely be a use-after-free.

    maflcko commented at 11:43 am on July 12, 2022:
    Maybe there could be a ReleaseObj helper to obtain the ownership?
  100. maflcko approved
  101. maflcko commented at 11:56 am on July 12, 2022: member

    review ACK 111ea3ab711414236f8678566a7884d48619b2d8 🎏

    Signature:

     0-----BEGIN PGP SIGNED MESSAGE-----
     1Hash: SHA512
     2
     3review ACK 111ea3ab711414236f8678566a7884d48619b2d8 🎏
     4-----BEGIN PGP SIGNATURE-----
     5
     6iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
     7pUjrYAv9ELJ7MVWMc6ec+xuvKeVMTZllPyJE0En9XpOhlJO5/icp4mlsxbVZeAN9
     89Hr5KE5gDE1YFWsBfIj/SAFVLURDx0vwZkCwcxqD8IsZL7PzZN8wUrnvJVgnL8FD
     9ul16AsIY7iy7JJabgdfBKOcSP8TMzma3xmKRZdOHtXwy33krjvVRT9fIj+04Lb3R
    10R3ODVlNZg4SQY3oZPBG06XhkBoSroU4mrYnE6BEYWG1iEYlJQ1n3Ivx9qNwweGWt
    11EDtHF9idu/pZJXS8aVzXZLjaLq/ALoLzvy9mtHVUv+OqOQ/Cvj/jfzhfm5CancE7
    12idf0Ku0bEO7NvxBQ90MUudnVZb3DSOcWpPZOGOv2lYdFq96eYF0TXPe8527NsM6O
    13CGikleM5JmI0Do/RAzvASiJmdnktFspCVXnl8Pto1I62LdMJDwnRKVU4/itvMyAn
    14swH8m6dRuX/Kn8bbzSdHKvCxx5fACxAyp2/sa/BD1IEkLq1VUFWrkbBu7PSw7QC4
    15K4XqkLYm
    16=SbFr
    17-----END PGP SIGNATURE-----
    
  102. maflcko merged this on Jul 12, 2022
  103. maflcko closed this on Jul 12, 2022

  104. sidhujag referenced this in commit 8b895389f3 on Jul 12, 2022
  105. in src/util/result.h:16 in 111ea3ab71
    11+/*
    12+ * 'BResult' is a generic class useful for wrapping a return object
    13+ * (in case of success) or propagating the error cause.
    14+*/
    15+template<class T>
    16+class BResult {
    


    Riahiamirreza commented at 5:02 pm on July 14, 2022:
    What does that B means at the beginning of class name?

    furszy commented at 12:26 pm on July 15, 2022:
    Comes from “Bilingual”. The story behind it is in the last paragraph of this comment: #25218 (comment)
  106. Riahiamirreza commented at 11:37 am on July 15, 2022: contributor
    I think this PR can have Good first review label. For someone like me which is very new to the project, understanding the concept and implementation of this PR was easy (relative to other PRs).
  107. maflcko commented at 12:59 pm on August 2, 2022: member
    There is a follow-up in case anyone wants to review it: #25721
  108. maflcko referenced this in commit a6fc293c0a on Aug 10, 2022
  109. furszy deleted the branch on May 27, 2023
  110. Fabcien referenced this in commit e0df40da76 on Dec 15, 2023
  111. bitcoin locked this on May 26, 2024

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: 2024-11-17 03:12 UTC

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