Support JSON-RPC 2.0 when requested by client #27101

pull pinheadmz wants to merge 9 commits into bitcoin:master from pinheadmz:jsonrpc-2.0 changing 13 files +355 −101
  1. pinheadmz commented at 7:13 pm on February 14, 2023: member

    Closes #2960

    Bitcoin Core’s JSONRPC server behaves with a special blend of 1.0, 1.1 and 2.0 behaviors. This introduces compliance issues with more strict clients. There are the major misbehaviors that I found:

    • returning non-200 HTTP codes for RPC errors like “Method not found” (this is not a server error or an HTTP error)
    • returning both "error" and "result" fields together in a response object.
    • different error-handling behavior for single and batched RPC requests (batches contain errors in the response but single requests will actually throw HTTP errors)

    #15495 added regression tests after a discussion in #15381 to kinda lock in our RPC behavior to preserve backwards compatibility.

    #12435 was an attempt to allow strict 2.0 compliance behind a flag, but was abandoned.

    The approach in this PR is not strict and preserves backwards compatibility in a familiar bitcoin-y way: all old behavior is preserved, but new rules are applied to clients that opt in. One of the rules in the JSON RPC 2.0 spec is that the kv pair "jsonrpc": "2.0" must be present in the request. Well, let’s just use that to trigger strict 2.0 behavior! When that kv pair is included in a request object, the response will adhere to strict JSON-RPC 2.0 rules, essentially:

    • always return HTTP 200 “OK” unless there really is a server error or malformed request
    • either return "error" OR "result" but never both
    • same behavior for single and batch requests

    If this is merged next steps can be:

    • Refactor bitcoin-cli to always use strict 2.0
    • Refactor the python test framework to always use strict 2.0 for everything
    • Begin deprecation process for 1.0/1.1 behavior (?)

    If we can one day remove the old 1.0/1.1 behavior we can clean up the rpc code quite a bit.

  2. DrahtBot commented at 7:13 pm on February 14, 2023: contributor

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

    Code Coverage

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK cbergqvist, ryanofsky, tdb3
    Concept ACK ajtowns, fanquake, sipa, stickies-v, vincenzopalazzo
    Stale ACK willcl-ark

    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:

    • #29946 (JSON-RPC request Content-Type is application/json 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.

  3. pinheadmz force-pushed on Feb 14, 2023
  4. in src/rpc/request.h:31 in 440d6a2e07 outdated
    26 
    27     void parse(const UniValue& valRequest);
    28 };
    29 
    30+UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id);
    31+UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id, bool is20);
    


    ajtowns commented at 9:59 am on February 15, 2023:
    Rather than bool is20, enum JSONVersion { JSON_1_BTC, JSON_2_0 } might be clearer?
  5. ajtowns commented at 7:06 pm on February 16, 2023: contributor
    Concept ACK
  6. fanquake commented at 5:41 pm on February 20, 2023: member
    Concept ACK
  7. pinheadmz force-pushed on Feb 27, 2023
  8. pinheadmz force-pushed on Feb 27, 2023
  9. pinheadmz force-pushed on Feb 27, 2023
  10. sipa commented at 9:33 pm on February 27, 2023: member
    Concept ACK. Migrating to strict JSON-RPC 2.0 for bitcoin-cli and whatever opts into 2.0 is cool, even if it means keeping support for kinda-sorta-JSON-RPC 1.0 around on the server side.
  11. willcl-ark commented at 12:31 pm on February 28, 2023: member

    Concept ACK.

    Might be worth updating a few other things at the same time if you continue to move ahead:

  12. pinheadmz force-pushed on Mar 1, 2023
  13. pinheadmz commented at 7:08 pm on March 1, 2023: member

    @willcl-ark thanks, I added comments and release notes.

    I also wrote a tiny testing package using libjson-rpc-cpp to check against an “outside” JSON-RPC 2.0 implementation. The package is https://github.com/pinheadmz/jsonrpc-bitcoin and seems to like the 2.0 implementation so far.

  14. pinheadmz force-pushed on Mar 1, 2023
  15. pinheadmz force-pushed on Mar 1, 2023
  16. pinheadmz force-pushed on Mar 1, 2023
  17. pinheadmz force-pushed on Mar 1, 2023
  18. stickies-v commented at 10:41 am on March 2, 2023: contributor
    Concept ACK
  19. pinheadmz force-pushed on Mar 2, 2023
  20. pinheadmz marked this as ready for review on Mar 2, 2023
  21. pinheadmz commented at 3:04 pm on March 20, 2023: member
    This PR is ready for code review if any of you fine handsome concept-ACKers have the time ❤️
  22. in test/functional/interface_rpc.py:141 in 277727714e outdated
    121@@ -91,6 +122,17 @@ def test_http_status_codes(self):
    122         assert_equal(response['error']['code'], -32700)                 # RPC_PARSE_ERROR
    123         assert_equal(status, 500)
    124 
    125+        self.log.info("Testing HTTP status codes for JSON-RPC 2.0 requests...")
    126+        AuthServiceProxy.jsonrpc20 = True
    127+        # All 200!
    128+        expect_http_status(200, -32601, self.nodes[0].invalidmethod)    # RPC_METHOD_NOT_FOUND
    129+        expect_http_status(200, -8,     self.nodes[0].getblockhash, 42) # RPC_INVALID_PARAMETER
    


    willcl-ark commented at 9:22 pm on March 20, 2023:
    Should this not be a status: 200, error: -32602 under json 2.0?

    pinheadmz commented at 6:05 pm on March 22, 2023:
    Hm yeah good point, a few error codes like this are specified. One problem with this is that our application error RPC_INVALID_PARAMETER (-8) is coded ~186 times in RPC functions everywhere. I suppose we could re-map those error codes back to the jsonrpc 2.0 spec in JSONRPCExecOne()? Only when the request indicates jsonrpc 2.0
  23. in test/functional/interface_rpc.py:56 in 277727714e outdated
    54-
    55+        self.log.info("Testing basic JSON-RPC 1.1 batch request...")
    56         results = self.nodes[0].batch([
    57             # A basic request that will work fine.
    58-            {"method": "getblockcount", "id": 1},
    59+            {"version": "1.1", "method": "getblockcount", "id": 1},
    


    willcl-ark commented at 9:29 pm on March 20, 2023:

    Were we not using the out-of-spec "jsonrpc": "1.0" here previously?

    edit: It seems it doesn’t make any difference as was not being validated? 🤷🏼‍♂️


    pinheadmz commented at 6:11 pm on March 22, 2023:
    In the tests at least it was originally version: "1.1" as seen in authproxy.py which is actually in the 1.1 spec. I’m not sure if "jsonrpc": "1.0" was part of an older spec or if thats a mistake here
  24. in test/functional/interface_rpc.py:151 in 277727714e outdated
    77@@ -79,9 +78,41 @@ def test_batch_request(self):
    78 
    79         assert_equal(result_by_id[4]['error']['code'], -32600)          # RPC_INVALID_REQUEST
    80         assert_equal(result_by_id[4]['error']['message'], "Missing method")
    81+        assert_equal(result_by_id[2]['result'], None)
    82+
    83+        self.log.info("Testing basic JSON-RPC 2.0 batch request...")
    


    willcl-ark commented at 9:46 am on March 21, 2023:

    pinheadmz commented at 6:14 pm on March 22, 2023:
    aw geez. Well, ok so it looks like disconnectnode is the only RPC that throws the (correct?) error code for invalid params, while the rest throw RPC_INVALID_PARAMETER (-8). Maybe it does make sense to use a scripted diff to replace all the -8s with -32602. Nice catch!

    pinheadmz commented at 6:17 pm on March 22, 2023:
    Not sure if RPC_INTERNAL_ERROR (-32603) is totally correct for estimating fee from a blocksonly node. It’s more of a bad request …
  25. in test/functional/interface_rpc.py:195 in 277727714e outdated
    129+        expect_http_status(200, -8,     self.nodes[0].getblockhash, 42) # RPC_INVALID_PARAMETER
    130+        expect_http_status(200, None,   self.nodes[0].getblockhash, 0)
    131+        # force-send invalidly formatted request
    132+        response, status = self.nodes[0]._request('POST', "/", b'this is bad')
    133+        assert_equal(response['error']['code'], -32700)                 # RPC_PARSE_ERROR
    134+        assert_equal(status, 500)
    


    willcl-ark commented at 9:46 am on March 21, 2023:
    Why do we choose status: 500, error: -32700 here vs status: 200, error: -32700? I feel inclined to argue that we didn’t experience an http server error, this is just a parsing error.

    pinheadmz commented at 6:27 pm on March 22, 2023:

    This error is caused by a failure to parse JSON in ReadBody() so theoretically we also can’t read the "jsonrpc": "2.0" pair either. I agree with you though, the HTTP transport is fine and should be 200, because the error is coming from the json parser. I guess the options are:

    1. always send 200 here and kinda break backwards compat
    2. attempt to parse the "jsonrpc" string and only send 200 if we can detect the “2.0” value
    3. leave as-is, meaning invalid requests will get 500 even if they otherwise intended to be “2.0”

    willcl-ark commented at 9:26 pm on March 22, 2023:
    I think I’d lean here towards option 3, not breaking backwards-compatibility and just let json2.0 users “suffer” a status: 500, error: -32700 :)
  26. in doc/release-notes-27101.md:24 in 2da823199b outdated
    19+HTTP/1.1 200 OK
    20+Content-Type: application/json
    21+Date: Wed, 01 Mar 2023 19:01:19 GMT
    22+Content-Length: 81
    23+
    24+{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":null}
    


    willcl-ark commented at 9:50 am on March 21, 2023:
    Perhaps in demonstrating best-practice it could be nice to not use a null value for id in the request, so we can avoid the null response. Per json-rpc 2.0 an id: null request is supposed to be a “notification”, which does not get responded to (although I think we should ignore actually parsing them like this interally, and respond to all requests).

    pinheadmz commented at 6:31 pm on March 22, 2023:
    yep thanks, will update
  27. in test/functional/interface_rpc.py:90 in 2da823199b outdated
    90+            # A basic request that will work fine.
    91+            {"jsonrpc": "2.0", "method": "getblockcount", "id": 5},
    92+            # Request that will fail.  The whole batch request should still work fine.
    93+            {"jsonrpc": "2.0", "method": "invalidmethod", "id": 6},
    94+            # Another call that should succeed.
    95+            {"jsonrpc": "2.0", "method": "getblockhash", "id": 7, "params": [0]},
    


    willcl-ark commented at 10:55 am on March 21, 2023:
    Just a behaviour quirk I noticed in mutating these a little for testing: we can actually service batch requests which consist of a mixture of 1.0 and 2.0 requests. This test itself will fail, as it’s asserting that error is not present (which if one is changed to 1.0 it is), but the server can handle it. Not that I think it’s a particularly realistic or common situation…

    pinheadmz commented at 6:44 pm on March 22, 2023:
    another good call, thats a bug. I force-pushed with a fix and added a mixed 1.1 + 2.0 in the batch test
  28. willcl-ark approved
  29. willcl-ark commented at 10:57 am on March 21, 2023: member

    ACK 2da823199

    Left a few comments which could be addressed here, in a followup or not at all. None of them materially affect the current implementation of this feature which works well.

    Although, I would be curious to know why we are still responding with http 500 errors for invalid json after the move to json 2.0.

  30. pinheadmz force-pushed on Mar 22, 2023
  31. pinheadmz force-pushed on Mar 22, 2023
  32. pinheadmz force-pushed on Mar 28, 2023
  33. pinheadmz commented at 6:17 pm on March 28, 2023: member
    Added a scripted-diff to completely replace all occurrences of the application-defined RPC_INVALID_PARAMETER (-8) with the JSONRPC 2.0 spec-defined RPC_INVALID_PARAMS (-32602)
  34. pinheadmz force-pushed on Mar 28, 2023
  35. DrahtBot added the label CI failed on Apr 19, 2023
  36. pinheadmz force-pushed on Apr 27, 2023
  37. pinheadmz force-pushed on Apr 27, 2023
  38. DrahtBot removed the label CI failed on Apr 27, 2023
  39. DrahtBot added the label CI failed on May 9, 2023
  40. DrahtBot added the label Needs rebase on May 10, 2023
  41. pinheadmz force-pushed on May 11, 2023
  42. pinheadmz force-pushed on May 11, 2023
  43. DrahtBot removed the label Needs rebase on May 11, 2023
  44. DrahtBot removed the label CI failed on May 11, 2023
  45. willcl-ark commented at 8:48 am on May 12, 2023: member

    reACK af86462

    Now using const reference in parse().

  46. DrahtBot added the label Needs rebase on Jun 1, 2023
  47. pinheadmz force-pushed on Jun 2, 2023
  48. pinheadmz commented at 3:22 pm on June 2, 2023: member

    🐙 This pull request conflicts with the target branch and needs rebase.

    done, very easy with scripted diff!

  49. DrahtBot removed the label Needs rebase on Jun 2, 2023
  50. DrahtBot added the label Needs rebase on Jun 29, 2023
  51. pinheadmz force-pushed on Jul 3, 2023
  52. pinheadmz commented at 5:18 pm on July 3, 2023: member

    🐙 This pull request conflicts with the target branch and needs rebase.

    👌

  53. pinheadmz requested review from ajtowns on Jul 3, 2023
  54. pinheadmz requested review from stickies-v on Jul 3, 2023
  55. pinheadmz requested review from willcl-ark on Jul 3, 2023
  56. DrahtBot removed the label Needs rebase on Jul 3, 2023
  57. in test/functional/interface_rpc.py:72 in 5f9179a339 outdated
    68@@ -68,17 +69,75 @@ def test_batch_request(self):
    69         assert_equal(result_by_id[1]['error'], None)
    70         assert_equal(result_by_id[1]['result'], 0)
    71 
    72-        assert_equal(result_by_id[2]['error']['code'], -32601)
    73+        assert_equal(result_by_id[2]['error']['code'], -32601)          # RPC_METHOD_NOT_FOUND
    


    willcl-ark commented at 7:04 pm on July 3, 2023:

    Just thinking out loud here; we use a code comment to indicate what the error code represents but in many of the other tests it appears we don’t (admittedly here it is the point of the test, and in the other locations it’s not). I wonder whether it might be clearer to have these enumerated in a single location, e.g. util.py, and then they could be used by name here and elsewhere? (this might be a change someone else will just make in the future otherwise, too).

    I think it’s probably better left for a followup though, if at all.

  58. DrahtBot added the label CI failed on Jul 3, 2023
  59. willcl-ark approved
  60. willcl-ark commented at 9:28 am on July 4, 2023: member

    re-ACK 5f9179a

    Verified that since my last review git range-diff af86462e...5f9179a comprised rebase-related changes. The CI failure is unrelated. Left a non-blocking comment.

  61. craigraw commented at 10:19 am on July 5, 2023: none
    Successfully tested against Sparrow Wallet’s internal RPC client, Cormorant. By way of context I need this PR to make batch JSON-RPC requests as the Java client I am using requires JSON-RPC 2.0 formatted responses when a batched call is made.
  62. DrahtBot removed the label CI failed on Jul 5, 2023
  63. in test/functional/interface_rpc.py:20 in d11fb70b3f outdated
    15@@ -16,7 +16,8 @@ def expect_http_status(expected_http_status, expected_rpc_code,
    16                        fcn, *args):
    17     try:
    18         fcn(*args)
    19-        raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    20+        if expected_rpc_code is not None:
    21+            raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    


    achow101 commented at 8:30 pm on July 5, 2023:

    In d11fb70b3fea7c1be5f48e32837d2c81dc7cbd10 “test: cover more HTTP error codes in interface_rpc.py”

    This seems unnecessary?


    pinheadmz commented at 3:47 pm on July 7, 2023:

    Reverting this change fails the test because we are divorcing HTTP status from RPC error, so you can have an HTTP OK with RPC error and some test cases now also ensure both are OK.

     02023-07-07T15:44:27.147000Z TestFramework (ERROR): Assertion failed
     1Traceback (most recent call last):
     2  File "/Users/matthewzipkin/Desktop/work/bitcoin/test/functional/test_framework/test_framework.py", line 131, in main
     3    self.run_test()
     4  File "/Users/matthewzipkin/Desktop/work/bitcoin/test/functional/interface_rpc.py", line 157, in run_test
     5    self.test_http_status_codes()
     6  File "/Users/matthewzipkin/Desktop/work/bitcoin/test/functional/interface_rpc.py", line 125, in test_http_status_codes
     7    expect_http_status(200, None,   self.nodes[0].getblockhash, 0)
     8  File "/Users/matthewzipkin/Desktop/work/bitcoin/test/functional/interface_rpc.py", line 19, in expect_http_status
     9    raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    10AssertionError: Expected RPC error None, got none
    

    achow101 commented at 4:22 pm on July 7, 2023:
    Hmm, the tests still seemed to work when I had run it without this change..

    ryanofsky commented at 2:09 am on December 18, 2023:

    In commit “test: cover more HTTP error codes in interface_rpc.py” (b41f09cbc09a6f5d75636eb2f7ba8bc6d8802942)

    I also don’t see why this should be necessary, and the change doesn’t make sense logically because now an http status code integer is passed to the expect_http_status function but the value is ignored in this case. Would suggest reverting this change and, if necessary, just writing a new expect_ function instead of trying to extend expect_http_status in a way where it appears to be checking things it’s not.

  64. achow101 commented at 8:51 pm on July 5, 2023: member

    I think we aren’t handling missing ids correctly?

    From the spec:

    id An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2]

    When there is no id, we should treat the as a notification and can just ignore the request, rather than responding to it as we currently do. It seems like it should be fairly easy to require enforcement of id, which also makes it trivial to handle notifications.


    500 seems like it’s the wrong http status to return when there’s a parse error, although I don’t think there’s a way to change that without breaking backwards compatibility.

  65. pinheadmz force-pushed on Jul 8, 2023
  66. pinheadmz commented at 12:25 pm on July 8, 2023: member
    @achow101 I added IsNotification() and a test. Also rebased the PR with more atomic commits for hopefully easier reviewing. As far as 500, that should only be returned by a “legacy” json rpc request. The legacy protocol should be unchanged by this PR. Unfortunately if the request is unparseable we can not read the jsonrpc:"2.0" identifier so the default is to assume legacy.
  67. in src/httprpc.cpp:207 in 94d02376c8 outdated
    203@@ -204,6 +204,7 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
    204             }
    205 
    206             jreq.parse(valRequest);
    207+            if (jreq.IsNotification()) return false;
    


    achow101 commented at 8:35 pm on July 11, 2023:

    In 94d02376c853aa4835de413d3f75b8b06bcb5193 “rpc: JSON-RPC 2.0 should not respond to “notifications””

    This results in 500 and a response body of “Unhandled request”. I think it should actually be a 200 with no response body.

    0            if (jreq.IsNotification()) {
    1                req->WriteReply(HTTP_OK);
    2                return true;
    3            }
    

    pinheadmz commented at 8:46 pm on July 17, 2023:
    I’m looking into this, the spec repeats no response to notifications but it doesn’t indicate whether an HTTP OK is reasonable or whether the request should be completely ignored. I’m having some trouble getting libevent to completely cancel a request without sending anything back (and still letting bitcoind shutdown without hanging!) so maybe a 200 OK is best

    ajtowns commented at 4:40 am on July 18, 2023:
    There’s https://www.simple-is-better.org/json-rpc/transport_http.html (linked from the wikipedia page) which says 202 or 204; there’s also https://www.simple-is-better.org/json-rpc/extension_transport.html by the same author but dated two months earlier, which suggests 200 or 204.

    pinheadmz commented at 1:57 pm on July 18, 2023:
    ah awesome thank you, I’ll go with 204 No Response

    pinheadmz commented at 5:02 pm on July 18, 2023:
    Added in push to 26dcb359acf421dc3f448b49e0b564e714c54792. Also had to tweak authproxy.py to accommodate code 204
  68. pinheadmz force-pushed on Jul 18, 2023
  69. DrahtBot added the label CI failed on Jul 22, 2023
  70. pinheadmz force-pushed on Jul 24, 2023
  71. DrahtBot removed the label CI failed on Jul 24, 2023
  72. pinheadmz commented at 4:30 pm on July 24, 2023: member
    Rebased on master to re-apply scripted diff
  73. in src/httprpc.cpp:217 in 35c516c75d outdated
    207-            // Send reply
    208-            strReply = JSONRPCReply(result, NullUniValue, jreq.id);
    209+            jreq.parse(valRequest);
    210+            if (jreq.IsNotification()) {
    211+                req->WriteReply(HTTP_NO_CONTENT);
    212+                return true;
    


    luke-jr commented at 7:08 pm on July 27, 2023:
    Shouldn’t we still execute the notification? While it’s not possible for the client to confirm it worked, it should still execute…

    pinheadmz commented at 7:26 pm on August 22, 2023:
    Yep good call after doing some more reading about this in other implementations, I updated this branch to match and tested it. So now notifications are executed but send no response.
  74. luke-jr changes_requested
  75. DrahtBot added the label Needs rebase on Aug 17, 2023
  76. pinheadmz force-pushed on Aug 22, 2023
  77. DrahtBot removed the label Needs rebase on Aug 22, 2023
  78. pinheadmz force-pushed on Aug 22, 2023
  79. pinheadmz commented at 7:26 pm on August 22, 2023: member
    also rebased on master again to apply scripted diff
  80. DrahtBot added the label Needs rebase on Aug 24, 2023
  81. vincenzopalazzo commented at 7:00 pm on September 14, 2023: none

    Concept ACK https://github.com/bitcoin/bitcoin/pull/27101/commits/437f611e2fc3e87cbea458dc58589c50bca39991

    I did a lite review of all the code and I deep review of the JSON RPC 2.0 protocol and looks good to me. I would like to give another shot because I may missing something in the RPC implementation.

    P.S: If was a mine PR I was implementing the Protocol and did not use it in one PR and then use the protocol everywhere in another PR just to make the PR smaller to review, but this is just my side note.

  82. pinheadmz commented at 7:40 pm on October 17, 2023: member
    @vincenzopalazzo great point thanks, I’ll chop off the scripted diff and rebase on master. It has been annoying to keep that scripted diff up to date anyway
  83. pinheadmz force-pushed on Oct 17, 2023
  84. DrahtBot removed the label Needs rebase on Oct 17, 2023
  85. in src/httprpc.cpp:203 in 125ef7a618 outdated
    198@@ -199,10 +199,8 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
    199                 req->WriteReply(HTTP_FORBIDDEN);
    200                 return false;
    201             }
    202-            UniValue result = tableRPC.execute(jreq);
    203 
    204-            // Send reply
    205-            strReply = JSONRPCReply(result, NullUniValue, jreq.id);
    206+            reply = JSONRPCExecOne(jreq, valRequest);
    


    ryanofsky commented at 9:24 pm on December 4, 2023:

    In commit “rpc: refactor single/batch requests” (125ef7a61885d2ffc0d32b66cdfb3dced2a4417b)

    This change doesn’t seem right because jreq.parse was just called above on line 196, and now the request is going to be parsed for a second time inside JSONRPCExecOne for no reason. I think this could be fixed by just dropping the jreq.parse call inside JSONRPCExecOne. It looks like later commit 4f22286c4aa65a113c44153847c857075a4225d6 does this anyway, so would be better to drop it up front and not change semantics multiple times. It would also be good to make the ExecOne jreq parameter const at that point since it should no longer be modifying the request. I would also rename ExecOne and ExecBatch functions to avoid giving the misleading impression that they are alternatives to each other, when actually they are operating at different levels and do not do the same things. ExecBatch handles errors, ExecOne does not. ExecBatch parses and executes, ExecOne should only execute requests without parsing them. Actually, I would probably just drop the ExecBatch function entirely since it is short and only called in one place. It would be clearer to just inline it, IMO.


    pinheadmz commented at 8:02 pm on December 8, 2023:

    Great review, thank you! I cleaned up the commit history in a rebase. The diff is minimal but in the range diff you can see the rebasing removed a commit:

    git range-diff fbcf1029a7ba47d07a4566cf1f9d2e7b4870304a 576ec3e2debdb63e142eee2c2fa00af995542f42 5d5e8cbca785db851747c5143dc1d651a1ea11e6

    Regarding naming and in-lining, I could see the two functions being named JSONRPCParseBatch() and JSONRPCExec()? I still like both of those definitions in server.cpp but if you think the “parse batch” logic can live happily ever after in HTTPReq_JSONRPC() I won’t protest. Either way would these 1 or 2 changes make more sense as a nice easy commit on top? Or should be rebased into the branch.


    ryanofsky commented at 5:45 pm on December 14, 2023:

    In commit “rpc: refactor single/batch requests” (fcc8cd08dbed9a44bab277ad267306dd2a9f5199)

    re: #27101 (review)

    I’d specifically suggest deleting the JSONRPCExecBatch function in this commit and moving the code it contains to httprpc.cpp (the only place it’s called). Also would rename JSONRPCExecOne to JSONRPCExec. Reasons:

    • It would make division of responsibilities of httprpc and rpc/server.cpp clear. Instead of both of these files iterating over the batch object (former for method whitelisting, latter for method execution), the batch iterating code would live in one place and the server.cpp would not need to be concerned with batching.
    • It would avoid giving false impression in httprpc.cpp that JSONRPCExecBatch and JSONRPCExecOne functions are alternatives to each other, when they aren’t because they don’t handle errors the same way, and one modifies the request while the other doesn’t.
    • It would make error handling more straightforward. The fact that ExecBatch function catches exceptions internally, while ExecOne throws them up to the caller is a footgun.
    • It would reduce the size of the code and as well as the size of the commit diff.
  86. pinheadmz force-pushed on Dec 8, 2023
  87. in src/rpc/server.cpp:359 in fcc8cd08db outdated
    355@@ -356,36 +356,32 @@ bool IsDeprecatedRPCEnabled(const std::string& method)
    356     return find(enabled_methods.begin(), enabled_methods.end(), method) != enabled_methods.end();
    357 }
    358 
    359-static UniValue JSONRPCExecOne(JSONRPCRequest jreq, const UniValue& req)
    360+UniValue JSONRPCExecOne(JSONRPCRequest& jreq)
    


    ryanofsky commented at 5:34 pm on December 14, 2023:

    In commit “rpc: refactor single/batch requests” (fcc8cd08dbed9a44bab277ad267306dd2a9f5199)

    Would be clearer if jreq were a const reference, since it is not modified

  88. in src/rpc/request.h:20 in bcf90ae6b7 outdated
    25@@ -40,4 +26,18 @@ class JSONRPCRequest
    26     void parse(const UniValue& valRequest);
    27 };
    28 
    29+UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id);
    


    ryanofsky commented at 6:08 pm on December 14, 2023:

    In commit “MOVEONLY: define class before functions in request.h” (bcf90ae6b7fd4685755c3f2e1e736053e613e0f6)

    It seems ok to move these functions, but for future reference, there should be no need to do this because you can forward declare the class with class JSONRPCRequest; at the top of the file. I do think it would be slightly preferable to do that instead of moving code, but it’s not important.

  89. in src/rpc/request.cpp:198 in 8c619f70ba outdated
    193+    m_json_version = JSONVersion::JSON_1_BTC;
    194+
    195+    // Check for JSON-RPC 2.0
    196+    const UniValue& valJsonRPC = request.find_value("jsonrpc");
    197+    if (!valJsonRPC.isNull()) {
    198+        if (!valJsonRPC.isStr())
    


    ryanofsky commented at 6:12 pm on December 14, 2023:

    In commit “MOVEONLY: define class before functions in request.h” (bcf90ae6b7fd4685755c3f2e1e736053e613e0f6)

    Not important, but I think the developer guide suggests braces around multiline if statements because it can prevent subtle bugs from sneaking in. Generally only older code in bitcoin core omits the braces on multiline if statements.

  90. in src/rpc/request.cpp:45 in 55dae852d8 outdated
    46-    else
    47-        reply.pushKV("result", result);
    48-    reply.pushKV("error", error);
    49+    if (json_version == JSONVersion::JSON_2_0) {
    50+        reply.pushKV("jsonrpc", "2.0");
    51+        // JSON-RPC 2.0: error and result are mutually exclusive
    


    ryanofsky commented at 7:22 pm on December 14, 2023:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (55dae852d88cdc6a4ad7e4a5cd3ba1988af9872c)

    This comment is confusing because result and error values are mutually exclusive in both cases.

    You could clarify the comment saying “error and result keys are mutually exclusive, to clarify that the comment is referring to keys not values. But I think the way this is written with duplicated logic is unnecessarily confusing anyway. Would suggest just writing the logic once and explaining what it is doing:

     0// Add JSON-RPC version number field in v2 only.
     1if (json_version == JSONVersion::JSON_2_0) reply.pushKV("jsonrpc", "2.0");
     2
     3// Add both result and error fields in v1, even though one will be null.
     4// Omit the null field in v2.
     5if (error.isNull()) {
     6    reply.pushKV("result", result);
     7    if (json_version == JSONVersion::JSON_1_BTC) reply.pushKV("error", NullUniValue);
     8} else {
     9    if (json_version == JSONVersion::JSON_1_BTC) reply.pushKV("result", NullUniValue);
    10    reply.pushKV("error", error);
    11}
    
  91. in src/httprpc.cpp:212 in 28e4d9e7dd outdated
    208+            if (!maybe_result) {
    209+                // Even though we do execute notifications, we do not respond to them
    210+                req->WriteReply(HTTP_NO_CONTENT);
    211+                return true;
    212+            } else {
    213+                reply = maybe_result.value();
    


    ryanofsky commented at 7:31 pm on December 14, 2023:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (28e4d9e7dd1604f1504f191d258a32f495c2fb4f)

    Would suggest reply = std::move(*maybe_result); to avoid copying the result object

  92. in src/rpc/server.cpp:395 in 28e4d9e7dd outdated
    389@@ -386,7 +390,9 @@ UniValue JSONRPCExecBatch(JSONRPCRequest& jreq, const UniValue& vReq)
    390         // in "HTTP OK" responses.
    391         try {
    392             jreq.parse(vReq[reqIdx]);
    393-            ret.push_back(JSONRPCExecOne(jreq));
    394+            auto maybe_result = JSONRPCExecOne(jreq);
    395+            if (maybe_result)
    396+                ret.push_back(maybe_result.value());
    


    ryanofsky commented at 8:01 pm on December 14, 2023:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (28e4d9e7dd1604f1504f191d258a32f495c2fb4f)

    Would suggest std::move(*maybe_result) here as well to avoid copying

  93. in src/rpc/server.cpp:382 in 28e4d9e7dd outdated
    374@@ -375,7 +375,11 @@ UniValue JSONRPCExecOne(JSONRPCRequest& jreq)
    375         result = tableRPC.execute(jreq);
    376     }
    377 
    378-    return JSONRPCReplyObj(result, NullUniValue, jreq.id, jreq.m_json_version);
    379+    if (jreq.IsNotification()) {
    380+        return std::nullopt;
    381+    } else {
    382+        return JSONRPCReplyObj(result, NullUniValue, jreq.id, jreq.m_json_version);;
    383+    }
    


    ryanofsky commented at 1:30 am on December 18, 2023:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (28e4d9e7dd1604f1504f191d258a32f495c2fb4f)

    The implementation of this notification feature seems unnecessarily complicated. If you take earlier suggestion to delete the JSONRPCExecBatch function, support for notifications could be implemented entirely in the HTTPReq_JSONRPC function without sprawl across multiple files and extra complexity in rpc/server.cpp. rpc/server.cpp could just generate replies, not worry about what happens to replies after they are returned, and not have to deal with notifications or batching.

  94. in test/functional/interface_rpc.py:63 in b41f09cbc0 outdated
    60+            # Request that will fail.  The whole batch request should still work fine.
    61             {"method": "invalidmethod", "id": 2},
    62             # Another call that should succeed.
    63             {"method": "getblockhash", "id": 3, "params": [0]},
    64+            # Invalid request format
    65+            {"pizza": "sausage", "id": 4},
    


    ryanofsky commented at 2:17 am on December 18, 2023:

    In commit “test: cover more HTTP error codes in interface_rpc.py” (b41f09cbc09a6f5d75636eb2f7ba8bc6d8802942)

    Is this new line just expanding test coverage and not testing new behavior? Not critical, but in general it helps reviewing if test coverage is expanded first, before commits making code changes. Then it is a lot easier to review the code changes, because you can see how they affect tests. And it is easier to identify gaps in test coverage. An initial commit expanding test coverage before making any code changes is an ideal way to clarify current behavior before changing it.

  95. in test/functional/interface_rpc.py:88 in b41f09cbc0 outdated
    86 
    87-        expect_http_status(404, -32601, self.nodes[0].invalidmethod)
    88-        expect_http_status(500, -8, self.nodes[0].getblockhash, 42)
    89+        expect_http_status(404, -32601, self.nodes[0].invalidmethod)    # RPC_METHOD_NOT_FOUND
    90+        expect_http_status(500, -8,     self.nodes[0].getblockhash, 42) # RPC_INVALID_PARAMETER
    91+        expect_http_status(200, None,   self.nodes[0].getblockhash, 0)
    


    ryanofsky commented at 2:20 am on December 18, 2023:

    In commit “test: cover more HTTP error codes in interface_rpc.py” (b41f09cbc09a6f5d75636eb2f7ba8bc6d8802942)

    As noted above, 200 status code appears to be ignored here and not checked. Would suggest just dropping this line or finding a different way to test this code path, if it’s important.

  96. in test/functional/test_framework/authproxy.py:164 in c85c9998dc outdated
    160@@ -161,12 +161,14 @@ def _get_response(self):
    161                 'code': -342, 'message': 'missing HTTP response from server'})
    162 
    163         content_type = http_response.getheader('Content-Type')
    164-        if content_type != 'application/json':
    165+        if content_type != 'application/json' and http_response.status != HTTPStatus.NO_CONTENT:
    


    ryanofsky commented at 2:34 am on December 18, 2023:

    In commit “test: handle 204 NO_CONTENT correctly in authproxy” (c85c9998dc98d15a7e42a0d2eb142cb516126243)

    The logic in this function seems too permissive:

    • If an empty “application/json” response is returned, now it returns None instead of treating the missing JSON as an error
    • It does not treat responses that have NO_CONTENT status code but do have content as errors. It will even parse json and return from NO_CONTENT responses and ignore the content_type value.

    The code is also more confusing now that these cases are mixed together instead of being handled separately. Would suggest just checking for the NO_CONTENT status up front, actually verifying that no content is present, and not mixing up the no content code with the json parsing code.

  97. in test/functional/interface_rpc.py:62 in 65c2f4448d outdated
    63             # Another call that should succeed.
    64-            {"method": "getblockhash", "id": 3, "params": [0]},
    65+            {"version": "1.1", "method": "getblockhash", "id": 3, "params": [0]},
    66             # Invalid request format
    67-            {"pizza": "sausage", "id": 4},
    68+            {"version": "1.1", "pizza": "sausage", "id": 4},
    


    ryanofsky commented at 1:22 pm on December 18, 2023:

    In commit “test: enable JSON-RPC 2.0 calls in tests and verify expected behavior” (65c2f4448dc556e7a922ebac4abed7ddfc8d7745)

    This test code is broken because the requests do not have unique ids. So when the responses come back, the later responses override earlier ones in the result_by_id array, the existing test cases without a version fields will not be checked, and any result they return will be treated as valid.

    I also think copying and pasting existing test cases for version 1.1 and version 2.0 makes this code harder to understand and less maintainable. The differences between versions should be small so IMO it would be better to just add a version argument (or version options like #27101 (review)) to the test_batch_request method and call it 3 times. The other advantage of making the version variable is that it will make it more obvious what the differences between versions actually are. For example it is easy to miss the fact that 1.1 and 2.0 test cases are actually passing the version number in two different fields.

  98. in test/functional/test_framework/authproxy.py:128 in 65c2f4448d outdated
    127+               'id': AuthServiceProxy.__id_count}
    128+        if AuthServiceProxy.jsonrpc20:
    129+            ret["jsonrpc"] = "2.0"
    130+        else:
    131+            ret["version"] = "1.1"
    132+        return ret
    


    ryanofsky commented at 1:51 pm on December 18, 2023:

    In commit “test: enable JSON-RPC 2.0 calls in tests and verify expected behavior” (65c2f4448dc556e7a922ebac4abed7ddfc8d7745)

    I think instead of hardcoding this logic in this AuthServiceProxy method and not allowing it to be reused for batch requests, it would be better to define a simple utility function that could be used both here and in the test_batch_request test case, and by any other code that needs to send batch requests. Something like:

    0def format_rpc_request(req, v1=False, v2=False):
    1    if v1: req["version"] = "1.1"
    2    if v2: req["jsonrpc"] = "2.0"
    3    return req
    
  99. in doc/release-notes-27101.md:5 in 5d5e8cbca7 outdated
    0@@ -0,0 +1,41 @@
    1+JSON-RPC
    2+--------
    3+
    4+The JSON-RPC server will behave and respond to JSON-RPC 2.0 requests with
    5+strict adherence to the specification (https://www.jsonrpc.org/specification).
    


    ryanofsky commented at 2:11 pm on December 18, 2023:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (5d5e8cbca785db851747c5143dc1d651a1ea11e6)

    This is vague and doesn’t say what’s changing. Would suggest something more like “The server now recognizes JSON-RPC 2.0 requests and treats them differently. Previously the JSON-RPC version was ignored, now JSON-RPC 2.0 requests follow the specification by:

    • Returning HTTP “204 No Content” responses to JSON-RPC 2.0 notifications instead of full responses.
    • Returning HTTP “200 OK” responses in all other cases, rather than 404 responses for unknown methods, 500 responses for invalid parameters, etc.
    • Returning either “result” fields or “error” fields in JSON-RPC responses, rather than returning both fields with one field set to null.”
  100. in src/rpc/util.cpp:163 in 5d5e8cbca7 outdated
    159@@ -160,8 +160,9 @@ std::string HelpExampleCliNamed(const std::string& methodname, const RPCArgList&
    160 
    161 std::string HelpExampleRpc(const std::string& methodname, const std::string& args)
    162 {
    163-    return "> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", \"id\": \"curltest\", "
    164-        "\"method\": \"" + methodname + "\", \"params\": [" + args + "]}' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n";
    165+    return "> curl --user myusername --data-binary "
    


    ryanofsky commented at 2:27 pm on December 18, 2023:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (5d5e8cbca785db851747c5143dc1d651a1ea11e6)

    Review note: The only thing actually changing here is the number 1 to the number 2. The new quotes and line breaks have no affect on the program.

  101. in src/rpc/request.cpp:32 in 5d5e8cbca7 outdated
    25@@ -26,6 +26,14 @@
    26  *
    27  * 1.0 spec: http://json-rpc.org/wiki/specification
    28  * 1.2 spec: http://jsonrpc.org/historical/json-rpc-over-http.html
    29+ *
    30+ * If the server receives a request with the JSON-RPC 2.0 marker `{"jsonrpc": "2.0"}`
    31+ * then Bitcoin will respond with a strictly specified response, and without
    32+ * HTTP errors (unless there actually is a server error or invalid request).
    


    ryanofsky commented at 2:29 pm on December 18, 2023:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (5d5e8cbca785db851747c5143dc1d651a1ea11e6)

    Maybe say “invalidly formatted” rather than just “invalid” because otherwise it’s not obvious what types of requests would or wouldn’t cause http errors.

  102. in doc/release-notes-27101.md:7 in 5d5e8cbca7 outdated
    0@@ -0,0 +1,41 @@
    1+JSON-RPC
    2+--------
    3+
    4+The JSON-RPC server will behave and respond to JSON-RPC 2.0 requests with
    5+strict adherence to the specification (https://www.jsonrpc.org/specification).
    6+
    7+Examples including HTTP response codes:
    


    ryanofsky commented at 2:34 pm on December 18, 2023:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (5d5e8cbca785db851747c5143dc1d651a1ea11e6)

    Instead of including these examples here where it’s less they will be seen after next release, would suggest adding a JSON-RPC 2.0 section to the https://github.com/bitcoin/bitcoin/blob/master/doc/JSON-RPC-interface.md doc and incorporating these examples in that documentation


    fanquake commented at 3:13 pm on December 18, 2023:
    Yea, I think that’s better, as we don’t really want to copy-paste these big blocks of curl output into the middle of our release notes.
  103. in src/rpc/request.cpp:199 in 8c619f70ba outdated
    194+
    195+    // Check for JSON-RPC 2.0
    196+    const UniValue& valJsonRPC = request.find_value("jsonrpc");
    197+    if (!valJsonRPC.isNull()) {
    198+        if (!valJsonRPC.isStr())
    199+            throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
    


    ryanofsky commented at 2:36 pm on December 18, 2023:

    In commit “rpc: identify JSON-RPC 2.0 requests” (8c619f70ba0021265041493b25a98f77afa96c55)

    I might have missed it, but I don’t think I noticed a test case for this condition. Could consider adding one

  104. ryanofsky approved
  105. ryanofsky commented at 3:10 pm on December 18, 2023: contributor

    Code review ACK 5d5e8cbca785db851747c5143dc1d651a1ea11e6. This is definitely an improvement overall, but I think it leaves httprpc.cpp and rpc/server.cpp organization and the functional tests in a messier state, and I left suggestions for cleanup.

    I was also confused by parts of the PR description. I’m not sure what “different behavior for single and batched RPC requests” and “same behavior for single and batch requests” refers to, and think these parts could be omitted or clarified. Also “This patch does NOT implement JSON RPC 2.0 “notifications”” doesn’t seem accurate anymore.

  106. DrahtBot requested review from vincenzopalazzo on Dec 18, 2023
  107. DrahtBot requested review from willcl-ark on Dec 18, 2023
  108. DrahtBot requested review from sipa on Dec 18, 2023
  109. DrahtBot requested review from fanquake on Dec 18, 2023
  110. DrahtBot removed review request from vincenzopalazzo on Dec 18, 2023
  111. DrahtBot requested review from vincenzopalazzo on Dec 18, 2023
  112. DrahtBot removed review request from vincenzopalazzo on Dec 18, 2023
  113. DrahtBot requested review from vincenzopalazzo on Dec 18, 2023
  114. DrahtBot removed review request from vincenzopalazzo on Dec 18, 2023
  115. DrahtBot requested review from vincenzopalazzo on Dec 18, 2023
  116. DrahtBot added the label CI failed on Dec 23, 2023
  117. pinheadmz force-pushed on Dec 31, 2023
  118. pinheadmz force-pushed on Dec 31, 2023
  119. pinheadmz force-pushed on Jan 1, 2024
  120. DrahtBot removed the label CI failed on Jan 1, 2024
  121. pinheadmz force-pushed on Jan 2, 2024
  122. pinheadmz commented at 4:13 pm on January 2, 2024: member

    Thanks @ryanofsky for the thorough review. I was misunderstanding the different roles of httprpc and server and I think tiptoeing around minimal commits when more changes were needed to really clean everything up.

    JSONRPCExec: the only function now that executes calls, batches are just handled in HTTPReq_JSONRPC as you suggested.

    Tests: additional coverage is now the first commit, and new tests are in the last commits. I replaced the helper functions in interface_rpc.py to provide lower level control over the requests and responses than provided by the authproxy module which I was trying to hack in previous versions of this branch. In a future PR we can update authproxy to use jsonrpc 2.0 without having to change this (or I think, any) existing test

    Docs: I moved the more verbose explanation to JSON-RPC-interface.md as suggested by you and @fanquake and cut down the release note to a brief summary.

    PR description: updated for the code changes.

  123. in src/httprpc.cpp:227 in 27afe01560 outdated
    221@@ -223,13 +222,27 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
    222                     }
    223                 }
    224             }
    225-            strReply = JSONRPCExecBatch(jreq, valRequest.get_array());
    226+
    227+            // Execute each request
    228+            UniValue ret(UniValue::VARR);
    


    ryanofsky commented at 2:22 am on January 10, 2024:

    In commit “rpc: refactor single/batch requests” (27afe01560b1d930c3b072a180cab95c857eff24)

    This ret variable seems unnecessary. Can this just say reply = UniValue{UniValue::VARR} and use the reply variable instead of ret below?

    The name is also confusing because ret normally refers to a return value, which this is not what this is.

  124. in src/rpc/server.cpp:367 in 27afe01560 outdated
    390-    UniValue ret(UniValue::VARR);
    391-    for (unsigned int reqIdx = 0; reqIdx < vReq.size(); reqIdx++)
    392-        ret.push_back(JSONRPCExecOne(jreq, vReq[reqIdx]));
    393-
    394-    return ret.write() + "\n";
    395+    return JSONRPCReplyObj(result, NullUniValue, jreq.id);
    


    ryanofsky commented at 2:30 am on January 10, 2024:

    In commit “rpc: refactor single/batch requests” (27afe01560b1d930c3b072a180cab95c857eff24)

    Not new behavior but the result object, which is potentially large, is copied for no reason here. It would be nice in a separate commit to change JSONRPCReplyObj to accept a plain UniValue parameter instead of const UniValue& so std::move(result) could be passed here, and the result wouldn’t be copied unnecessarily.


    pinheadmz commented at 8:10 pm on January 22, 2024:
    Ok I understand. I’m taking this suggestion and also using std::move a few places in bitcoin-cli where potentially large result objects are being passed. I am leaving as-is however when the argument is NullUniValue, I think that’s correct but please let me know if I should use std::move at the other call sites as well.
  125. in src/rpc/request.cpp:199 in 9e1c6fa84e outdated
    178+    const UniValue& valJsonRPC = request.find_value("jsonrpc");
    179+    if (!valJsonRPC.isNull()) {
    180+        if (!valJsonRPC.isStr()) {
    181+            throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
    182+        }
    183+        if (valJsonRPC.get_str() == "2.0") {
    


    ryanofsky commented at 3:27 am on January 10, 2024:

    In commit “rpc: identify JSON-RPC 2.0 requests” (9e1c6fa84eef606d5504ebcbbd2a2ca7a4f748dc)

    The spec says this field ‘MUST be exactly “2.0”’ (https://www.jsonrpc.org/specification#request_object), so maybe it should be an error if the field is set and contains any other value. This way if there is ever a JSON-RPC 2.1 or 3.0 protocol, clients can easily check for support without getting back a strange JSON 1.1 response. FWIW, chatgpt suggestd the following response:

    0{
    1    "jsonrpc": "2.0",
    2    "error": {
    3        "code": -32600,
    4        "message": "Invalid Request",
    5        "data": "JSON-RPC version not supported."
    6    },
    7    "id": null
    8}
    

    And setting the id field to the actual request ID if possible to determine

  126. in src/rpc/request.cpp:63 in c9196a2e5b outdated
    53         reply.pushKV("result", result);
    54-    reply.pushKV("error", error);
    55+        if (json_version == JSONVersion::JSON_1_BTC) reply.pushKV("error", NullUniValue);
    56+    } else {
    57+        reply.pushKV("error", error);
    58+        if (json_version == JSONVersion::JSON_1_BTC) reply.pushKV("result", NullUniValue);
    


    ryanofsky commented at 3:35 am on January 10, 2024:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (c9196a2e5b747753e0d5ee59e1bcb987943eedbf)

    I think these two lines should be switched to always return the result field before the error field. This would make responses look more consistent and avoid changing previous behavior.

  127. in src/rpc/server.cpp:370 in c9196a2e5b outdated
    368+        // JSONRPC 2.0 behavior: only throw HTTP error if the server is actually
    369+        // broken. Otherwise errors are sent back in "HTTP OK" responses.
    370+        try {
    371+            result = tableRPC.execute(jreq);
    372+        } catch (const UniValue& objError) {
    373+            return JSONRPCReplyObj(NullUniValue, objError, jreq.id, jreq.m_json_version);
    


    ryanofsky commented at 3:41 am on January 10, 2024:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (c9196a2e5b747753e0d5ee59e1bcb987943eedbf)

    Again suggestion for an separate earlier commit, but it would be good if JSONRPCReplyObj took a UniValue error parameter instead of const UniValue& error so the error object could be moved instead of copied.


    pinheadmz commented at 8:43 pm on January 24, 2024:
    I updated the function JSONRPCReplyObj to expect plain ol’ UniValue for both response and error, and added std::move() where it seemed applicable. Tidy didn’t like std::move() here because objError is already a const, so my understanding is that the error message will continue to be passed by reference through the end of JSONRPCReplyObj but my understanding is limited

    ryanofsky commented at 0:22 am on March 8, 2024:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (9cae84c18706ce3b23b6f31466553161112deb70)

    re: #27101 (review)

    my understanding is that the error message will continue to be passed by reference through the end of JSONRPCReplyObj

    It will be passed by value, but it will be copied because std::move is not specified, so this is less than ideal. The fix here is just to drop const from the catch declaration and then add std::move to move the value from the exception to the reply object.

  128. in src/httprpc.cpp:248 in bc2d55898b outdated
    247+                    response = JSONRPCReplyObj(NullUniValue, objError, jreq.id, jreq.m_json_version);
    248                 } catch (const std::exception& e) {
    249-                    ret.push_back(JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version));
    250+                    response = JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
    251                 }
    252+                if (!jreq.IsNotification()) ret.push_back(response);
    


    ryanofsky commented at 3:44 am on January 10, 2024:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (bc2d55898b315d1ee80a70377d65f79c829246ed)

    Would be good to std::move(response) here to avoid a copy

  129. ryanofsky commented at 3:47 am on January 10, 2024: contributor
    Reviewed 756da6d1e67fba65a992dc0090ee8c0cfa26abe3 and this looks very good. So far I reviewed all the c++ code, but not the python or documentation yet. Left several suggestions, but feel free to ignore them, they are not important.
  130. in test/functional/interface_rpc.py:27 in d61e469179 outdated
    31-        req = body
    32-        req["version"] = "1.1"
    33-        req["id"] = Format.ID_COUNT
    34-        Format.ID_COUNT += 1
    35+    def rpc_request(body, version=1, notification=False):
    36+        req = deepcopy(body)
    


    ryanofsky commented at 3:56 pm on January 10, 2024:

    In commit “test: cover JSONRPC 2.0 requests, batches, and notifications” (d61e469179dae4e94fd00f47c2eeb8533d19e13c)

    It’s not great to make this a static method and use a global variable, because now tests are no longer independent of each other and can potentially interfere with each other. Also using deep copy is unnecessary here because only the top level dictionary is modified. Also the name of the class “Format” is not very descriptive. Would suggest cleaning all this up:

     0--- a/test/functional/interface_rpc.py
     1+++ b/test/functional/interface_rpc.py
     2@@ -6,7 +6,6 @@
     3 
     4 import json
     5 import os
     6-from copy import deepcopy
     7 from test_framework.test_framework import BitcoinTestFramework
     8 from test_framework.util import assert_equal, assert_greater_than_or_equal
     9 from threading import Thread
    10@@ -19,19 +18,19 @@ RPC_INVALID_REQUEST        = -32600
    11 RPC_PARSE_ERROR            = -32700
    12 
    13 
    14-class Format:
    15-    ID_COUNT = 0
    16+class RequestBuilder:
    17+    def __init__(self):
    18+        self.id_count = 0
    19 
    20- [@staticmethod](/bitcoin-bitcoin/contributor/staticmethod/)
    21-    def rpc_request(body, version=1, notification=False):
    22-        req = deepcopy(body)
    23+    def rpc_request(self, fields, version=1, notification=False):
    24+        req = dict(**fields)
    25         if version == 1:
    26             req["version"] = "1.1"
    27         if version == 2:
    28             req["jsonrpc"] = "2.0"
    29         if not notification:
    30-            req["id"] = Format.ID_COUNT
    31-            Format.ID_COUNT += 1
    32+            req["id"] = self.id_count
    33+            self.id_count += 1
    34         return req
    35 
    36 
    37@@ -45,7 +44,7 @@ def send_json_rpc(node, body: object) -> tuple[object, int]:
    38 
    39 
    40 def expect_http_rpc_status(expected_http_status, expected_rpc_error_code, node, method, params=None, version=1, notification=False):
    41-    req = Format.rpc_request({"method": method, "params": params}, version, notification)
    42+    req = RequestBuilder().rpc_request({"method": method, "params": params}, version, notification)
    43     response, status = send_json_rpc(node, req)
    44 
    45     if expected_rpc_error_code is not None:
    46@@ -91,11 +90,12 @@ class RPCInterfaceTest(BitcoinTestFramework):
    47             # Invalid request format
    48             {"pizza": "sausage"}
    49         ]
    50+        builder = RequestBuilder()
    51 
    52         self.log.info("Testing basic JSON-RPC 1.1 batch request...")
    53         body = []
    54         for cmd in commands:
    55-            body.append(Format.rpc_request(cmd))
    56+            body.append(builder.rpc_request(cmd))
    57         response, status = send_json_rpc(self.nodes[0], body)
    58         assert_equal(status, 200)
    59         # JSON 1.1: Every response has a "result" and an "error"
    60@@ -112,7 +112,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
    61         self.log.info("Testing basic JSON-RPC 2.0 batch request...")
    62         body = []
    63         for cmd in commands:
    64-            body.append(Format.rpc_request(cmd, version=2))
    65+            body.append(builder.rpc_request(cmd, version=2))
    66         response, status = send_json_rpc(self.nodes[0], body)
    67         assert_equal(status, 200)
    68         # JSON 2.0: Each response has either a "result" or an "error"
    69@@ -130,7 +130,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
    70         body = []
    71         version = 1
    72         for cmd in commands:
    73-            body.append(Format.rpc_request(cmd, version=version))
    74+            body.append(builder.rpc_request(cmd, version=version))
    75             version = 2 if version == 1 else 1
    76         response, status = send_json_rpc(self.nodes[0], body)
    77         assert_equal(status, 200)
    78@@ -149,7 +149,7 @@ class RPCInterfaceTest(BitcoinTestFramework):
    79         body = []
    80         notification = False
    81         for cmd in commands:
    82-            body.append(Format.rpc_request(cmd, version=2, notification=notification))
    83+            body.append(builder.rpc_request(cmd, version=2, notification=notification))
    84             notification = True
    85         response, status = send_json_rpc(self.nodes[0], body)
    86         assert_equal(status, 200)
    
  131. in test/functional/interface_rpc.py:41 in 29ebb0d777 outdated
    45+def send_json_rpc(node, body: object) -> tuple[object, int]:
    46+    raw = json.dumps(body).encode("utf-8")
    47+    return send_raw_rpc(node, raw)
    48+
    49+
    50+def expect_http_rpc_status(expected_http_status, expected_rpc_error_code, node, method, params=None):
    


    ryanofsky commented at 4:20 pm on January 10, 2024:

    In commit “test: cover more HTTP error codes in interface_rpc.py” (29ebb0d7774ee2d10aa274086e8ff4b2395f7047)

    Setting params to None seems like a bug because it means a "params": null JSON field will be sent, when previously authproxy would send an empty params object. Would suggest changing this to params={} to avoid a change in behavior.

    Also, I’m not sure if it is a bug for our server to accept null params. The spec says “If present, parameters for the rpc call MUST be provided as a Structured value” and “params MUST be an Array” or “params MUST be an Object”, so it seems like null is not supposed to be allowed, and probably should trigger a server error.


    pinheadmz commented at 6:27 pm on January 22, 2024:

    I actually had this as params=[] a few updates ago but that fails lint (see below) but I’ll clean it up with a different work-around:

     0A mutable list or dict seems to be used as default parameter value:
     1
     2test/functional/interface_rpc.py:41:def expect_http_rpc_status(expected_http_status, expected_rpc_error_code, node, method, params={}):
     3
     4This is how mutable list and dict default parameter values behave:
     5
     6>>> def f(i, j=[], k={}):
     7...     j.append(i)
     8...     k[i] = True
     9...     return j, k
    10...
    11>>> f(1)
    12([1], {1: True})
    13>>> f(1)
    14([1, 1], {1: True})
    15>>> f(2)
    16([1, 1, 2], {1: True, 2: True})
    17
    18The intended behaviour was likely:
    19
    20>>> def f(i, j=None, k=None):
    21...     if j is None:
    22...         j = []
    23...     if k is None:
    24...         k = {}
    25...     j.append(i)
    26...     k[i] = True
    27...     return j, k
    28...
    29>>> f(1)
    30([1], {1: True})
    31>>> f(1)
    32([1], {1: True})
    33>>> f(2)
    34([2], {2: True})
    35^---- failure generated from lint-python-mutable-default-parameters.py
    
  132. in doc/JSON-RPC-interface.md:83 in 756da6d1e6 outdated
    78+
    79+The server recognizes [JSON-RPC v2.0](https://www.jsonrpc.org/specification) requests
    80+and responds accordingly. A 2.0 request is identified by the presence of
    81+`"jsonrpc": "2.0"` in the request body. If that key + value is not present in a request,
    82+the legacy JSON-RPC v1.1 protocol is followed instead, which was the only available
    83+protocol in previous releases. Protocols can be mixed in a batch request.
    


    ryanofsky commented at 4:42 pm on January 10, 2024:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (756da6d1e67fba65a992dc0090ee8c0cfa26abe3)

    Would drop “Protocols can be mixed in a batch request.” It seems ok, but not a good thing to encourage, and unless it’s required by the specs, I don’t think we should commit to this behavior by adding it to our documentation.

  133. in doc/JSON-RPC-interface.md:89 in 756da6d1e6 outdated
    83+protocol in previous releases. Protocols can be mixed in a batch request.
    84+
    85+|| 1.1 | 2.0 |
    86+|-|-|-|
    87+| Request marker | `"version": "1.1"` (or none) | `"jsonrpc": "2.0"` |
    88+| `"error"` and `"result"` fields in response | both present | only one is present |
    


    ryanofsky commented at 4:44 pm on January 10, 2024:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (756da6d1e67fba65a992dc0090ee8c0cfa26abe3)

    Think this should also mention that responses contain “jsonrpc”: “2.0” field. Or this could be an additional row in the table.

  134. in src/rpc/request.h:48 in bc2d55898b outdated
    44@@ -45,6 +45,7 @@ class JSONRPCRequest
    45     JSONVersion m_json_version = JSONVersion::JSON_1_BTC;
    46 
    47     void parse(const UniValue& valRequest);
    48+    [[nodiscard]] bool IsNotification() const { return id.isNull() && m_json_version == JSONVersion::JSON_2_0; };
    


    ryanofsky commented at 4:56 pm on January 10, 2024:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (bc2d55898b315d1ee80a70377d65f79c829246ed)

    This does not seem correct. According to https://www.jsonrpc.org/specification#notification a notification is a ‘Request object without an “id” member’, not a request object with a null id member. It also explicitly says requests that have “Null as a value for the id member” are treated as “Responses with an unknown id” not notifications.

    Probably a reasonable way to fix this would be to change the JSONRPCRequest::id type from UniValue to std::optional<UniValue> to distinction between a null id and a missing id.

  135. in src/rpc/request.cpp:32 in 756da6d1e6 outdated
    25@@ -26,6 +26,17 @@
    26  *
    27  * 1.0 spec: http://json-rpc.org/wiki/specification
    28  * 1.2 spec: http://jsonrpc.org/historical/json-rpc-over-http.html
    29+ *
    30+ * If the server receives a request with the JSON-RPC 2.0 marker `{"jsonrpc": "2.0"}`
    31+ * then Bitcoin will respond with a strictly specified response.
    32+ * It will only return an HTTP error code in response to HTTP errors like if
    


    ryanofsky commented at 5:09 pm on January 10, 2024:

    In commit “doc: add comments and release-notes for JSON-RPC 2.0” (756da6d1e67fba65a992dc0090ee8c0cfa26abe3)

    Maybe replace “errors like if…” with “errors in cases like the endpoint not being found (404) or the RPC request not being formatted correctly (500).” Grammar sounds a little off the way it is written now.

  136. in test/functional/test_framework/authproxy.py:171 in 174a995edf outdated
    159@@ -160,6 +160,11 @@ def _get_response(self):
    160             raise JSONRPCException({
    161                 'code': -342, 'message': 'missing HTTP response from server'})
    162 
    163+        if http_response.status == HTTPStatus.NO_CONTENT:
    164+            if len(http_response.read()) != 0:
    165+                raise JSONRPCException({'code': -342, 'message': 'Content received with NO CONTENT status code'})
    166+            return None, http_response.status
    167+
    


    ryanofsky commented at 5:13 pm on January 10, 2024:

    In commit “test: handle 204 NO_CONTENT correctly in authproxy” (174a995edf594dccf745d2593c281d3e4a1ff7f7)

    It would be good to have a comment that explains why this block of code is needed, because it seems like an impossible case to hit through normal usage of authproxy. Maybe: “# Check for no-content HTTP status code, which can be returned when an RPC client requests a JSON-RPC 2.0 “notification” with no response. Currently this is only possible if clients call the _request() method directly to send a raw request.”

  137. ryanofsky commented at 5:19 pm on January 10, 2024: contributor
    Code review 756da6d1e67fba65a992dc0090ee8c0cfa26abe3. Everything looks pretty good, and most of my suggestions can be ignored, but it seems like this is currently detecting notifications incorrectly and not sending responses in cases when they should be sent, when the request id field is present but null.
  138. DrahtBot added the label CI failed on Jan 13, 2024
  139. pinheadmz force-pushed on Jan 23, 2024
  140. pinheadmz force-pushed on Jan 24, 2024
  141. pinheadmz force-pushed on Jan 24, 2024
  142. pinheadmz commented at 7:31 pm on January 24, 2024: member

    @ryanofsky thanks again for thorough review. I addressed all your comments.

    The one commit I am not fully confident in is 50debf781dbc4ac73cd1c0138ed9d405ea60eacb “rpc: use move semantics in JSONRPCReplyObj()” which is the commit I added to address

    #27101 (review) and #27101 (review)

  143. pinheadmz force-pushed on Jan 24, 2024
  144. DrahtBot removed the label CI failed on Jan 24, 2024
  145. in src/httprpc.cpp:221 in 98031401c7 outdated
    213+                req->WriteReply(HTTP_NO_CONTENT);
    214+                return true;
    215+            }
    216 
    217         // array of requests
    218         } else if (valRequest.isArray()) {
    


    tdb3 commented at 3:07 pm on January 28, 2024:

    nit: Author’s choice on whether this is addressed or not.

    Minor non-compliance to the JSON RPC 2.0 spec (rpc call with an empty Array in https://www.jsonrpc.org/specification#examples). Providing an empty array results in an empty array provided in the response, but should result in a single error response (e.g. "jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}).

    A check for valRequest.size() == 0 before entering the for loop in 236 could enable error response (rather than empty response array). Perhaps this is best handled when future updates attempt more strict compliance to 2.0?


    pinheadmz commented at 6:55 pm on January 29, 2024:

    This is a good catch but unfortunately I think we need to stick with this behavior for backwards compatibility. If we get a request that does not have the exact key-value pair "jsonrpc":"2.0" we have to resort to legacy behavior. In that case, the user has submitted an empty batch and what they get back is an empty vector.

    The closest 2.0 example I can think of is this:

    0--> [{"jsonrpc":"2.0", "id":""}]
    1<-- [{'jsonrpc': '2.0', 'error': {'code': -32600, 'message': 'Missing method'}, 'id': ''}]
    

    if "jsonrpc" is removed, we revert to legacy behavior and if "id" is removed we get no response at all since that is a notification:

    0--> [{"jsonrpc":"2.0"}]
    1<-- []
    

    This last example actually does look non-compliant according to the last example with an all-notification batch. So I think I will address that


    pinheadmz commented at 7:11 pm on January 29, 2024:

    Thanks!!!

    Fixed in force push to 771d1e1d206efe687b8661ab966cc1a62cc7ba39

     0diff --git a/src/httprpc.cpp b/src/httprpc.cpp
     1index 54354a8625..f678668fd3 100644
     2--- a/src/httprpc.cpp
     3+++ b/src/httprpc.cpp
     4@@ -247,6 +247,11 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
     5                 }
     6                 if (!jreq.IsNotification()) reply.push_back(std::move(response));
     7             }
     8+            if (reply.empty()) {
     9+                // All-notification batch expects no response
    10+                req->WriteReply(HTTP_NO_CONTENT);
    11+                return true;
    12+            }
    13         }
    14         else
    15             throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
    16diff --git a/test/functional/interface_rpc.py b/test/functional/interface_rpc.py
    17index 309345731c..dcb0b66921 100755
    18--- a/test/functional/interface_rpc.py
    19+++ b/test/functional/interface_rpc.py
    20@@ -161,6 +161,14 @@ class RPCInterfaceTest(BitcoinTestFramework):
    21             ]
    22         )
    23 
    24+        self.log.info("Testing JSON-RPC 2.0 batch of ALL notifications...")
    25+        body = []
    26+        for cmd in commands:
    27+            body.append(builder.rpc_request(cmd, version=2, notification=True))
    28+        response, status = send_json_rpc(self.nodes[0], body)
    29+        assert_equal(status, 204) # HTTP_NO_CONTENT
    30+        assert_equal(response, None) # not even an empty array
    31+
    32     def test_http_status_codes(self):
    33         self.log.info("Testing HTTP status codes for JSON-RPC 1.1 requests...")
    34         # OK
    

    tdb3 commented at 1:44 am on February 1, 2024:

    Pulled changes (771d1e1d206efe687b8661ab966cc1a62cc7ba39), built, ran all functional tests (all passed). Re-ran the following on 771d1e1d206efe687b8661ab966cc1a62cc7ba39 and on release v26.0.

    Action (1): Unexpected difference

    curl --user test --data-binary '[]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Unexpected difference

    v26.0 behavior: []

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: (no content) (in alignment with 2.0 spec, but not matching legacy behavior) Applicable line(s): httprpc.cpp:250 if (reply.empty()) { seems to be treating 2.0 and 1.x requests identically. At first glance, seems like a check for 2.0 here might rememdy this (e.g., would need to be both 2.0 and reply.empty()).

    Action (2): Minor Unexpected difference

    curl --user test --data-binary '' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Unexpected difference

    v26.0 behavior: {"result":null,"error":{"code":-32700,"message":"Parse error"},"id":null}

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: {"result":null,"error":{"code":-32700,"message":"Parse error"}} Diff: legacy includes "id":null while new code does not.

    Action (3): Expected difference

    curl --user test --data-binary '[{"jsonrpc":"2.0", "id":""}]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Expected difference

    v26.0 behavior: [{"result":null,"error":{"code":-32600,"message":"Missing method"},"id":""}]

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: [{"jsonrpc":"2.0","error":{"code":-32600,"message":"Missing method"},"id":""}] Seems conformant to 2.0 spec.

    Action (4): Expected difference

    curl --user test --data-binary '[{"jsonrpc":"2.0"}]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Expected difference

    v26.0 behavior: [{"result":null,"error":{"code":-32600,"message":"Missing method"},"id":null}]

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: (no content) Seems compliant to 2.0 spec (batch with only a notification, albeit with a missing method, so no error response).


    ryanofsky commented at 7:32 pm on February 1, 2024:

    re: #27101 (review)

    Action (1): Unexpected difference

    curl --user test --data-binary '[]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Unexpected difference

    v26.0 behavior: []

    PR 27101 (771d1e1) behavior: (no content) (in alignment with 2.0 spec, but not matching legacy behavior) Applicable line(s): httprpc.cpp:250 if (reply.empty()) { seems to be treating 2.0 and 1.x requests identically. At first glance, seems like a check for 2.0 here might rememdy this (e.g., would need to be both 2.0 and reply.empty()).

    I think this is unavoidable if we want to comply with the 2.0 spec which says “If there are no Response objects contained within the Response array as it is to be sent to the client, the server MUST NOT return an empty Array and should return nothing at all.”

    Although from discussion thread above it sounds like maybe we are willing to not fully comply with 2.0 spec if we are prioritizing backwards compatibility over spec compatibility in cases where 2.0 spec is mandating a certain response even when the request does not contain a "jsonrpc":"2.0" field.

    Either approach seems fine but we should probably note this corner case in the RPC doc if favoring compatibility over compliance with the spec, or note it in the release notes if we are going to favor compliance with the spec over compatibility in a case where jsonrpc 2.0 is not actually specified.

    Action (2): Minor Unexpected difference

    curl --user test --data-binary '' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Unexpected difference

    v26.0 behavior: {"result":null,"error":{"code":-32700,"message":"Parse error"},"id":null}

    PR 27101 (771d1e1) behavior: {"result":null,"error":{"code":-32700,"message":"Parse error"}} Diff: legacy includes "id":null while new code does not.

    This is a little surprising. Seems better to keep previous behavior if this isn’t actually specifying jsonrpc 2.0?

    Action (3): Expected difference

    @tdb3, are actions 3-7 you listed bringing up potential bugs or they just recording what you tested? I didn’t look closely all of these but on the surface they seem like expected behavior.


    tdb3 commented at 9:37 pm on February 1, 2024:

    Although from discussion thread above it sounds like maybe we are willing to not fully comply with 2.0 spec if we are prioritizing backwards compatibility over spec compatibility in cases where 2.0 spec is mandating a certain response even when the request does not contain a “jsonrpc”:“2.0” field.

    Yeah, exactly this. If/when support for jsonrpc 1.0 and 1.1 is removed, then the implementation could be updated to achieve full/closer compliance to the 2.0 spec. If it’s decided that jsonrpc 1.0/1.1 support is never to be removed, then we’d have to accept imperfect conformance to 2.0.

    we should probably note this corner case in the RPC doc if favoring compatibility over compliance with the spec, or note it in the release notes if we are going to favor compliance with the spec over compatibility in a case where jsonrpc 2.0 is not actually specified.

    Agreed. This would inform users (of 2.0) and increase awareness for any 1.0/1.1 removal PR. @pinheadmz, how about something like this for doc/JSON-RPC-interface.md:

    Existing text:

    A 2.0 request is identified by the presence of "jsonrpc": "2.0" in the request body. If that key + value is not present in a request, the legacy JSON-RPC v1.1 protocol is followed instead, which was the only available protocol in previous releases.

    Addition:

    0Note: Since backwards compatibility is being prioritized at this time, 
    1and legacy JSON-RPC protocol is followed for responses to requests 
    2without presense of "jsonrpc": "2.0", some non-conformance to the 
    3JSON-RPC 2.0 specification will occur.  For example, a request 
    4containing an empty array ("[]", i.e. body containing an empty array) 
    5would receive a response of "[]" (empty array, matching legacy response 
    6behavior) while the JSON-RPC specification prohibits an empty array as 
    7a response to an all-notification batch.
    

    This is a little surprising. Seems better to keep previous behavior if this isn’t actually specifying jsonrpc 2.0?

    Agreed. 58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb was observed to fix this, so now the response maches the legacy response.

    are actions 3-7 you listed bringing up potential bugs or they just recording what you tested? I didn’t look closely all of these but on the surface they seem like expected behavior.

    Yeah, thank you. I should have been clearer. Actions 3 through 7 are just recording what was tested. All expected behavior (no bugs observed) for 3 through 7.

  146. in src/rpc/request.cpp:207 in 98031401c7 outdated
    199+            throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
    200+        }
    201+        if (valJsonRPC.get_str() == "2.0") {
    202+            m_json_version = JSONVersion::JSON_2_0;
    203+        } else {
    204+            throw JSONRPCError(RPC_INVALID_REQUEST, "JSON-RPC version not supported");
    


    tdb3 commented at 3:09 pm on January 28, 2024:

    Issuing an example command from https://developer.bitcoin.org/reference/rpc/getblockchaininfo.html resulted in error response {"result":null,"error":{"code":-32600,"message":"JSON-RPC version not supported"}} due to the example’s use of "jsonrpc":"1.0". It looks like JSON RPC spec for 1.0 had no version field in the spec, 1.1 (WD/ALT) had the “version” field to specify version, and 2.0 spec has “jsonrpc” (must be set to “2.0”).

    If a client uses a “jsonrpc” field other than “2.0” it will receive the error (from exception thrown in line 202), whereas in the previous implementation the request would be processed successfully. This does seem to affect backwards compatibility with 1.x implementations, although I’m unsure how widespread the usage of the jsonrpc field is in existing client implementations. The original issue #2960 claims 61/70 implementions work only with 2.0 (so likely to use 2.0 as the value in the jsonrpc field and not encounter the error response). At the very least, the developer.bitcoin.org examples seem to be non-comformant to the JSON-RPC specs (1.0 doesn’t define the “jsonrpc” field, 1.1 requires “version”, and 2.0 requires “jsonrpc”).

    Increased backwards compatibility at the expense of strict compliance to 2.0 could be achieved by adding else if clauses to fallback to 1.x handling if the jsonrpc field is “1.0” (example below). Author’s choice. I’d support either way (not continuing to enable non-conformant requests, or enabling non-comformant requests with future notification of 1.x deprecation).

    0} else if(valJsonRPC.get_str() == "1.0") {
    1    /* m_json_version is JSONVersion::JSON_1_BTC */
    2}
    

    pinheadmz commented at 6:43 pm on January 29, 2024:

    Hm this is a bit of a mess, you’re right. The behavior in this PR makes sense in response to @ryanofsky comment: #27101 (review)

    The help text has also been modified in src/rpc/util.cpp (98031401c744ea3c2b62927fabf27c22b639c3cf) to always recommend "jsonrpc":"2.0"

    I think you are illustrating an important issue though – anyone who has used bitcoin-cli help (for ANY RPC command!) would have seen instructions to include "jsonrpc":"1.0" in their raw curl request. That was not only unnecessary, but also wrong.

    So even though I’m trying to maintain complete backwards compatibility, there is a likelihood that users have copied this old help text verbatim and will encounter errors when upgrading.

    I’m not sure how to proceed on this, might need input from other reviewers. "jsonrpc":"1.0" is not a standard anywhere, not even in Bitcoin… but it has been in the RPC help text for a looooong time 😭


    ryanofsky commented at 8:04 pm on January 29, 2024:

    I’m not sure how to proceed on this, might need input from other reviewers. "jsonrpc":"1.0" is not a standard anywhere, not even in Bitcoin… but it has been in the RPC help text for a looooong time 😭

    This is a good find, and I haven’t looked closely, but it seems like there is not any conflict here. We can continue to accept "jsonrpc":"1.0" for compatibility, continue to require `“jsonrpc”:“2.0” for JSON-RPC 2.0 behavior to comply with the 2.0 spec, and reject any other values so if there is a JSON-RPC 2.1 or 3.0 spec in the future, future versions of bitcoin can support it, and clients can determine that they are not supported right now.


    pinheadmz commented at 8:43 pm on January 29, 2024:
    ok, carved out "jsonrpc":"1.0" and tested with push to ec5e7cea2bddfd55f07f9b0654f11d60f5ab0a48

    tdb3 commented at 1:46 am on February 1, 2024:

    Updated with some tests. ACK. Looks fixed!

    Action (5): Successfully executed response, legacy behavior retained

    curl --user test --data-binary '{"jsonrpc":"1.0","method":"getblockchaininfo","id":"tester"}' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Successfully executed response

    v26.0 behavior: {"result":{"chain":"signet","blocks":4320,"headers":4320,"bestblockhash":"0000013e07d388dd5c8cb66c990caa546fc04e7533cad18c27ed3d603f37a6c0","difficulty":0.001127075738664131,"time":1706750596,"mediantime":1706747596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"00000000000000000000000000000000000000000000000000000004de2f42db","size_on_disk":44084938,"pruned":false,"warnings":""},"error":null,"id":"tester"}

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: {"result":{"chain":"signet","blocks":4320,"headers":4320,"bestblockhash":"0000013e07d388dd5c8cb66c990caa546fc04e7533cad18c27ed3d603f37a6c0","difficulty":0.001127075738664131,"time":1706750596,"mediantime":1706747596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"00000000000000000000000000000000000000000000000000000004de2f42db","size_on_disk":44084938,"pruned":false,"warnings":"This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"},"error":null,"id":"tester"}

    Action (6): Successfully executed response, legacy behavior retained

    curl --user test --data-binary '{"version":"1.1","method":"getblockchaininfo","id":"tester"}' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Successfully executed response

    v26.0 behavior: {"result":{"chain":"signet","blocks":4320,"headers":4320,"bestblockhash":"0000013e07d388dd5c8cb66c990caa546fc04e7533cad18c27ed3d603f37a6c0","difficulty":0.001127075738664131,"time":1706750596,"mediantime":1706747596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"00000000000000000000000000000000000000000000000000000004de2f42db","size_on_disk":44084938,"pruned":false,"warnings":""},"error":null,"id":"tester"}

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: {"result":{"chain":"signet","blocks":4320,"headers":4320,"bestblockhash":"0000013e07d388dd5c8cb66c990caa546fc04e7533cad18c27ed3d603f37a6c0","difficulty":0.001127075738664131,"time":1706750596,"mediantime":1706747596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"00000000000000000000000000000000000000000000000000000004de2f42db","size_on_disk":44084938,"pruned":false,"warnings":"This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"},"error":null,"id":"tester"}

    Action (7): Successfully executed response, legacy behavior retained

    curl --user test --data-binary '{"jsonrpc":"2.0","method":"getblockchaininfo","id":"tester"}' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Successfully executed response

    v26.0 behavior: {"result":{"chain":"signet","blocks":4321,"headers":4321,"bestblockhash":"00000356f9dc5bec54dc61f44bd0302859342d2b881a6b5febe984616f465abc","difficulty":0.001127075738664131,"time":1706751196,"mediantime":1706748196,"verificationprogress":1,"initialblockdownload":false,"chainwork":"00000000000000000000000000000000000000000000000000000004de792056","size_on_disk":44085350,"pruned":false,"warnings":""},"error":null,"id":"tester"}

    PR 27101 (771d1e1d206efe687b8661ab966cc1a62cc7ba39) behavior: {"jsonrpc":"2.0","result":{"chain":"signet","blocks":4320,"headers":4320,"bestblockhash":"0000013e07d388dd5c8cb66c990caa546fc04e7533cad18c27ed3d603f37a6c0","difficulty":0.001127075738664131,"time":1706750596,"mediantime":1706747596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"00000000000000000000000000000000000000000000000000000004de2f42db","size_on_disk":44084938,"pruned":false,"warnings":"This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"},"id":"tester"}

  147. tdb3 commented at 4:20 pm on January 28, 2024: contributor

    ACK. Great work. I support the near-term goal of adding handling of JSON-RPC 2.0 and the longer-term goal of deprecating 1.x, culminating in the eventual removal of 1.x support (and cleanup from refactoring out support of 1.x).

    Noting for archival purposes: Cloned, checked out the PR branch (98031401c744ea3c2b62927fabf27c22b639c3cf), built, ran all functional tests (all passed), then executed bitcoind against a private signet. Issued RPC requests with curl (getblockchaininfo in non-batch and in a batch). Inline comments provided.

  148. pinheadmz force-pushed on Jan 29, 2024
  149. pinheadmz force-pushed on Jan 29, 2024
  150. pinheadmz force-pushed on Feb 1, 2024
  151. pinheadmz commented at 7:15 pm on February 1, 2024: member

    Thanks for your fantastic in-depth review, @tdb3. I force-pushed to 58cb22c83c

    This addresses the two discrepancies you caught in your actions 1 & 2:

    • Empty batch requests are no longer considered “all notification” batches, by specifically handling the empty batch edge case
    • Unparsable requests are responded to with "id":null again, by making null the default value of the id property in JSONRPCRequest. If a (parseable) request is 2.0 with no id field, we remove the null using std::optional<T>::reset()
  152. tdb3 commented at 9:40 pm on February 1, 2024: contributor

    ACK for 58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb. Thanks for the great feature update. Verification steps taken below.

    Pulled 58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb. Each of the seven cases (from above) exhibited expected behavior (i.e. non-2.0 requests match previous response behavior, 2.0-marked requests appeared spec-compliant).

    The content below is as-run request/response for posterity.

    Action (1):

    curl --user test --data-binary '[]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Matches legacy behavior (as expected).

    v26.0 behavior: []

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: []

    Action (2):

    curl --user test --data-binary '' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Matches legacy behavior (as expected).

    v26.0 behavior: {"result":null,"error":{"code":-32700,"message":"Parse error"},"id":null}

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: {"result":null,"error":{"code":-32700,"message":"Parse error"},"id":null}

    Action (3):

    curl --user test --data-binary '[{"jsonrpc":"2.0", "id":""}]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Difference observed (as expected)

    v26.0 behavior: [{"result":null,"error":{"code":-32600,"message":"Missing method"},"id":""}]

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: [{"jsonrpc":"2.0","error":{"code":-32600,"message":"Missing method"},"id":""}]

    Action (4):

    curl --user test --data-binary '[{"jsonrpc":"2.0"}]' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Difference observed (as expected)

    v26.0 behavior: [{"result":null,"error":{"code":-32600,"message":"Missing method"},"id":null}]

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: (no content)

    Action (5):

    curl --user test --data-binary '{"jsonrpc":"1.0","method":"getblockchaininfo","id":"tester"}' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Matches legacy behavior (as expected).

    v26.0 behavior: {"result":{"chain":"signet","blocks":4440,"headers":4440,"bestblockhash":"000000728c28f3e386ba4dc4a4734ae2be877324056ff0fcd3f6ff56fe033291","difficulty":0.001127075738664131,"time":1706822596,"mediantime":1706819596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"0000000000000000000000000000000000000000000000000000000500cf1483","size_on_disk":44140411,"pruned":false,"warnings":""},"error":null,"id":"tester"}

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: {"result":{"chain":"signet","blocks":4440,"headers":4440,"bestblockhash":"000000728c28f3e386ba4dc4a4734ae2be877324056ff0fcd3f6ff56fe033291","difficulty":0.001127075738664131,"time":1706822596,"mediantime":1706819596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"0000000000000000000000000000000000000000000000000000000500cf1483","size_on_disk":44140411,"pruned":false,"warnings":"This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"},"error":null,"id":"tester"}

    Action (6):

    curl --user test --data-binary '{"version":"1.1","method":"getblockchaininfo","id":"tester"}' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Matches legacy behavior (as expected).

    v26.0 behavior: {"result":{"chain":"signet","blocks":4440,"headers":4440,"bestblockhash":"000000728c28f3e386ba4dc4a4734ae2be877324056ff0fcd3f6ff56fe033291","difficulty":0.001127075738664131,"time":1706822596,"mediantime":1706819596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"0000000000000000000000000000000000000000000000000000000500cf1483","size_on_disk":44140411,"pruned":false,"warnings":""},"error":null,"id":"tester"}

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: {"result":{"chain":"signet","blocks":4440,"headers":4440,"bestblockhash":"000000728c28f3e386ba4dc4a4734ae2be877324056ff0fcd3f6ff56fe033291","difficulty":0.001127075738664131,"time":1706822596,"mediantime":1706819596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"0000000000000000000000000000000000000000000000000000000500cf1483","size_on_disk":44140411,"pruned":false,"warnings":"This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"},"error":null,"id":"tester"}

    Action (7):

    curl --user test --data-binary '{"jsonrpc":"2.0","method":"getblockchaininfo","id":"tester"}' -H 'content-type: text/plain;' http://127.0.0.1:38332/

    Result: Difference observed (as expected)

    v26.0 behavior: {"result":{"chain":"signet","blocks":4440,"headers":4440,"bestblockhash":"000000728c28f3e386ba4dc4a4734ae2be877324056ff0fcd3f6ff56fe033291","difficulty":0.001127075738664131,"time":1706822596,"mediantime":1706819596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"0000000000000000000000000000000000000000000000000000000500cf1483","size_on_disk":44140411,"pruned":false,"warnings":""},"error":null,"id":"tester"}

    PR 27101 (58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb) behavior: {"jsonrpc":"2.0","result":{"chain":"signet","blocks":4440,"headers":4440,"bestblockhash":"000000728c28f3e386ba4dc4a4734ae2be877324056ff0fcd3f6ff56fe033291","difficulty":0.001127075738664131,"time":1706822596,"mediantime":1706819596,"verificationprogress":1,"initialblockdownload":false,"chainwork":"0000000000000000000000000000000000000000000000000000000500cf1483","size_on_disk":44140411,"pruned":false,"warnings":"This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"},"id":"tester"}

  153. DrahtBot removed review request from vincenzopalazzo on Feb 1, 2024
  154. DrahtBot requested review from ryanofsky on Feb 1, 2024
  155. DrahtBot requested review from vincenzopalazzo on Feb 1, 2024
  156. cbergqvist commented at 10:45 pm on February 16, 2024: contributor

    Reviewed 58cb22c.

    Great to finally add support for JSON-RPC 2.0, thanks for doing this!

    (Super-late concept reflection: it might be more modern to provide something like OpenAPI instead of JSON-RPC in the future (HN discussion). This would facilitate using tools like Postman and more easily signal which requests are cacheable/idempotent etc. On the other hand the codebase already has Cap’n’Proto and ZMQ, so also makes sense to not add yet another).

    Custom test utility

    Built a minimal Rust test to validate the PR using both positional and named parameters. Running with PR applied on signet:

     0$ cargo run
     1...
     2Genesis block hash with positional params array: 00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6
     3Genesis block data with named params object: Object {
     4    "bits": String("1e0377ae"),
     5    "chainwork": String("000000000000000000000000000000000000000000000000000000000049d414"),
     6    "confirmations": Number(6591),
     7    "difficulty": Number(0.001126515290698186),
     8    "hash": String("00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6"),
     9    ...
    10}
    

    Without the PR:

    0$ cargo run
    1...
    2Error: ParseError(Error("invalid type: null, expected struct ErrorObject", line: 1, column: 89))
    

    Confirmed with Wireshark that the jsonrpsee crate used is including jsonrpc: "2.0" in the JSON body of HTTP POST requests.

    Implicit version

    I understand you want to avoid refactoring bitcoin-cli to use v2 in this change, but what is the reasoning behind the version parameter having a default value? (From 9cae84c). https://github.com/bitcoin/bitcoin/blob/58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb/src/rpc/request.h#L22 Similar case for the Python functional test framework, but in this case it’s new code. https://github.com/bitcoin/bitcoin/blob/58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb/test/functional/interface_rpc.py#L25-L28 https://github.com/bitcoin/bitcoin/blob/58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb/test/functional/interface_rpc.py#L48

    No named parameter test?

    This may be covered elsewhere, but interface_rpc.py only used positional parameter arrays and is missing a test with named parameters such as:

    0    def test_named_params(self):
    1        req = RequestBuilder().rpc_request({"method": "getblockhash", "params": {"height": 0}}, version=2, notification=False)
    2        response, status = send_json_rpc(self.nodes[0], req)
    3        assert_equal(status, 200)
    4        assert_equal(
    5            response,
    6            {"jsonrpc": "2.0", "result": "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206", "id": 0}
    7        )
    

    Nits

    Have you considered using Python Enums for the version instead of integers?

    Seems to be extra non-tab aligned space towards the end in test_http_status_codes: https://github.com/bitcoin/bitcoin/blob/58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb/test/functional/interface_rpc.py#L242

    Tests executed

    0$ ./test/lint/lint-python.py
    1Success: no issues found in 280 source files
    
    0$ ./test/util/test_runner.py -v
    

    All PASSED.

    0$ ./src/test/test_bitcoin 
    1Running 593 test cases...
    2
    3*** No errors detected
    
    0$ ./test/functional/test_runner.py --cachedir=/mnt/tmp/cache --tmpdir=/mnt/tmp --extended --jobs=$(nproc)
    

    Which successfully completed 292 tests, 0 failures… listed interface_usdt_mempool.py and 14 others as skipped, seems to be due to being fringe & nondeterministic (#28088) among other things.

    Meta

    Feedback on my tone/style is welcome, I’m a beginner at reviews in this context.

  157. DrahtBot removed review request from vincenzopalazzo on Feb 16, 2024
  158. DrahtBot requested review from vincenzopalazzo on Feb 16, 2024
  159. pinheadmz commented at 8:19 pm on February 20, 2024: member

    @cbergqvist thanks so much for your thorough review!


    re: Implicit version

    Yes the default 1.1 parameter in cpp code is to keep default behavior from bitcoin-cli. In python it’s to match existing behavior from the RPC client in the test framework, i.e.:

    https://github.com/bitcoin/bitcoin/blob/207220ce8b767d8efdb5abf042ecf23d846ded73/test/functional/test_framework/authproxy.py#L120

    I look forward to upgrading both cli and the tests to v2.0 as a follow-up PR


    re: named params

    This is covered in the unit tests: https://github.com/bitcoin/bitcoin/blob/master/src/test/rpc_tests.cpp

    We could cover as well in the functional tests in a follow up but I don’t think that’s necessary right now. Good catch though!


    re: nits

    These are good calls too, I think integers is simpler than an enum, and you’re right about the extra space. If I retouch the interface_rpc.py file I will fix those as well.

  160. DrahtBot removed review request from vincenzopalazzo on Feb 20, 2024
  161. DrahtBot requested review from vincenzopalazzo on Feb 20, 2024
  162. cbergqvist commented at 9:43 pm on February 20, 2024: contributor

    ACK 58cb22c

    All reasonable arguments! Would be a scary to see new default values for parameters in consensus critical code - but this is not, and it’s very unlikely it would cause any harm.

  163. DrahtBot removed review request from vincenzopalazzo on Feb 20, 2024
  164. DrahtBot requested review from vincenzopalazzo on Feb 20, 2024
  165. test: refactor interface_rpc.py
    Refactors the helper functions in the test to provide more
    fine-grained control over RPC requests and responses than
    the usual AuthProxy methods.
    
    No change in test behavior, the same RPC requests are made.
    4202c170da
  166. test: cover JSONRPC 2.0 requests, batches, and notifications 09416f9ec4
  167. rpc: Avoid copies in JSONRPCReplyObj()
    Change parameters from const references to values, so they can be moved into
    the reply instead of copied. Also update callers to move instead of copy.
    df6e3756d6
  168. rpc: refactor single/batch requests
    Simplify the request handling flow so that errors and results only
    come from JSONRPCExec()
    a64a2b77e0
  169. in test/functional/interface_rpc.py:123 in 92c651394b outdated
    119@@ -104,16 +120,133 @@ def test_batch_request(self):
    120             ]
    121         )
    122 
    123+        self.log.info("Testing basic JSON-RPC 1.0 batch request...")
    


    ryanofsky commented at 5:01 pm on March 7, 2024:

    In commit “test: cover JSONRPC 2.0 requests, batches, and notifications” (92c651394bcb1c476cc62989936edd621b5d4aa5)

    This is confusing because there should be no such thing as a JSON-RPC 1.0 batch request, batches were introduced in 2.0. Also specifying req[“jsonrpc”] = “1.0” is nonstandard. Would be good to clarify what behavior is standard and what is just being done for backwards compatibility.

  170. in src/rpc/request.cpp:181 in d62dcf81e1 outdated
    176@@ -177,10 +177,8 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
    177         throw JSONRPCError(RPC_INVALID_REQUEST, "Invalid Request object");
    178     const UniValue& request = valRequest.get_obj();
    179 
    180-    // Parse id now so errors from here on will have the id
    181-    id = request.find_value("id");
    182-
    183     // Check for JSON-RPC 2.0 (default 1.1)
    184+    // We must do this before looking for the id field
    


    ryanofsky commented at 10:28 pm on March 7, 2024:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (d62dcf81e152d03aa3cf02d0eb0e7e9c44807464)

    This is just stated without a reason and doesn’t appear to be true.

    Also, checking for the version field after the id field is bad because it means if a batch request includes an unrecognized version number, the id is lost and there is no indication which call in the batch is causing the problem.

  171. in src/httprpc.cpp:76 in 9cae84c187 outdated
    72@@ -73,8 +73,11 @@ static std::vector<std::vector<std::string>> g_rpcauth;
    73 static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
    74 static bool g_rpc_whitelist_default = false;
    75 
    76-static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const UniValue& id)
    77+static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const JSONRPCRequest& jreq)
    


    ryanofsky commented at 11:06 pm on March 7, 2024:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (9cae84c18706ce3b23b6f31466553161112deb70)

    This is not a great commit description because it is only describing one of the 3 changes made in this commit. In addition to making error and result multually exclusive it is also:

    • Adding jsonrpc 2.0 field to responses
    • Making non-batch jsonrpc 2.0 requests return 200 http status instead of error statuses when the RPC method fails

    In the branch I posted #27101#pullrequestreview-1922496475 this commit is split into two parts and includes tests to clarify the changes.

  172. in src/rpc/request.h:21 in 9cae84c187 outdated
    18 };
    19 
    20 UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id);
    21-UniValue JSONRPCReplyObj(UniValue result, UniValue error, const UniValue& id);
    22-std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id);
    23+UniValue JSONRPCReplyObj(UniValue result, UniValue error, const UniValue& id, JSONVersion json_version = JSONVersion::JSON_1_BTC);
    


    ryanofsky commented at 11:08 pm on March 7, 2024:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (9cae84c18706ce3b23b6f31466553161112deb70)

    Would be safest to drop default argument and make it required to specify the version. I think differences between versions are subtle and confusing enough that the version should always be explicit.


    pinheadmz commented at 2:25 pm on May 1, 2024:
    ok thanks, originally I wanted to use a default value to ensure that bitcoin-cli continued to work without any changes

    pinheadmz commented at 3:33 pm on May 1, 2024:
    Added the explicit arguments to the calls in bitcoin-cli.cpp, so far that’s the only change i made to your branch
  173. in src/rpc/request.h:14 in 9cae84c187 outdated
    10@@ -11,14 +11,15 @@
    11 
    12 #include <univalue.h>
    13 
    14+class JSONRPCRequest;
    


    ryanofsky commented at 11:17 pm on March 7, 2024:

    In commit “rpc: make error and result mutually exclusive in JSON-RPC 2.0 replies” (9cae84c18706ce3b23b6f31466553161112deb70)

    This declaration appears to be unused


    pinheadmz commented at 7:16 pm on April 30, 2024:

    Without it there is a compile error:

    0In file included from ./rpc/server.h:9:
    1./rpc/request.h:22:79: error: unknown type name 'JSONRPCRequest'
    2   22 | std::string JSONRPCReply(const UniValue& result, const UniValue& error, const JSONRPCRequest& jreq);
    3      |                                                                               ^
    41 error generated.
    
  174. in src/rpc/request.cpp:40 in 51a071f24c outdated
    36@@ -37,7 +37,7 @@ UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params,
    37     return request;
    38 }
    39 
    40-UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id)
    41+UniValue JSONRPCReplyObj(UniValue result, UniValue error, const UniValue& id)
    


    ryanofsky commented at 0:05 am on March 8, 2024:

    In commit “rpc: use move semantics in JSONRPCReplyObj()” (51a071f24caa89b3dab2e9a7b46d5b3a75ef4913)

    To make sense, this commit needs to use std::move below for result, error, and id. The point of changing these arguments from const to non-const is to allow them to be moved from, and avoid the copies that would otherwise happen below.

  175. in src/httprpc.cpp:254 in d62dcf81e1 outdated
    253+                if (!jreq.IsNotification()) {
    254+                    reply.push_back(std::move(response));
    255+                    all_notifications = false;
    256                 }
    257             }
    258+            // All-notification batch expects no response
    


    ryanofsky commented at 0:41 am on March 8, 2024:

    In commit “rpc: JSON-RPC 2.0 should not respond to “notifications”” (d62dcf81e152d03aa3cf02d0eb0e7e9c44807464)

    I think this needs some explanation about how valRequest.size() check is here for backward compatibility since it is surprising and violates the 2.0 spec.

  176. ryanofsky approved
  177. ryanofsky commented at 1:23 am on March 8, 2024: contributor

    Code review ACK 58cb22c83c1b72af7f3c580dc6261cf50e7ed2cb with some reservations.

    I would say the current implementation of this looks pretty good, but it is hard to be confident about it due to gaps in test coverage (no coverage for batch request without version numbers, or with unrecognized version numbers, for non-2.0 batch notifications, or for empty requests where a behavior change was reported earlier)

    Also most of the test coverage that is added is added after the code changes, so there is not much assurance individual code changes have the expected effects on behavior.

    But aside from gaps in tests I didn’t see any major issues with the implementation, other than:

    • Parsing of request id seemed to have been moved incorrectly, so “JSON-RPC version not supported” exception returns "id": null instead of id of the request
    • Const references and std::move were not used correctly in a few places so objects are copied instead of moved
    • Some things are not well documented

    I left some more specific comments below, but to save time, I implemented a branch that has all the improvements I would like to see here: review.27101.5-edit (diff). The main thing the branch does is add more test coverage, and add it before the code changes rather than after, so it possible to actually see the effects code changes have on responses and understand the them better. The branch can be fetched/compared locally with:

    0git fetch -n https://github.com/ryanofsky/bitcoin review.27101.5-edit:review.27101.5-edit tag review.27101.5-base tag review.27101.5
    1git diff review.27101.5 review.27101.5-edit
    2git range-diff review.27101.5-base review.27101.5 review.27101.5-edit
    
  178. DrahtBot removed review request from vincenzopalazzo on Mar 8, 2024
  179. DrahtBot requested review from vincenzopalazzo on Mar 8, 2024
  180. DrahtBot removed review request from vincenzopalazzo on Mar 8, 2024
  181. DrahtBot requested review from vincenzopalazzo on Mar 8, 2024
  182. somecodingwitch approved
  183. pinheadmz force-pushed on May 1, 2024
  184. pinheadmz commented at 7:39 pm on May 1, 2024: member
    @ryanofsky thanks so much, again, for your help. Especially in understanding the std::move semantics. I’ve reviewed each commit in your branch and force pushed it to mine with the only change being specifying json request version in JSONRPCReplyObj() calls in bitcoin-cli.cpp
  185. in src/httprpc.cpp:244 in 8583c2f93d outdated
    240+                UniValue response;
    241+                try {
    242+                    jreq.parse(valRequest[i]);
    243+                    response = JSONRPCExec(jreq);
    244+                } catch (UniValue& e) {
    245+                    response = JSONRPCReplyObj(NullUniValue, e, jreq.id, jreq.m_json_version);
    


    cbergqvist commented at 12:40 pm on May 8, 2024:
    Here the caught UniValue& is not explicitly std::moved. But on line 272 it is. Is that intentional?

    pinheadmz commented at 2:08 pm on May 8, 2024:

    Good catch thanks. I’ll need @ryanofsky to weigh in here because I keep misunderstanding c++ move semantics. But here’s what I think is happening:

    In JSONRPCReplyObj() in request.cpp the really import move-instead-of-copy is when we call reply.pushKV(...): https://github.com/pinheadmz/bitcoin/blob/8583c2f93d4eb020bf1e3b74860b691ec12e5eee/src/rpc/request.cpp#L64

    That makes me think that the std::move() currently in JSONErrorReply() in httprpc.cpp is unnecessary: https://github.com/esotericnonsense/bitcoin/blob/8583c2f93d4eb020bf1e3b74860b691ec12e5eee/src/httprpc.cpp#L90

    …and also unnecessary is the one you see on line 272.

    So if I’m finally understanding, which is unlikely, then there are two places where std::move can be removed but actually this line 244 right here is OK, because the import move-instead-of-copy happens inside the function being called.


    ryanofsky commented at 2:27 pm on May 8, 2024:

    re: #27101 (review)

    I think @cbergqvist is right here and e should be replaced by std::move(e) on line 244.

    The JSONRPCReplyObj signature is:

    0UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue> id, JSONVersion json_version);
    

    So without std::move(e), the error parameter will be initialized by making a copy of e instead of moving from e. This is because e is an lvalue, while std::move(e) is an rvalue.

    In JSONRPCReplyObj() in request.cpp the really import move-instead-of-copy is when we call reply.pushKV(...): https://github.com/pinheadmz/bitcoin/blob/8583c2f93d4eb020bf1e3b74860b691ec12e5eee/src/rpc/request.cpp#L64

    That is true. The signature for pushKV is void pushKV(std::string key, UniValue val), so without std::move(error), on that line the function parameter val would be initialized by copying not moving.

    That makes me think that the std::move() currently in JSONErrorReply() in httprpc.cpp is unnecessary: https://github.com/esotericnonsense/bitcoin/blob/8583c2f93d4eb020bf1e3b74860b691ec12e5eee/src/httprpc.cpp#L90

    This is not true. std::move is needed to avoid copy-initializing the JSONRPCReplyObj error parameter there, just like on line 272 and on line

    So if I’m finally understanding, which is unlikely, then there are two places where std::move can be removed but actually this line 244 right here is OK, because the import move-instead-of-copy happens inside the function being called.

    Yeah this is a little off. What you are saying is true if the function signature accepts parameters by reference. For example if you have function that looks like:

    0void MyFunction(SomeType& param);
    1void MyFunction(const SomeType& param);
    2void MyFunction(SomeType&& param);
    

    In this case, no matter whether you call MyFunction with std::move or without it, the parameter param is going to be a reference not a value, so no copy will be created just by calling the function. There could copies of param made inside the function, depending on what it does internally, as you say. But merely calling the function will not copy the parameter.

    However, if you have a function that takes a non-reference value parameter like:

    0void MyFunction(SomeType param);
    

    No matter what the function does internally, just calling the function is going to copy-construct param variable or move-construct it. And unless you use std::move or the argument is already an rvalue, the argument will be copied.


    cbergqvist commented at 9:15 pm on May 8, 2024:
    Had to check whether UniValue even had a move-constructor. It seems like one should be generated implicitly if my readings of https://en.cppreference.com/w/cpp/language/move_constructor#Implicitly-declared_move_constructor and univalue.h are correct. @ryanofsky’s explanation rings true with my long underutilized C++11 neurons.
  186. in src/httprpc.cpp:276 in 8583c2f93d outdated
    269 
    270         req->WriteHeader("Content-Type", "application/json");
    271-        req->WriteReply(HTTP_OK, strReply);
    272-    } catch (const UniValue& objError) {
    273-        JSONErrorReply(req, objError, jreq.id);
    274+        req->WriteReply(HTTP_OK, reply.write() + "\n");
    


    cbergqvist commented at 1:28 pm on May 8, 2024:
    Might be more correct & compatible to append "\r\n". See https://stackoverflow.com/questions/5757290/http-header-line-break-style

    pinheadmz commented at 1:58 pm on May 8, 2024:

    That’s a reasonable comment but I kept the single character which is how it worked on master (see below). I see a few examples of \r\n in the codebase but its rare and I’m not sure how it’s decided.

    https://github.com/bitcoin/bitcoin/blob/63d0b930f821132badd804822a46232a5f98bbef/src/rpc/request.cpp#L52-L56


    ryanofsky commented at 3:13 pm on May 8, 2024:

    In commit “rpc: refactor single/batch requests” (a64a2b77e09bff784a2635ba19ff4aa6582bb5a5)

    re: #27101 (review)

    HTTP headers use \r\n, but this line is generating the JSON object in the body of the response, after the headers. And the JSON could be written with either \r\n or \n or no trailing line break at all. I think it would probably make sense not to include any trailing line break, but existing code uses \n, so in a refactoring commit it seems best not to change this.

  187. cbergqvist commented at 1:43 pm on May 8, 2024: contributor

    reACK 8583c2f93d4eb020bf1e3b74860b691ec12e5eee

    Ran most functional tests including interface_rpc.py without any failures. Ran make check without failures or skips.

    Happy that the version parameters to JSONRPCReplyObj() etc are now explicit.

  188. DrahtBot requested review from ryanofsky on May 8, 2024
  189. DrahtBot requested review from tdb3 on May 8, 2024
  190. in src/rpc/request.cpp:177 in 6d0be75f30 outdated
    172+    const UniValue& valJsonRPC = request.find_value("jsonrpc");
    173+    if (!valJsonRPC.isNull()) {
    174+        if (!valJsonRPC.isStr()) {
    175+            throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
    176+        }
    177+        if (valJsonRPC.get_str() == "1.0") {
    


    ryanofsky commented at 3:24 pm on May 8, 2024:

    In commit “rpc: identify JSON-RPC 2.0 requests” (6d0be75f3040c3190759c3ba6b5740897f2d3d29)

    Might be good to add a comment noting that the “jsonrpc” version field was added in the JSON-RPC 2.0 spec, so value “1.0” is nonstandard. But it is accepted for backwards compatibility, because some old documentation (incorrectly) suggested adding it.

  191. in src/rpc/request.cpp:172 in 6d0be75f30 outdated
    166@@ -167,6 +167,22 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
    167     // Parse id now so errors from here on will have the id
    168     id = request.find_value("id");
    169 
    170+    // Check for JSON-RPC 2.0 (default 1.1)
    171+    m_json_version = JSONVersion::JSON_1_BTC;
    172+    const UniValue& valJsonRPC = request.find_value("jsonrpc");
    


    ryanofsky commented at 3:26 pm on May 8, 2024:

    In commit “rpc: identify JSON-RPC 2.0 requests” (6d0be75f3040c3190759c3ba6b5740897f2d3d29)

    Variable name valJsonRPC doesn’t really follow the suggested coding style which calls for snake_case. Could rename it to “jsonrpc_version”

  192. in src/rpc/request.h:14 in 6d0be75f30 outdated
    10@@ -11,6 +11,11 @@
    11 
    12 #include <univalue.h>
    13 
    14+enum class JSONVersion {
    


    ryanofsky commented at 3:27 pm on May 8, 2024:

    In commit “rpc: identify JSON-RPC 2.0 requests” (6d0be75f3040c3190759c3ba6b5740897f2d3d29)

    Technically JSON-RPC version would be more accurate the JSON version. could s/JSON/JSONRPC/ throughout this enum


    cbergqvist commented at 7:00 pm on May 8, 2024:
    Is JSON/JSONRPC really necessary for the values themselves if the enum is of the type enum class so that one always has to prepend the type? - JSONRPCVersion::JSONRPC_1_BTC vs JSONRPCVersion::1_BTC.

    ryanofsky commented at 7:11 pm on May 8, 2024:

    re: #27101 (review)

    Is JSON/JSONRPC really necessary for the values themselves if the enum is of the type enum class so that one always has to prepend the type? - JSONRPCVersion::JSONRPC_1_BTC vs JSONRPCVersion::1_BTC.

    Good point. I’d probably call the constants V1_LEGACY and V2 if renaming.

  193. in src/httprpc.cpp:233 in 56b6c33646 outdated
    228@@ -226,7 +229,8 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
    229             // Execute each request
    230             reply = UniValue::VARR;
    231             for (size_t i{0}; i < valRequest.size(); ++i) {
    232-                // Batches include errors in the batch response, they do not throw
    233+                // Batches never throw HTTP errors, they are always just included
    234+                // in "HTTP OK" responses.
    


    ryanofsky commented at 3:35 pm on May 8, 2024:

    In commit “rpc: Avoid returning HTTP errors for JSON-RPC 2.0 requests” (56b6c33646f398e1b2efab64cbaf7ed86e3bbd06)

    I think the commit message has a typo. It says “Non-batch requests already did not return HTTP errors previously.” but I think it is supposed to say “Batch requests already did not return HTTP errors previously.”

  194. in src/rpc/server.cpp:375 in 56b6c33646 outdated
    373+            return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
    374+        } catch (const std::exception& e) {
    375+            return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
    376+        }
    377+    } else {
    378+        // Legacy Bitcoin JSONRPC 1.0/1.2 behavior:
    


    ryanofsky commented at 3:49 pm on May 8, 2024:

    In commit “rpc: Avoid returning HTTP errors for JSON-RPC 2.0 requests” (56b6c33646f398e1b2efab64cbaf7ed86e3bbd06)

    This is probably supposed to say 1.1 not 1.2

  195. in src/rpc/server.cpp:372 in 56b6c33646 outdated
    370+        try {
    371+            result = tableRPC.execute(jreq);
    372+        } catch (UniValue& e) {
    373+            return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
    374+        } catch (const std::exception& e) {
    375+            return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
    


    ryanofsky commented at 4:06 pm on May 8, 2024:

    In commit “rpc: Avoid returning HTTP errors for JSON-RPC 2.0 requests” (56b6c33646f398e1b2efab64cbaf7ed86e3bbd06)

    Returning RPC_PARSE_ERROR does not seem accurate if the request was parsed successfully but there was an error executing it. This was also a preexisting problem in HTTPReq_JSONRPC for batch requests, but now the behavior will happen for all version 2 single requests as well. Would suggest cleaning this up with a change like:

     0--- a/src/httprpc.cpp
     1+++ b/src/httprpc.cpp
     2@@ -204,7 +204,12 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
     3                 return false;
     4             }
     5 
     6-            reply = JSONRPCExec(jreq);
     7+            // Legacy 1.0/1.1 behavior is for failed requests to throw
     8+            // exceptions which return HTTP errors and RPC errors to the client.
     9+            // 2.0 behavior is to catch exceptions and return HTTP success with
    10+            // RPC errors, as long as there is not HTTP server error.
    11+            const bool catch_errors{jreq.m_json_version == JSONVersion::JSON_2_0};
    12+            reply = JSONRPCExec(jreq, catch_errors);
    13 
    14         // array of requests
    15         } else if (valRequest.isArray()) {
    16@@ -233,12 +238,12 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
    17                 // in "HTTP OK" responses.
    18                 try {
    19                     jreq.parse(valRequest[i]);
    20-                    reply.push_back(JSONRPCExec(jreq));
    21                 } catch (UniValue& e) {
    22                     reply.push_back(JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version));
    23                 } catch (const std::exception& e) {
    24                     reply.push_back(JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version));
    25                 }
    26+                reply.push_back(JSONRPCExec(jreq, /*catch_errors=*/true));
    27             }
    28         }
    29         else
    30--- a/src/rpc/server.cpp
    31+++ b/src/rpc/server.cpp
    32@@ -358,22 +358,18 @@ bool IsDeprecatedRPCEnabled(const std::string& method)
    33     return find(enabled_methods.begin(), enabled_methods.end(), method) != enabled_methods.end();
    34 }
    35 
    36-UniValue JSONRPCExec(const JSONRPCRequest& jreq)
    37+UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors)
    38 {
    39     UniValue result;
    40-    if (jreq.m_json_version == JSONVersion::JSON_2_0) {
    41-        // JSONRPC 2.0 behavior: only throw HTTP error if the server is actually
    42-        // broken. Otherwise errors are sent back in "HTTP OK" responses.
    43+    if (catch_errors) {
    44         try {
    45             result = tableRPC.execute(jreq);
    46         } catch (UniValue& e) {
    47             return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
    48         } catch (const std::exception& e) {
    49-            return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
    50+            return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_MISC_ERROR, e.what()), jreq.id, jreq.m_json_version);
    51         }
    52     } else {
    53-        // Legacy Bitcoin JSONRPC 1.0/1.2 behavior:
    54-        // Single requests may throw HTTP errors, handled by caller or client
    55         result = tableRPC.execute(jreq);
    56     }
    57 
    58--- a/src/rpc/server.h
    59+++ b/src/rpc/server.h
    60@@ -181,7 +181,7 @@ extern CRPCTable tableRPC;
    61 void StartRPC();
    62 void InterruptRPC();
    63 void StopRPC();
    64-UniValue JSONRPCExec(const JSONRPCRequest& jreq);
    65+UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors);
    66 
    67 // Drop witness when serializing for RPC?
    68 bool RPCSerializationWithoutWitness();
    

    pinheadmz commented at 3:03 pm on May 14, 2024:

    Thanks so much for the patch. I had to move this line back up to where it was, otherwise the test fails because after pushing the error into the batch reply, we would also then push a weird extra junk reply with the current id but the result from the last successful query

     0diff --git a/src/httprpc.cpp b/src/httprpc.cpp
     1index 0e3e66c515..777ad32bbe 100644
     2--- a/src/httprpc.cpp
     3+++ b/src/httprpc.cpp
     4@@ -238,12 +238,12 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
     5                 // in "HTTP OK" responses.
     6                 try {
     7                     jreq.parse(valRequest[i]);
     8+                    reply.push_back(JSONRPCExec(jreq, /*catch_errors=*/true));
     9                 } catch (UniValue& e) {
    10                     reply.push_back(JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version));
    11                 } catch (const std::exception& e) {
    12                     reply.push_back(JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version));
    13                 }
    14-                reply.push_back(JSONRPCExec(jreq, /*catch_errors=*/true));
    15             }
    16         }
    17         else
    

    pinheadmz commented at 3:15 pm on May 14, 2024:
    I’m trying to figure out how to write a test to catch one of these RPC_MISC_ERROR since they obviously weren’t catching a RPC_PARSE_ERROR before adding your patch to the commit. If I understand correctly, that “misc” error would only throw if there was a bug in an RPC command because most of our try blocks try to catch a UniValue error first with a specific code.
  196. ryanofsky approved
  197. ryanofsky commented at 4:17 pm on May 8, 2024: contributor
    Code review ACK 8583c2f93d4eb020bf1e3b74860b691ec12e5eee. Looks very good! I left several comments, but none are critical and they could be followed up later.
  198. rpc: identify JSON-RPC 2.0 requests 2ca1460ae3
  199. rpc: Add "jsonrpc" field and drop null "result"/"error" fields
    Only for JSON-RPC 2.0 requests.
    466b90562f
  200. rpc: Avoid returning HTTP errors for JSON-RPC 2.0 requests
    Avoid returning HTTP status errors for non-batch JSON-RPC 2.0 requests if the
    RPC method failed but the HTTP request was otherwise valid. Batch requests
    already did not return HTTP errors previously.
    bf1a1f1662
  201. rpc: JSON-RPC 2.0 should not respond to "notifications"
    For JSON-RPC 2.0 requests we need to distinguish between
    a missing "id" field and "id":null. This is accomplished
    by making the JSONRPCRequest id property a
    std::optional<UniValue> with a default value of
    UniValue::VNULL.
    
    A side-effect of this change for non-2.0 requests is that request which do not
    specify an "id" field will no longer return "id": null in the response.
    e7ee80dcf2
  202. doc: add comments and release-notes for JSON-RPC 2.0 cbc6c440e3
  203. pinheadmz force-pushed on May 14, 2024
  204. pinheadmz commented at 5:28 pm on May 14, 2024: member

    force push to cbc6c440e3:

    • rename enum to JSONRPCVersion::{V1_LEGACY, V2}
    • use std::move() in httprpc
    • pass catch_errors argument to JSONRPCExec()
    • replace RPC_PARSE_ERROR with RPC_MISC_ERROR in JSONRPCExec()

    thanks again for the reviews @cbergqvist @ryanofsky

  205. in src/httprpc.cpp:76 in cbc6c440e3
    72@@ -73,8 +73,11 @@ static std::vector<std::vector<std::string>> g_rpcauth;
    73 static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
    74 static bool g_rpc_whitelist_default = false;
    75 
    76-static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const UniValue& id)
    77+static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCRequest& jreq)
    


    cbergqvist commented at 12:42 pm on May 15, 2024:
    nit: JSONErrorReply -> JSONRPCErrorReply, although it could be argued that it actually does write a JSON object in the response.
  206. in src/rpc/server.cpp:361 in cbc6c440e3
    374-    }
    375-    catch (const std::exception& e)
    376-    {
    377-        rpc_result = JSONRPCReplyObj(NullUniValue,
    378-                                     JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
    379+UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors)
    


    cbergqvist commented at 12:46 pm on May 15, 2024:
    Would have gone the opposite way and called it throw_errors since it is an.. exception.. to maintain legacy behavior. Sorry for not catching that earlier.

    ryanofsky commented at 2:19 pm on May 15, 2024:

    re: #27101 (review)

    Would have gone the opposite way and called it throw_errors since it is an.. exception.. to maintain legacy behavior. Sorry for not catching that earlier.

    I think either way is fine, but catch_errors does seem more literally correct since the argument is just controlling whether the exceptions will be caught. Errors will be still be thrown regardless.

  207. cbergqvist approved
  208. cbergqvist commented at 1:04 pm on May 15, 2024: contributor

    re ACK cbc6c440e3811d342fa570713702900b3e3e75b9

    All functional tests passed (with a few automatic skips), except feature_dbcrash - slow, unrelated => excluded, and feature_index_prune => timed out because rebase with bumped timeout has been held-off.

  209. DrahtBot requested review from ryanofsky on May 15, 2024
  210. in src/rpc/request.cpp:179 in 2ca1460ae3 outdated
    174+        if (!jsonrpc_version.isStr()) {
    175+            throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
    176+        }
    177+        // The "jsonrpc" key was added in the 2.0 spec, but some older documentation
    178+        // incorrectly included {"jsonrpc":"1.0"} in a request object, so we
    179+        // maintain that for backwards compatibility.
    


    ryanofsky commented at 2:14 pm on May 15, 2024:

    In commit “rpc: identify JSON-RPC 2.0 requests” (2ca1460ae3a7217eaa8c5972515bf622bedadfce)

    I think it would be a little clearer to say “continue to accept that” instead of “maintain that.” Otherwise it sounds like we are trying to maintain incorrectly including the field, not just allowing it if it is specified.

  211. ryanofsky approved
  212. ryanofsky commented at 2:26 pm on May 15, 2024: contributor
    Code review ACK cbc6c440e3811d342fa570713702900b3e3e75b9. Just suggested changes since the last review: changing uncaught exception error code from PARSE_ERROR to MISC_ERROR, renaming a few things, and adding comments.
  213. ryanofsky commented at 2:31 pm on May 15, 2024: contributor
    @willcl-ark, @tdb3, any interest in re-acking? This seems like it could definitely be merged with another ack
  214. tdb3 commented at 2:48 pm on May 15, 2024: contributor

    @willcl-ark, @tdb3, any interest in re-acking? This seems like it could definitely be merged with another ack

    Definitely. I’ll plan to take a look tonight.

  215. tdb3 approved
  216. tdb3 commented at 0:52 am on May 16, 2024: contributor

    re ACK for cbc6c440e3811d342fa570713702900b3e3e75b9

    Great work. Performed brief code review. Re-ran the tests described in #27101 (comment) again (actions 1 through 7, with the caveat that v27.0 was used as the baseline for comparison rather v26.0, and regtest was used). Everything worked as expected. Also ran all functional tests (passed).

  217. ryanofsky merged this on May 16, 2024
  218. ryanofsky closed this on May 16, 2024

  219. in doc/JSON-RPC-interface.md:83 in cbc6c440e3
    78+
    79+The server recognizes [JSON-RPC v2.0](https://www.jsonrpc.org/specification) requests
    80+and responds accordingly. A 2.0 request is identified by the presence of
    81+`"jsonrpc": "2.0"` in the request body. If that key + value is not present in a request,
    82+the legacy JSON-RPC v1.1 protocol is followed instead, which was the only available
    83+protocol in previous releases.
    


    maflcko commented at 12:35 pm on May 20, 2024:
    nit: Now that this is merged, it could say “in 27.0 and prior releases.” Otherwise, on 29.x it will read as-if 28.0 had it missing.
  220. in doc/release-notes-27101.md:9 in cbc6c440e3
    0@@ -0,0 +1,9 @@
    1+JSON-RPC
    2+--------
    3+
    4+The JSON-RPC server now recognizes JSON-RPC 2.0 requests and responds with
    5+strict adherence to the specification (https://www.jsonrpc.org/specification):
    6+
    7+- Returning HTTP "204 No Content" responses to JSON-RPC 2.0 notifications instead of full responses.
    8+- Returning HTTP "200 OK" responses in all other cases, rather than 404 responses for unknown methods, 500 responses for invalid parameters, etc.
    9+- Returning either "result" fields or "error" fields in JSON-RPC responses, rather than returning both fields with one field set to null.
    


    maflcko commented at 12:36 pm on May 20, 2024:
    nit instead of duplicating the section from doc/JSON-RPC-interface.md here, it seems better to link/refer to it. Otherwise, if the section is updated, this may go stale or must be updated at the same time.
  221. in test/functional/interface_rpc.py:17 in cbc6c440e3
    21-        fcn(*args)
    22-        raise AssertionError(f"Expected RPC error {expected_rpc_code}, got none")
    23-    except JSONRPCException as exc:
    24-        assert_equal(exc.error["code"], expected_rpc_code)
    25-        assert_equal(exc.http_status, expected_http_status)
    26+RPC_INVALID_ADDRESS_OR_KEY = -5
    


    maflcko commented at 1:45 pm on May 20, 2024:
    Looks like this is unused?

    fanquake commented at 3:52 pm on May 31, 2024:
    @pinheadmz did you end up following up to these comments?

    pinheadmz commented at 3:53 pm on May 31, 2024:
    Getting there…
  222. fanquake referenced this in commit a44b0f771f on Jun 8, 2024

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2024-12-22 03:12 UTC

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