Running smart contracts without a blockchain
CosmWasm is WebAssembly plus a specific list of imports. Once that list is written down it stops being a platform and becomes an interface you can implement.
Motivation
A smart contract on Terra was a program compiled to WebAssembly, and WebAssembly is a smaller thing than most people assume.
- It can compute, and it can read and write a block of memory that belongs to it. That is the end of the list.
- No files, no packets, no clock, because none of those exist inside the machine.
- Anything else it does, it does by calling a function the host handed it. Handed-in functions are imports. Functions the module offers back are exports.
The host decides the entire vocabulary, which is why the format shows up wherever untrusted code runs next to something valuable. Whatever is not in the import list did not fail, it was never expressible.
CosmWasm is not WebAssembly. It is WebAssembly plus one specific import list and one specific export list, and those structures are what encode CosmWasm rather than anything in the machine underneath. The consequence is the subject here: open experimentation at the wasm level and alternative contract languages can run on Terra.
A compilation target is a contract between a producer and a consumer. Write the contract down and anything satisfying it can be a producer. Rust was the only producer in practice because the standard library you write contracts against was a Rust crate, and nothing about the machine required that.
The import list
Fifteen functions, short enough to read in a minute, which is the fact everything else here depends on. Quoted from the CosmWasm project's own source into the README of one of the experiments:
| Imports | For | Always present |
|---|---|---|
db_read, db_write, db_remove | reading and writing the contract's own storage | yes |
db_scan, db_next | iterating over stored keys | only with the iterator feature |
addr_validate, addr_canonicalize, addr_humanize | checking and converting addresses | yes |
secp256k1_verify, secp256k1_recover_pubkey | signature checks | yes |
ed25519_verify, ed25519_batch_verify | signature checks | yes |
query_chain | asking the surrounding chain a question | yes |
debug | writing a diagnostic string | yes |
abort | stopping on a failed assertion | only with the abort feature |
Much of it is optional. Three storage functions are unconditional, and the two that iterate compile in only if the host advertises the feature, so a host that never needs iteration never implements it.
Going the other way, the contract exports functions for the host to call. Four are required:
allocateanddeallocate, which let the host place data inside the contract's memoryinstantiateinterface_version_8, a marker that exists only so the host can refuse a contract built against a different version of the interface
Everything a contract does for a living is optional, including execute and query, and later
migrate, reply and sudo. Optional entry points and a mandatory version marker tell you what
the host cares about: it will not run code it cannot verify it understands, and beyond that it does
not mind what the code is for.
Fifteen functions in and a handful out is the entire surface between a contract and the world it runs in. Written as a list, it stops looking like a platform and starts looking like an interface you could implement yourself.
Implementing the host in JavaScript
cosmwasm-vm-js does one thing: run CosmWasm smart contracts in Node.js and web browsers.
The real VM is written in Rust and driven by Wasmer, so running a contract needed a chain, or at
least the chain's software and the Rust toolchain that builds it. Implementing the same import list
in TypeScript removes both: a .wasm file compiled for a blockchain executes in a browser tab,
because the browser already has a WebAssembly engine and the fifteen functions it needs can be
supplied by ordinary JavaScript.
The seam is one interface with three fields:
const backend: IBackend = {
backend_api: new BasicBackendApi('terra'),
storage: new BasicKVIterStorage(),
querier: new BasicQuerier(),
};
const vm = new VMInstance(backend);Everything the contract can reach is behind backend:
backend_api, the address helpersstorage, the five storage functionsquerier, the one chain question
Swap any of the three for your own implementation and the contract runs against your version without knowing.
The library is structured to resemble the Rust library it imitates, which is not tidiness. If the JavaScript implementation has the same seams in the same places as the real one, a tool written against one seam has an obvious counterpart on the other side. If the reimplementation invents its own arrangement, every instrument built on it is stranded in the fake.
Alternative source languages
Claiming the target is open is worth nothing until something other than Rust hits it. Two contract toolchains test that, and they return different kinds of answer.
AssemblyScript. TypeScript's syntax, compiled to WebAssembly. It worked, and the property I cared about is that it compiles in the browser, so it could sit next to the JavaScript VM and make something like a JSFiddle for smart contracts: write a contract in a text box, compile it in the page, run it in the page, see the state change. No toolchain, no chain, no install.
It ships as an experiment rather than a proposal, and the README says why:
- Confio, who build CosmWasm, doubted AssemblyScript's viability as a serious CosmWasm language on security grounds.
- The same objection arrives independently from the other side: making a Wasm language for CosmWasm is easy, and the hard part is the security risks and non-determinism.
Neither names which source of non-determinism, which is the useful part. An objection nobody has reduced to a named mechanism cannot be discharged language by language.
C++. A second producer, built and set aside. The recorded verdict is three words, do not use, with no reason attached to it, so it counts as evidence that the target takes more than one language and not as an argument about C++.
Where safety belongs
Both experiments raise a larger question than they answer. If any language can hit the target, something has to guarantee a carelessly built producer cannot take the chain down with it, and the same entry says where: a contract should not be able to bring down a blockchain, and preventing that belongs at the VM layer rather than in the source language.
That is forced once you have claimed the target is open.
- Safety as a property of the language. It has to be re-established from scratch for every new language, and every language that gets it slightly wrong is a new way for the chain to halt. Which makes an open compilation target a liability, because each producer you invite is another chance to break the consumer.
- Safety as a property of the host. It holds no matter who wrote the producer, so adding a language stops being a security event and becomes an ordinary piece of work.
Confio's objection to AssemblyScript and my own were both real, and both of them dissolve at the layer below the one they were aimed at.
The target's semantic vocabulary
Moving a guarantee into the host moves the question with it, from which languages can hit the target to whether the target is the right shape.
CosmWasm has a semantics problem: the API is limited in expression. Augmenting it means wasm imports
and exports closer to the way humans talk about contracts, sketched as a mapping from the low-level
set, db_write, db_read and db_scan, up to a meta-level set that says save a variable and check
a condition.
The whole import list is a key-value store, some address helpers, some signature checks and one question. Every idea a contract has about ownership, permission, balances or validity is something the author built on top of five storage calls, and the machine underneath knows none of it. A fine substrate and a poor vocabulary, and the gap between them is where contract bugs live.
The augmented import set stays a specification. The design splits instead into two things that work against the imports as they are: a language that compiles down to them, and a simulator that instruments them.
What it made possible
A reimplementation of a virtual machine is a claim that two systems agree, and no amount of care turns that claim into a proof. How you state a claim you cannot prove is the last design question here.
The README carries the answer: great care was taken to match the behaviour of the original VM, this implementation may not produce identical results, it should not be used as a drop-in replacement, and results should be verified against the original for anything critical.
Writing the limit into the README is how the claim stays honest, and everything built on top of the JavaScript VM inherits both the convenience and the caveat.