Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: document rpc api versioning process. #2901

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ https://decred.org/community/

* [JSON-RPC Reference](https://github.com/decred/dcrd/tree/master/docs/json_rpc_api.mediawiki)
* [RPC Examples](https://github.com/decred/dcrd/tree/master/docs/json_rpc_api.mediawiki#8-example-code)
* [RPC API Versioning](https://github.com/decred/dcrd/tree/master/docs/rpc_api_versioning.md)

<a name="GoModules" />

Expand Down
357 changes: 357 additions & 0 deletions docs/rpc_api_versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
### Table of Contents
1. [Per-Method Versioning](#PerMethodVersioning)<br />
2. [Versioning Scheme](#VersioningScheme)<br />
3. [Versioned Endpoint Accessibility Scheme](#AccessibilityScheme)<br />
4. [Versioned Endpoint Example](#Example)<br />
5. [Versioned Endpoint Deprecation And Removal Scheme](#DeprecationAndRemovalScheme)<br />
6. [Versioned Endpoint JSON-RPC Unit Testing](#UnitTesting)<br />
7. [Versioned Endpoint Documentation](#Documentation)<br />
8. [Websocket Endpoints And Example](#Websockets)


<a name="PerMethodVersioning" />

### 1. Per-Method Versioning

To prevent API compatibility issues when transitioning between release
versions, dcrd's JSON-RPC API methods will each have an individually versioned
endpoint. Per-method endpoint versioning provides the flexibility needed to
isolate the development of each method, and allows each method to evolve
individually and exclusively from each other.

<a name="VersioningScheme" />

### 2. Versioning Scheme

The versioning scheme constitutes appending the version to the type name or
handler name.
- JSON-RPC types will have the version spliced between the method name and
`Cmd` suffix. For example, the `V2` version of the type `ExistsAddressCmd`
becomes `ExistsAddressV2Cmd`.

- JSON-RPC handler names will have the version appended to the handler name.
For example, the `V2` version of the handler `handleExistsAddress` becomes
`handleExistsAddressV2`.

- JSON-RPC method names will have the lowercase version appended to the
method name. For example the `V2` version of method `existsaddress` becomes
`existsaddressv2`.

<a name="AccessibilityScheme" />

### 3. Versioned Endpoint Accessibility Scheme

A versioned method endpoint infers there is a current version and an older
version of the endpoint. To ensure a smooth transition between method versions
for consumers, both versions should be accessible. The current and older
versions should be explicitly accessible via their respective method names
while the most recent version becomes the default version.

Using `existsaddress` as an example:

- The `V2` version of the endpoint, which is the current version, should be
explicitly accessible via `existsaddressv2`.

- The `V1` version of the endpoint, which is now the older version, should be
explicitly accessible via `existsaddressv1`.

- The default method name, `existsaddress` will resolve to the older version
of the endpoint which is `existsaddress` in this case.

This scheme will be enforced via the JSON-RPC type and handler registrations.

<a name="Example" />

### 4. Versioned Endpoint Example

Taking into consideration the accessibility scheme outlined above, implementing
the `V2` version of the `existaddress` JSON-RPC endpoint will comprise of the
following components:

```go
// JSON-RPC versioned endpoint command definition.

// ExistsAddressV2Cmd defines the existsaddressv2 JSON-RPC command.
type ExistsAddressV2Cmd struct {
Address string
PaymentScript string
}
// ...
```
```go
// ExistsAddress JSON-RPC type registrations.
dcrjson.MustRegister(Method("existsaddress"), (*ExistsAddressCmd)(nil), flags)
dcrjson.MustRegister(Method("existsaddressv1"), (*ExistsAddressCmd)(nil), flags)
dcrjson.MustRegister(Method("existsaddressv2"), (*ExistsAddressV2Cmd)(nil), flags)
// ...
```

```go
// ExistsAddressV2 JSON-RPC Handler definition.

// handleExistsAddress implements the existsaddress command.
//
// Deprecated: This will be removed on the next versioned endpoint
// implementation.
func handleExistsAddress(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
if s.cfg.ExistsAddresser == nil {
return nil, rpcInternalError("Exists address index disabled",
"Configuration")
}

c := cmd.(*types.ExistsAddressCmd)
// ...
}

// handleExistsAddressV2 implements the existsaddressv2 command.
func handleExistsAddressV2(_ context.Context, s *Server, cmd interface{}) (interface{}, error) {
if s.cfg.ExistsAddresser == nil {
return nil, rpcInternalError("Exists address index disabled",
"Configuration")
}

c := cmd.(*types.ExistsAddressV2Cmd)
// ...
}

```

```go
// ExistsAddress JSON-RPC handler registrations.
var rpcHandlersBeforeInit = map[types.Method]commandHandler{
// ...
"existsaddress": handleExistsAddress,
"existsaddressv1": handleExistsAddress,
"existsaddressv2": handleExistsAddressV2,
// ...
}
```

```go
// ExistsAddress JSON-RPC result type registrations.
var rpcResultTypes = map[types.Method][]interface{}{
// ...
"existsaddress": {(*bool)(nil)},
"existsaddressv1": {(*bool)(nil)},
"existsaddressv2": {(*bool)(nil)},
// ...
}
```

<a name="DeprecationAndRemovalScheme" />

### 5. Versioned Endpoint Deprecation And Removal Scheme

Each JSON-RPC method will only have two maintained endpoint versions. The
introduction of a new endpoint version should immediately indicate that the
oldest maintained endpoint will be deprecated. This is expected to be done
by the contributor in the pull request introducing the new versioned endpoint.
Using `existsaddress` as an example, with the introduction of the versioned
endpoint `existsaddressv2` `existsaddressv1` will be marked as deprecated and
eventually removed when the next versioned endpoint `existsaddressv3` is
introduced.

<a name="UnitTesting" />

### 6. Versioned Endpoint JSON-RPC Unit Testing

All versioned method handlers are required to have their own set of unit tests.
Contributors versioning these endpoints will be required to include unit tests
in the pull request introducing the versioned JSON-RPC endpoint. Usually the
existing unit tests for the endpoint being versioned will serve as the base
for the new set of tests, these units will be copied and modified as needed.

Using `existsaddress` as an example, a new unit test for the versioned endpoint
`existsaddressv2` will be added to the rpcserver test set. Note that the unit
test for the `existaddress` will remain in the rpcserver unit test set. It will
only be removed when the deprecated endpoint is being removed, when
`existsaddressv3` is introduced.

```go
// handleExistsAddressV2 JSON-RPC unit test
func TestHandleExistsAddressV2(t *testing.T) {
t.Parallel()

validAddr := "DcurAwesomeAddressmqDctW5wJCW1Cn2MF"
validPaymentScript := "a914f59833f104faa3c7fd0c7dc1e3967fe77a9c152387"
wrongPaymentScript := "a114f59833f104faa3c7fd0c7dc1e3967fe77a9c152354"
testRPCServerHandler(t, []rpcTest{{
name: "handleExistsAddressV2: ok, index is synced",
handler: handleExistsAddressV2,
cmd: &types.ExistsAddressV2Cmd{
Address: validAddr,
PaymentScript: validPaymentScript,
},
result: false,
}, {
name: "handleExistsAddressV2: wrong payment script, index is synced",
handler: handleExistsAddressV2,
cmd: &types.ExistsAddressV2Cmd{
Address: validAddr,
PaymentScript: wrongPaymentScript,
},
result: false,
}
// ...
})
}

// ...
```

<a name="Documentation" />

### 7. Versioned Endpoint Documentation

Documentation for the newly versioned endpoint will be expected in the
pull request introducing the versioned endpoint. Documentation for versioned
endpoints are expected to be grouped, contributors should add the new
documentation entry after the old version's documentation. The old version's
handler documentation should also indicate it has been deprecated in favour of
the new version and scheduled to the removed on the introduction of the next
endpoint version.

Using `existsaddress` as an example, with the introduction of `existsaddressv2`
the JSON-RPC API documentation will be updated to include documentation for
`existsaddressv2`.

```
...

====existsaddressv2====
{|
!Method
|existsaddressv2
|-
!Parameters
|
# <code>address</code>: <code>(string, required)</code> The address to check.
# <code>paymentscript</code>: <code>(string, required)</code> The hex encoded payment script to check.
|-
!Description
|Returns the existence of the provided address with the provided payment script.
|-
!Returns
|<code>boolean</code>
|-
!Example Return
|<code>true</code>
|}

----

...

|[[#existsaddress|existsaddress]]
|Y
|Returns the existence of the provided address.
|-
|[[#existsaddressv2|existsaddressv2]]
|Y
|Returns the existence of the provided address with the provided payment script.
|-
...
```

``` go
// RPC Server Help Documentation

// ...
// ExistsAddressCmd help.
"existsaddress--synopsis": "Test for the existence of the provided address",
"existsaddress-address": "The address to check",
"existsaddress--result0": "Bool showing if address exists or not",

// ExistsAddressV2Cmd help.
"existsaddressv2--synopsis": "Test for the existence of the provided address and whether it has the provided payment script",
"existsaddressv2-address": "The address to check",
"existsaddressv2-paymentscript": "The address payment script to check",
"existsaddressv2--result0": "Bool showing if address exists or not",
// ...
```

<a name="Websockets" />

### 8. Websocket Endpoints & Example
Websocket endpoints will be versioned using the accessibility scheme outlined
above. Using `rescan` websocket endpoint and the introduction of `rescanv2`
as an example:

```go
// Rescan websocket handler registrations.
var wsHandlersBeforeInit = map[types.Method]wsCommandHandler{
// ...
"rescan": handleRescan,
"rescanv1": handleRescan,
"rescanv2": handleRescanV2,
// ...
}
```

```go
// Versioned JSON-RPC endpoint command definition.

// RescanCmd defines the rescan JSON-RPC command.
type RescanCmd struct {
BlockHashes []string
}

// RescanV2Cmd defines the rescanv2 JSON-RPC command.
type RescanV2Cmd struct {
From string
To string
}
```

```go
// Rescan JSON-RPC result type registrations.
var rpcResultTypes = map[types.Method][]interface{}{
// Websocket commands.

// ...
"rescan": nil,
"rescanv1": nil,
"rescanv2": nil,
// ...
}
```

```go
// Rescan Websocket type registrations.
func init() {
// The commands in this file are only usable by websockets.
flags := dcrjson.UFWebsocketOnly

//...
dcrjson.MustRegister(Method("rescan"), (*RescanCmd)(nil), flags)
dcrjson.MustRegister(Method("rescanv1"), (*RescanCmd)(nil), flags)
dcrjson.MustRegister(Method("rescanv2"), (*RescanV2Cmd)(nil), flags)
//...
}
```

```go
// handleRescan implements the rescan command extension for websocket
// connections.
func handleRescan(wsc *wsClient, icmd interface{}) (interface{}, error) {
cmd, ok := icmd.(*types.RescanCmd)
if !ok {
return nil, dcrjson.ErrRPCInternal
}
// ...
}

// handleRescanV2 implements the rescanv2 command extension for websocket
// connections.
func handleRescanV2(wsc *wsClient, icmd interface{}) (interface{}, error) {
cmd, ok := icmd.(*types.RescanV2Cmd)
if !ok {
return nil, dcrjson.ErrRPCInternal
}
// ...
}
```

Once all dcrd JSON-RPC API methods are updated using the schema outlined above,
the versioned endpoints will be ready for use. However, dcrctl and other
consumers will need to update their `rpc/jsonrpc/types` dependencies and logic.