rpc: support a formal description of our JSON-RPC interface #34683

pull willcl-ark wants to merge 8 commits into bitcoin:master from willcl-ark:json-rpc-schema changing 14 files +516 −15
  1. willcl-ark commented at 3:22 PM on February 26, 2026: member

    Ref #29912.

    This PR adds a machine-readable OpenRPC 1.3.2 specification of out JSON-RPC interface, auto-generated from existing RPCHelpMan metadata.

    There is currently no formal, machine-readable specification of the RPC API. As discussed in #29912, this has knock-on consequences:

    • Client libraries re-implement the API manually, leading to bugs like unit mistakes (sats vs BTC, vB vs kvB) and missing/incorrect argument types. No existing client library fully and correctly implements the API in a type-safe manner.
    • When the API changes, every downstream client must manually discover and adapt, creating downstream maintenance burden. There is no artifact they can diff between releases.
    • Implementing a new client in a new language requires reading C++ source or help text and transcribing it, which is error-prone and tedious, and represents an on-going porting cost.
    • Existing documentation is either stale or not machine-readable. The developer.bitcoin.org docs are wrong/outdated in places, and the bitcoincore.org/en/doc/ pages are rendered from help output but not in a standard schema format.
    • (new/extra) AI/LLM tooling increasingly builds on structured API specifications. A standard spec format enables AI-assisted client generation and integration without the ambiguity of parsing human-readable help text.

    This draft builds on prior art by @casey and the observations by @laanwj, @stickies-v, @kilianmh, @hodlinator, and @cdecker in #29912. Casey's work demonstrated that RPCHelpMan already contains all the structured information needed, which makes this feasible without duplicating any API definitions.

    This differs from Casey's branches in that it uses the OpenRPC standard rather than an ad-hoc format or raw JSON Schema.

    Why OpenRPC

    I seletced OpenRPC for a number of reasons:

    • It's purpose-built for JSON-RPC APIs (suggested by @stickies-v, @nflatrea, and @kilianmh).
    • It wraps JSON schema for params/results, so consumers get both the method-level structure and the type-level schemas.
    • Unlike OpenAPI, it is not path-centric, which better fits our single-endpoint JSON-RPC model (concern raised by @hodlinator).
      • Although it therefore does not cover our REST interface.
    • It's kind of a standard format with (some) existing tooling for type generation (TypeScript, Rust, Python, Go) and client scaffolding, though maturity varies by language. More importantly though, IMO, a ~standardised format is inherently more useful than any ad-hoc one: any JSON Schema validator works, any LLM can consume it directly, and anyone can write a bespoke generator against a known schema rather than parsing help text.

    Approach

    RPCHelpMan metadata → getopenrpcinfo RPC → OpenRPC JSON

    Tradeoffs

    vs an ad-hoc format OpenRPC gives us interoperability with the (admittedly surprisingly limited) tooling, documentation generators, code generators, and validators, at the cost of needing x-bitcoin-* extensions for Bitcoin-specific concepts. As Casey noted after trying both approaches, JSON Schema "is probably not a great fit". OpenRPC's method-level framing on top of JSON Schema addresses the ergonomic issues while keeping the schema benefits. After testing both, I think I agree.

    Types: JSON Schema cannot natively express units like "BTC/kvB" or semantic distinctions like "this hex string is a txid." These are preserved in description text and x-bitcoin-type-str extensions, but are not machine-enforceable from the schema alone. This was recognized as a fundamental limitation by several commenters in the issue. Future work could enrich the x-bitcoin-* extensions with a more structured unit vocabulary.

    Some RPCs return different types depending on argument values (e.g. verbosity levels). These are represented as oneOf in the result schema with free-text condition descriptions. This is accurate but not fully machine-parseable — a code generator cannot automatically determine which result variant corresponds to which argument value without parsing the description. I still we have enough information to satisfy humans an agents alike though.

    Regenerating the spec

    The current functional test simply tests that the RPC runs, and produces valid JSON. We might want to consider extending this..

    The RPC output documents which RPCs are available for any given built binary.

    Discussion questions

    • Is this valuable/wanted?
    • Do we like openrpc format? (less relevant if we don't want this in this repo, as another repo could generate one or many definitions).
    • Should we cover "hidden" RPCs? They are currently hidden, but don't have to be...

    My personal thoughts are that this is very nice to have.

  2. DrahtBot commented at 3:23 PM on February 26, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34683.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK sedited, janb84, nervana21, stickies-v, hodlinator, w0xlt, rustaceanrob
    Stale ACK dergoegge, satsfy

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35587 (Remove boost as a unit test runner by rustaceanrob)
    • #33186 (wallet, test: Ancient Wallet Migration from v0.14.3 (no-HD and Single Chain) by w0xlt)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. sedited commented at 3:30 PM on February 26, 2026: contributor

    Concept ACK

  4. janb84 commented at 3:56 PM on February 26, 2026: contributor

    Concept ACK. This was already useful for bitcoin-tui

  5. in test/functional/rpc_openrpc.py:36 in 2846fab06d
      31 | +
      32 | +        openrpc_path = srcdir / "doc" / "openrpc.json"
      33 | +        committed = json.loads(openrpc_path.read_text(encoding="utf-8"))
      34 | +
      35 | +        if generated != committed:
      36 | +            self.log.info("Generated doc/openrpc.json:\n%s",
    


    maflcko commented at 4:48 PM on February 26, 2026:

    I guess it could be distracting to have to see every rpc change twice. Maybe the test could just check that generating the json does not crash?

    In theory, there could be a diff output like #26706 (comment) somewhere for each pull request, but that seems better to be optional and hidden.

    the doc/openrpc.json could have the same This is a placeholder file. content like the manpages.

    (Of course it is fine to keep both commits in this pull for now, so that the json is visible here, and only drop them before merge)


    willcl-ark commented at 11:47 AM on February 27, 2026:

    Thank you for this comment.

    Having the json checked in as a skeleton (like manpages) is a nice approach. It's kind of the opposite to stickies thoughts, so I guess I'll see which folks seem to generally prefer (and will leave the file in this PR for now for reference).

    I guess it could be distracting to have to see every rpc change twice. Maybe the test could just check that generating the json does not crash?

    This is also a nice idea/possibility. If we are not worrying about having a live artifact in this repo, then I agree this is almost certainly how it should work.


    maflcko commented at 12:17 PM on February 27, 2026:

    I am also thinking about devs on platforms that do not support a full build (IIRC external signer was not present on Windows for some time?). I guess they could manually undo the hunk that removes the $feature-related RPC from the json, or copy-paste the full json output from the CI log somehow.

    In any case, either way seems fine and should be trivial to adjust post-merge. This is just a nit.

  6. nervana21 commented at 11:56 PM on February 26, 2026: contributor

    Concept ACK

  7. stickies-v commented at 4:18 AM on February 27, 2026: contributor

    Big concept ACK, very nice work. If we want to cram less functionality in Bitcoin Core, we need to make it easier for people to build on top of it. A change like this will make it much easier for consumers to use the RPC. In addition, I think it will also help with our RPC stability guarantees and testing, and to make it easier for consumers to quickly find and incorporate any changes that have been made to the interface.

    it could also cause more work for devs who change an rpc, as the workflow may involve: ...

    This seems like a feature, not a bug? As long as devs can compile the binaries they need to test/dev, I think this doesn't impose any unnecessary overhead?

    If so, should it live in this repo?

    If OpenRPC is the best choice for our repo (from quick glance, it seems to work well), and we have enough support to commit to keeping our RPC OpenRPC compatible, I think it absolutely should live in this repo, so that we can quickly and strictly enforce continued compatibility with the spec.

    I'm less opinionated on whether the output json should live in this repo. I think it's fine to do so - the changes should be a LOT less frequent than e.g. doxygen changes, and it's nice having it easily accessible. However, I think it's also acceptable for users to generate this on-the-fly, or use any other hosted mirror of this documentation, which should be trivial to do.

  8. ajtowns commented at 12:15 PM on February 27, 2026: contributor

    → Python gen-openrpc.py → doc/openrpc.json (the artifact that gets committed)

    I don't really see why it wouldn't be better to have a getopenrpcinfo command that returns the json directly, giving you a direct report of the interface your bitcoind supports (whether the wallet or zmq is compiled in or not). Is there any reason you need python code to do the conversion rather than having it be built in to the bitcoind binary?

    If people want to grab the artifact from the web instead of their node, then providing a url on bitcoincore.org (like https://bitcoincore.org/en/doc/30.0.0/rpc/network/disconnectnode/ eg) seems plausible enough.

    Having the json output in the repo just seems annoying to me; more data, more conflicts, more diffs to check, more things you have to manually mess around with that could be automated.

    For the test suite, picking a rarely updated but somewhat interesting RPC, and checking that the "getopenrpcinfo" result for that command matches a fixed value, and that the commands listed match the output of "help" is probably fine, or at least seems like it would be to me.

    OpenRPC seems fine to me; doesn't seem like there's much benefit in worrying about the details, beyond getting something that's moderately machine readable.

  9. willcl-ark commented at 1:32 PM on February 27, 2026: member

    I don't really see why it wouldn't be better to have a getopenrpcinfo command that returns the json directly, giving you a direct report of the interface your bitcoind supports (whether the wallet or zmq is compiled in or not). Is there any reason you need python code to do the conversion rather than having it be built in to the bitcoind binary?

    Good question, sorry I didn't answer in OP initially, I had meant to provide a comparison of sorts...

    I tried this approach, and basically my reasoning was that you end up with a much larger c++ change (which I thought might be less desirable), although you get the nice benefits you note.

    I think it could be useful to codify the possible approaches I see, and their impact on this codebase. So roughly in ascending order of LoC needed in this repo:

    1. a single dump_all_command_descriptions RPC command (and basic functional test), ~ the first commit here. Doesn't emit json. up to downstream to write their own transformer.
    2. The above + a transformer contrib/ script + skeleton openrpc.json file
    3. RPC to return fully-structured JSON (This needed a fair amount of cpp code, in my attempt)
    4. Something like this PR being proposed (including a living openrpc.json file)

    I am going to rework my branch which outputs the json directly from RPC, and will post an update here with a comparison.

    If people want to grab the artifact from the web instead of their node, then providing a url on bitcoincore.org (like bitcoincore.org/en/doc/30.0.0/rpc/network/disconnectnode eg) seems plausible enough.

    agree, this would be fine.

    Having the json output in the repo just seems annoying to me; more data, more conflicts, more diffs to check, more things you have to manually mess around with that could be automated.

    Yeah I think I agree here too. I did test breaking an RPC to see how annoying it was to detect and fix. Quite annoying, was the answer.

    OpenRPC seems fine to me; doesn't seem like there's much benefit in worrying about the details, beyond getting something that's moderately machine readable.

    👍🏼 Thank you for articulating this, as I did spend a fair bit of time "worrying" about which format to select, but I see now you're correct; as long as it's structured (i.e valid json) and machine readable, it should be OK, and people can easily transform it into their own formats.

  10. satsfy commented at 10:08 PM on February 27, 2026: contributor

    Hey @willcl-ark, have you checked my comment on corepc? Is something like that you wanted to build?

  11. willcl-ark force-pushed on Mar 2, 2026
  12. DrahtBot added the label CI failed on Mar 2, 2026
  13. DrahtBot commented at 12:09 PM on March 2, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task Windows native, fuzz, VS: https://github.com/bitcoin/bitcoin/actions/runs/22572396443/job/65383261925</sub> <sub>LLM reason (✨ experimental): Fuzz test failed because RPC command "getopenrpcinfo" is not listed in allowed RPC commands for fuzzing.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  14. willcl-ark force-pushed on Mar 2, 2026
  15. willcl-ark force-pushed on Mar 2, 2026
  16. willcl-ark force-pushed on Mar 2, 2026
  17. DrahtBot removed the label CI failed on Mar 2, 2026
  18. willcl-ark commented at 9:49 AM on March 3, 2026: member

    I don't really see why it wouldn't be better to have a getopenrpcinfo command that returns the json directly @ajtowns I have updated the draft here now to work like this. It was not as much extra c++ code in sever.cpp as I thought, which seems nice, and I agree it's a better approach. Comparing the python output to the c++ output only differed on formatting. New output file from a --preset dev-mode build here.

    Hey @willcl-ark, have you checked my https://github.com/rust-bitcoin/corepc/issues/4#issuecomment-3935551647? Is something like that you wanted to build? @natobritto I had not heard of this one, no. I knew there were one or two? unmaintained rust bitcoin core json rpc libs, but pretty sure neither was this one. I will check it out.

  19. janb84 commented at 1:01 PM on March 3, 2026: contributor

    I like the new C++ approach!

    I found one conundrum:

    As per openRPC output the command getopenrpcinfo should return an empty info object (explicit "additionalProperties": false):

    <details>

    
        {
          "name": "getopenrpcinfo",
          "description": "Returns an OpenRPC document for currently available RPC commands.",
          "params": [
          ],
          "result": {
            "name": "result",
            "schema": {
              "type": "object",
              "properties": {
                "openrpc": {
                  "type": "string",
                  "description": "OpenRPC specification version."
                },
                "info": {
                  "type": "object",
                  "properties": {
                  },
                  "additionalProperties": false,
                  "description": "Metadata about this JSON-RPC interface."
                },
                "methods": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                    },
                    "additionalProperties": false
                  },
                  "description": "Documented RPC methods."
                }
              },
              "additionalProperties": false,
              "required": [
                "openrpc",
                "info",
                "methods"
              ]
            }
          },
          "x-bitcoin-category": "control"
        },
    

    </details>

    The output is enriched with an info object filled (so it has additional properties) :

      "info": {
        "title": "Bitcoin Core JSON-RPC",
        "version": "v30.99.0-dev",
        "description": "Autogenerated from Bitcoin Core RPC metadata."
      },
    

    This invalidates the generated openRPC spec

  20. willcl-ark force-pushed on Mar 3, 2026
  21. willcl-ark commented at 5:02 PM on March 3, 2026: member

    This invalidates the generated openRPC spec

    Doh! You're right that the getopenrpcinfo schema was indeed self-invalidating!

    I've updated the RPCResult to fully describe the info fields (title, version, description) and the method object structure (name, description, params, result, x-bitcoin-category), including the param and result Content Descriptor shapes. Dynamic JSON Schema fields within those use unconstrained schemas since their structure varies per method. I don't think there's anything we can do about that. There are also a few bits related to optional args before required, but those are legacy RPC artefacts.

    I also added a sample helper script I used to validate that it's now actually valid, per the meta-schema (you can run this yourself with uv run contrib/devtools/check-openrpc.py <path_to_generated_schema>). Could be useful to have something like this to avoid regressions, although if it's not running in the test suite, then I'm not so sure...

  22. DrahtBot added the label CI failed on Mar 3, 2026
  23. DrahtBot commented at 6:20 PM on March 3, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task Windows native, fuzz, VS: https://github.com/bitcoin/bitcoin/actions/runs/22633855642/job/65591195202</sub> <sub>LLM reason (✨ experimental): Fuzz test failure: assertion failed in fuzz/rpc.cpp (trigger_internal_bug not found) during RPC fuzz processing.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  24. willcl-ark force-pushed on Mar 3, 2026
  25. DrahtBot removed the label CI failed on Mar 3, 2026
  26. hodlinator commented at 2:41 PM on March 4, 2026: contributor

    Concept ACK

    My gut says we should be maintaining a shared spec and generate stub C++ binding code from it, as is done for .capnp files. However, the current reverse approach of generating the spec from the C++ declarations has the following benefits:

    • Doesn't add more codegen complexity to the build.
    • Doesn't add complexity of keeping generated C++ code and version controlled code in sync.
    • Doesn't touch existing C++ RPC declarations.
    • If we were to decide we want to switch to non-C++ spec + codegen, the current approach gives us the raw material for that spec.
  27. in doc/openrpc.json:6 in d4a754688d
       0 | @@ -0,0 +1,9 @@
       1 | +{
       2 | +  "openrpc": "1.3.2",
       3 | +  "info": {
       4 | +    "title": "Bitcoin Core JSON-RPC",
       5 | +    "version": "v30.99.0-dev",
       6 | +    "description": "Placeholder OpenRPC skeleton. Regenerate this file for release candidates using `bitcoin-cli getopenrpcinfo`."
    


    ajtowns commented at 5:06 PM on March 5, 2026:

    Not clear to me if this should be in this repo, or done like the rpc help at https://github.com/bitcoin-core/bitcoincore.org/blob/master/contrib/doc-gen/generate.go


    willcl-ark commented at 8:44 PM on March 5, 2026:

    Yes, I think the final 4 commits here are up for conceptual-inclusion review; we could... drop them all. The other extreme is to vendor the schema into this repo and use that in the functional test. Or we could keep something like the basic sanity test I have here and drop the final 3.

    IMO if we want to distribute in guix tarballs keeping the skeleton file checked in isn't too bad. But I'm not strongly wedded to the idea either. If we didn't want to distribute a static file, we could drop both the skeleton file and guix commits.


    casey commented at 12:32 AM on March 6, 2026:

    I think it's very useful to be able to interrogate bitcoind for its schema, it makes the schema much more discoverable, and makes it much more likely that the code will be kept up to date. It's also probably the path to integrate it with the least code, since there's minimal additional code for a new API call, and the C++ objects which represent the RPC API are all available to the schema generator.

  28. w0xlt commented at 8:38 PM on March 6, 2026: contributor

    Concept ACK

  29. satsfy commented at 11:18 PM on March 9, 2026: contributor

    Concept ACK

    Glad to see this moving forward. I recently experimented with a similar OpenRPC generation approach, so I have some context here. I was able to use the generated spec to codegen types for Rust successfully, which gives me confidence that the overall approach is sound.

    This review covers the spec output only. I will review the code separately.


    getblockstats.stats currently has a redundant oneOf:

    "oneOf": [
    	{ "type": "string" },
    	{ "type": "string" }
    ]
    

    x-bitcoin-type-str has inconsistent values. It appears in 5 fields, including values like ["", "string or numeric"]. If we keep this extension, it would help to define it more tightly, otherwise dropping it for now may be cleaner.

    Dummy/ignored params should carry some annotation, like getbalance.dummy, sendmany.dummy, sendmany.minconf, prioritisetransaction.dummy, submitblock.dummy. These are long-lived positional placeholders that cannot realistically be removed without shifting indices and breaking callers. I’m not sure deprecated: true is the right fit, since these are compatibility placeholders rather than removal candidates. Perhaps this warrants a Bitcoin-specific extension instead.

    There are no minimum/maximum constraints on numeric fields. For example, walletpassphrase.timeout is capped at 100000000 per its description and getblock.verbosity accepts 0-3, but the schema does not encode those bounds.

    Similarly, many hex string fields use ^[0-9a-fA-F]*$, which allows empty strings (312 occurrences). Since identifiers like txids and block hashes are fixed-width hex, fields like these could carry "minLength": 64, "maxLength": 64. More generally, this area could benefit from tighter field-specific constraints.

    The spec currently inlines everything. It may be worth leveraging OpenRPC reuse via $ref and components/schemas, since structures like scriptPubKey, transaction objects, fee objects, and UTXO shapes appear repeatedly. The most important cases seem to be the ones currently falling back to additionalProperties: true (10 occurrences). I opened #34764 to address the most common cause (ELISION placeholders that should be explicit result fields), though recursive structures like getaddressinfo.embedded would likely require $ref support in the generator. That seems like follow-up work rather than something for this PR.

    type: "number" is used universally; there is no type: "integer". Block heights, confirmations, vsize, weight, sequence numbers, byte sizes, and timestamps are all integers in practice. Distinguishing integer from non-integer matters for code generators, which otherwise map these to floats/doubles. I ran into this while generating Rust types for corepc. Fixing it likely requires deeper RPCArg typing changes, so probably outside the scope here.

    estimate_mode is plain string with no enum (11 occurrences). The valid values are "unset", "economical", "conservative", but the schema itself has no constraint. If we added enum support to the help metadata (even just an optional std::vector<std::string> on string args), code generators downstream would get proper constrained types. Example:

    "schema": {
    	"type": "string",
    	"enum": ["unset", "economical", "conservative"]
    }
    

    This likely affects other params too (sighashtype, address_type, change_type). This could be a follow-up.

    On the spec file: I agree with @ajtowns that checking the generated JSON into the repo creates ongoing friction for contributors touching RPCs. Consumers who want diffs between releases can run bitcoin-cli getopenrpcinfo on two versions. CI could instead validate generation and enforce a few structural invariants, for example, that every method in help appears in the output, no oneOf contains duplicate entries, and every param has required.

    Happy to help later with follow-up patches for any of these.

  30. willcl-ark force-pushed on Mar 10, 2026
  31. willcl-ark force-pushed on Mar 10, 2026
  32. willcl-ark commented at 2:17 PM on March 10, 2026: member

    Glad to see this moving forward. I recently experimented with a similar OpenRPC generation approach, so I have some context here. I was able to use the generated spec to codegen types for Rust successfully, which gives me confidence that the overall approach is sound.

    This review covers the spec output only. I will review the code separately.

    Thanks for the review!

    I have picked your changes from #34764 (thanks) and stacked them (with a small fixup) in here, with additional fixes for the following from your review:

    • getblockstats.stats having a redundant oneOf

      Now deduplicated

    • x-bitcoin-type-str having inconsistent values

      Now removed

    • Dummy/ignored params needing annotation

      Fixed using x-bitcoin-placeholder metadata

    • Hex regex allowing empty strings

      Partially addressed by changing ^[0-9a-fA-F]*$ to ^[0-9a-fA-F]+$. I did not add field-specific fixed lengths like minLength/maxLength as this would be more invasive than I'd like here.

    • additionalProperties: true caused by ELISION

      Partially fixed via cherry-pick from your branch. However recursive cases like getaddressinfo.embedded still need $ref-style support, which I certainly plan to leave out of this PR/for a followup, if I can :)

    I have also dropped the skeleton document, and Guix packaging commits.

  33. willcl-ark commented at 2:20 PM on March 10, 2026: member

    If we are generally Concept ACK in here, it would probably make sense to split this up as:

    1. i) First 5 'setup' commits here ii) PR #34764
    2. The final two commits here as a standalone PR once prep work has been done
  34. willcl-ark force-pushed on Mar 10, 2026
  35. DrahtBot added the label CI failed on Mar 10, 2026
  36. DrahtBot removed the label CI failed on Mar 10, 2026
  37. willcl-ark force-pushed on Mar 16, 2026
  38. dergoegge commented at 11:02 AM on March 17, 2026: member

    Concept ACK!

  39. willcl-ark renamed this:
    RFC: Support a format description of our JSON-RPC interface
    RFC: Support a formal description of our JSON-RPC interface
    on Mar 17, 2026
  40. DrahtBot added the label Needs rebase on Mar 18, 2026
  41. willcl-ark force-pushed on Mar 19, 2026
  42. DrahtBot added the label CI failed on Mar 19, 2026
  43. DrahtBot removed the label Needs rebase on Mar 19, 2026
  44. fanquake referenced this in commit f6e6fad0d9 on Mar 30, 2026
  45. DrahtBot added the label Needs rebase on Mar 31, 2026
  46. willcl-ark force-pushed on Apr 20, 2026
  47. DrahtBot removed the label Needs rebase on Apr 20, 2026
  48. DrahtBot removed the label CI failed on Apr 20, 2026
  49. achow101 requested review from achow101 on May 8, 2026
  50. willcl-ark marked this as ready for review on May 11, 2026
  51. willcl-ark renamed this:
    RFC: Support a formal description of our JSON-RPC interface
    rpc: support a formal description of our JSON-RPC interface
    on May 11, 2026
  52. DrahtBot added the label RPC/REST/ZMQ on May 11, 2026
  53. willcl-ark commented at 8:51 AM on May 11, 2026: member

    There seems to be ~ conceptual agreement, here so I've opened this up for review.

    Note that this PR is currently stacked on top of #34764 so ideally review would go there first.

  54. rustaceanrob commented at 9:01 AM on May 13, 2026: member

    Concept ACK

    I created a demo of what it might look like to auto-generate Rust code based on the specification here. I suspect this would automate/reduce maintenance for consumers of the RPC quite a bit.

  55. sedited commented at 9:28 AM on June 3, 2026: contributor

    For a PR with 9 Concept ACKs, this has not received a lot of review yet. Pinging some people to prioritize this PR and the dependent PR #34764 up their review stacks: @janb84 @nervana21 @stickies-v @hodlinator @w0xlt @satsfy @dergoegge @rustaceanrob @maflcko

  56. dergoegge commented at 9:43 AM on June 3, 2026: member

    ACK bbd7e5525c5b98f9fe3d88c7d507d04276921fce

    I didn't review the code in depth but looks fine and low risk to me. This ranks low on my list of things to do, so I probably won't spend more time on this, but seems fine to merge.

    I've used the generated rpc description to auto-generate some tests, which did work but didn't really go anywhere in terms of finding bugs, but it did demonstrates that this PR works and may be useful.

  57. DrahtBot requested review from janb84 on Jun 3, 2026
  58. DrahtBot requested review from hodlinator on Jun 3, 2026
  59. DrahtBot requested review from stickies-v on Jun 3, 2026
  60. DrahtBot requested review from rustaceanrob on Jun 3, 2026
  61. DrahtBot requested review from sedited on Jun 3, 2026
  62. satsfy commented at 10:35 PM on June 4, 2026: contributor

    ACK bbd7e5525c5b98f9fe3d88c7d507d04276921fce

    I have been using the PR's OpenRPCs successfully to generate Rust types.

    PR #34764 has changes that should be brought for correctness.

    One great follow-up from here is typing properly amount returns, such as sats, BTC, vB, kvB, or string representation, because this is a pain point when working with the OpenRPC.

  63. in src/rpc/server.cpp:493 in bbd7e5525c outdated
     488 | +                                {
     489 | +                                    {RPCResult::Type::STR, "name", "Result name."},
     490 | +                                    {RPCResult::Type::ANY, "schema", "JSON Schema for the result."},
     491 | +                                }},
     492 | +                            {RPCResult::Type::STR, "x-bitcoin-category", "RPC category."},
     493 | +                        }}}},
    


    satsfy commented at 6:56 PM on June 8, 2026:

    A follow-up or potential extension would be to include the errors in RPCErrorCode into the schema (e.g. RPC_OUT_OF_MEMORY...). This is the shape of an RPC with errors codes:

    "RPC_INVALID_PARAMETER": { "code": -8, "message": "Invalid, missing or duplicate parameter" },
    "RPC_INVALID_ADDRESS_OR_KEY": { "code": -5, "message": "Invalid address or key" },
    

    <details><summary>Click to see a proposed diff</summary> <p>

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index 0000000000..c7c538832f 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -487,6 +483,75 @@
    +UniValue OpenRPCErrorComponents()
    +{
    +    struct ErrorEntry {
    +        RPCErrorCode code;
    +        const char* name;
    +        const char* message;
    +    };
    +    // ERR stringizes the enum name so it can't drift from the symbol it labels.
    +#define ERR(code, message) ErrorEntry{code, #code, message}
    +    static const ErrorEntry entries[]{
    +        ERR(RPC_MISC_ERROR, "std::exception thrown in command handling"),
    +        ERR(RPC_FORBIDDEN_BY_SAFE_MODE, "Server is in safe mode, and command is not allowed in safe mode"),
    +        ERR(RPC_TYPE_ERROR, "Unexpected type was passed as parameter"),
    +        ERR(RPC_WALLET_ERROR, "Unspecified problem with wallet (key not found etc.)"),
    +        ERR(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address or key"),
    +        ERR(RPC_WALLET_INSUFFICIENT_FUNDS, "Not enough funds in wallet or account"),
    +        ERR(RPC_OUT_OF_MEMORY, "Ran out of memory during operation"),
    +        ERR(RPC_INVALID_PARAMETER, "Invalid, missing or duplicate parameter"),
    +        ERR(RPC_CLIENT_NOT_CONNECTED, "Bitcoin is not connected"),
    +        ERR(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Still downloading initial blocks"),
    +        ERR(RPC_WALLET_INVALID_LABEL_NAME, "Invalid label name"),
    +        ERR(RPC_WALLET_KEYPOOL_RAN_OUT, "Keypool ran out, call keypoolrefill first"),
    +        ERR(RPC_WALLET_UNLOCK_NEEDED, "Enter the wallet passphrase with walletpassphrase first"),
    +        ERR(RPC_WALLET_PASSPHRASE_INCORRECT, "The wallet passphrase entered was incorrect"),
    +        ERR(RPC_WALLET_WRONG_ENC_STATE, "Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.)"),
    +        ERR(RPC_WALLET_ENCRYPTION_FAILED, "Failed to encrypt the wallet"),
    +        ERR(RPC_WALLET_ALREADY_UNLOCKED, "Wallet is already unlocked"),
    +        ERR(RPC_WALLET_NOT_FOUND, "Invalid wallet specified"),
    +        ERR(RPC_WALLET_NOT_SPECIFIED, "No wallet specified (error when there are multiple wallets loaded)"),
    +        ERR(RPC_DATABASE_ERROR, "Database error"),
    +        ERR(RPC_DESERIALIZATION_ERROR, "Error parsing or validating structure in raw format"),
    +        ERR(RPC_CLIENT_NODE_ALREADY_ADDED, "Node is already added"),
    +        ERR(RPC_CLIENT_NODE_NOT_ADDED, "Node has not been added before"),
    +        ERR(RPC_VERIFY_ERROR, "General error during transaction or block submission"),
    +        ERR(RPC_VERIFY_REJECTED, "Transaction or block was rejected by network rules"),
    +        ERR(RPC_VERIFY_ALREADY_IN_UTXO_SET, "Transaction already in utxo set"),
    +        ERR(RPC_IN_WARMUP, "Client still warming up"),
    +        ERR(RPC_CLIENT_NODE_NOT_CONNECTED, "Node to disconnect not found in connected nodes"),
    +        ERR(RPC_CLIENT_INVALID_IP_OR_SUBNET, "Invalid IP/Subnet"),
    +        ERR(RPC_CLIENT_P2P_DISABLED, "No valid connection manager instance found"),
    +        ERR(RPC_METHOD_DEPRECATED, "RPC method is deprecated"),
    +        ERR(RPC_CLIENT_MEMPOOL_DISABLED, "No mempool instance found"),
    +        ERR(RPC_CLIENT_NODE_CAPACITY_REACHED, "Max number of outbound or block-relay connections already open"),
    +        ERR(RPC_WALLET_ALREADY_LOADED, "This same wallet is already loaded"),
    +        ERR(RPC_WALLET_ALREADY_EXISTS, "There is already a wallet with the same name"),
    +    };
    +#undef ERR
    +
    +    UniValue errors{UniValue::VOBJ};
    +    for (const auto& e : entries) {
    +        UniValue error{UniValue::VOBJ};
    +        error.pushKV("code", static_cast<int64_t>(e.code));
    +        error.pushKV("message", e.message);
    +        error.pushKV("x-bitcoin-error-name", e.name);
    +        errors.pushKV(e.name, std::move(error));
    +    }
    +
    +    UniValue components{UniValue::VOBJ};
    +    components.pushKV("errors", std::move(errors));
    +    return components;
    +}
     } // namespace
     
     static RPCMethod getopenrpcinfo()
    @@ -529,6 +594,18 @@
                                     }},
                                 {RPCResult::Type::STR, "x-bitcoin-category", "RPC category."},
                             }}}},
    +                {RPCResult::Type::OBJ, "components", "Reusable OpenRPC components.",
    +                    {
    +                        {RPCResult::Type::OBJ_DYN, "errors", "Catalog of application-level RPC error codes, keyed by name.",
    +                            {
    +                                {RPCResult::Type::OBJ, "", "An error object.",
    +                                    {
    +                                        {RPCResult::Type::NUM, "code", "The numeric error code."},
    +                                        {RPCResult::Type::STR, "message", "A description of the error type."},
    +                                        {RPCResult::Type::STR, "x-bitcoin-error-name", "The symbolic error name."},
    +                                    }},
    +                            }},
    +                    }},
                 },
                 {.skip_type_check = true}},
             RPCExamples{
    @@ -922,6 +999,7 @@
         doc.pushKV("openrpc", "1.3.2");
         doc.pushKV("info", std::move(info));
         doc.pushKV("methods", std::move(methods));
    +    doc.pushKV("components", OpenRPCErrorComponents());
         return doc;
     }
     
    
    

    </p> </details>


    willcl-ark commented at 8:17 AM on June 20, 2026:

    Although I agree conceptually, I feel that this could be a good candidate for a followup, so leaving this out for now.

  64. in src/rpc/server.cpp:346 in bbd7e5525c
     341 | +    case RPCArg::Type::OBJ_NAMED_PARAMS: {
     342 | +        UniValue properties{UniValue::VOBJ};
     343 | +        UniValue required{UniValue::VARR};
     344 | +        for (const auto& inner : arg.m_inner) {
     345 | +            if (inner.m_opts.hidden) continue;
     346 | +            UniValue prop{OpenRPCArgSchema(inner)};
    


    satsfy commented at 6:57 PM on June 8, 2026:

    How about including default values? Currently, RPCs have RPCArg::Default (the actual value) and RPCArg::DefaultHint (a string hinting at the value to the user). We could add both, or at least, the first.

    OpenRPC schema supports the default argument, and we could have the hint as x-bitcoin-default-hint.

    Example in getblock's verbosity:

    { "name": "verbosity", "required": false, "schema": { "type": "number", "default": 1 } }
    

    <details><summary>Click to see proposed diff</summary> <p>

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index 7b3d574e9a..bf673f7489 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -344,6 +344,11 @@
             for (const auto& inner : arg.m_inner) {
                 if (inner.m_opts.hidden) continue;
                 UniValue prop{OpenRPCArgSchema(inner)};
    +            if (const auto* def = std::get_if<RPCArg::Default>(&inner.m_fallback)) {
    +                prop.pushKV("default", *def);
    +            } else if (const auto* hint = std::get_if<RPCArg::DefaultHint>(&inner.m_fallback)) {
    +                prop.pushKV("x-bitcoin-default-hint", *hint);
    +            }
                 if (!inner.m_description.empty()) prop.pushKV("description", inner.m_description);
                 if (inner.m_opts.placeholder) prop.pushKV("x-bitcoin-placeholder", true);
                 if (inner.m_opts.also_positional) prop.pushKV("x-bitcoin-also-positional", true);
    @@ -819,7 +824,14 @@
                 UniValue param{UniValue::VOBJ};
                 param.pushKV("name", arg.GetFirstName());
                 param.pushKV("required", !arg.IsOptional());
    -            param.pushKV("schema", OpenRPCArgSchema(arg));
    +            UniValue schema{OpenRPCArgSchema(arg)};
    +            if (const auto* def = std::get_if<RPCArg::Default>(&arg.m_fallback)) {
    +                schema.pushKV("default", *def);
    +            } else if (const auto* hint = std::get_if<RPCArg::DefaultHint>(&arg.m_fallback)) {
    +                schema.pushKV("x-bitcoin-default-hint", *hint);
    +            }
    +            param.pushKV("schema", std::move(schema));
    +
     
                 std::vector<std::string> names{SplitString(arg.m_names, '|')};
                 if (names.size() > 1) {
    
    

    </p> </details>


    willcl-ark commented at 8:18 AM on June 20, 2026:

    Added default values in 2236b7ad236 at top level OpenRPCArgSchema()

  65. in src/rpc/server.cpp:347 in bbd7e5525c outdated
     342 | +        UniValue properties{UniValue::VOBJ};
     343 | +        UniValue required{UniValue::VARR};
     344 | +        for (const auto& inner : arg.m_inner) {
     345 | +            if (inner.m_opts.hidden) continue;
     346 | +            UniValue prop{OpenRPCArgSchema(inner)};
     347 | +            if (!inner.m_description.empty()) prop.pushKV("description", inner.m_description);
    


    satsfy commented at 6:57 PM on June 8, 2026:

    How about implementing the field type_str in schema? For example, the field says "string or numeric" for a type such as hash_or_height of gettxoutsetinfo, which could correspond to a OpenRPC one_of. Unfortunately, type_str is a string that accepts any format, but there are only 3 instances of it happening the codebase:

    • importdescriptors timestamp.
    • gettxoutsetinfo hash_or_height.
    • getblockstats hash_or_height.

    Example:

    { "name": "hash_or_height", "required": true, "schema": { "oneOf": [ { "type": "number" }, { "type": "string" } ] } }
    

    This could also be a follow-up, but here is a hacked-up solution that covers all cases in the codebase today:

    <details><summary>Click to see proposed diff</summary> <p>

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index 2143f305bf..f91bf79704 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -294,6 +294,33 @@
         return items;
     }
     
    +void ApplyTypeStrOverride(UniValue& schema, const RPCArg& arg)
    +{
    +    if (arg.m_opts.type_str.size() != 2) return;
    +    const std::string& type_label{arg.m_opts.type_str[1]};
    +    if (type_label.empty()) return;
    +
    +    static const std::unordered_set<std::string> number_or_string{
    +        "string or numeric",
    +        "integer / string",
    +    };
    +    if (number_or_string.contains(type_label)) {
    +        UniValue one_of{UniValue::VARR};
    +        one_of.push_back(MakeObject({{"type", "number"}}));
    +        one_of.push_back(MakeObject({{"type", "string"}}));
    +        schema = UniValue{UniValue::VOBJ};
    +        schema.pushKV("oneOf", std::move(one_of));
    +    } else {
    +        schema.pushKV("x-bitcoin-type-override", type_label);
    +    }
    +}
    +
     // NOLINTNEXTLINE(misc-no-recursion)
     UniValue OpenRPCArgSchema(const RPCArg& arg)
     {
    @@ -344,6 +371,7 @@
                 UniValue prop{OpenRPCArgSchema(inner)};
    +            ApplyTypeStrOverride(prop, inner);
                 if (!inner.m_description.empty()) prop.pushKV("description", inner.m_description);
    @@ -820,6 +848,7 @@
                 param.pushKV("name", arg.GetFirstName());
                 param.pushKV("required", !arg.IsOptional());
                 UniValue schema{OpenRPCArgSchema(arg)};
    +            ApplyTypeStrOverride(schema, arg);
                 if (const auto* def = std::get_if<RPCArg::Default>(&arg.m_fallback)) {
                     schema.pushKV("default", *def);
                 } else if (const auto* hint = std::get_if<RPCArg::DefaultHint>(&arg.m_fallback)) {
    

    </p> </details>


    willcl-ark commented at 8:21 AM on June 20, 2026:

    Also taken in 2236b7ad236

  66. in src/rpc/server.cpp:380 in bbd7e5525c outdated
     375 | +UniValue OpenRPCResultSchema(const RPCResult& result)
     376 | +{
     377 | +    switch (result.m_type) {
     378 | +    case RPCResult::Type::STR:
     379 | +    case RPCResult::Type::STR_AMOUNT:
     380 | +        return MakeObject({{"type", "string"}});
    


    satsfy commented at 6:57 PM on June 8, 2026:

    Should we add an amount label to the result string amounts so the machine knows that a particular string is an amount? We already use x-bitcoin-unit for unix-time. Take this situation in the field estimated_feerate of analyzepsbt. Machine cannot tell it is a float:

    "estimated_feerate": {
      "type": "string",
      "description": "Estimated feerate of the final signed transaction in BTC/kvB..."
    },
    
        case RPCResult::Type::STR_AMOUNT:
            UniValue schema{UniValue::VOBJ};
            schema.pushKV("type", "string");
            schema.pushKV("x-bitcoin-unit", "amount");
            return schema;
    

    willcl-ark commented at 8:22 AM on June 20, 2026:

    Also taken in 2236b7ad236

  67. satsfy commented at 7:00 PM on June 8, 2026: contributor

    I've thought more about this OpenRPC schema. Here are some improvements that could be done in this PR or left as a follow-up.

  68. in src/rpc/server.cpp:806 in bbd7e5525c
     801 | +{
     802 | +    std::vector<std::string> method_names;
     803 | +    for (const auto& [name, cmds] : mapCommands) {
     804 | +        if (cmds.empty()) continue;
     805 | +        const CRPCCommand* cmd{cmds.front()};
     806 | +        if (cmd->category == "hidden" || !cmd->metadata_fn) continue;
    


    satsfy commented at 2:31 PM on June 11, 2026:

    Just encountered a problem with codegen because we omit hidden RPCs and arguments. I believe we should include them in getopenrpcinfo. Or at least provide an optional argument show_hidden in the RPC for completeness and to enable codegen testing on regtest.

            if ((!include_hidden && cmd->category == "hidden") || !cmd->metadata_fn) continue;
    

    willcl-ark commented at 8:23 AM on June 20, 2026:

    Added show_hidden. Kept hidden args omitted by default but they can be included when getopenrpcinfo(true) is called.

  69. in src/rpc/server.cpp:818 in bbd7e5525c
     813 | +        const CRPCCommand* cmd{mapCommands.at(method_name).front()};
     814 | +        RPCMethod helpman{cmd->metadata_fn()};
     815 | +
     816 | +        UniValue params{UniValue::VARR};
     817 | +        for (const auto& arg : helpman.GetArgs()) {
     818 | +            if (arg.m_opts.hidden) continue;
    


    satsfy commented at 2:35 PM on June 11, 2026:

    This fixes the hidden fields.

                if (!include_hidden && arg.m_opts.hidden) continue;
    
  70. achow101 referenced this in commit 5883ba77ea on Jun 19, 2026
  71. DrahtBot added the label Needs rebase on Jun 19, 2026
  72. rpc: add missing string_view include to server.h
    CRPCTable::help() takes std::string_view but server.h relies on
    transitive includes for it.
    
    Add the direct include (probably makes iwyu happier too?)
    d4d64ae739
  73. rpc: erase empty map entry in removeCommand
    After removing the last CRPCCommand pointer for a given name,
    erase the now-empty vector from mapCommands. Without this,
    listCommands() returns the name of a fully removed command
    because it iterates mapCommands keys unconditionally.
    
    For example, when unloading the wallet the RPCs are deregistered, and
    this prevents getopenrpcinfo from returning non-existant RPCs.
    06de34a033
  74. rpc: render Type::ANY in help text instead of aborting
    RPCResult::Type::ANY triggers NONFATAL_UNREACHABLE() in ToSections(),
    which crashes the help() RPC when a command uses Type::ANY in a
    nested result field.
    
    Previously this was never hit because Type::ANY was only used as a
    top-level alternate result type, filtered out before ToSections() is
    called.
    
    getopenrpcinfo() will use this result type, so render it like other
    types allowing it to be used in nested result definitions like schema.
    6a1a66c180
  75. rpc: expose RPC metadata for introspection 26c221a980
  76. rpc: add placeholder annotation for deprecated params f5116c587f
  77. rpc: add getopenrpcinfo command
    Expose the generated OpenRPC document through RPC so clients can inspect the interface supported by the running node. The command can optionally include hidden RPC commands and arguments for complete code generation and regtest coverage.
    2236b7ad23
  78. test: add functional test for getopenrpcinfo
    Cover the new RPC with a lightweight functional test that verifies the result is JSON serializable and exposes the expected top-level OpenRPC document fields.
    95cdef42a4
  79. rpc: factor getaddressinfo embedded field docs
    Keep the repeated embedded address documentation in one helper so the OpenRPC metadata and help text stay consistent without duplicating the same field list.
    dc9942984a
  80. willcl-ark force-pushed on Jun 20, 2026
  81. willcl-ark commented at 8:24 AM on June 20, 2026: member

    Thank for your review and comments @satsfy.

    I took many of your suggestions in the latest push, which also now includes the merged ELISION changes.

  82. DrahtBot removed the label Needs rebase on Jun 20, 2026
  83. satsfy commented at 9:50 PM on June 20, 2026: contributor

    tACK d4d64ae73938754bbca6f47b0e3b948d1c994c45 - Generated the OpenRPC and inspected it.

    I have been successfully using PR's OpenRPCs to generate Rust types. It is complete enough for most Bitcoin clients' needs.

  84. in src/rpc/server.cpp:414 in dc9942984a
     409 | +{
     410 | +    switch (result.m_type) {
     411 | +    case RPCResult::Type::STR:
     412 | +        return MakeObject({{"type", "string"}});
     413 | +    case RPCResult::Type::STR_AMOUNT:
     414 | +        return MakeObject({{"type", "string"}, {"x-bitcoin-unit", "amount"}});
    


    w0xlt commented at 5:39 AM on June 22, 2026:

    RPCResult::Type::STR_AMOUNT is represented as a JSON number, not a JSON string. The result type checker expects it to be UniValue::VNUM:

    static std::optional<UniValue::VType> ExpectedType(RPCResult::Type type)
    {
        ...
        case Type::NUM:
        case Type::STR_AMOUNT:
        case Type::NUM_TIME: {
            return UniValue::VNUM;
        }
    }
    

    So the OpenRPC schema should use "type": "number" while keeping the amount annotation:

    case RPCResult::Type::STR_AMOUNT:
        return MakeObject({{"type", "number"}, {"x-bitcoin-unit", "amount"}});
    

    Suggestion:

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index ca383a065a..8f62948fbd 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -411,7 +411,7 @@ UniValue OpenRPCResultSchema(const RPCResult& result)
         case RPCResult::Type::STR:
             return MakeObject({{"type", "string"}});
         case RPCResult::Type::STR_AMOUNT:
    -        return MakeObject({{"type", "string"}, {"x-bitcoin-unit", "amount"}});
    +        return MakeObject({{"type", "number"}, {"x-bitcoin-unit", "amount"}});
         case RPCResult::Type::STR_HEX:
             return MakeObject({{"type", "string"}, {"pattern", "^[0-9a-fA-F]+$"}});
         case RPCResult::Type::NUM:
    diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py
    index 6d48470578..c041162a0f 100755
    --- a/test/functional/rpc_openrpc.py
    +++ b/test/functional/rpc_openrpc.py
    @@ -55,7 +55,7 @@ class OpenRPCDocTest(BitcoinTestFramework):
             analyzepsbt = find_method(openrpc, "analyzepsbt")
             result_schema = analyzepsbt["result"]["schema"]
             estimated_feerate = result_schema["properties"]["estimated_feerate"]
    -        assert_equal(estimated_feerate["type"], "string")
    +        assert_equal(estimated_feerate["type"], "number")
             assert_equal(estimated_feerate["x-bitcoin-unit"], "amount")
    
  85. in src/rpc/server.cpp:332 in dc9942984a
     327 | +
     328 | +// NOLINTNEXTLINE(misc-no-recursion)
     329 | +UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
     330 | +{
     331 | +    UniValue schema{UniValue::VOBJ};
     332 | +    switch (arg.m_type) {
    


    w0xlt commented at 6:32 AM on June 22, 2026:

    It looks like getopenrpcinfo does not account for skip_type_check.

    For example, createrawtransaction.outputs is declared as RPCArg::Type::ARR with skip_type_check = true. Runtime accepts both an array and a direct object, but OpenRPC still emits an array-only schema.

    Similarly, getwalletinfo.scanning is declared as RPCResult::Type::OBJ with skip_type_check = true. It can also return false when no scan is active, but OpenRPC still emits an object-only schema.

    So the generated schema can be stricter than the actual RPC behavior. Maybe skip_type_check fields should either emit a less restrictive schema, like {}, or explicit alternatives with oneOf where the accepted shapes are known.

    Suggestion:

    <details> <summary>diff</summary>

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index ca383a065a..a1a927fa8c 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -329,6 +329,18 @@ void ApplyArgFallback(UniValue& schema, const RPCArg& arg)
     UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
     {
         UniValue schema{UniValue::VOBJ};
    +    if (arg.m_opts.skip_type_check) {
    +        ApplyTypeStrOverride(schema, arg);
    +        if (schema.empty() && arg.m_type == RPCArg::Type::ARR) {
    +            UniValue one_of{UniValue::VARR};
    +            one_of.push_back(MakeObject({{"type", "array"}}));
    +            one_of.push_back(MakeObject({{"type", "object"}}));
    +            schema.pushKV("oneOf", std::move(one_of));
    +        }
    +        ApplyArgFallback(schema, arg);
    +        return schema;
    +    }
    +
         switch (arg.m_type) {
         case RPCArg::Type::STR:
             schema = MakeObject({{"type", "string"}});
    @@ -407,6 +419,24 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
     // NOLINTNEXTLINE(misc-no-recursion)
     UniValue OpenRPCResultSchema(const RPCResult& result)
     {
    +    if (result.m_opts.skip_type_check) {
    +        RPCResultOptions opts{result.m_opts};
    +        opts.skip_type_check = false;
    +        if (result.m_type == RPCResult::Type::OBJ) {
    +            UniValue obj_schema{OpenRPCResultSchema(RPCResult{result, std::move(opts)})};
    +            if (result.m_key_name.empty()) return obj_schema;
    +
    +            UniValue one_of{UniValue::VARR};
    +            one_of.push_back(std::move(obj_schema));
    +            one_of.push_back(MakeObject({{"const", false}}));
    +            UniValue schema{UniValue::VOBJ};
    +            schema.pushKV("oneOf", std::move(one_of));
    +            return schema;
    +        }
    +        if (result.m_type == RPCResult::Type::ARR) return OpenRPCResultSchema(RPCResult{result, std::move(opts)});
    +        return UniValue{UniValue::VOBJ};
    +    }
    +
         switch (result.m_type) {
         case RPCResult::Type::STR:
             return MakeObject({{"type", "string"}});
    diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py
    index 6d48470578..51d3e97ca3 100755
    --- a/test/functional/rpc_openrpc.py
    +++ b/test/functional/rpc_openrpc.py
    @@ -44,6 +44,21 @@ class OpenRPCDocTest(BitcoinTestFramework):
             hash_or_height = find_param(getblockstats, "hash_or_height")
             assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "number"}, {"type": "string"}]})
     
    +        self.log.info("Checking skipped type check schemas")
    +        createrawtransaction = find_method(openrpc, "createrawtransaction")
    +        outputs = find_param(createrawtransaction, "outputs")
    +        assert_equal(outputs["schema"], {"oneOf": [{"type": "array"}, {"type": "object"}]})
    +
    +        getdescriptoractivity = find_method(openrpc, "getdescriptoractivity")
    +        activity = getdescriptoractivity["result"]["schema"]["properties"]["activity"]
    +        assert_equal(activity["type"], "array")
    +
    +        if "getwalletinfo" in [method["name"] for method in openrpc["methods"]]:
    +            getwalletinfo = find_method(openrpc, "getwalletinfo")
    +            scanning = getwalletinfo["result"]["schema"]["properties"]["scanning"]
    +            assert_equal(scanning["oneOf"][0]["type"], "object")
    +            assert_equal(scanning["oneOf"][1], {"const": False})
    +
             self.log.info("Checking argument fallback annotations")
             getblock = find_method(openrpc, "getblock")
             verbosity = find_param(getblock, "verbosity")
    

    </details>

  86. in src/rpc/server.cpp:353 in dc9942984a
     348 | +        one_of.push_back(MakeObject({{"type", "string"}}));
     349 | +        schema.pushKV("oneOf", std::move(one_of));
     350 | +        break;
     351 | +    }
     352 | +    case RPCArg::Type::RANGE: {
     353 | +        UniValue prefix_items{UniValue::VARR};
    


    w0xlt commented at 6:58 AM on June 22, 2026:

    If I understand the specification correctly, OpenRPC Schema Objects are based on JSON Schema Draft 7, where tuple validation uses array-valued items plus additionalItems, not prefixItems.

    https://spec.open-rpc.org/#schema-object

    https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.6.4.1

    Suggestion:

    <details> <summary>diff</summary>

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index ca383a065a..5394da5bab 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -350,12 +350,13 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
             break;
         }
         case RPCArg::Type::RANGE: {
    -        UniValue prefix_items{UniValue::VARR};
    -        prefix_items.push_back(MakeObject({{"type", "number"}}));
    -        prefix_items.push_back(MakeObject({{"type", "number"}}));
    +        UniValue items{UniValue::VARR};
    +        items.push_back(MakeObject({{"type", "number"}}));
    +        items.push_back(MakeObject({{"type", "number"}}));
             UniValue range_schema{UniValue::VOBJ};
             range_schema.pushKV("type", "array");
    -        range_schema.pushKV("prefixItems", std::move(prefix_items));
    +        range_schema.pushKV("items", std::move(items));
    +        range_schema.pushKV("additionalItems", false);
             range_schema.pushKV("minItems", 2);
             range_schema.pushKV("maxItems", 2);
             UniValue one_of{UniValue::VARR};
    @@ -434,13 +435,14 @@ UniValue OpenRPCResultSchema(const RPCResult& result)
             return schema;
         }
         case RPCResult::Type::ARR_FIXED: {
    -        UniValue prefix_items{UniValue::VARR};
    +        UniValue items{UniValue::VARR};
             for (const auto& inner : result.m_inner) {
    -            prefix_items.push_back(OpenRPCResultSchema(inner));
    +            items.push_back(OpenRPCResultSchema(inner));
             }
             UniValue schema{UniValue::VOBJ};
             schema.pushKV("type", "array");
    -        schema.pushKV("prefixItems", std::move(prefix_items));
    +        schema.pushKV("items", std::move(items));
    +        schema.pushKV("additionalItems", false);
             schema.pushKV("minItems", uint64_t(result.m_inner.size()));
             schema.pushKV("maxItems", uint64_t(result.m_inner.size()));
             return schema;
    diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py
    index 6d48470578..3568d47498 100755
    --- a/test/functional/rpc_openrpc.py
    +++ b/test/functional/rpc_openrpc.py
    @@ -44,6 +44,26 @@ class OpenRPCDocTest(BitcoinTestFramework):
             hash_or_height = find_param(getblockstats, "hash_or_height")
             assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "number"}, {"type": "string"}]})
     
    +        self.log.info("Checking Draft 7 tuple schemas")
    +        deriveaddresses = find_method(openrpc, "deriveaddresses")
    +        range_array = find_param(deriveaddresses, "range")["schema"]["oneOf"][1]
    +        assert "prefixItems" not in range_array
    +        assert_equal(range_array, {
    +            "type": "array",
    +            "items": [{"type": "number"}, {"type": "number"}],
    +            "additionalItems": False,
    +            "minItems": 2,
    +            "maxItems": 2,
    +        })
    +
    +        feerate_percentiles = getblockstats["result"]["schema"]["properties"]["feerate_percentiles"]
    +        assert "prefixItems" not in feerate_percentiles
    +        assert_equal(feerate_percentiles["type"], "array")
    +        assert_equal(feerate_percentiles["items"], [{"type": "number"}] * 5)
    +        assert_equal(feerate_percentiles["additionalItems"], False)
    +        assert_equal(feerate_percentiles["minItems"], 5)
    +        assert_equal(feerate_percentiles["maxItems"], 5)
    +
             self.log.info("Checking argument fallback annotations")
             getblock = find_method(openrpc, "getblock")
             verbosity = find_param(getblock, "verbosity")
    

    </details>

  87. w0xlt commented at 6:59 AM on June 22, 2026: contributor

    A few review comments:

  88. willcl-ark commented at 9:14 AM on June 23, 2026: member

    Thanks for your review @w0xlt (and @satsfy!)

    In making amendments here to @w0xlt's comments I re-read the spec again and noticed two more issues I am considering fixing up:

    1. I have targetted an older openrpc spec version (1.3.2) vs current 1.4.1
    2. OpenRPC specifies a discovery method

    Regarding 1. As far as I can tell this just relaxes the versionning requirements a little (no code changes needed our side), so I will update to the current latest version.

    On 2., I have a --fixup which adds bitcoin-cli rpc.discover as an alias for getopenrpcinfo, but I wonder if supporting both methods is just unnecessary? Should we simply switch to the (spec-defined) rpc.discover rpc method only?

  89. janb84 commented at 2:18 PM on June 25, 2026: contributor
    1. I have targetted an older openrpc spec version (1.3.2) vs current 1.4.1

    bump it :D there is no code currently that depends on 1.3.2-specific semantics.

    1. OpenRPC specifies a discovery method

    On 2., I have a --fixup which adds bitcoin-cli rpc.discover as an alias for getopenrpcinfo, but I wonder if supporting both methods is just unnecessary? Should we simply switch to the (spec-defined) rpc.discover rpc method only?

    By that, if you mean, they both call the primitive CRPCTable::buildOpenRPCDoc(bool) than yes :)

    something like this? :

    <details><summary> diff </summary>

    diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
    index ca383a065a..63d610d64a 100644
    --- a/src/rpc/server.cpp
    +++ b/src/rpc/server.cpp
    @@ -479,6 +479,67 @@ UniValue OpenRPCResultSchema(const RPCResult& result)
     }
     } // namespace
     
    +static RPCResult OpenRPCDocResult()
    +{
    +    return RPCResult{
    +        RPCResult::Type::OBJ, "", "",
    +        {
    +            {RPCResult::Type::STR, "openrpc", "OpenRPC specification version."},
    +            {RPCResult::Type::OBJ, "info", "Metadata about this JSON-RPC interface.",
    +                {
    +                    {RPCResult::Type::STR, "title", "API title."},
    +                    {RPCResult::Type::STR, "version", "Bitcoin Core version string."},
    +                    {RPCResult::Type::STR, "description", "API description."},
    +                }},
    +            {RPCResult::Type::ARR, "methods", "Documented RPC methods.",
    +                {{RPCResult::Type::OBJ, "", "An RPC method description object.",
    +                    {
    +                        {RPCResult::Type::STR, "name", "Method name."},
    +                        {RPCResult::Type::STR, "description", "Method description."},
    +                        {RPCResult::Type::ARR, "params", "Method parameters.",
    +                            {{RPCResult::Type::OBJ, "", "A parameter.",
    +                                {
    +                                    {RPCResult::Type::STR, "name", "Parameter name."},
    +                                    {RPCResult::Type::BOOL, "required", "Whether the parameter is required."},
    +                                    {RPCResult::Type::ANY, "schema", "JSON Schema for the parameter."},
    +                                    {RPCResult::Type::STR, "description", /*optional=*/true, "Parameter description."},
    +                                    {RPCResult::Type::ARR, "x-bitcoin-aliases", /*optional=*/true, "Alternative parameter names.",
    +                                        {{RPCResult::Type::STR, "", "An alias."}}},
    +                                    {RPCResult::Type::BOOL, "x-bitcoin-placeholder", /*optional=*/true, "Whether the parameter is retained only for compatibility."},
    +                                    {RPCResult::Type::BOOL, "x-bitcoin-also-positional", /*optional=*/true, "Whether the parameter can also be passed positionally."},
    +                                }}}},
    +                        {RPCResult::Type::OBJ, "result", "Method result.",
    +                            {
    +                                {RPCResult::Type::STR, "name", "Result name."},
    +                                {RPCResult::Type::ANY, "schema", "JSON Schema for the result."},
    +                            }},
    +                        {RPCResult::Type::STR, "x-bitcoin-category", "RPC category."},
    +                    }}}},
    +        },
    +        {.skip_type_check = true}};
    +}
    +
    +static RPCMethod rpc_discover()
    +{
    +    return RPCMethod{
    +        "rpc.discover",
    +        "Returns the OpenRPC document describing this JSON-RPC service.\n"
    +        "This is the OpenRPC service discovery method; it describes the public\n"
    +        "interface only. Use getopenrpcinfo to additionally include hidden RPC\n"
    +        "commands and arguments.\n",
    +        {},
    +        OpenRPCDocResult(),
    +        RPCExamples{
    +            HelpExampleCli("rpc.discover", "")
    +            + HelpExampleRpc("rpc.discover", "")
    +        },
    +        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
    +{
    +    return tableRPC.buildOpenRPCDoc(/*include_hidden=*/false);
    +},
    +    };
    +}
    +
     static RPCMethod getopenrpcinfo()
     {
         return RPCMethod{
    @@ -487,42 +548,7 @@ static RPCMethod getopenrpcinfo()
             {
                 {"show_hidden", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also include hidden RPC commands and arguments."},
             },
    -        RPCResult{
    -            RPCResult::Type::OBJ, "", "",
    -            {
    -                {RPCResult::Type::STR, "openrpc", "OpenRPC specification version."},
    -                {RPCResult::Type::OBJ, "info", "Metadata about this JSON-RPC interface.",
    -                    {
    -                        {RPCResult::Type::STR, "title", "API title."},
    -                        {RPCResult::Type::STR, "version", "Bitcoin Core version string."},
    -                        {RPCResult::Type::STR, "description", "API description."},
    -                    }},
    -                {RPCResult::Type::ARR, "methods", "Documented RPC methods.",
    -                    {{RPCResult::Type::OBJ, "", "An RPC method description object.",
    -                        {
    -                            {RPCResult::Type::STR, "name", "Method name."},
    -                            {RPCResult::Type::STR, "description", "Method description."},
    -                            {RPCResult::Type::ARR, "params", "Method parameters.",
    -                                {{RPCResult::Type::OBJ, "", "A parameter.",
    -                                    {
    -                                        {RPCResult::Type::STR, "name", "Parameter name."},
    -                                        {RPCResult::Type::BOOL, "required", "Whether the parameter is required."},
    -                                        {RPCResult::Type::ANY, "schema", "JSON Schema for the parameter."},
    -                                        {RPCResult::Type::STR, "description", /*optional=*/true, "Parameter description."},
    -                                        {RPCResult::Type::ARR, "x-bitcoin-aliases", /*optional=*/true, "Alternative parameter names.",
    -                                            {{RPCResult::Type::STR, "", "An alias."}}},
    -                                        {RPCResult::Type::BOOL, "x-bitcoin-placeholder", /*optional=*/true, "Whether the parameter is retained only for compatibility."},
    -                                        {RPCResult::Type::BOOL, "x-bitcoin-also-positional", /*optional=*/true, "Whether the parameter can also be passed positionally."},
    -                                    }}}},
    -                            {RPCResult::Type::OBJ, "result", "Method result.",
    -                                {
    -                                    {RPCResult::Type::STR, "name", "Result name."},
    -                                    {RPCResult::Type::ANY, "schema", "JSON Schema for the result."},
    -                                }},
    -                            {RPCResult::Type::STR, "x-bitcoin-category", "RPC category."},
    -                        }}}},
    -            },
    -            {.skip_type_check = true}},
    +        OpenRPCDocResult(),
             RPCExamples{
                 HelpExampleCli("getopenrpcinfo", "")
                 + HelpExampleRpc("getopenrpcinfo", "")
    @@ -537,6 +563,7 @@ static RPCMethod getopenrpcinfo()
     
     static const CRPCCommand vRPCCommands[]{
         /* Overall control/query calls */
    +    {"control", &rpc_discover},
         {"control", &getopenrpcinfo},
         {"control", &getrpcinfo},
         {"control", &help},
    

    </details>

  90. willcl-ark commented at 7:56 AM on June 26, 2026: member

    By that, if you mean, they both call the primitive CRPCTable::buildOpenRPCDoc(bool) than yes :)

    yes pretty much! I will push something up shortly, as I also think we should make both of those changes.


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-06-27 10:51 UTC

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