[Proposal] Use WebAssembly Interface Types to describe all standards interfaces and Application Contract Interface (ACI)

[PROPOSAL] Use WebAssembly Interface Types to describe all standards interfaces and Application Contract Interface (ACI)

Problem

Standards Are Not Consistent

Currently there is no standard for how the high level types used in contracts are defined.

For example the contract metadata is defined in the standard using a TS like type definitions[1]


type NFTContractMetadata = {
  spec: string, // required, essentially a version like "nft-2.0.0", replacing "2.0.0" with the implemented version of NEP-177
  name: string, // required, ex. "Mochi Rising — Digital Edition" or "Metaverse 3"
  symbol: string, // required, ex. "MOCHI"
  icon: string|null, // Data URL
  base_uri: string|null, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs
  reference: string|null, // URL to a JSON file with more info
  reference_hash: string|null, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
}

function nft_metadata(): NFTContractMetadata {}

In other places the standards are defined used Rust like types[2]:

/// A mapping of NEAR accounts to the amount each should be paid out, in
/// the event of a token-sale. The payout mapping MUST be shorter than the
/// maximum length specified by the financial contract obtaining this
/// payout data. Any mapping of length 10 or less MUST be accepted by
/// financial contracts, so 10 is a safe upper limit.
#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct Payout {
  pub payout: HashMap<AccountId, U128>,
}

Deployed contracts lack discoverable interfaces

Currently deploy contracts’ Wasm only lists the names of the functions that are exported and provide no information about the arguments or their types.

Divergent solutions in the current ecosystem

As too examples of solutions in the ecosystem that would benefit from standardization:

https://github.com/near/near-sdk-rs/pull/831 and https://github.com/raendev/guide

Solution

Use WebAssembly Interface Types [3] (WIT) to create an Application Contract Interface (ACI).

Ethereum created a schema called Contract Application Binary Interface (ABI)[9]. Unlike a traditional binary interface, an ACI can define high level types. This ACI should be embedded in the contract or have an official registry. This will allow code to be generated to interact with contracts remotely or for making cross contract calls.

Background

As stated at the bottom of [3]:

The intention of wit is that it maps down to interface types, so the goal of name resolution is to effectively create the type section of a wasm module using interface types.

Currently there is a WebAssembly (Wasm) working group that are creating a components model [4], which aims to extend the current Wasm standard with higher level types that will allow Wasm binaries be linked together regardless of the language used to create the binary.

However, before this goal is achieved there is the .wit file format, which allows for defining the higher level types which will eventually be part of the Wasm standard.

Benefits and Ecosystem Impact

Language Agnostic

wit provides a human readable format that can generate the corresponding types in a given programming language or other type representations such as JSON schema. It is more compact since it doesn’t have the overhead of JSON syntax, e.g. "s and {}s. Furthermore the type declarations are familiar to developers and map very nicely to the most common language on NEAR, Rust.

For example,

/// Retrieve a message for a given account id
pub fn get_status(&self, account_id: AccountId) -> Option<String> {
    self.records.get(&account_id)
}

generates to

/// Retrieve a message for a given account id
get_status: func(account-id: account-id)-> option<string>

As you can see the option type is part of WIT, along with a result type.

Documentation Strings

wit supports documentation strings. This allows for both type definitions and contract methods to have a documentation that will be available to generate type documentation in a given language, e.g. TS and Rust.

Attributes on Doc strings

This allows adding constraints on types and methods. For example, a NEAR account ID type could be defined like:

///  @minLength 2
///  @maxLength 64
///  @pattern ^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$
type account-id = string

This can then translate to a JSON schema and validate any input to a contract call that is of type account-id. Validating contract call inputs will reduce traffic on the network that would lead to a failed executions and transactions with missing or malformed inputs.

Note here that wit requires kebab-case[5], which makes it easier to generate the language convention for types, e.g. AccountId.

Supports Markdown generation

This will allow the markdown source of the official standards to be generated directly from the wit.

Can easily be generated from Rust

Currently Rust is the best language to define the standards. Tools like witgen[12] allow generating wit directly from Rust source. This would allow writing the official standard traits and types directly in Rust and have the doc strings handle. This also helps promote reference implementations.

Provides a use syntax to import types

If the ACI is embedded into the Wasm binary it ensures that interface and contract are always in sync. Furthermore, with the use syntax a contract can declare the types defined in another contract:

use * from "nearsdk.near"

This would allow the official types to be “published” as a smart contract and requires less space in the contract taken up by the common type declarations.

This could also be used to for official standard interfaces:

use * from "nft.nep-standards.near"

This would ensure that the contract implements the core nft standard and provide all the necessary types.

Can be used to generate corresponding client Wasm binaries

Currently the biggest offender in both binary size and execution cost is JSON serialization. Each exported function that takes arguments must convert a JSON string into a types it expects.

Since Rust types can be generated (already done in the wit-bindgen[11], a Wasm binary can be auto-generated that can execute on the client side. This client side binary would then be responsible for de-serializing the JSON string and encoding the data more efficiently. Currently projects like aurora use Borsh on the client side for the exact reason. However, the borsh is hand written JS that is brittle if the contract’s interfaces changes.

Has result type

Rust and many functional programming languages have a generic result type that either contains a value or an error. This is equivalent to exceptions in other languages, except that functions must specify what types of errors might be returned.

Having result types of contract methods allows clients to be able to understand and respond accordingly to failed transactions.

Example implementation already exists

witme (https://github.com/AhaLabs/witme) already provides a library and CLI for generating wit from Rust smart contracts.

raen (https://github.com/raendev/raen) provides a build tool which injects an ACI into the Wasm binary.

raen admin (https://raen.dev/admin) provides a front end that downloads the contract and is able to list, validate, and execute contract methods. Even including the documentation strings.

Addressing Potential Drawbacks

wit is a work in progress

Currently the wit format has not be formally standardized and is working in lock step with the component model (with plans to standardize[6]). However, the syntax has mostly standardized and they have started to version the syntax.

The bytecode alliance[10], which maintains wasmtime, is the creator of wit-bindgen[13] so there is guaranteed future development. Furthermore, as the ecosystem grows the tooling around it can be reused for NEAR specific purposes. The same could not be said for a NEAR specific solution like Ethereum’s ABI, which is domain specific.

Lastly, as stated above the ultimate goal of WIT is to become part of the formal spec of a Wasm Binary. This is exciting because

  1. Tooling around WIT will continue to be useful, e.g. Wasm → WIT is already exists[7],
  2. presents future extensions to the NEAR blockchain that would allow for contracts to link with other Wasm binaries at runtime. This allows contracts not to have to supply the same functionality everytime, e.g. standard libraries.
  3. Even without the runtime linking contracts deployed to the blockchain could be used to statically link your contract to known and audited Wasm binaries written in any language that compiles to Wasm.

Further Work

Currently attributes on types used by witme are borrowed from json schema[8]. However, the @change attribute is used above contract methods. It could also include if a method is payable, how much attached deposit and gas are required, and more.
So standardizing the set of contract method attributes is also required.

tl;dr

NEAR currently lacks ecosystem wide standards for Application Contract Interfaces and Standards. wit provides a future proof non-domain specific solution that will help validate inputs and describe return types and errors.

sources

[1] https://nomicon.io/Standards/Tokens/NonFungibleToken/Metadata#interface
[2] https://nomicon.io/Standards/Tokens/NonFungibleToken/Payout#reference-level-explanation
[3] https://github.com/bytecodealliance/wit-bindgen/blob/main/WIT.md
[4] https://github.com/WebAssembly/component-model
[5] https://en.wikipedia.org/wiki/Letter_case#Kebab_case
[6] https://github.com/WebAssembly/WASI/issues/484
[7] https://github.com/bytecodealliance/wit-bindgen/tree/main/crates/wit-component
[8] https://json-schema.org/
[9] https://docs.soliditylang.org/en/develop/abi-spec.html
[10] https://bytecodealliance.org/
[11] https://github.com/bytecodealliance/wit-bindgen/tree/main/crates/gen-rust
[12] https://www.github.com/bnjjj/witgen
[13] https://github.com/bytecodealliance/wit-bindgen

6 Likes