Skip to content

Message Processing

Message processing begins either when clients receive an SPMessage over the WebSocket, or by calling one of the RESTful APIs like /eventsAfter and processing the list of events the server sends back.

The events in a contract chain are processed in the order they appear in the contract, to build up a consistent shared state across clients (although in some cases, it is possible for clients to arrive at slightly different state).

A contract chain is instantiated using OP_CONTRACT. From that point on, other opcodes are added in an append-only manner, and processed in turn.

Simple diagram showing two SPMessages, the first OP_CONTRACT, the next, OP_ACTION_ENCRYPTED

đź’ˇ The arrows in the picture above point right to indicate which message comes next in the contract chain, but in reality the connections are actually backwards, with each new message pointing to the previous message by referencing it using previousHEAD. The server keeps track of the latest message for any given contract by storing the latest message hash under a special key in the database. Chelonia uses head=<contractID>.

Client-side Processing

Message processing consists of the following steps on the client:

  1. Verifies it is expecting this message (because the message is part of a contract that it subscribed to).
  2. Verifies the message builds upon the previous one it’s seen and optionally stores a copy of the message to its local key-value database (if a “full node”).
    • If the message does not build upon the most recent message seen for this contract, then it is discarded, and an attempt to “reingest” the message is made by syncing the contract on the assumption that there are messages between the last one seen and this message.
  3. Temporarily saves a copy of the entire local contract state (in case the next step fails).
  4. Processes the message and its opcode to modify contract state. Loads any contract in a sandbox as needed via its manifest hash, and passes the message data to any corresponding contract actions so that they can update the contract state.
    • If this step fails, it reverts all mutations using the state that was saved in (3), emits errors as needed, and skips executing side effects.
  5. Updates the most recent hash seen for this contract (aka “contract HEAD”) to equal the hash of this message.
  6. Executes any contract side effects.
    • If there are any errors during side effect execution, inform the user’s code and proceed with processing further messages.

⚠︎ During the processing step, contracts can only access state that comes from the contract itself, or the data that is passed in along with the opcode. The boundary between a contract and the rest of the application occurs in side effects.

Side Effects

In order to ensure a consistent contract state is reproduced among clients in a decentralized way, contract code that modifies contract state must not be able to interact with application code in any way (since local application state might be different from client-to-client). This means that during processing (step 4 above), contract mutations cannot read application data, nor can they interact with application code (or other contracts) in any way, nor can they perform any asynchronous actions.

However, sometimes we want our contracts to be able to interact with our application code (or other contracts) and trigger activity upon receiving some action. To facilitate this, Shelter Protocol specifies that contracts can specify a separate processing step called side effects.

Side effects are able to read application data and cause the application to perform new activity (asynchronous or not). The one thing side effects cannot do is modify contract state. This ensures that even if the side effects behave differently across clients, the contract state will remain consistent.

There is one situation where inconsistent state is still possible, and that is covered next.

Inconsistent State

In order to facilitate certain features, Shelter Protocol attempts to decrypt messages using all private keys that are available to the client, including keys from other contracts. For example, it can be useful for clients to decrypt some information stored in an identity contract (like a display name or profile picture), but not be able to read all of the information stored on someone else’s identity contract. However, this opens the door for inconsistent contract state across clients.

When messages sent using OP_ACTION_ENCRYPTED are encrypted using keys that Client A has that Client B does not have, inconsistent client state is the result. Client A will be able to decrypt and process all messages, but Client B might only be able to decrypt some of them, leading to partially inconsistent state.

It is up to developers to be aware of such possibilities and handle them accordingly.

Contract & VM State

Special keys on contract state are prefixed with _. These are managed by Shelter Protocol implementations and must not be set by developers. Currently, two special keys are defined:

  • _vm — is used to store various metadata critical to interpreting messages that also needs to be sync’d across clients. For example, _vm.authorizedKeys stores keys that are used to verify message signatures.
  • _volatile — This represents client-local information for contract related data like private keys. Implementations could store this information someplace else if they prefer. _volatile must not be shared publicly. For example, _volatile.keys is used to store private keys that decrypt messages. This attribute is not stored in state snapshots and never leaves the client device.

See the opcodes documentation for more details of what is stored on _vm and _volatile.

🚧 This section is under construction. 🚧

Sandbox

All message contracts are loaded in a sandbox by the client. This makes it safe for clients to view the state of arbitrary contracts.

🚧 This section is under construction. 🚧

Server-side Processing

Because the server is only interested in ensuring that authorized parties are allowed to write to contracts, the server processes all opcodes except for OP_ACTION_ENCRYPTED and OP_ACTION_UNENCRYPTED.

🚧 This section is under construction. 🚧