coswasm-wasm合约学习

前言

参考工程,以下工程均可在 GitHub找到

  1. cosmwasm: branch 0.13
  2. wasmd: branch v0.15.1
  3. cosmwasm-template: branch 0.13
  4. wasmvm: branch 0.13

Rust编译

注意点

  1. win系统删除所有不需要的代码进行编译,需要修改.cargo配置文件

    [build]
    rustflags = "-C link-arg=-s"
    复制代码
  2. 优化程序,提高运行速度:

    如果用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-vmcosmwasm-storagecosmwasm-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

configuration.png

  • 正在执行的合约和被查询的合约在执行当前 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 DataContext 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合约的运行目录

run.png

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;
}
复制代码

合约正确执行,操作完数据后,返回结果给区块链,区块链根据错误信息更新世界状态,大致流程图如下,根据数据流向画的:

state.png

cosmos集成

wasmd/INTEGRATION.md