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.
đź’ˇ 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 useshead=<contractID>
.
Client-side Processing
Message processing consists of the following steps on the client:
- Verifies it is expecting this message (because the message is part of a contract that it subscribed to).
- 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.
- Temporarily saves a copy of the entire local contract state (in case the next step fails).
- 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.
- Updates the most recent hash seen for this contract (aka “contract HEAD”) to equal the hash of this message.
- 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. 🚧