Skip to content

Latest commit

 

History

History
141 lines (101 loc) · 11.9 KB

ic-ws-protocol.md

File metadata and controls

141 lines (101 loc) · 11.9 KB

IC WebSocket Protocol

Establishment

To establish a new IC WebSocket connection, the client (via the IC WebSocket Frontend SDK):

  • chooses a WS Gateway among the available ones and creates a new instance of IcWebSocket and passes the client’s identity to it (if any, otherwise a random one is generated by the SDK).
  • opens a WebSocket connection to the specified Gateway.
  • once the WebSocket connection is open, it creates a WebSocket Actor which is used to send requests, signed with the provided identity, to the Gateway. Each request specifies the canister and the method which the request is for.
  • the first request sent is a signed envelope with content of Call variant. The content contains, among other things, the principal of the canister the client is connecting to, ws_open as the method name, and the argument of type CanisterWsOpenArguments.
  • sends the envelope to the WS Gateway.
  • once it receives the response containing the result of type CanisterWsOpenResultValue, triggers the onWsOpen callback.

The Gateway:

  • receives the envelope with content of variant Call from the client and relays it to the canister/<canister_id>/call endpoint of the Internet Computer.
  • receives the HTTP response from the IC containing Ok(()) in the body and relays it to the client. This response is not enough for the client to trigger onWsOpen.
  • creates a mapping between the client_id assigned to the WebSocket connection and the client’s key composed of a principal (either corresponding to the client’s identity or to the one randomly generated by the SDK) and the nonce specified by the client SDK.
  • if the client is the first connecting to the specified canister via this WS Gateway, the latter starts polling the canister by querying the ws_get_messages endpoint, otherwise the Gateway is already polling the canister.
  • once the poller returns certified messages from the canister, it relays each of them to the clients via their corresponding WebSocket connection, together with the certificate.

The canister (via the IC WebSocket Backend CDK):

  • receives a request on the ws_open method from the client (relayed by the Gateway in a way that is transparent to the canister).
  • if the client is the first connecting via this Gateway, it creates a message queue where it stores all the messages of clients connected via that Gateway, and which only this can poll. Otherwise, the queue already exists.
  • once the canister processes the request to the ws_open method, it puts the message containing the result of type CanisterWsOpenResultValue in the respective message queue which the Gateway fetches in the next polling iteration.
  • triggers the on_open callback.

Types:

  • CanisterWsOpenArguments
    • client nonce used by the canister to distinguish two different connections from the same client.
  • CanisterWsOpenResult
    • result with empty Ok value. Needed only to let the client know that the IC WebSocket connection has been opened.

Relay Client Messages

Once the connection is established, the client can send WebSocket messages to the canister. In order to do so, the client (via the IC WebSocket Frontend SDK):

  • creates a signed envelope with content of Call variant. The content contains, among other things, the principal of the canister the client is connected to, ws_message as the method name, and the argument of type CanisterWsMessageArguments.
  • sends the envelope to the WS Gateway.

The Gateway:

  • receives the envelope from the client and relays it to the canister/<canister_id>/call endpoint of the Internet Computer.
  • receives the HTTP response from the canister containing Ok(()) in the body and relays it to the client. This is not enough to acknowledge the client’s message.

The canister (via the IC WebSocket Backend CDK):

  • receives a request on the ws_message method from the client (relayed by the Gateway in a way that is transparent to the canister).
  • checks whether the sequence number of the WebSocketMessage (included automatically by the client’s SDK) corresponds to the next expected sequence number from the respective client.
  • triggers the on_message callback.

Types:

  • CanisterWsMessageArguments
    • message of type WebSocketMessage
      • sequence number used to identify the client message
      • serialized content
      • client key composed of client’s principal and nonce specified during the opening of the connection
      • timestamp
      • is_service_message flag used to determine whether the message is only used by the CDK and SDK to detect eventual bad behaviour of the WS Gateway. Messages flagged as true are not passed to the client.

Relay Canister Messages

Once the connection is established, the canister can send WebSocket messages to the client. In order to do so, the canister (via the IC WebSocket Backend CDK):

  • calls the ws_send method, specifying the client key of the client it wants to send the message to and the serialized message to be sent.
  • the message of type CanisterOutputMessage is stored in the message queue corresponding to the WS Gateway which the client is connected to.

The Gateway:

  • fetches the messages of type CanisterOutputMessage from the respective queue of the canister in the next polling iteration by querying the ws_get_messages method with the argument of type CanisterWsGetMessagesArguments. The list of fetched messages is of type CanisterOutputCertifiedMessages.
  • for each message of type CanisterOutputMessage, gets the client_id corresponding to client key specified in the message.
  • constructs a message of type CanisterToClientMessage from the one of type CanisterOutputMessage.
  • relays the message of type CanisterToClientMessage to the client via the WebSocket connection identified by the client_id.

The client (via the IC WebSocket Frontend SDK):

  • receives the message of type CanisterToClientMessage from the Gateway via WebSocket.
  • verifies the certificate which proves that the message has been created by the canister.
  • checks whether the sequence number of the WebSocketMessage corresponds to the next expected sequence number from the canister.
  • triggers the onWsMessage callback.

Types:

  • CanisterWsGetMessagesArguments
    • nonce used by the WS Gateway to let the CDK know which was the last polled message. This way the CDK does not return messages that have already been relayed to the clients.
  • CanisterOutputCertifiedMessages
    • vector of messages of type CanisterOutputMessage
    • certificate of all the messages
    • certified state tree
  • CanisterOutputMessage
    • client key of the client which the message is for. This is used by the WS Gateway to get the client_id corresponding to the WebSocket connection with the respective client.
    • content of type WebSocketMessage
      • sequence number used to identify the client message
      • serialized content
      • client key composed of client’s principal and nonce specified during the opening of the connection
      • timestamp
      • is_service_message flag used to determine whether the message is only used by the CDK and SDK to detect eventual bad behaviour of the WS Gateway. Messages flagged as true are not passed to the client.
    • key constructed by appending the next outgoing message nonce to the gateway principal. This key is used by the client to verify the certificate of the response and by the Gateway to determine the nonce to poll from in the next polling iteration.
  • CanisterToClientMessage
    • content of type WebSocketMessage (same as in CanisterOutputMessage).
    • key constructed by appending the next outgoing message nonce to the gateway principal (same as in CanisterOutputMessage).
    • certificate of all the messages
    • certified state tree, containing that message

Message Acknowledgement

All messages are relayed by the Gateway which acts as a man-in-the-middle and could therefore tamper, reorder, or block messages. Tampering and reordering can be detected thanks to signed messages and sequence numbers, respectively. However, client and canister have to be able to detect whether the Gateway is blocking the messages within reasonable time. To achieve this, the canister (via the IC WebSocket Backend CDK):

  • periodically (with configurable period T) creates a service message of type WebsocketServiceMessageContent::AckMessage containing an acknowledgement message of type CanisterAckMessageContent and pushes it in the message queue for each client connected to it.
  • once it receives a service message of type WebsocketServiceMessageContent::KeepAliveMessage containing a keep alive message of type ClientKeepAliveMessageContent for a certain client, it records the time current time.
  • 1/2 T after the acknowledgement for a client has been sent (i.e. pushed to the message queue), it checks whether the time of the last keep alive message received for that client is less than 3/2 T. If that’s not the case, the gateway blocked the messages.

The gateway:

  • fetches the acknowledgement message for a client from the respective queue of the canister in the next polling iteration, as if it were a normal canister message.
  • checks the client_id corresponding to the client key of the client which the message has to be delivered to.
  • relays the message to the client via the WebSocket connection identified by the client_id.
  • keeps relaying the signed envelopes received from the client to the canister as these include, among others, the keep alive message sent by the client in response to the acknowledgement message from the canister.

The client (via the IC WebSocket Frontend SDK):

  • upon sending a message via the WebSocket, it records the current time.
  • upon receiving a service message of type WebSocketMessage from the Gateway with content of type WebsocketServiceMessageContent::AckMessage(CanisterAckMessageContent), first performs the same steps as when receiving a relayed canister message (as explained in a previous section). Then, considers all the messages with sequence number lower or equal to the one contained in the service message as acknowledged. For all the others, it checks whether more than 3/2 T has passed, if so the gateway blocked some messages. Otherwise, it still does not considers these messages as acknowledged (and therefore an other acknowledgement for these will be expected) but it sends a service WebSocketMessagee with content of type WebsocketServiceMessageContent::AckMessage(ClientKeepAliveMessageContent).

Types:

  • CanisterAckMessageContent
    • sequence number of the last message received by the canister from the respective client. This acknowledgement is serialized and used as content of a WebsocketMessage message where is_service_message is set to true and sequence_num is the next sequence number of the messages sent from the canister to the respective client.
  • ClientKeepAliveMessageContent
    • sequence number of the last message that the client sending the keep alive message received from the canister.