ics | title | stage | category | kind | implements | author | created | modified |
---|---|---|---|---|---|---|---|---|
8 |
Wasm Client |
draft |
IBC/TAO |
instantiation |
2 |
Parth Desai <[email protected]>, Mateusz Kaczanowski <[email protected]> |
2020-10-13 |
2020-10-13 |
This specification document describes an interface to a client (verification algorithm) stored as a Wasm bytecode for a blockchain.
Currently adding a new client implementation or upgrading an existing one requires a hard upgrade because the client implementations are part of the static chain binary. Any change to the on-chain light client code is dependent on chain governance to approve an upgrade before it can be deployed.
This may be acceptable when adding new client types, since the number of unique consensus algorithms that need to be supported is currently small. However, this process will become very tedious when it comes to upgrading light clients.
Without dynamically upgradable light clients, a chain that wishes to upgrade its consensus algorithm (and thus break existing light clients) must wait for all counterparty chains to perform a hard upgrade that adds support for the upgraded light client before it itself can perform an upgrade on its chain. Examples of a consensus-breaking upgrade would be an upgrade from Tendermint v1 to a light-client breaking Tendermint v2 or switching from Tendermint consensus to Honeybadger. Changes to the internal state-machine logic will not affect consensus, e.g. changes to staking module do not require an IBC upgrade.
Requiring all counterparties to statically add new client implementations to their binaries will inevitably slow the pace of upgrades in the IBC network, since the deployment of an upgrade on even a very experimental, fast-moving chain will be blocked by an upgrade to a high-value chain that will be inherently more conservative.
Once the IBC network broadly adopts dynamically upgradable clients, a chain may upgrade its consensus algorithm whenever it wishes and relayers may upgrade the client code of all counterparty chains without requiring the counterparty chains to perform an upgrade themselves. This prevents a dependency on counterparty chains when considering upgrading one's own consensus algorithm.
Functions & terms are as defined in ICS 2.
currentTimestamp
is as defined in ICS 24.
Wasm Client Code
refers to Wasm bytecode stored in the client store, which provides a target blockchain specific implementation of ICS 2.
Wasm Client
refers to a particular instance of Wasm Client Code
defined as a tuple (Wasm Client Code, ClientID)
.
Wasm VM
refers to a virtual machine capable of executing valid Wasm bytecode.
This specification must satisfy the client interface defined in ICS 2.
This specification depends on the correct instantiation of the Wasm client
and is decoupled from any specific implementation of the target blockchain
consensus algorithm.
The Wasm client state tracks location of the Wasm bytecode via codeId
. Binary data represented by data
field is opaque and only interpreted by the Wasm Client Code. type
represents client type.
type
and codeId
both are immutable.
interface ClientState {
codeId: []byte
data: []byte
latestHeight: Height
}
The Wasm consensus state tracks the timestamp (block time), Wasm Client code
specific fields and commitment root for all previously verified consensus states.
type
and codeId
both are immutable.
interface ConsensusState {
codeId: []byte
data: []byte
timestamp: uint64
}
The height of a Wasm light client instance consists of two uint64
s: the revision number, and the height in the revision.
interface Height {
revisionNumber: uint64
revisionHeight: uint64
}
Comparison between heights is implemented as follows:
function compare(a: Height, b: Height): Ord {
if (a.revisionNumber < b.revisionNumber)
return LT
else if (a.revisionNumber === b.revisionNumber)
if (a.revisionHeight < b.revisionHeight)
return LT
else if (a.revisionHeight === b.revisionHeight)
return EQ
return GT
}
This is designed to allow the height to reset to 0
while the revision number increases by one in order to preserve timeouts through zero-height upgrades.
Contents of Wasm client headers depend upon Wasm Client Code
.
interface Header {
data: []byte
height: Height
}
The Misbehaviour
type is used for detecting misbehaviour and freezing the client - to prevent further packet flow - if applicable.
Wasm client Misbehaviour
consists of two conflicting headers both of which the light client would have considered valid.
interface Misbehaviour {
clientId: string
h1: Header
h2: Header
}
Wasm client initialisation requires a (subjectively chosen) latest consensus state interpretable by the target Wasm Client Code. wasmCodeId
field is unique identifier for Wasm Client Code
, and initializationData
refers to opaque data required for initialization of the particular client managed by that Wasm Client Code.
function initialise(
wasmCodeId: String,
initializationData: []byte,
consensusState: []byte,
): ClientState {
codeHandle = getWasmCode(wasmCodeId)
assert(codeHandle.isInitializationDataValid(initializationData, consensusState))
store = getStore("clients/{identifier}")
return codeHandle.initialise(store, initializationData, consensusState)
}
The latestClientHeight
function returns the latest stored height, which is updated every time a new (more recent) header is validated.
function latestClientHeight(clientState: ClientState): Height {
codeHandle = clientState.codeHandle();
codeHandle.latestClientHeight(clientState)
}
Wasm client validity checking uses underlying Wasm Client code. If the provided header is valid, the client state is updated & the newly verified commitment written to the store.
function checkValidityAndUpdateState(
clientState: ClientState,
header: Header) {
store = getStore("clients/{identifier}")
codeHandle = clientState.codeHandle()
// verify that provided header is valid and state is saved
assert(codeHandle.validateHeaderAndCreateConsensusState(store, clientState, header))
}
Wasm client misbehaviour checking determines whether or not two conflicting headers at the same height would have convinced the light client.
function checkMisbehaviourAndUpdateState(
clientState: ClientState,
misbehaviour: Misbehaviour) {
store = getStore("clients/{identifier}")
codeHandle = clientState.codeHandle()
assert(codeHandle.handleMisbehaviour(store, clientState, misbehaviour))
}
The chain which this light client is tracking can elect to write a special pre-determined key in state to allow the light client to update its client state (e.g. with a new chain ID or revision) in preparation for an upgrade.
As the client state change will be performed immediately, once the new client state information is written to the predetermined key, the client will no longer be able to follow blocks on the old chain, so it must upgrade promptly.
function upgradeClientState(
clientState: ClientState,
newClientState: ClientState,
height: Height,
proof: CommitmentPrefix) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyNewClientState(clientState, newClientState, height, proof))
// update client state
clientState = newClientState
set("clients/{identifier}", clientState)
}
In case of Wasm client, upgrade of Wasm Client Code
is also possible via blockchain specific management functionality.
Wasm client state verification functions check a Merkle proof against a previously validated commitment root.
function verifyClientConsensusState(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
clientIdentifier: Identifier,
consensusStateHeight: Height,
consensusState: ConsensusState) {
codeHandle = getCodeHandleFromClientID(clientIdentifier)
assert(codeHandle.verifyClientConsensusState(clientState, height, prefix, clientIdentifier, proof, consensusStateHeight, consensusState))
}
function verifyConnectionState(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
connectionIdentifier: Identifier,
connectionEnd: ConnectionEnd) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyConnectionState(clientState, height, prefix, proof, connectionIdentifier, connectionEnd))
}
function verifyChannelState(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
channelEnd: ChannelEnd) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyChannelState(clientState, height, prefix, proof, portIdentifier, channelIdentifier, channelEnd))
}
function verifyPacketCommitment(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
sequence: uint64,
commitment: bytes) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyPacketCommitment(clientState, height, prefix, proof, portIdentifier, channelIdentifier, sequence, commitment))
}
function verifyPacketAcknowledgement(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
sequence: uint64,
acknowledgement: bytes) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyPacketAcknowledgement(clientState, height, prefix, proof, portportIdentifier, channelIdentifier, sequence, acknowledgement))
}
function verifyPacketReceiptAbsence(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
sequence: uint64) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyPacketReceiptAbsence(clientState, height, prefix, proof, portIdentifier, channelIdentifier, sequence))
}
function verifyNextSequenceRecv(
clientState: ClientState,
height: Height,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
nextSequenceRecv: uint64) {
codeHandle = clientState.codeHandle()
assert(codeHandle.verifyNextSequenceRecv(clientState, height, prefix, proof, portIdentifier, channelIdentifier, nextSequenceRecv))
}
Code handle is an object that facilitates interaction between Wasm code and go code. For example, consider the method isValidClientState
which could be implemented like this:
func (c *CodeHandle) isValidClientState(clientState ClientState, height u64) {
clientStateData := json.Serialize(clientState)
packedData := pack(clientStateData, height)
// VM specific code to call Wasm contract
}
Every Wasm client code need to support ingestion of below messages in order to be used as light client.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MisbehaviourMessage {
pub client_state: Vec<u8>,
pub consensus_state: Vec<u8>,
pub height: Height,
pub header1: Vec<u8>,
pub header2: Vec<u8>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct CreateConsensusMessage {
pub client_state: Vec<u8>,
pub height: Height
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct InitializeClientStateMessage {
pub initialization_data: Vec<u8>,
pub consensus_state: Vec<u8>
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum HandleMsg {
HandleMisbehaviour(MisbehaviourMessage),
TryCreateConsensusState(CreateConsensusMessage),
InitializeClientState(InitializeClientStateMessage)
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ValidateClientStateMessage {
client_state: Vec<u8>,
height: Height
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ValidateNewClientStateMessage {
client_state: Vec<u8>,
new_client_state: Vec<u8>,
height: Height
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ValidateInitializationDataMessage {
init_data: Vec<u8>,
consensus_state: Vec<u8>
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ValidityPredicate {
ClientState(ValidateClientStateMessage),
NewClientState(ValidateNewClientStateMessage),
InitializationData(ValidateInitializationDataMessage),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
IsValid(ValidityPredicate),
LatestClientHeight(Vec<u8>),
}
Correctness guarantees as provided by the underlying algorithm implemented by Wasm Client Code
.
Not applicable.
As long as Wasm Client Code
keeps interface consistent with ICS 02
it should be forward compatible
None yet.
None at present.
All content herein is licensed under Apache 2.0.