Skip to content

Conversation

maflcko
Copy link
Member

@maflcko maflcko commented Aug 7, 2023

Currently the RPC method implementations have many issues:

  • Default RPC argument values (and their optionality state) are duplicated in the documentation and the C++ code, with no checks to prevent them from going out of sync.
  • Getting an optional RPC argument is verbose, using a ternary operator, or worse, a multi-line if.

Fix all issues by adding default helper that can be called via self.Arg<int>(0). The helper needs a few lines of code in the src/rpc/util.h header file. Everything else will be implemented in the cpp file once and if an RPC method needs it.

There is also an self.MaybeArg<int>(0) helper that works on any arg to return the argument, the default, or a falsy value.

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 7, 2023

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

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK stickies-v, TheCharlatan, ajtowns
Approach ACK ryanofsky
Stale ACK achow101

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #28172 (refactor: use string_view for passing string literals to Parse{Hash,Hex} by jonatack)
  • #27101 (Support JSON-RPC 2.0 when requested by client by pinheadmz)
  • #25722 (refactor: Use util::Result class for wallet loading by ryanofsky)
  • #24963 (RPC/Wallet: Convert walletprocesspsbt to use options parameter by luke-jr)

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.

@stickies-v
Copy link
Contributor

Concept ACK

@maflcko maflcko force-pushed the 2308-rpc-default- branch 2 times, most recently from fa13ff4 to fa8cb8b Compare August 7, 2023 12:22
@maflcko maflcko changed the title rpc: Add Param() default helper rpc: Add Arg() default helper Aug 7, 2023
@maflcko maflcko force-pushed the 2308-rpc-default- branch 3 times, most recently from fac0196 to fa6b2da Compare August 7, 2023 14:02
Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

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

Approach ACK fa6b2da

This makes for a much more elegant interface throughout the RPC methods, with minimal overhead.

@maflcko maflcko force-pushed the 2308-rpc-default- branch from fa6b2da to fa5468d Compare August 8, 2023 08:47
Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

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

ACK fa5468d, really like this!

@TheCharlatan
Copy link
Contributor

Concept ACK

@maflcko maflcko force-pushed the 2308-rpc-default- branch 3 times, most recently from faa8309 to fa56ad7 Compare August 9, 2023 08:09
src/rpc/util.h Outdated
Comment on lines 404 to 407
* Helper to get a request argument or its default value.
* This function only works during m_fun(), i.e. it should only be used in RPC method implementations.
* Use Arg<Type> to get the argument or its default value.
* Use Arg<Type*> to get the argument or a falsy value.
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "rpc: Add Arg() default helper" (fa56ad7)

Haven't really looked at implementation yet, but can documentation be updated to say what happens with type mismatches? E.g. if arg is an int and you request a bool? Is the value just ignored and is a default returned instead? Is there an exception?

Also might be nice to add a short examples here showing what equivalent low level code is so developers can know how to use this helper and interpret it. Like Arg<int>(3) is equivalent to request.params[3].isNumber() ? request.params[3].getInt<int>() : default_value (or whatever the equivalent is)

Overall this seems like a nice improvement that should cut down on boilerplate. I am wondering if it might be possible later to extend this:

Copy link
Contributor

Choose a reason for hiding this comment

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

to enforces only expected types are passed at compile time

Do we want to revisit #22978 first before doing compile time type checking for RPC?

I think compile time RPC type checks would require templatising RPCArg by what is currently m_type (and m_inner perhaps), changing RPCHelpMan to be templatised based on a std::tuple<...> of RPCArg<X>s, and likewise for RPCMethodImpl which then gives the actual RPC implementation compile time access to its expected argument types.

I don't think you could reasonably access params by name and get compile time type checking; for the named-only params via options, you could turn options into an ordered tuple and access named-only params by their position from when you defined the options object.

For multitype parameters, I think it'd be best to just have a Type::ANY placeholder.

Copy link
Member Author

Choose a reason for hiding this comment

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

Haven't really looked at implementation yet, but can documentation be updated to say what happens with type mismatches?

This sound like a bug in the source code, so I don't think it makes sense to document unreachable or unexpected code paths. Generally, in the RPC server non-fatal bugs are expected to throw an internal bug message.

I haven't looked at the code, but types of arguments passed in by the user should be checked before m_fun is run. m_fun, and thus, Arg() isn't run when the types passed in by the user mismatch the ones in the documentation. If Arg() specifies the wrong type, the getter will throw. Currently all getters are univalue getters, so they'll throw the univalue internal exception. (This is the current behavior and preserved)

I think this is fine as-is, but I am happy to change it to something else, for example:

  • Re-implement the type check and throw a new, special exception. (This will be a change in "behavior", or rather "bug behavior")
  • Implement a compile-time json and use the compiler to avoid the case altogether, or alternatively, use clang-tidy to do it?

Also might be nice to add a short examples here showing what equivalent low level code is so developers can know how to use this helper and interpret it.

The diff in this commit should explain it. I've also added docs about the internals (isNull check and fallback to RPCArg::m_fallback)

to support arrays/objects

Yeah, it should be possible to return an object whose operator[] is defined. Would it be fine to do this in a follow-up?

to enforces only expected types are passed at compile time

Maybe as follow-up, unless the patch is trivial?

to enforce default not requested when there is no default at compile time

This is already done, no?

to somehow be compatible with luke's

Not sure about adding dead code, but once and if it is needed, it should be trivial to implement by mapping the types to a Arg<std::variant<int, bool>>(1) in C++ code. However, I am not sure if this is worth it, when it is only used once. Might as well use the "legacy" method for now?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think compile time RPC type checks would require templatising RPCArg by what is currently m_type (and m_inner perhaps), changing RPCHelpMan to be templatised based on a std::tuple<...> of RPCArg<X>s, and likewise for RPCMethodImpl which then gives the actual RPC implementation compile time access to its expected argument types.

I haven't looked at this, but some RPCHelpMan have run-time inputs, so I am not sure if they can be trivially converted to compile-time inputs. Maybe this can be done in a follow-up? The diff should be a trivial change from Arg<int>(0) to Arg<int, 0>(), right? Or with future C++ versions no change in m_fun implementations is needed at all?

Copy link
Contributor

Choose a reason for hiding this comment

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

This sound like a bug in the source code, so I don't think it makes sense to document unreachable or unexpected code paths.

If it's a bug to to call the the Arg function with an unexpected type, it's important for the documentation to at say what types it is allowed to be called with, because I don't think there's another way of knowing this without a having a pretty deep understanding of the RPCHelpMan framework and digging into template and macro code.

If you just want the documentation to say "It is a bug to pass X type" instead of "Passing X type will throw an exception" that is fine of course.

Copy link
Member Author

Choose a reason for hiding this comment

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

it's important for the documentation to at say what types it is allowed to be called with

I guess long term it could make sense to make RPCArg::Type an enum of C++ types and then have the documentation say that the type passed to Arg<Type>(i) must match exactly the corresponding RPCArg::Type.

For now, my recommendation would be to call Arg<std::string>(i) for STR* args, Arg<bool>(i) for BOOL args, and Arg<double/(u)int${n}_t>(i) for NUM args.

Copy link
Member Author

@maflcko maflcko Aug 14, 2023

Choose a reason for hiding this comment

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

Would it be fine to add "The Type passed to this helper must match the corresponding RPCArg::Type" to the doxygen of Arg?

Edit: Done.

src/rpc/util.cpp Outdated
return &std::get<RPCArg::Default>(fallback);
}

#define TMPL_INST(no_default, ret_type, get_arg, return_code) \
Copy link
Contributor

Choose a reason for hiding this comment

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

We could remove no_default from the function signature here, and deduce it from the ret_type being a std::optional or pointer type? I think both approaches have merit, so no very strong preference either way atm, but since we're already doing those deductions in Arg I think it would make sense here, too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, mind providing a patch that compiles, which I can take?

Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

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

Thanks for extending this to cover all kinds of argument types, I think this will make it much more straightforward for users to add in their own template instantiations where needed.

I still don't really have a clear preference for std::optional vs pointer, and I think it's not that important, so ACK fa56ad7

@maflcko
Copy link
Member Author

maflcko commented Aug 18, 2023

Addressed all (style) feedback so far, I think 😅

@ajtowns
Copy link
Contributor

ajtowns commented Aug 19, 2023

utACK faac216

Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

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

ACK faac216

nit: pull description & title have gotten a bit outdated as the PR changed quite a bit since its initial version. I think a brief mention of MaybeArg and the optionality of arguments could be helpful to include here?

As a nice bonus, it seems adding the RPCHelpMan::m_req pointer also makes it quite trivial to get an argument by name and builds nicely on this pull. I quickly pulled something together in stickies-v@c19d186 just as a PoC that passes functional tests, and (the delta to this PR) seems significantly less work than #27788 (but I've not properly tested/dug into this, just something I thought of).

@maflcko maflcko changed the title rpc: Add Arg() default helper rpc: Add MaybeArg() and Arg() default helper Aug 22, 2023
@maflcko
Copy link
Member Author

maflcko commented Aug 22, 2023

Thanks, edited subject line and description.

Copy link
Contributor

@TheCharlatan TheCharlatan left a comment

Choose a reason for hiding this comment

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

ACK faac216

@achow101
Copy link
Member

ACK faac216

src/rpc/util.cpp Outdated
}; \
return return_code \
} \
void force_semicolon()
Copy link
Member

Choose a reason for hiding this comment

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

../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:675:1: note: in expansion of macro ‘TMPL_INST’
  675 | TMPL_INST(nullptr, std::optional<bool>, maybe_arg ? std::optional{maybe_arg->get_bool()} : std::nullopt;);
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:674:1: note: in expansion of macro ‘TMPL_INST’
  674 | TMPL_INST(nullptr, std::optional<double>, maybe_arg ? std::optional{maybe_arg->get_real()} : std::nullopt;);
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:676:1: note: in expansion of macro ‘TMPL_INST’
  676 | TMPL_INST(nullptr, const std::string*, maybe_arg ? &maybe_arg->get_str() : nullptr;);
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:675:1: note: in expansion of macro ‘TMPL_INST’
  675 | TMPL_INST(nullptr, std::optional<bool>, maybe_arg ? std::optional{maybe_arg->get_bool()} : std::nullopt;);
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:679:1: note: in expansion of macro ‘TMPL_INST’
  679 | TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:676:1: note: in expansion of macro ‘TMPL_INST’
  676 | TMPL_INST(nullptr, const std::string*, maybe_arg ? &maybe_arg->get_str() : nullptr;);
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:680:1: note: in expansion of macro ‘TMPL_INST’
  680 | TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:679:1: note: in expansion of macro ‘TMPL_INST’
  679 | TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:681:1: note: in expansion of macro ‘TMPL_INST’
  681 | TMPL_INST(CheckRequiredOrDefault, const std::string&, CHECK_NONFATAL(maybe_arg)->get_str(););
      | ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
  671 |     void force_semicolon()
      |          ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:680:1: note: in expansion of macro ‘TMPL_INST’
  680 | TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
      | ^~~~~~~~~
cc1plus: all warnings being treated as errors

Copy link
Member

Choose a reason for hiding this comment

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

Seems to work fine without this line.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could drop it and just not have the semi-colons after the macro invocations too.

Could also change it to void force_semicolon(ret_type dummy) to avoid the warning.

clang accepts but ignores -Wredundant-decls, clang-tidy defaults to ignoring the warning in macros (yay!), but seems like the current code will be annoying for compiling with gcc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Any reason why CI didn't fail? I guess this is #25972

@maflcko maflcko force-pushed the 2308-rpc-default- branch from faac216 to c00000d Compare August 24, 2023 08:44
@maflcko
Copy link
Member Author

maflcko commented Aug 24, 2023

Force pushed to fix the two nits, should be trivial to re-ACK

Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

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

re-ACK c00000d

Verified that this:

Copy link
Contributor

@TheCharlatan TheCharlatan left a comment

Choose a reason for hiding this comment

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

re-ACK c00000d

@ajtowns
Copy link
Contributor

ajtowns commented Aug 24, 2023

reACK c00000d

@DrahtBot DrahtBot removed the request for review from ajtowns August 24, 2023 10:33
@fanquake fanquake merged commit 083316c into bitcoin:master Aug 24, 2023
@maflcko maflcko deleted the 2308-rpc-default- branch August 24, 2023 12:22
Frank-GER pushed a commit to syscoin/syscoin that referenced this pull request Sep 8, 2023
achow101 added a commit to bitcoin-core/gui that referenced this pull request Nov 2, 2023
…ring literals to Parse{Hash,Hex}

bb91131 doc: remove out-of-date external link in src/util/strencodings.h (Jon Atack)
7d494a4 refactor: use string_view to pass string literals to Parse{Hash,Hex} (Jon Atack)

Pull request description:

  as `string_view` is optimized to be trivially copiable, whereas the current code creates a `std::string` copy at each call.

  These utility methods are called by quite a few RPCs and tests, as well as by each other.

  ```
  $ git grep "ParseHashV\|ParseHashO\|ParseHexV\|ParseHexO" | wc -l
  61
  ```

  Also remove an out-of-date external link.

ACKs for top commit:
  jonatack:
    Rebased per `git range-diff c9273f6 b94581a bb91131` for an include header from the merge of bitcoin/bitcoin#28230. Should be trivial to re-ACK.
  maflcko:
    lgtm ACK bb91131
  ns-xvrn:
    ACK bitcoin/bitcoin@bb91131
  achow101:
    ACK bb91131
  brunoerg:
    crACK bb91131

Tree-SHA512: 9734fe022c9e43fd93c23a917770d332dbbd3132c80a234059714c32faa6469391e59349954749fc86c4ef0b18d5fd99bf8f4b7b82d9f799943799c1253272ae
@bitcoin bitcoin locked and limited conversation to collaborators Aug 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants