Skip to content
Snippets Groups Projects

RFC 4 : Duniter WS2P API v1

Merged nanocryk requested to merge ws2p_v1 into master
Compare and
1 file
+ 662
0
Compare changes
  • Side-by-side
  • Inline
+ 662
0
# Duniter WS2P API v1
This document details the current specifications of WS2P v1 as they are already integrated in duniter-ts 1.6.
## Contents
* [Contents](#contents)
* [Conventions](#conventions)
* [JSON format](#json-format)
* [What is WS2P ?](#what-is-ws2p-)
* [WS2P Endpoints](#ws2p-endpoints)
* [Endpoint format](#endpoint-format)
* [Getting endpoints from other Duniter nodes](#getting-endpoints-from-other-duniter-nodes)
* [Priority of WS2P outcoming connections](#priority-of-ws2p-outcoming-connections)
* [Overview on the format of ws2p messages](#overview-on-the-format-of-ws2p-messages)
* [Establishing a WS2P connection](#establishing-a-ws2p-connection)
* [Rules for accepting an incoming connection](#rules-for-accepting-an-incoming-connection)
* [HEAD messages](#head-messages)
* [Documents messages](#documents-messages)
* [Peer format](#peer-format)
* [Rebound policy](#rebound-policy)
* [HEAD Rebound policy](#head-rebound-policy)
* [Documents rebound policy](#documents-rebound-policy)
* [WS2P requests](#ws2p-requests)
* [getCurrent](#getcurrent)
* [getBlock](#getblock)
* [getBlocks](#getBlocks)
* [getRequirementsPending](#getrequirementspending)
* [List of error messages](#list-of-error-messages)
## Conventions
### JSON format
All data in JSON format is presented in this RFC with explicit typing.
Types list :
Number(field description),
String(field description),
Bool(field description),
Object(field description)
Example :
{
"pubkey": String(identity pubkey in base58),
"expires_on": Number(timestamp),
"wasMember": Bool(true if the identity has already been a member at least once),
"pendingMembership" : Object(pending membership document)
]
## What is WS2P ?
WS2P means "**W**eb **S**ocket **To** **P**eer".
WS2P is the inter-node network layer, it is the only guarantor of synchronization between the different nodes of the network, so its role is critical.
WS2P is the network part of the Duniter protocol, so any Duniter implementation must integrate the WS2P network layer in order to work.
WS2P is exclusively based on websocket technology.
## WS2P Endpoints
### Endpoint format
In the peer card : `API uuid host port path`
Resolved in* : `ws://host:port/path`
*If the port is `443`, the `wss://` protocol will be used.
api := Field indicating api type. The only two types are `WS2P` and `WS2PTOR`.
uuid := Random sequence of 8 hexadecimal characters. (This uuid, coupled with the public key, makes it possible to identify a Duniter node in a unique way.)
host := Domain name or ipv4 or ipv6 (it is not possible to declare both an ipv4 and an ipv6, for this it is necessary to use a domain name.)
port := Mandatory. Must be an integer. (_By convention, port 20900 is used for currency g1-test and port 20901 for currency g1._)
path := Optional access path
Example of a valid endpoint : `WS2P c1c39a0a g1-monit.librelois.fr 443 ws2p`
This endpoint is resolved as follows: `wss://g1-monit.librelois.fr:443/ws2p`
Other valid endpoint : `WS2P a0a45ed2 88.174.120.187 20901`
This endpoint is resolved as follows: `ws://88.174.120.187:20901`
Any WS2P endpoint must match this regexp : `/^WS2P (?:[1-9][0-9]* )?([a-f0-9]{8}) ([a-z_][a-z0-9-_.]*|[0-9.]+|[0-9a-f:]+) ([0-9]+)(?: (.+))?$/`
### Getting endpoints from other Duniter nodes
Initially, Duniter is totally endpoint agnostic and don't know any node.
The ws2p module requires that at least one or more peer cards are already stored in the database when launching the node,
it's up to the Duniter sync command to handle this (ws2p does not handle this).
_*peer card : Signed document declaring all the access points of a peer._
When duniter-ts starts up, the WS2P module accesses the peer cards stored in duniter-ts's database following synchronization.
## Priority of WS2P outcoming connections
The WS2P module attempts to establish outcoming connections at the kick-start of the node and then every 10 minutes.
At each attempt, all known endpoints are contacted until the outcoming quota is reached.
If the outcoming quota is already reached at the beginning of an attempt, the least priority connection is removed in order to keep the network dynamic and scalable.
The WS2P module does not attempt to connect randomly to any node among those of which it knows a WS2P endpoint,
it classifies them according to the following criteria :
### 1st criterion: the network layer
If the node is configured to use an X network layer, then all nodes that accept a connection through this X network layer will have priority.
Currently, only the TOR network layer is implemented.
_Note: the first criterion takes precedence over the second one._
### 2nd criterion: priority score
The priority score of a node depends on its public key and is calculated as follows :
by default the priority score is zero
If the key is a member: +1
If the key is preferred/privileged: +2
If the key is the same as yourself: +4 (for multi-node)
## Overview on the format of ws2p messages
Most ws2p messages exist in two formats :
JSON: this is the format used to transmit the message through the websocket.
RAW: this is the format used to sign the message. Messages received in JSON must therefore be converted to raw format in order to verify the validity of their signature.
## Establishing a WS2P connection
After sorting all WS2P endpoints according to the criteria described in the previous section,
Duniter-ts tries to connect to the other nodes by group of 5.
for each connection attempt, the process is as follows:
1. Opening a websocket pointing to the remote endpoint. If the websocket has been successfully opened before the timeout*, send a CONNECT message. Otherwise, log the error and close the websocket.
3. Wait to receive a CONNECT message from the remote node, checking the validity. If the CONNECT message is valid, then sending an ACK message. Otherwise, log the error and close the websocket.
4. Wait to receive an ACK message from the remote node, checking the validity. If the ACK message is valid, then sending an OK message. Otherwise, log the error and close the websocket.
5. Wait to receive an OK message from the remote node, checking the validity. If the OK message is valid, then consider the ws2p connection completely established. Otherwise, log the error and close the websocket.
*timeout : 15 seconds for connection to a conventional endpoint. 30 seconds for connection to a hidden tor service.
### CONNECT message
JSON format :
{
"auth": String("CONNECT"),
"pub": String("D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx"),
"challenge": String("28170b84-3468-4c21-806e-cf6457d43298055df085-8e5e-43cd-9907-e5c4f9de5bc7"),
"sig": String("wIa/gohWYcJUt10xgsMAjlBiMYhxu2DOKDJdPiEFVB3OVynFvPPW4S/gGZQE7vlxzplSHUE3dCSWfrtjGtlGCw==")
}
pub := Local node public key
challenge := random string
sig := ed25519 signature of the RAW format message in base64.
RAW format : `WS2P:CONNECT:currency_name:pub:challenge`
RAW format of the example message above : `WS2P:CONNECT:g1:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:28170b84-3468-4c21-806e-cf6457d43298055df085-8e5e-43cd-9907-e5c4f9de5bc7`
### ACK message
JSON format :
{
"auth": String("ACK"),
"pub": String("D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx"),
"sig": String("wIa/gohWYcJUt10xgsMAjlBiMYhxu2DOKDJdPiEFVB3OVynFvPPW4S/gGZQE7vlxzplSHUE3dCSWfrtjGtlGCw==")
}
pub := Local node public key
sig := ed25519 signature of the RAW format message in base64.
raw format : `WS2P:ACK:currency_name:pub:challenge`
_\*The ACK message is signed with the challenge of the other one.
So your ACK message is signed with the challenge given by the remote node in its CONNECT message,
and its ACK message is signed with the challenge you gave it in your CONNECT message.
The challenge is not retransmitted in json format of ACK messages because the remote node is supposed to already have this information._
### OK message
JSON format :
{
"auth: String("OK"),
"sig": String(ed25519 signature of the RAW format message in base64)
}
raw format : `WS2P:OK:currency_name:pub:challenge`
_**Be careful**, this time each one signs his message OK with his own challenge that he sent in his message CONNECT.
The challenge is not retransmitted in json format of OK messages because the remote node is supposed to already have this information._
## Rules for accepting an incoming connection
When a CONNECT message is received from another node, three decisions are possible :
1. Accept connection
2. Refuse connection
3. not answering
The node will always try to answer when possible, to do so it launches a timeout on its side as soon as it receives the request.
When the timeout is reached, if the request has not been fully processed, it is purely abandoned and the remote node will not receive an answer.
In cases where the request is fully processed before the timeout, the response is determined according to the following algorithm :
If the public key of the remote node is the same as yourself
If the uuid is not the same, accept the connection. Otherwise, refuse connection. (do not accept oneself connetion).
If the pubkey is banned, refuse connection.
If priorityKeysOnly is enable and the pubkey isn't privileged, refuse connection.
If there is already an active ws2p connection with this key (whether incoming or outcoming), refuse connection.
If the incoming quota is not reached, accept the connection.
If there is a strictly lower priority* incoming connection, accept the connection.
Refuse connection.
Following the acceptance of an incoming connection, it may happen that the quota is exceeded,
in this case a low priority* connection is deleted until the quota is respected.
_*The priority score is calculated in the same way as for outcoming connections._
## HEAD messages
Each duniter node informs the network of its current state via a HEAD, which is renewed with each new valid block.
The heads received from other nodes are recorded and relayed, so by bouncing, each Duniter node to a global view of the network.
The set of heads known by a duniter node at the moment is public information available via the BMA client api (url /network/ws2p/heads).
Each new head received from the same node overwrites the previous head in memory, so that only the current state of the network is stored.
HEADs message format :
{
name: "HEAD",
body: {
heads: [
HEAD_0,
HEAD_1,
...
]
}
}
### HEAD v0
Accepted since duniter-ts **v1.6.0**
{
"message": String("API:MESSAGE_TYPE:PUBKEY:BLOCKSTAMP"),
"sig": String("ZkatBTCYlp1KC/AS2TcDUYmxsWo0SaIDgkTZnhJzT2HU2OdJTqYr5s5JA+8iGCf0Qml8UgiwidscAEyeEl+WBg=="")
}
#### message field
API = "WS2P"
MESSAGE_TYPE = "HEAD"
PUBKEY := public key of the issuer node of this head
BLOCKSTAMP = `blockNumber-Hash`
#### sig field
sig: ed25519 signature of the message field in base64.
### HEAD v1
Accepted since duniter-ts **v1.6.9**
{
"message": String("API:MESSAGE_TYPE:1:PUBKEY:BLOCKSTAMP:WS2PID:SOFTWARE:SOFT_VERSION:POW_PREFIX"),
"sig": String("MY90zXICfbYhLlz8VrL4HWPkphZEFR+bT2JWsoKdDMadgn0R0ZjsowDsnlfNqX4F4qeWeFoxhvdVgTO9VSghCA==")
}
#### message field
API = `WS2P`
MESSAGE_TYPE = `HEAD`
PUBKEY := public key of the issuer node of this head
BLOCKSTAMP = `blockNumber-Hash`
WS2PID := unique identifier of the ws2p endpoint
SOFTWARE = `duniter-ts`
SOFT_VERSION = `X.Y.Z` (for example `1.6.14`)
POW_PREFIX := nonce prefix for proof of work (manually fixed by the user)
#### sig field
sig: ed25519 signature of the message field in base64.
### HEAD v2
Accepted since duniter-ts **v1.6.9**, relayed fully since duniter-ts **v1.6.15**
_Nodes with versions lower than 1.6.15 bounce a degraded version of the head, only `message` and `sig` fields are relayed._
{
"message": String("API:HEAD:1:PUBKEY:BLOCKSTAMP:WS2PID:SOFTWARE:SOFT_VERSION:POW_PREFIX"),
"sig": String("TPh2A3NS8cHj8yrJk1Yeldx2H6bPEp46cFAGZXKfxJcNgXL2sWrlirhIOlp8pkUFSrwDawWY1zO1jlgUqMvlAg=="),
"messageV2": String("API:HEAD:2:PUBKEY:BLOCKSTAMP:WS2PID:SOFTWARE:SOFT_VERSION:POW_PREFIX:FREE_MEMBER_ROOM:FREE_MIRROR_ROOM"),
"sigV2": String("ta1lRrWsjGcYHcLdS75JgEW5B8ByRetFVUVVpakKNJBirhRe8HcYUHEOM7xj/+gUQGGOit6Gm5Q/lsvfsngWAQ=="),
"step": Number(0)
}
#### message & messageV2 fields
API := field indicating the network layer type and giving some information about the Network configuration. (See section "API field" for details)
MESSAGE_TYPE = `HEAD`
PUBKEY := public key of the issuer node of this head
BLOCKSTAMP = `blockNumber-Hash`
WS2PID := unique identifier of the ws2p endpoint
SOFTWARE = `duniter-ts`
SOFT_VERSION = `X.Y.Z` (for example `1.6.14`)
POW_PREFIX := nonce prefix for proof of work (manually fixed by the user)
FREE_MEMBER_ROOM := Number of incoming connection requests that can still be accepted from member nodes (FREE_MIROR_ROOM - Number of incoming connections established by non-priority mirror nodes).
FREE_MIROR_ROOM := An integer indicating the number of incoming connections that the node can still receive.
##### API field
general scheme : `WS2P[PrivateConf][PublicConf]`
We have 4 types of WS2P Private:
OCA : clear all
OTM : tor mixed
OTA : tor all
OTS : tor strict
| WS2P Private Type | Reach Clear Endpoint | Reach Tor Endpoint
|:----:|:----:|:----:
|OCA|in clear|never
|OTM|in clear|per Tor
|OTA|per Tor |per Tor
|OTS|never |per Tor
And two types of WS2P Public :
IC clear endpoint
IT tor endpoint
The WS2P Private conf is prefixed with an O and the ws2p public conf with an I, so the classic nodes will be of type `WS2POCAIC`.
If WS2P Public is disabled, the [PublicConf] part is absent : `WS2POCA`
#### sig & sigv2 fields
sig: ed25519 signature of the message field in base64.
sigV2: ed25519 signature of the messageV2 field in base64.
#### step field
Distance of the corresponding node on the network = Number of intermediate nodes through which this head bounces -1
**WARNING:** The step field is not signed, so its value cannot be authenticated and should only be used as an indication.
## Documents messages
All documents are sent in json format, the body format of the request is always as follows :
{
body: {
name: Number(DOCUMENT_TYPE_ID),
"DOCUMENT_TYPE_NAME": Object(DOCUMENT)
}
}
table of document types :
|DOCUMENT_TYPE_ID|DOCUMENT_TYPE_NAME|
| 0| peer|
| 1| transaction|
| 2| membership|
| 3| certification|
| 4| identity|
| 5| block|
With the exception of the peer format detailed below, the json format of each document is already detailed in the DUP protocol v10 : https://git.duniter.org/nodes/typescript/duniter/blob/1.6/doc/Protocol.md
### Peer format
{
"version": Number(10),
"currency": String(CURRENCY_NAME),
"endpoints": [
String(ENDPOINT_1),
String(ENDPOINT_2),
...
],
"status": String("UP"),
"block": String(BLOCKSTAMP),
"signature": String(SIGNATURE),
"raw": String(RAW_FORMAT),
"pubkey": String(PUBKEY)
}
Real example :
{
"version": 10,
"currency": "g1",
"endpoints": [
"BMAS g1.monnaielibreoccitanie.org 443",
"BASIC_MERKLED_API g1.monnaielibreoccitanie.org 443",
"WS2P b48824f0 g1.monnaielibreoccitanie.org 20901"
],
"status": "UP",
"block": "89908-000006F1C135E1D1CD41BF13DC3A406F2DF577144BEEAB49F437D661FF3E8018",
"signature": "zTPLWdmHm5c3uIfTKxYGtSd59b13Lc+FCcbCBuHEEllYj+3xwquo/wo3VbF5J1gzMewamB9JYOV74uNOm8itAQ==",
"raw": "Version: 10\nType: Peer\nCurrency: g1\nPublicKey: 7v2J4badvfWQ6qwRdCwhhJfAsmKwoxRUNpJHiJHj7zef\nBlock: 89908-000006F1C135E1D1CD41BF13DC3A406F2DF577144BEEAB49F437D661FF3E8018\nEndpoints:\nBMAS g1.monnaielibreoccitanie.org 443\nBASIC_MERKLED_API g1.monnaielibreoccitanie.org 443\nWS2P b48824f0 g1.monnaielibreoccitanie.org 20901\nzTPLWdmHm5c3uIfTKxYGtSd59b13Lc+FCcbCBuHEEllYj+3xwquo/wo3VbF5J1gzMewamB9JYOV74uNOm8itAQ==\n",
"pubkey": "7v2J4badvfWQ6qwRdCwhhJfAsmKwoxRUNpJHiJHj7zef"
}
## Rebound policy
### HEAD Rebound policy
### HEADCache
The local node has a HEADCache (only in RAM), which represents its network view.
This cache is an associative array, the keys correspond to the pair (pubkey+guid) and the values correspond to the complete HEAD in JSON format.
Note : In duniter-ts 1.6, the content of this HEADCache is provided by a BMA url in the form of a JSON array of values (without keys) : /network/ws2p/heads
#### Local HEAD
The local node issues a new HEAD every time the last block of its local branch changes, this change can have two causes:
1. Arrival of a new block valid and stackable on the local branch
2. Local branch change caused by the fork resolution algorithm
Each time the local node emits a new head, it sends it to all established ws2p connections.
### Remotes HEADs
When the local node receives a HEAD message, it checks whether all of the following conditions are verified :
1. The head has a valid format and a valid signature
2. The head has a block number equal to or greater than the head of the same key in the cache (hereafter referred "cached head").
3. If the block number is equal to the cached head, the head is of a higher or equal version.
4. If the block number is equal to the cached head, and the version is identical, the step field is smaller.
5. If the head pubkey is not a member, make sure there is an active ws2p connection with this pubkey.
If all these conditions are respected, perform the following actions :
1. Save this head in the headCache, overwrite the existing head with same key.
2. Increment the step field of this head then send it to all established ws2p connections.
### Documents rebound policy
Each time Duniter receives a new document, either via the client api or via ws2p, it performs the following checks:
1. Checking the document's newness (it is not already present in bdd)
2. Verification of the document's conformity (correct format and valid signatures)
3. Verification of compliance with all local index rules
If all conditions are satisfied, then the document is saved in the local bdd of the node and is transmitted to all active ws2p connections.
## WS2P requests
Specific queries can be sent via ws2p to obtain specific blocks or sandbox data.
Each type of query has a unique identifier called "name".
### getCurrent
JSON Message :
{
reqId: String(REQUESTS_UNIQUE_ID),
body: {
name: String("CURRENT"),
params: {}
}
}
REQUESTS_UNIQUE_ID := Random sequence of 8 hexadecimal characters. (This unique identifier will be returned to the header of the response,
it allows the requester node to identify to which query the answers it receives correspond, because the answers are asynchronous).
JSON response to success :
{
resId: String(REQUESTS_UNIQUE_ID),
body: Object(BLOCK_IN_JSON_FORMAT)
}
JSON error response :
{
resId: String(REQUESTS_UNIQUE_ID),
err: String(error message)
}
### getBlock
JSON Message :
{
reqId: String(REQUESTS_UNIQUE_ID),
body: {
name: String("BLOCK_BY_NUMBER"),
params: {
number: Number(BLOCK_NUMBER)
}
}
}
REQUESTS_UNIQUE_ID := Random sequence of 8 hexadecimal characters. (This unique identifier will be returned to the header of the response,
it allows the requester node to identify to which query the answers it receives correspond, because the answers are asynchronous).
JSON response to success :
{
resId: String(REQUESTS_UNIQUE_ID),
body: Object(BLOCK)
}
JSON error response :
{
resId: String(REQUESTS_UNIQUE_ID),
err: String(error message)
}
### getBlocks
JSON Message :
{
reqId: REQUESTS_UNIQUE_ID,
body: {
name: "BLOCKS_CHUNK",
params: {
count: Number(number of blocks requested),
fromNumber: Number(number of the 1st block of the requested interval)
}
}
}
REQUESTS_UNIQUE_ID := Random sequence of 8 hexadecimal characters. (This unique identifier will be returned to the header of the response,
it allows the requester node to identify to which query the answers it receives correspond, because the answers are asynchronous).
JSON response to success :
{
resId: String(REQUESTS_UNIQUE_ID),
body: [
Object(BLOCK_1),
Object(BLOCK_2),
...
]
}
JSON error response :
{
resId: String(REQUESTS_UNIQUE_ID),
err: String(error message)
}
### getRequirementsPending
Requests the "requirements" of all identities that have received at least `minCert` certifications.
JSON Message :
{
reqId: String(REQUEST_UNIQUE_ID),
body: {
name: String("WOT_REQUIREMENTS_OF_PENDING"),
params: {
minCert: Number()
}
}
}
REQUESTS_UNIQUE_ID := Random sequence of 8 hexadecimal characters. (This unique identifier will be returned to the header of the response,
it allows the requester node to identify to which query the answers it receives correspond, because the answers are asynchronous).
JSON response to success :
{
"resId": REQUESTS_UNIQUE_ID,
"body": {
"identities": [
{
"certifications": [
{
"expiresIn": Number(timestamp),
"from": String(issuer pubkey in base58),
"timestamp": Number(),
"to": String(receiver pubkey in base58)
}),
...
]
"expired": Bool(),
"isSentry": Bool(),
"membershipExpiresIn": Number(),
"membershipPendingExpiresInm": Number(),
"meta": {
"timestamp": String(identity document creation blockstamp)
},
"outdistanced": Bool(),
"pendingCerts": [
{
"block": Number(doubloon with "block_number" field),
"block_hash": String(certification document creation block hash),
"block_number": Number(certification document creation block number),
"blockstamp": String(certification document creation blockstamp),
"expired": Number(is always zero),
"expires_on": Number(timestamp),
"from": String(issuer pubkey in base58),
"linked": Bool(is always false),
"sig": String(certification document signature in base64),
"target": String(hash of the target identity),
"to": String(receiver pubkey in base58),
"written": Bool(is always false),
"written_block": is always Null,
"written_hash": is always Null
},
Object(PEDNIGN_CERT_2),
...
],
"pendingMemberships": [
{
"block": String(doubloon with "blockstamp" field),
"blockHash": String(membership document creation block hash),
"blockNumber": Number(99156),
"blockstamp": String(membership document creation blockstamp),
"certts": String(doubloon with "blockstamp" field),
"expired": Null,
"expires_on": Number(timestamp),
"fpr": String(doubloon with "blockHash" field),
"idtyHash": String(hash of the target identity),
"issuer": String(issuer pubkey in base58),
"membership": String(user request type : "IN" or "OUT"),
"number": Number(doubloon with "blockNumber" field),
"sig": String(membership document signature in base64),
"signature": String(doubloon with "sig" field),
"type": String(doubloon with "membership" field),
"userid": String(username),
"written": Bool(is always false),
"written_number": Null
},
...
],
"pubkey": String(identity pubkey in base58),
"revocation_sig": String(revocation document signature in base64, empty where the identity is not revoked),
"revoked": Bool(),
"revoked_on": Number(obsolete field to be deleted),
"sig":String(identity document signature in base64),
"uid": String(username),
"wasMember": Bool(true if the identity has already been a member at least once)
},
Object(IDTY_2),
...
]
}
}
JSON error response :
{
resId: String(REQUESTS_UNIQUE_ID),
err: String(error message)
}
### List of error messages
* "Wrong param `number`"
* "Wrong param `count`"
* "Wrong param `fromNumber`"
* "Wrong param `minCert`"
* "Unknow request"
\ No newline at end of file
Loading