With this post I’d like to provide an overview over the chain runtime upgrades for developers interacting with the registry chain through radicle-registry-client
. A special focus is on how the client is updated along the runtime and how it deals with different runtime versions.
Please ask questions if I left something out or something is unclear.
Client and runtime
The chain runtime is the piece of code that determines what the semantics of the chain are. Specifically, it determines what the transactions are and how each of them modifies the state. The runtime also determines the shape of transactions (i.e. what parameters it has and how it is encoded) and how objects are stored and encoded in the state.
The client provides an API to exchange data (transactions or state) with a chain and make that data available as Rust types. This is accomplished by reusing the types used by the runtime.
Runtime updates
The runtime of a chain can be updated by submitting the new runtime code a special “update runtime” transaction We call this deploying a runtime. The code for every runtime includes a runtime version. For consumers interacting with the chain (in particular users of the client library) only the spec version of a runtime is of interest. The spec version is a single integer that is incremented whenever the runtime changes in an incompatible way. The runtime version currently running on a chain can be queried through the client.
Since the deplyoment of a runtime happens independently of updating the client software we need to provide a client that is compatible with at least two runtime versions during the transition phase when we plan to release a new runtime version.
It is possible to implement the client so that it is compatible (at least partial) with unknown future versions. Since this requires a lot of effort and timely updates make this unnecessary we have decided against implementing this. This means that a version of the client library is only compatible with the latest known runtime version and some versions before that. It is important to point out that the latest known runtime is not deployed when the client is released.
Client changes
Runtime changes may not necessarily affect the client API. For example we may require that a user has been registered for two weeks before they can register projects. This change would result in a bump of the runtime spec version but have no effect on the client because none of the data that is exchanged between the client and chain has changed.
The changes that affect the client can be put into two groups: Adding or removing a transaction and adding or removing fields from an entity stored in the state. Let’s give some examples for these changes and explain who they are visible in the client.
Adding a message
Let’s consider we want to add a message RegisterUnicorn
that registers a unicorn on chain. This will bump the runtime version from 4 to 5. This change would be implemented in the runtime and thus available to the client which links against the runtime. However the client would make sure that one can only submit the new transaction if it is talking to a chain that is running runtime version 5. The client would refuse to send a transaction to a chain that is running version 4.
The client will continue to work with runtime version 4. This is necessary since it will take some time for version 5 of the runtime to be deployed to a chain. This allows upstream to update the client and be prepared for the new version while still being able to work with the old version.
On the app side we need to ensure that the UI for the RegisterUnicorn
message is only exposed if the client is talking to a chain that is running version 5. The UI will be hidden or otherwise unavailable if the chain is running version 4.
Changing the state
State changes are different from changes to the transaction. One complication is that state cannot be migrated and a new version of the runtime needs to be able to handle all old state.
Consider the following example: In version 5 of the runtime a user only has a name
field. Now with version 6 of the runtime we want to add a karma
field to the user. To address this the runtime always tags every value it stores in the state with a version. This means that it can now whether an existing user was stored with the old format without the karma
field or if it was stored with the karma
field. In Rust this is presented with an enum
enum User {
V1(UserV1),
V2(UserV2),
}
struct UserV1 {
name: String
}
struct UserV2 {
name: String
karma: u32
}
The client developers can choose to either hide these state changes or expose them. If the client wants to expose the differences than the get_user
function would return a User
enum from above directly.
On other hand if we decide to not expose the different versions then get_user
will just return a User
struct with the name
and karma
fields. If the client reads an old user version from the state it would simply default karma
to zero.
We have yet to figure out how we expose state changes. Whether we want to decide on a case-by-case basis or go consistently with just one of the approaches above.