前言
参考工程,以下工程均可在 GitHub
找到
cosmwasm
: branch 0.13wasmd
: branch v0.15.1cosmwasm-template
: branch 0.13wasmvm
: branch 0.13
Rust
编译
注意点
-
win系统删除所有不需要的代码进行编译,需要修改
.cargo
配置文件[build] rustflags = "-C link-arg=-s"
-
优化程序,提高运行速度:
如果用
cargo
编译,使用--release
标志;如果用rustc
编译,使用-O
标志。但是优化会降低编译速度,而且在开发过程中通常是不可取的。
wasm
虚拟机
编译原理
wasmvm
是基于wasmer.io
引擎工作的,依赖这个库:cosmwasm
,这库是用rust
写的,为了便于go调用,最终编译成了libwasmvm.so
动态库,并生成了让cgo
调用的bindings.h
头文件。
Contract
就是一些上传到区块链系统的wasm
字节码,在创建Contract
时初始化,除了包含在wasm
代码中的状态,没有其他状态。对于一个Contract
,要经过三步:
- 创建:用
rust
把逻辑代码编译成wasm
字节码,然后把字节码上传到区块链系统 - 实例化一个instance:把上传到系统的
wasm
字节码拿出来放到虚拟机中 - 调用实例:根据
json scheme
中的字段,在虚拟机中执行对应的函数,涉及到了cosmwasm-vm
、cosmwasm-storage
、cosmwasm-std
Contract
是不可变的,因为逻辑代码固定了;instance是可变的,因为状态可变。
运行机制
-
所有查询都是作为交易的一部分执行的,每个合约都定义了一个公开的
query
函数,该函数只能以只读模式访问合约的数据存储,并且可以对加载的数据进行计算。查询的数据格式定义在公开的State
中,见cosmwasm-template/src/state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub count: i32, pub owner: Addr, }
-
cosmos-sdk
中公开的查询设计了执行时间限制,用来限制滥用,但是无法避免DoS
攻击,比如无限循环的wasm
合约。为了避免此类问题,设计了query_custom
用来为交易定义固定的gas
限制,query_custom
可以在特定app
的配置文件中定义所有调用的 gas 限制,该文件可由每个节点操作员自定义,并具有合理的默认值,配置见wasmd/x/wasm/README.md -> Configuration
-
正在执行的合约和被查询的合约在执行当前
CosmWasm
消息之前,都具有对状态快照的只读访问,见cosmwasm/vm/src/calls.rs -> call_query_raw
,这也避免了重入攻击。/// Calls Wasm export "query" and returns raw data from the contract. /// The result is length limited to prevent abuse but otherwise unchecked. pub fn call_query_raw<A, S, Q>( instance: &mut Instance<A, S, Q>, env: &[u8], msg: &[u8], ) -> VmResult<Vec<u8>> where A: Api + 'static, S: Storage + 'static, Q: Querier + 'static, { instance.set_storage_readonly(true); call_raw(instance, "query", &[env, msg], MAX_LENGTH_QUERY) }
当前合约只写入缓存,成功后刷新,见
cosmwasm/vm/src/calls.rs -> call_raw
。/// Calls a function with the given arguments. /// The exported function must return exactly one result (an offset to the result Region). fn call_raw<A, S, Q>( instance: &mut Instance<A, S, Q>, name: &str, args: &[&[u8]], result_max_length: usize, ) -> VmResult<Vec<u8>> where A: Api + 'static, S: Storage + 'static, Q: Querier + 'static, { let mut arg_region_ptrs = Vec::<Val>::with_capacity(args.len()); for arg in args { let region_ptr = instance.allocate(arg.len())?; instance.write_memory(region_ptr, arg)?; arg_region_ptrs.push(region_ptr.into()); } let result = instance.call_function1(name, &arg_region_ptrs)?; let res_region_ptr = ref_to_u32(&result)?; let data = instance.read_memory(res_region_ptr, result_max_length)?; // free return value in wasm (arguments were freed in wasm code) instance.deallocate(res_region_ptr)?; Ok(data) }
-
为了避免重入攻击,合约不能直接调用其他的合约,而是通过返回一个消息列表,这些消息在合约执行后在同一事务中发送给其他合约和被验证。如果,消息执行和验证失败,合约也将回滚,见
wasmd/x/wasm/internal/keeper/handler_plugin.go -> handleSdkMessage
。func (h MessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) error { if err := msg.ValidateBasic(); err != nil { return err } // make sure this account can send it for _, acct := range msg.GetSigners() { if !acct.Equals(contractAddr) { return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") } } // find the handler and execute it handler := h.router.Route(ctx, msg.Route()) if handler == nil { return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, msg.Route()) } res, err := handler(ctx, msg) if err != nil { return err } events := make(sdk.Events, len(res.Events)) for i := range res.Events { events[i] = sdk.Event(res.Events[i]) } // redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler) ctx.EventManager().EmitEvents(events) return nil }
数据交互机制
有两种数据用来合约与区块链之间的交互:Message Data 和 Context Data。
-
Message Data
:是由交易发送者签名的在事务中传递的任意字节数据,标准的JSON
编码,cosmwasm/packages/std/src/results/cosmos_msg.rs -> CosmosMsg
。#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] // See https://github.com/serde-rs/serde/issues/1296 why we cannot add De-Serialize trait bounds to T pub enum CosmosMsg<T = Empty> where T: Clone + fmt::Debug + PartialEq + JsonSchema, { Bank(BankMsg), // by default we use RawMsg, but a contract can override that // to call into more app-specific code (whatever they define) Custom(T), Staking(StakingMsg), Wasm(WasmMsg), }
-
Context Data
:是由cosmos SDK
运行时传入的,提供了一些有凭证的上下文。上下文数据可能包括签名者的地址、合约的地址、发送的Token
数量、块的高度,以及合同可能需要控制内部逻辑的任何其他信息,见cosmwasm/packages/std/src/types.rs -> Env
。#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, JsonSchema)] pub struct Env { pub block: BlockInfo, pub contract: ContractInfo, }
由于go/cgo
不能处理c
类型的数据,eg.
strings,也不支持对堆分配数据的引用,所以不管是Message Data
还是Context Data
,都是用JSON
编码的。
数据存储机制
Instance State(实例状态)只能被合约的一个实例访问,具有完全的读写访问权限。实例状态存储有两种方式:
-
singleton
:只有一个键(合约或配置作为键)的数据访问模式,见cosmwasm/packages/storage/src/singleton.rs -> Singleton
/// Singleton effectively combines PrefixedStorage with TypedStorage to /// work on a single storage key. It performs the to_length_prefixed transformation /// on the given name to ensure no collisions, and then provides the standard /// TypedStorage accessors, without requiring a key (which is defined in the constructor) pub struct Singleton<'a, T> where T: Serialize + DeserializeOwned, { storage: &'a mut dyn Storage, key: Vec<u8>, // see https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-type-parameters for why this is needed data: PhantomData<T>, }
-
kvstore
:多个键的数据访问模式,可以在实例化时设置实例状态,并且在调用时读取和修改它,它是唯一的并带前缀的"db"
,只能被这个实例访问。见cosmwasm/packages/storage/src/prefixed_storage.rs -> PrefixedStorage
pub struct PrefixedStorage<'a> { storage: &'a mut dyn Storage, prefix: Vec<u8>, }
还设计了只读合约状态来实现所有实例之间的共享数据,见
cosmwasm/packages/storage/src/prefixed_storage.rs -> ReadonlyPrefixedStorage
pub struct ReadonlyPrefixedStorage<'a> { storage: &'a dyn Storage, prefix: Vec<u8>, }
合约状态可以用某一种或两种方式来存储,根据需要进行配置,见cosmwasm-template/src/state.rs
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
singleton(storage, CONFIG_KEY)
}
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<State> {
singleton_read(storage, CONFIG_KEY)
}
来看看wasm
合约的运行目录
wasm
:存放部署到区块链的合约二进制字节码
modules
:存放实例化的合约以及合约状态数据,合约状态数据先从内存存储中加载,若没有则从文件中加载,然后放入内存。文件加载合约数据见:cosmwasm/packages/vm/src/file_system_cache.rs -> load
/// Loads an artifact from the file system and returns a module (i.e. artifact + store).
pub fn load(&self, checksum: &Checksum, store: &Store) -> VmResult<Option<Module>> {
let filename = checksum.to_hex();
let file_path = self.latest_modules_path().join(filename);
let result = unsafe { Module::deserialize_from_file(store, &file_path) };
match result {
Ok(module) => Ok(Some(module)),
Err(DeserializeError::Io(err)) => match err.kind() {
io::ErrorKind::NotFound => Ok(None),
_ => Err(VmError::cache_err(format!(
"Error opening module file: {}",
err
))),
},
Err(err) => Err(VmError::cache_err(format!(
"Error deserializing module: {}",
err
))),
}
}
合约数据与世界状态交互
合约初始化时,wasm
的数据存储被实例化为db
传递给合约,见wasmvm/lib.go -> Instantiate
func (vm *VM) Instantiate(
code CodeID,
env types.Env, // 区块数据和合约账号
info types.MessageInfo,
initMsg []byte,
store KVStore, // 数据存储
goapi GoAPI,
querier Querier,
gasMeter GasMeter,
gasLimit uint64,
) (*types.InitResponse, uint64, error) {
envBin, err := json.Marshal(env)
if err != nil {
return nil, 0, err
}
infoBin, err := json.Marshal(info)
if err != nil {
return nil, 0, err
}
// 传递给合约
data, gasUsed, err := api.Instantiate(vm.cache, code, envBin, infoBin, initMsg, &gasMeter, store, &goapi, &querier, gasLimit, vm.memoryLimit, vm.printDebug)
if err != nil {
return nil, gasUsed, err
}
var resp types.InitResult
err = json.Unmarshal(data, &resp)
if err != nil {
return nil, gasUsed, err
}
if resp.Err != "" {
return nil, gasUsed, fmt.Errorf("%s", resp.Err)
}
return resp.Ok, gasUsed, nil
}
合约调用初始化函数将数据存放至db
中,见cosmwasm-template/src/contract.rs -> init
pub fn init(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InitMsg,
) -> Result<InitResponse, ContractError> {
let state = State {
count: msg.count,
owner: deps.api.canonical_address(&info.sender)?,
};
config(deps.storage).save(&state)?;
Ok(InitResponse::default())
}
来看deps.storage
,它实现了两种存储:ExternalStorage
(将vm
中提供的数据包装成state
,是空结构体)和MemoryStorage
(结构体的属性是基于b
树的散列表),两种存储都是为了方便对db
的操作,具体的操作方法见:cosmwasm/packages/std/imports.rs
,具体的实现见:cosmwasm/packages/vm/imports.rs
extern "C" {
fn db_read(key: u32) -> u32;
fn db_write(key: u32, value: u32);
fn db_remove(key: u32);
// scan creates an iterator, which can be read by consecutive next() calls
#[cfg(feature = "iterator")]
fn db_scan(start_ptr: u32, end_ptr: u32, order: i32) -> u32;
#[cfg(feature = "iterator")]
fn db_next(iterator_id: u32) -> u32;
fn canonicalize_address(source_ptr: u32, destination_ptr: u32) -> u32;
fn humanize_address(source_ptr: u32, destination_ptr: u32) -> u32;
fn debug(source_ptr: u32);
/// Executes a query on the chain (import). Not to be confused with the
/// query export, which queries the state of the contract.
fn query_chain(request: u32) -> u32;
}
合约正确执行,操作完数据后,返回结果给区块链,区块链根据错误信息更新世界状态,大致流程图如下,根据数据流向画的:
与cosmos
集成
见wasmd/INTEGRATION.md