owned this note
owned this note
Published
Linked with GitHub
# WebAssembly integration in Cosmos-SDK
This is a proposal for how to integrate the wasmer engine into the cosmos-sdk. Not on the technical details of the runtime implementation, but on how it connects to the rest of the SDK framework and all modules build upon it.
## External Requirements
This is based on work with the wasmer.io engine, including the following points:
* Exposing gas metering in go API
* Repeatable gas metering (fixed backend compiler options)
* Testing serialization in go API
* Benchmarks to correllate wasmer gas to sdk gas usage
It also assumes the following build system:
* Configuration to build rust to WebAssembly
* Small wasm binaries (<100KB typical)
* Deterministic builds (so we can map rust to on-chain wasm)
These are done in other repositories and are out of the scope of this document.
----------------------------------
## High-Level Design
This first section, covers high-level design and architecure decisions.
Any code here is just illustrative pseudo-code. The main point to focus
on are the concepts.
## Where to Integrate WebAssembly?
When we discuss Web Assembly, most people see it as a magic fix that
allows us to upload sandboxed, user-defined code *anywhere*. The truth
is that while Web Assembly allows us to upload sandboxed functions to
run, we need to design the connections (arguments, return value, and environment)
with one specific case in mind.
As Aaron put it well, anywhere we accept a Go *interface* in the SDK,
we could place a WASM adapter there. But just as a struct only
implements one interface, we need to concentrate on one interface to adapt.
The majority of the use cases could be covered by a properly designed adapter for *Handler* and this is where we will focus our work:
However, there are a number of other places where we could potentially provide another interface for custom web assembly contracts. To be added after the original integration work:
* Payment Channels
* Atomic Swap
* Escrow (with arbiter)
* Automatic payment distribution based on shares
* Multi-party escrow triggered by calculations - querying other data on the chain
* Programmable NFTs: just as ethereum NFTs include some custom code to perform actions
Some other important use cases that will need different adapters:
* **IBC verification function** The current ICS23 spec mentions wasm uploaded code to verify external proofs. This should be easily creatable with a custom interface .
* **Delegation Rewards** Support different commission to different validators based on on-chain rules.
* **Signature Verification** Allow uploading new algorithms, like `ed25519` or `BLS`. These would obviously need to be enabled by a governance vote.
## The Handler Interface
For the work below, we are just looking at the "Handler" interface. In go, this is `type Handler func(ctx Context, msg Msg) Result`. In addition to the message to process, it gets a Context that allows it to view and mutate state:
```go
func (c Context) Context() context.Context { return c.ctx }
func (c Context) MultiStore() MultiStore { return c.ms }
func (c Context) BlockHeight() int64 { return c.header.Height }
func (c Context) BlockTime() time.Time { return c.header.Time }
func (c Context) ChainID() string { return c.chainID }
func (c Context) TxBytes() []byte { return c.txBytes }
func (c Context) Logger() log.Logger { return c.logger }
func (c Context) VoteInfos() []abci.VoteInfo { return c.voteInfo }
func (c Context) GasMeter() GasMeter { return c.gasMeter }
func (c Context) BlockGasMeter() GasMeter { return c.blockGasMeter }
func (c Context) IsCheckTx() bool { return c.checkTx }
func (c Context) MinGasPrices() DecCoins { return c.minGasPrice }
func (c Context) EventManager() *EventManager { return c.eventManager }
```
It is clearly not desirable to expose all this to arbitrary, unaudited code, but we can grab a subset of this to expose to the wasm contract.
Readonly:
* Block Context
* BlockHeight
* BlockTime
* ChainID
* Message Data
* Signer (who authorized this message)
* Tokens Sent with message
* User-defined data
* Contract State
* Contract Address
* Contract Account (just balance or more info?)
Read/Write:
* A sandboxed sub-store
* Events
### Security Concerns
We clearly cannot just pass in a Controller to another module into an unknown wasm contract. But we do need some way to allow a wasm contract to integrate with other modules to make it interesting. Above we allow it access to its internal state and ability to read its own balance.
In terms of security, we can view the wasm contract as a "client" on-chain with its own account and address. It can theoretically read any on-chain data, and send any message "signed" by its own address, without opening up security holes. Provided these messages are processed just like external messages, and that gas limits are enforced in CPU time and those queries.
### Calling Other Modules
Ethereum provides a nice dispatch model, where a contract can make arbitrary calls to the public API of any other contract. However, we have seen many issues and bugs, especially related to re-entrancy attacks. To simplify this, we propose that the contract cannot directly call any other contract, but instead returns a list of messages, which will be dispatched and validated *after contract execution* but in *the same transaction*. This means that if they fail, the contract will also roll back, but we don't allow any cycles or re-entrancy possibilities.
We could conceive of this as something like: `ProcessMessage(info ReadOnlyInfo, db SubStore) []Msg`. Note that we also want to allow it to return a `Result` and `Events`, so this may end up with a much larger pseudo-function signature, like: `ProcessMessage(info ReadOnlyInfo, db SubStore) (*Result, []Event, []Msg, error)`
This allows the contract to easily move the tokens it controls (via `SendMsg`) or even vote, stake tokens, or take any other action its account has authority to do. The potential actions increase with the delegation work being done as part of Key Managament.
### Querying Other Modules
While it is great to change state in other modules, the design until this point leave the contract blind. Sure, it can emit a message in order to stake some tokens, but it cannot check its current stake, or the number of tokens available to withdraw. To do so, we need to expose some interface to query other modules.
Going along with the client analogy above , we definitely **do not** want to allow *write* access to the other substores. Instead we can allow something like the high level `abci_query` interface, where the smart contract would send a path and data (key), and receive the requested object - likely serialized as json rather than amino for ease
of parsing in the smart contract.
Whether we wrap the existing `Query` interface or provide a different interface just for WASM contract is an open question, which we touch in the next section.
## Upgradeability
This can be a **major problem** or even **blocker** for enabling Web Assembly contracts, unless we make some very conscious design decisions, both in the WASM interfaces, as well as the SDK as a whole.
If we allow the contract to query arbitrary data in other modules, this contract is dependent on those not changing. If the chain upgrades (gracefully or hardfork) and the queries return data in a different format, or change the path they respond to, then the contract will break. The same is true with the format of the messages we return. If cosmos-sdk modifies the format of the staking message, after an upgrade the module will continue to emit staking messages in the old format, which will fail - leave the contract broken and fund stuck.
One proposed solution was to allow us to "upgrade contracts" as well, but I find this highly problematic. A core pillar of most smart contract designs is immutability, which is what allows us to trust them. If the author could change them *after* I send it my funds, then there can be no trustless execution. Maybe we then decide that governance can update the contracts, or only change them in the context of a hard-fork. This provides safe-guards, but the issue arises that the contract author and those updating the application code, and the validators all are different entities. Do we now need to contact every contract author and involve them in preparing every upgrade? Or will the validators just re-wrire contracts as they see fit? Seems extremely risky in any case.
One alternative here is to either freeze every API that a Handler touches in cosmos-sdk, as well as every data structure exposed over `abci_query`. But this would have the effect of a huge stagnation of the codebase and very determental to innovation.
Another alternative, and what we propose here, is to limit the interfaces exposed to the wasm contracts to a minimalistic subset of all possible functionality. And provide a fixed format with strong immutability guarantees. This will likely require some wrapper between the structs used in the wasm interface, and those used elsewhere in the sdk. As we noticed, even the transaction type changed in the upgrade to Gaia 2.0.
We could, for example, expose a custom SendMsg, `{type: 'send', to, from, amount}` and then in the Golang wrapper code (which can be updated easily during a hard-fork), we translate this *well-defined*, *immutable*, and *forward-comaptible* message definition into the actual structure used by the cosmos-sdk, before dispatching this to other modules. We would like-wise have to provide clear definitions for a minimal set of queries we want to expose, and then make sure to translate fields from the current struct into this static definition.
This means we would have to manually enable each Message or Query we would want to expose to all Web Assembly contract, and provide strong guarantees to each of them *forever*. We could easily add new message types, or queries, such that new contracts deployed after version X could make use of them, but all the types that were exposed to the first contract deployed on the system must remain valid for the lifetime of the chain (including any hardforks, dump-state-and-reset, etc.).
**WARNING**
Even with a buffer class, this will have a noticeable strong impact on a number of development practices in the core cosmos-sdk team, especially related to version and migration, and we need to have a clear and open discussion on possible approaches here.
Relevant link (recommended by Aaron): https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/Spec_ulation.md
----------------------------------
## Specification
This second section attempts to codify the architecture defined above with a number of concrete implementation details, function signatures, naming choices, etc. We should discuss the details here - both names and functionality.
## Definitions
**Contract** is as some wasm code uploaded to the system, *possibly* with some "global" state, initialized at the creation of the contract.
**Instance** is one instantiation of the contract. This contains a reference to the contract, as well as some "local" state to this instance, initialized at the creation of the instance.
Example: we could upload a generic "ERC20 mintable" contract, and many people could create independent instances of the same bytecode, where the local data defines the token name, the issuer, the max issuance, etc.
* First you **create** a *contract*
* Then you **instantiate** an *instance*
* Finally users **invoke** the *instance*
*Contracts* are immutible (code/logic is fixed), but *instances* are mutible (state changes)
## Serialization
There are two pieces of data that must be considered here. **Message Data**, which is arbitrary binary data passed in the transaction by the end user signing it, and **Context Data**, which is passed in by the cosmos sdk runtime, providing some guaranteed context. Context data may include the signer's address, the instance's address, number of tokens sent, block height, and any other information a contract may need to control the internal logic.
**Message Data** comes from a binary transaction and must be serialized. The most standard and flexible codec is (unfortunately) JSON. This allows the contract to define any schema it wants, and the client can easily provide the proper data. We recommend using a `string` field in the `InvokeMsg`, to contain the user-defined *message data*.
**Contact Data** comes from the go runtime and can either be serialized by sdk and deserialized by the contract, or we can try to do some ffi magic and use the same memory layout for the struct in Go and Wasm and avoid any serialization overhead. Note that the context data struct will be well-defined at compile time and guaranteed not to change between invocations (the same cannot be said for *message data*).
In spite of possible performance gains or compiler guarantees with C-types, I would recommend using JSON for this as well. Or another well-defined binary format, like protobuf. However, I will document some links below for those who would like to research the shared struct approach.
* [repr( c )](https://doc.rust-lang.org/nomicon/other-reprs.html) is a rust directive to produce cannonical C-style memory layouts. This is typically used in FFI (which wasm calls are).
* [wasm-ffi](https://github.com/DeMille/wasm-ffi) demos how to pass structs between wasm/rust and javascript painlessly. Not into golang, but it provides a nice explanation and design overview.
* [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen/) also tries to convert types and you can [read some success and limitations of the approach](https://github.com/rustwasm/wasm-bindgen/issues/111)
* [cgo](https://golang.org/cmd/cgo/#hdr-Go_references_to_C) has some documentation about accessing C structs from Go (which is what we get with the repr( c ) directive)
## State Access
**Contract State** is globally accessible to all instances of the contract. This is optional and I would consider a read-only singleton as sufficient for most customizations of the contract (using a contract with different configurations), and more secure than other access modes. This should be discussed in the light of actual use cases. Maybe it is unneeded, maybe write access is needed.
We can set contract state upon *creation*. This is a unique db key that cannot be accessed excpet by this contract code. I would conceive of this as a data section baked into the binary, which is actually probably the better approach.
**Instance State** is accessible only by one instance of the contract, with full read-write access. This can contain either a singleton (one key - simple contract or config) or a kvstore (subspace with many keys that can be accessed - like erc20 instance holding balances for many accounts). Sometimes the contract may want one or the other or even both (config + much data) access modes.
We can set the instance state upon *instantiation*. We can read and modify it upon *invocation*. This is a unique "prefixed db" subspace that can only be accessed by this instance. The read-only contract state should suffice for shared data between all instances. (Discuss this design in light of all use cases)
## Function Definitions
As discussed above, all data structures passed between web assembly and the cosmos-sdk will be sent in their JSON representation. For simplicity, I will show them as Go structs in this section, and we can assume a cannonical snake_case conversion of the field name. This allows us to be explicit about types.
Function: `Process(request Request) Response`
This Read-Only info is available to every contract:
```go
type Request struct {
Block BlockInfo
Message MessageInfo
Contract ContractInfo
}
type BlockInfo struct {
Height int64
Time int64 // ??? seconds (nanoseconds) since unix epoch???
ChainID string
}
type MessageInfo struct {
Signer string // bech32 encoding of authorizing address
TokensSent CoinInfo // money transfered by sdk as part of this message (before execution)
UserData string // arbitrary data set in transaction
}
type ContractInfo struct {
Address string // bech32 encoding of address this contract controls
Balance []CoinInfo
}
type CoinInfo struct {
Amount string // encoing of decimal value, eg. "12.3456"
Denom string // type, eg. "ATOM"
}
```
This is the information the contract can return:
```go
type Response struct {
Result ABCIResult
// Msg struct is an "interface" discussed in a later section
Messages []Msg
}
type ABCIResult struct {
Data string // hex-encoded
Log string
Code int32 // non-zero on error
CodeSpace string // non-empty on error
Events []Event
}
// do we give all this power to wasm, or provide a simpler interface (eg. fixing type)
type Event struct {
Type string
Attributes []struct{
Key string
Value string
}
}
```
Note that I intentionally redefine a number of core types, rather than importing them from sdk/types. This is to guarantee immutibility. These types will be passed to and from the contract, and the contract adapter code (in go) can convert them to the go types used in the rest of the app. But these are decoupled, so they can remain constant while other parts of the sdk evolve.
## Exposed imports
**TODO**
Precise imports it can expect to be able to call (eg. query modules, modify local state)
## Define Query and Message types
**TODO**
After upgrade discussion above, some fixed subset of types here