- 26/11/2020: Initial Draft
Draft, needs updates
In the Cosmos SDK light clients are current hardcoded in Go. This makes upgrading existing IBC light clients or adding support for new light client a multi step process involving on-chain governance which is time-consuming.
To remedy this, we are proposing a WASM VM to host light client bytecode, which allows easier upgrading of existing IBC light clients as well as adding support for new IBC light clients without requiring a code release and corresponding hard-fork event.
Currently in the SDK, light clients are defined as part of the codebase and are implemented as submodules under
ibc-go/core/modules/light-clients/
.
Adding support for new light client or update an existing light client in the event of security issue or consensus update is multi-step process which is both time consuming and error prone:
-
To add support for new light client or update an existing light client in the event of security issue or consensus update, we need to modify the codebase and integrate it in numerous places.
-
Governance voting: Adding new light client implementations require governance support and is expensive: This is not ideal as chain governance is gatekeeper for new light client implementations getting added. If a small community want support for light client X, they may not be able to convince governance to support it.
-
Validator upgrade: After governance voting succeeds, validators need to upgrade their nodes in order to enable new IBC light client implementation.
Another problem stemming from the above process is that if a chain wants to upgrade its own consensus, it will need to convince every chain or hub connected to it to upgrade its light client in order to stay connected. Due to time consuming process required to upgrade light client, a chain with lots of connections needs to be disconnected for quite some time after upgrading its consensus, which can be very expensive in terms of time and effort.
We are proposing simplifying this workflow by integrating a WASM light client module which makes adding support for a new light client a simple transaction. The light client bytecode, written in Wasm-compilable Rust, runs inside a WASM VM. The Wasm light client submodule exposes a proxy light client interface that routes incoming messages to the appropriate handler function, inside the Wasm VM for execution.
With WASM light client module, anybody can add new IBC light client in the form of WASM bytecode (provided they are able to pay the requisite gas fee for the transaction) as well as instantiate clients using any created client type. This allows any chain to update its own light client in other chains without going through steps outlined above.
We decided to use WASM light client module as a light client proxy which will interface with the actual light client
uploaded as WASM bytecode. This will require changing client selection method to allow any client if the client type
has prefix of wasm/
.
// IsAllowedClient checks if the given client type is registered on the allowlist.
func (p Params) IsAllowedClient(clientType string) bool {
if p.AreWASMClientsAllowed && isWASMClient(clientType) {
return true
}
for _, allowedClient := range p.AllowedClients {
if allowedClient == clientType {
return true
}
}
return false
}
To upload new light client, user need to create a transaction with Wasm byte code which will be processed by IBC Wasm module.
func (k Keeper) UploadLightClient (wasmCode: []byte, description: String) {
wasmRegistry = getWASMRegistry()
id := hex.EncodeToString(sha256.Sum256(wasmCode))
assert(!wasmRegistry.Exists(id))
assert(wasmRegistry.ValidateAndStoreCode(id, description, wasmCode, false))
}
As name implies, Wasm registry is a registry which stores set of Wasm client code indexed by its hash and allows client code to retrieve latest code uploaded.
ValidateAndStoreCode
checks if the wasm bytecode uploaded is valid and confirms to VM interface.
The light client proxy behind the scenes will call a cosmwasm smart contract instance with incoming arguments in json serialized format with appropriate environment information. Data returned by the smart contract is deserialized and returned to the caller.
Consider an example of CheckProposedHeaderAndUpdateState
function of ClientState
interface. Incoming arguments are
packaged inside a payload which is json serialized and passed to callContract
which calls vm.Execute
and returns the
array of bytes returned by the smart contract. This data is deserialized and passed as return argument.
func (c *ClientState) CheckProposedHeaderAndUpdateState(context sdk.Context, marshaler codec.BinaryMarshaler, store sdk.KVStore, header exported.ClientMessage) (exported.ClientState, exported.ConsensusState, error) {
// get consensus state corresponding to client state to check if the client is expired
consensusState, err := GetConsensusState(store, marshaler, c.LatestHeight)
if err != nil {
return nil, nil, sdkerrors.Wrapf(
err, "could not get consensus state from clientstore at height: %d", c.LatestHeight,
)
}
payload := make(map[string]map[string]interface{})
payload[CheckProposedHeaderAndUpdateState] = make(map[string]interface{})
inner := payload[CheckProposedHeaderAndUpdateState]
inner["me"] = c
inner["header"] = header
inner["consensus_state"] = consensusState
encodedData, err := json.Marshal(payload)
if err != nil {
return nil, nil, sdkerrors.Wrapf(ErrUnableToMarshalPayload, fmt.Sprintf("underlying error: %s", err.Error()))
}
out, err := callContract(c.CodeId, context, store, encodedData)
if err != nil {
return nil, nil, sdkerrors.Wrapf(ErrUnableToCall, fmt.Sprintf("underlying error: %s", err.Error()))
}
output := clientStateCallResponse{}
if err := json.Unmarshal(out.Data, &output); err != nil {
return nil, nil, sdkerrors.Wrapf(ErrUnableToUnmarshalPayload, fmt.Sprintf("underlying error: %s", err.Error()))
}
if !output.Result.IsValid {
return nil, nil, fmt.Errorf("%s error ocurred while updating client state", output.Result.ErrorMsg)
}
output.resetImmutables(c)
return output.NewClientState, output.NewConsensusState, nil
}
- Adding support for new light client or upgrading existing light client is way easier than before and only requires single transaction.
- Improves maintainability of Cosmos SDK, since no change in codebase is required to support new client or upgrade it.
- Light clients need to be written in subset of rust which could compile in Wasm.
- Introspecting light client code is difficult as only compiled bytecode exists in the blockchain.