cuprate/rpc/json-rpc
Boog900 acd5380256
Some checks are pending
Audit / audit (push) Waiting to run
CI / fmt (push) Waiting to run
CI / typo (push) Waiting to run
CI / ci (macos-latest, stable, bash) (push) Waiting to run
CI / ci (ubuntu-latest, stable, bash) (push) Waiting to run
CI / ci (windows-latest, stable-x86_64-pc-windows-gnu, msys2 {0}) (push) Waiting to run
Deny / audit (push) Waiting to run
Fix clippy & Cargo update (#161)
* fix clippy

* run cargo update

* fix cargo deny

* remove duplicate `#[test]`
2024-06-13 19:08:34 +01:00
..
src Fix clippy & Cargo update (#161) 2024-06-13 19:08:34 +01:00
Cargo.toml rpc: implement json-rpc crate (#148) 2024-06-12 02:12:31 +01:00
README.md rpc: implement json-rpc crate (#148) 2024-06-12 02:12:31 +01:00

json-rpc

JSON-RPC 2.0 types and (de)serialization.

What

This crate implements the JSON-RPC 2.0 specification for usage in Cuprate.

It contains slight modifications catered towards Cuprate and isn't necessarily a general purpose implementation of the specification (see below).

This crate expects you to read the brief JSON-RPC 2.0 specification for context.

Batching

This crate does not have any types for JSON-RPC 2.0 batching.

This is because monerod does not support this, as such, neither does Cuprate.

TODO: citation needed on monerod not supporting batching.

Request changes

JSON-RPC 2.0's Request object usually contains these 2 fields:

  • method
  • params

This crate replaces those two with a body field that is #[serde(flatten)]ed, and assumes the type within that body field is tagged properly, for example:

# use pretty_assertions::assert_eq;
use serde::{Deserialize, Serialize};
use json_rpc::{Id, Request};

// Parameter type.
#[derive(Deserialize, Serialize)]
struct GetBlock {
    height: u64,
}

// Method enum containing all enums.
// All methods are tagged as `method`
// and their inner parameter types are
// tagged with `params` (in snake case).
#[derive(Deserialize, Serialize)]
#[serde(tag = "method", content = "params")] // INVARIANT: these tags are needed
#[serde(rename_all = "snake_case")]          // for proper (de)serialization.
enum Methods {
    GetBlock(GetBlock),
    /* other methods */
}

// Create the request object.
let request = Request::new_with_id(
    Id::Str("hello".into()),
    Methods::GetBlock(GetBlock { height: 123 }),
);

// Serializing properly shows the `method/params` fields
// even though `Request` doesn't contain those fields.
let json = serde_json::to_string_pretty(&request).unwrap();
let expected_json =
r#"{
  "jsonrpc": "2.0",
  "id": "hello",
  "method": "get_block",
  "params": {
    "height": 123
  }
}"#;
assert_eq!(json, expected_json);

This is how the method/param types are done in Cuprate.

For reasoning, see: https://github.com/Cuprate/cuprate/pull/146#issuecomment-2145734838.

Serialization changes

This crate's serialized field order slightly differs compared to monerod.

monerod's JSON objects are serialized in alphabetically order, where as this crate serializes the fields in their defined order (due to [serde]).

With that said, parsing should be not affected at all since a key-value map is used:

# use pretty_assertions::assert_eq;
use json_rpc::{Id, Response};

let response = Response::ok(Id::Num(123), "OK");
let response_json = serde_json::to_string_pretty(&response).unwrap();

// This crate's `Response` result type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
  "jsonrpc": "2.0",
  "id": 123,
  "result": "OK"
}"#;
assert_eq!(response_json, expected_json);

// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
  "id": 123,
  "jsonrpc": "2.0",
  "result": "OK"
}"#;

///---

let response = Response::<()>::invalid_request(Id::Num(123));
let response_json = serde_json::to_string_pretty(&response).unwrap();

// This crate's `Response` error type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
  "jsonrpc": "2.0",
  "id": 123,
  "error": {
    "code": -32600,
    "message": "Invalid Request"
  }
}"#;
assert_eq!(response_json, expected_json);

// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
  "error": {
    "code": -32600,
    "message": "Invalid Request"
  },
  "id": 123
  "jsonrpc": "2.0",
}"#;

Compared to other implementations

A quick table showing some small differences between this crate and other JSON-RPC 2.0 implementations.

Implementation Allows any case for key fields excluding method/params Allows unknown fields in main {}, and response/request objects Allows overwriting previous values upon duplicate fields (except [Response]'s result/error field)
monerod
jsonrpsee
This crate

Allows any case for key fields excluding method/params:

# use json_rpc::Response;
# use serde_json::from_str;
# use pretty_assertions::assert_eq;
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();

// Only `lowercase` is allowed.
let json = r#"{"jsonRPC":"2.0","id":123,"result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "missing field `jsonrpc` at line 1 column 40");

Allows unknown fields in main {}, and response/request objects:

# use json_rpc::Response;
# use serde_json::from_str;
//     unknown fields are allowed in main `{}`
//             v
let json = r#"{"unknown_field":"asdf","jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();

//                                                               and within objects
//                                                                      v
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":"","unknown_field":"asdf"}}"#;
from_str::<Response<String>>(&json).unwrap();

Allows overwriting previous values upon duplicate fields (except [Response]'s result/error field)

# use json_rpc::{Id, Response};
# use serde_json::from_str;
# use pretty_assertions::assert_eq;
//          duplicate fields will get overwritten by the latest one
//                             v        v
let json = r#"{"jsonrpc":"2.0","id":123,"id":321,"result":"OK"}"#;
let response = from_str::<Response<String>>(&json).unwrap();
assert_eq!(response.id, Id::Num(321));

// But 2 results are not allowed.
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK","result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 48");

// Same with errors.
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":""},"error":{"code":-1,"message":""}}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 66");