coswasm - wasm contract learning

Keywords: Blockchain Rust

preface

For reference projects, the following projects can be found in GitHub find

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

Rust compilation

Attention

  1. The win system deletes all unnecessary code for compilation, and the. cargo configuration file needs to be modified

    [build]
    rustflags = "-C link-arg=-s"
    
  2. Optimize the program and improve the running speed:

    If you compile with cargo, use the -- release flag; If you compile with rustc, use the - O flag. However, optimization will reduce the compilation speed, and it is usually not desirable in the development process.

wasm virtual machine

Compilation principle

wasmvm works based on wasmer.io engine and relies on this library: cosmwasm, which is written in t rust. In order to facilitate go calling, it is finally compiled into libwasmvm.so dynamic library and generates the bindings.h header file for cgo calling.

Contracts are some wasm bytecodes uploaded to the blockchain system. They are initialized when creating a Contract. There are no other states except the States contained in the wasm code. For a Contract, there are three steps:

  • Create: compile the logic code into wasm bytecode with t rust, and then upload the bytecode to the blockchain system
  • Instantiate an instance: take out the wasm bytecode uploaded to the system and put it into the virtual machine
  • Call instance: execute the corresponding functions in the virtual machine according to the fields in json scheme, involving cosmowasm VM, cosmowasm storage and cosmowasm STD

The Contract is immutable because the logic code is fixed; instance is variable because the state is variable.

operating mechanism

  • All queries are executed as part of the transaction. Each contract defines a public query function, which can only access the contract data store in read-only mode and calculate the loaded data. The data format of the query is defined in the public State. See cosmwasm template / SRC / State.rs

    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
    pub struct State {
        pub count: i32,
        pub owner: Addr,
    }
    
  • The queries exposed in cosmos SDK are designed with execution time constraints to limit abuse, but DoS attacks, such as the infinite loop wasm contract, cannot be avoided. In order to avoid such problems, query is designed_ Custom is used to define fixed gas restrictions for transactions. query_custom can define the gas limit of all calls in the configuration file of a specific app, which can be customized by each node operator and has reasonable default values. See wasmd / X / wasm / readme.md - > configuration for the configuration

  • Both the executing contract and the queried contract have read-only access to the status snapshot before executing the current CosmWasm message. See CosmWasm / VM / SRC / calls. RS - > call_ query_ Raw, which also avoids reentry attacks.

    /// 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)
    }
    

    The current contract is only written to the cache and refreshed after success. See 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)
    }
    
  • In order to avoid reentry attack, the contract cannot directly call other contracts, but by returning a message list. These messages are sent to other contracts and verified in the same transaction after the contract is executed. If the message execution and verification fail, the contract will also be rolled back. See 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
    }
    
    

Data interaction mechanism

There are two kinds of data used for the interaction between the contract and the blockchain: Message Data and Context Data.

  • Message Data: it is any byte data signed by the transaction sender and transmitted in the transaction. It is encoded in standard JSON, cosmowasm / 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: it is passed in by the cosmos SDK runtime and provides some context with credentials. The Context Data may include the address of the signer, the address of the contract, the number of tokens sent, the height of the block, and any other information that the contract may need to control the internal logic. See cosmowasm / packages / STD / SRC / types.rs - > env.

    #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, JsonSchema)]
    pub struct Env {
        pub block: BlockInfo,
        pub contract: ContractInfo,
    }
    

Because go/cgo cannot handle c-type data, eg.strings, and does not support references to heap allocation data, both Message Data and Context Data are encoded in JSON.

Data storage mechanism

Instance State can only be accessed by one instance of the contract and has full read-write access. There are two ways to store instance status:

  • Singleton: data access mode with only one key (contract or configuration as key). See cosmowasm / 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: the data access mode of multiple keys. You can set the instance state when instantiating, and read and modify it when calling. It is a unique and prefixed "db", which can only be accessed by this instance. See cosmwasm / packages / storage / SRC / prefixed_ storage.rs -> PrefixedStorage

    pub struct PrefixedStorage<'a> {
        storage: &'a mut dyn Storage,
        prefix: Vec<u8>,
    }
    

    A read-only contract state is also designed to share data between all instances. See cosmowasm / packages / storage / SRC / prefixed_ storage.rs -> ReadonlyPrefixedStorage

    pub struct ReadonlyPrefixedStorage<'a> {
        storage: &'a dyn Storage,
        prefix: Vec<u8>,
    }
    

Contract status can be stored in one or two ways and configured as needed. See 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)
}

Let's take a look at the running directory of the wasm contract

wasm: store the binary bytecode of the contract deployed to the blockchain

modules: store the instantiated contract and contract status data. The contract status data is loaded from the memory storage first. If not, it is loaded from the file and then put into the memory. See cosmwasm / packages / VM / SRC / file for the contract data loaded in the 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
        ))),
    }
}

Contract data and world state interaction

During contract initialization, the data store of wasm is instantiated as db and passed to the contract. See wasmvm / lib.go - > instantiate

func (vm *VM) Instantiate(
   code CodeID,
   env types.Env, // Block data and contract account
   info types.MessageInfo,
   initMsg []byte,
   store KVStore, // data storage 
   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
   }
    // Pass to contract
   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
}

The contract calls the initialization function to store the data in db. See 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())
}

Look at deps.storage, which implements two kinds of storage: ExternalStorage (packaging the data provided in vm into state, which is an empty structure) and MemoryStorage (the attribute of the structure is a hash based on b-tree). Both kinds of storage are for the convenience of db operation. See cosmwasm/packages/std/imports.rs for specific operation methods, See cosmwasm/packages/vm/imports.rs for specific implementation

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;
}

The contract is executed correctly. After operating the data, the result is returned to the blockchain. The blockchain updates the world state according to the error information. The approximate flow chart is as follows, drawn according to the data flow direction:

Integration with cosmos

See wasmd/INTEGRATION.md

Posted by newmember on Thu, 14 Oct 2021 20:19:34 -0700