Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ink! 极简入门 #31

Open
jackalchenxu opened this issue Oct 30, 2022 · 9 comments
Open

ink! 极简入门 #31

jackalchenxu opened this issue Oct 30, 2022 · 9 comments

Comments

@jackalchenxu
Copy link
Owner

jackalchenxu commented Oct 30, 2022

ink!的数据类型

ink!语言支持的类型,与Rust语言类型和智能合约的运行环境WebAssembly runtime都相关;

基本上可以分为两大类:

  • 在内存中做计算的数据类型
  • 处理后保存在链上的数据类型

在内存中做计算的数据类型

沿用了很多Rust内置的类型,如:

  • u8, i32, i64, u128, ...;
  • bool, Result, Option, ...
  • String,Vec
  • 还有诸多的collections类型(hash_map, hash_set, btree_map等等),可参见ink_prelude crate

保存在链上的数据类型

相比内存中做计算的数据类型,存在链上的数据类型多了一个限制:支持SCALE编解码(scale encode/decode trait),如:

  • i8, i16/u16, i32/u32, i64/u64, i128/u128 (i指有符号,u指无符号)
  • 压缩模式下的大整形数 (可超出u128的大小)
  • bool值
  • Result
  • Option
  • Vec
  • Strings
  • (T)
  • Structs
  • Enum

此外,ink还通过ink_storage,ink_env等crate,提供了

  • Mapping<K, V> (即hashmap)
  • AccountId
  • Hash

⚠️ 原本保存在链上的数据类型也包括collections::*的很多数据类型,但后来Polkadot官方移除了它们,原因是当前的实现,这些类型引入会极大增加合约size;官方的设计决定是:直到较完美的实现(不会引起合约size膨胀)出现之前,先不加入这些类型。(github.com/use-ink/ink/issues/1134)

特别声明

  • WebAssembly runtime,运行在no_std环境下,所以在ink!合约中使用的库,不能是std::xxxx
  • 上面提到Vec类型,实际有两种实现,一个在Rust core/std库中,另一个是ink_prelude crate中,这里说的Vec,都是后一类
  • 标记该数据要保存在链上,都需要加上#[ink(storage)]的标记,同时如果数据类型是非基本类型,如Struct,Enum,还需要加上 #[derive(scale::Encode, scale::Decode)]来让系统自动实现SCALE codec

SCALE编码介绍: https://docs.substrate.io/reference/scale-codec/
SCALE实现代码: https://github.com/paritytech/parity-scale-codec

@jackalchenxu
Copy link
Owner Author

jackalchenxu commented Oct 30, 2022

ink!中的函数

可以分为两类:

  • 普通的参与处理的辅助类函数,语法格式跟普通的rust函数一样
  • 合约函数(Dapp可调用), 特别加入声明宏 #[ink(message)]
    是的,尽管是函数调用,但也跟Solidity一样,用了消息这个词

要点

  • 合约函数的返回值需要实现 scale::Encode trait,如果函数要返回一个结构体,你需要手动为结构体实现 scale::Encode (简便的手段是在该结构体定义上加入声明宏 #[derive(scale::Encode)]

  • 合约函数中有一种特别的函数:合约构造函数,需用声明宏 #[ink(constructor)]

  • 合约构造函数中,如果合约结构体中包含有Mapping类型,此时需使用 ink_lang::utils::initialize_contract(闭包函数)来给合约结构体赋值;举一个例子:

      #[ink(storage)]
      #[derive(SpreadAllocate)]
      pub struct MyErc20 {
          total_supply: Balance,
          balances: Mapping<AccountId, Balance>,
          allowances: Mapping<(AccountId, AccountId), Balance>,
      }
    
     #[ink(constructor)]
     pub fn new(total_suppply: Balance) -> Self {
          ink_lang::utils::initialize_contract(|contract: &mut MyErc20| {
                contract.total_supply = total_suppply;
                contract.balances.insert(owner, &total_suppply); 
          })
     }   
  • 合约函数中,如果该函数在被调用时,同时需要能接收调用方(如Dapp)transfer过来的DOT代币,需要特别用 #[ink(payable)]来修饰;举一个例子:

    #[ink(message, payable)]
    pub fn flip(&mut self) {
         self.value = !self.value;
    }
    

ink!函数 VS. Solidity函数

Solidity函数声明:

function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]

作为与Solidity中的函数做对比,我列出了如下几点不同:

  1. ink!中没有特别声明pure/view, 函数中传参(&self),即不会修改链上数据; 传参(&mut self)的函数,会修改链上数据
    是否意味着所有调用均要消耗gas?(solidity pure or view函数不消耗gas) -- 待确定

  2. ink!中只有pub fn - 外部可调用或者fn - 仅供crate/module内部调用 (待确定)

  3. 不同合约之间调用函数 - 跨合约调用,我们在后面讲

@jackalchenxu
Copy link
Owner Author

ink!/Rust 与 Solidity 的语言不同

因为ink!站在Rust的肩膀上,所以相对Solidity而言,有一些特性无需特别声明即可拥有,同时Solidity具有的一些特性,实际上ink!也无需特别支持。我举几个例子:

  1. Solidity变量都会被自动赋默认初值
    ink! 中的变量,必须都要赋初值才能被使用 - 这是rust语言的安全特性

  2. Solidity 支持 解构式赋值

    uint256 _number;
    bool _bool;
    uint256[3] memory _array;
    (_number, _bool, _array) = returnNamed();
     
    或者是
    (, _bool_value, _) = returnNamed();

    ink!靠Rust内置支持,只是语法不同,而且还有更多匹配:

    (_, _bool_value, _) = returnNamed();
    (_number, ..) = returnNamed();
    

ink!因为站在Rust的肩膀上,在智能合约中可以更漓漓尽致的发挥现代语言的编程能力;我个人意见,ink!智能合约的编写会更强大的多。

@jackalchenxu
Copy link
Owner Author

Solidity modifier 与 ink!

Solidity上有一个特别的函数语法:modifier,对应到Python上的装饰器;它的使用场景是能在被修饰的函数执行前,做一些检查工作,比如OpenZeppelin库中的Owner,它会对合约调用账户做检查,当只有调用者不是合约的所有者owner时,函数会错误退出,交易revert。

在ink!上没有这个特性,但我们可以通过rust中的 过程宏 (procedure macros)来实现同样的效果:

#[ink(message)]
#[modifiers(only_owner)]
fn mint(&mut self, to: AccountId, amount:  Balance) { ... }

实际上,过程宏如此之强大,我们可以利用它完成更多的控制,如在语法层面防止重入等
目前,ink!智能合约领域已经有了类似OpenZeppelin这样的库,叫”OpenBrush“

@jackalchenxu jackalchenxu changed the title ink极简入门 ink! 极简入门 Oct 30, 2022
@jackalchenxu
Copy link
Owner Author

jackalchenxu commented Oct 30, 2022

ink! 中的事件(Event)

在Solidity中事件扮演了重要的作用, Dapp透过RPC接口监听特定的事件,获取交易执行后的结果;
同样的,ink! 也为事件声明,操作设计了一套实现。

  1. 事件声明
    事件可以由struct封装,其中包含要获取的数据;其中一部分字段数据可被索引,我们称该字段为topic;
    struct前使用宏 #[ink(event)]标记。
    举例来说:

    type Balance = <ink_env::DefaultEnvironment as ink_env::Environment>::Balance;
    pub type Option<T> = core::option::Option<T>;
    
    #[ink(event)]
    pub struct Transfer {
         #[ink(topic)]
         from: Option<AccountId>,
         #[ink(topic)]
         to: Option<AccountId>,
         tokens: Balance,
    }
    
  2. 发送事件(emit event)
    使用self.env().emit_event()来发送事件 (如果是在构造器中调用emit_event,则换成Self.env().emit_event())

    fn _transfer(&mut self, from: AccountId, to: AccountId, tokens: Balance) -> Result<(), ERC20Error> {
        ....
        ....
        self.env().emit_event(Transfer {
              from: Some(from),
              to: Some(to),
              tokens,
        });
    
        Ok(())
    }
    
  3. 检测事件

    • Dapp client - The Substrate RPC does not directly expose an endpoint for querying events. If you used the default implementation, you can see the list of events for the current block by querying the storage of the System pallet.
      这部分技术实现待后续补齐

    • 开发者UI工具 - https://polkadot.js.org/apps ,有很好的界面可以观察到event

    • 测试框架
      ink_env crate提供了函数 ink_env::test::recorded_events().collect::<Vec<_>>()来获取所有事件,以Vec数据结构保存;
      每一个event遵守固定格式,以上面的transfer event举例来说,也是一个Vec,包含三个元素:

      • vec[0]: 合约名+事件名
      • vec[1]: 合约名+事件名+from+from字段内容
      • vec[2]: 合约名+事件名+to+to字段内容
        内容组织方面还是跟Solidity是类似的

    其中每一个元素,如果长度不超过32字节,则维持元素内容不变;如果超过32字节,则进行Blake2*256 Hash,再截取前32字节作为元素内容。
    测试框架中,如果要进行事件比对,则需先写函数来对预期事件的内容进行编码,再把编码后的结果逐一跟ink_env抓取的事件内容做比较。

@jackalchenxu
Copy link
Owner Author

jackalchenxu commented Oct 30, 2022

ink! 中的接口,继承(?)

前面说过ink!是站在rust的肩膀上,而solidity中的接口在ink!中对应的是Trait
合约继承? no,在ink!是没有的,更不要说什么 ”多重继承“, ”菱形继承“ <-- 个人觉得,合约中还是不要使用类似设计。

但我觉得装饰器的设计是可以有的,请参见上面的OpenBrush.

在Rust的设计概念中,使用Trait,With Composable over Inheritance
如下是供参考的ink! ERC20 trait定义:

#[ink::trait_definition]
pub trait Erc20Trait {
    #[ink(message)]
    fn total_supply(&self) -> Balance;

    #[ink(message)]
    fn balance_of(&self, token_owner: AccountId) -> Balance;

    #[ink(message)]
    fn allowance(&self, token_owner: AccountId, spender: AccountId) -> Balance;

    #[ink(message)]
    fn transfer(&mut self, to: AccountId, tokens: Balance) -> Result<(), ERC20Error>;

    #[ink(message)]
    fn approve(&mut self, spender: AccountId, tokens: Balance) -> Result<(), ERC20Error>;

    #[ink(message)]
    fn transfer_from_to(&mut self, from: AccountId, to: AccountId, tokens: Balance) -> Result<(), ERC20Error>;
}

ink!中的Trait,跟Solidity的interface很像。

Solidity中,合约编译后除生成合约字节码之外,还会生成合约ABI,即接口信息;从中我们可以看到该合约提供哪一些服务调用,传入参数和返回参数类型等。
同样的,ink! 合约在编译后,也同样会生成合约的metadata.json,其中也包含接口信息;我们把合约上传到链上,响应的信息也同样随之上传。

提示:
ink!中的trait有#[ink::trait_definition]修饰,其trait定义中有一些限制,比如不能包含关联类型。

@jackalchenxu
Copy link
Owner Author

jackalchenxu commented Oct 30, 2022

ink! 中的错误和错误处理

ink!错误定义,采用与Rust一样的定义,使用在Result<T, E>中。

当函数在处理过程中发生了错误,代码可以做如下选择:

  1. 返回Err(错误值),如果合约接口函数,此时:

    • 错误信息会送到Dapp,caller可根据错误展示适合的报错描述信息给用户
    • 执行语句所在交易revert
  2. 代码panic,此时:

    • 错误信息CalleeTraped会送到Dapp
    • 执行语句所在交易revert
  3. 如果该函数是函数调用链中的一环,代码返回给caller函数,让caller函数选择合适的处理方法:

    • 要么继续往上返回Error,结果同1
    • 加入错误处理,做客制化处理

如下是范例中定义的错误和检测错误并返回的例子:

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum ERC20Error {
   /// Returned if caller is not the contract owner.
   InvalidCaller,
   /// Returned if not enough balance to fulfill a request is available.
   InsufficientBalance,
   /// Returned if not enough allowance to fulfill a request is available.
   InsufficientAllowance,
}
......
.....................
fn _transfer(&mut self, from: AccountId, to: AccountId,  tokens: Balance) -> Result<(), ERC20Error> {
   let mut from_balance = self.balance_of(from);
   if from_balance < tokens {
      return Err(ERC20Error::InsufficientBalance);
   }
   ...
   ...
}
.............
#[ink(message)]
fn transfer(&mut self, to: AccountId, tokens: Balance) -> Result<(), ERC20Error> {
   let from = ink_env::caller::<Environment>();
   self._transfer(from, to, tokens)?;

   Ok(())
}

合约代码中仍可方便的使用Rust的 ?语法糖

合约做条件检查,除了范例代码中用到的手动if检查,还可以使用assert!(), assert_eq!(),...等语句。
assert!()的行为是检查条件是否满足,如果不满足,则产生panic!()的效果。

参考:
https://substrate.stackexchange.com/questions/2391/panic-in-ink-smart-contracts

@jackalchenxu
Copy link
Owner Author

jackalchenxu commented Oct 30, 2022

ink! 合约如何使用第三方库

Cargo.toml中添加第三方库的dependencies,在代码中使用use 关键字引入该第三方库即可。

ink! 合约如何调用链上已有合约的函数 - 跨合约调用

简单来说,需要把该链上已有合约实例化,之后就可以调用实例(对象)的方法。我们举一个例子来讲述操作细节:
有两个合约dapp1和dapp2;

  • 首先在链上部署dapp2,并获取dapp2部署的合约地址
  • 部署dapp1,dapp1构造器中传入dapp2合约地址
  • dapp1调用dapp2中的方法

让我们来详细讲述操作步骤:

  1. 确保dapp2的Cargo.toml的[lib] 如下所示:

    crate-type = [
        "cdylib",
         "rlib",
    ]
    

    默认是cdylib,但要让dapp1能使用dapp2,需要同时生成rlib (不然rust compiler会在dapp1编译时报错说找不到dapp2)
    之后编译,部署,记下dapp2的合约地址

  2. dapp1中引入该库的dependency
    dapp1的Cargo.toml:

    dapp2 = { path = "../dapp2", default-features = false, features = ["ink-as-dependency"] }
    

    features中设定 ink-as-dependency会确保引入dapp2时用链接的方法,而不是转到dapp2去编译

  3. dapp1合约构造器中,传入dapp2的合约地址:

    use dapp2::Dapp2Ref as Dapp2;
    
    #[ink(storage)]
    pub struct Dapp1 {
         dapp2_instance: Dapp2,
         value: bool,
    }
    
    #[ink(constructor)]
    pub fn new(address: AccountId) -> Self {
       let dapp2_code_hash = ink_env::code_hash::<Environment>(&address).unwrap();
    
       Self {
           dapp2_instance: Dapp2::new()
              .code_hash(Hash::from(dapp2_code_hash))
              .salt_bytes(0u32.to_le_bytes())
              .endowment(25)
              .instantiate()
              .expect("failed at instantiating `Dapp2` contract");
          value: false,
       }
    }
    
  4. 当需要使用dapp2的方法,使用

    #[ink(message)]
    pub fn dapp2_do_something(&mut self) -> u8 {
       self.dapp2_instance.do_something()
    }
    

这里的模式是,生成dapp2的ContractRef,然后把它放在Dapp1的实例中;调用dapp2的方法时,用self.dapp2_instance即可。
ContractRef类型是合约编译器为合约结构体自动添加的(codegen),并自动为其添加了诸多trait实现,简单来说,可以把ContractRef看成是合约AccountId的简单包装(simple wrapper of AccountId)


小插曲

我们现在为Dapp1的合约成员添加一个Mapping,再次编译,结果报错:

 #[derive(SpreadAllocate)]
   |              ^^^^^^^^^^^^^^ the trait `ink_storage::traits::SpreadAllocate` is not implemented for `dapp2::Dapp2Ref`

因为Mapping要求Dapp1合约实现 SpreadAllocate trait,而目前ink/crates/lang/codegen/src/generator/as_dependency/contract_ref.rs中还没有为ContractRef自动生成SpreadAllocate 的支持

想解决这个问题,有如下两种解法:

  1. 手动添加ContractRef来支持SpreadAllocate Trait,不过这需要我们修改Dapp2合约的源代码
    我们在下一节讲相关的知识和怎么来实现这个trait

  2. 只需要把dapp2的地址存入合约结构体中即可;需要实例化Dapp2的时候,使用一些从合约地址生成合约实例的函数来生成Dapp2实例,然后调用其方法即可,没有必要保存Dapp2实例在Dapp1的Storage里面。
    参考代码如下:

use dapp2::Dapp2Ref as Dapp2;

#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct Dapp1 {
     value: bool,
     map: Mapping<AccountId, Balance>,
     dapp2_accountid: AccountId,
}

#[ink(constructor)]
pub fn new(address: AccountId) -> Self {
     ink_lang::utils::initialize_contract(|contract: &mut Dapp1| {
       contract.dapp2_accountid = address;
       contract.value = false;
       contract.map = Mapping::default();
    })
}   

当需要使用dapp2的方法,使用

let mut dapp2_instance: Dapp2 = ink_env::call::FromAccountId::from_account_id(self.dapp2_accountid);
dapp2_instance.do_something()

合约Code Hash

获取链上合约的code hash

  • 如果我们有该链上合约的源代码,则编译之后在metadata.json中会记录该合约code hash;
  • 合约部署上链后,有一些UI也会提供该合约code hash
  • 如果有该合约地址,则我们也可以由 ink_env::code_hash(contract_address)来获取合约code hash

Note:

  • rlib 是 Rust static library, 保存所有metadata of crate,导入该库等于link statically
  • cdylib 是 Rust shared/dynamic library, 当需要与外部FFI交互时,如C/C++要调用该Rust库时,就需要选用这个格式
  • dylib 是 另一种Rust shared/dynamic library, 当需要与其他Rust代码交互时,就需要选用这个格式

@jackalchenxu
Copy link
Owner Author

jackalchenxu commented Nov 3, 2022

Storage Layout

Solidity 和 Ink! 在这部分有一定差异,也有很多相同之处(某种程度来说,应该是说EVM和WebAssembly(Frame?)在这部分的支持)

ink!中的每一个合约,一般都选择用一个struct来定义,该struct中的每一个变量,其location都在Storage上(还需要在前面加上#[ink(storage)]宏声明)
在合约的方法函数中声明、使用到的变量,其location都在可变内存中;方法函数运行完毕,这些变量都会失效;想要保存它们的值,需要在合约struct中添加对应的数据域。

在Storage的设计上,目前有V3.x和v4.x,差别好像比较大。

storage model

在Solidity中,Storage的存储单元叫 'slot'; 存储单元的大小都是32字节,存储单元的索引index也是一个32比特长度的整形
在ink!这边Storage的存储单元叫 'cell',索引cell的是Key类型,一个u32整形数,cell内可以放任意长度数据,取数据都靠Key来做索引;底层实际的存储和可能带来的重叠等问题,在底层实现;ink合无需感知这些问题。

可以把storage区域看成是一个Key/Value的数据库,而数据结构读取和写入都要靠key的索引

storage_model

storage的一个cell能放下32个字节的数据,合约中的数据要怎么安排放到cell中,这就是layout。
有如下两种layout:

  • packed, 结构体中多个数据域,如:

    struct PackedData {
       u8 a;
       u16 b;
       u8 c;
    }
    

    a,b,c 排在一起,一个storage cell就放得下,就都放在一个cell中。
    优点是节约storage空间,这样合约的费用少
    缺点是,load a数据域,但整个cell的内容都会被load(一般而言并不是特别大的问题)
    packed

  • spread,还以上面为例子,如果换成spread方式来放置数据,则a,b,c都会各占一个cell,总体上占3个cell
    spread

默认Layout是Spread方式

我们之前说过,目前合约设计中,支持的数据结构有基本类型(primitives,定长数组)和 Mapping
Mapping都一点不一样,它确定是spread layout。

不过在ink! 编程方面,并没有把storage cell index这样的底层细节曝露给上层,而是采用:

  • 对合约storage加入derive宏来声明该结构使用packed或者spread layout
  • 存取其中的数据,都是用self.成员变量的方式
    了解这两点,就可以编写合约storage

下面的例子主要是展示layout实现和trait的关系:

#[ink(storage)]
pub struct Dapp2 {
	packed_key: Key,
	spread_key: Key,
}

#[derive(Debug, Encode, Decode, SpreadLayout)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct SpreadStruct {
	arr_1: [u8; 32],
	arr_2: [u32; 32],
}

//  是的,哪怕是 PackedLayout,也需要再derive SpreadLayout
#[derive(Debug, Encode, Decode, SpreadLayout, PackedLayout)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct PackedStruct {
	arr_1: [u8; 32],
	arr_2: [u32; 32],
}

#[ink(constructor)]
pub fn new() -> Self {
        // 为packed结构体和spread结构体分配不同的key,代表放置不同的storage cell
        // 可以看到key的值差一,不用担心两者数据会重叠,这与Solidity的slot number不一样。
	let mut packed_index = [0; 32];
	let mut spread_index = [0; 32];
	packed_index [0] = 2;
	spread_index [0] = 3;
	let packed_key: Key = Key::from(packed_index);
	let spread_key: Key = Key::from(spread_index);

	push_packed_root(
		&PackedStruct {
			arr_1: [1u8; 32],
			arr_2: [2u32; 32],
		},
		&packed_key,
	);
	push_spread_root(
		&SpreadStruct {
			arr_1: [3u8; 32],
			arr_2: [4u32; 32],
		},
		&spread_key,
	);
	Self {
		packed_key,
		spread_key,
	}
}

#[ink(message)]
pub fn fetch_packed(&mut self) -> PackedStruct {
	ink_env::debug_println!("{:?}", self.packed_key);
	let value = pull_packed_root(&mut self.packed_key);
	return value;
}

#[ink(message)]
pub fn fetch_packed_arr1(&mut self) -> [u8; 32] {
	let value: [u8; 32] = pull_packed_root(&self.packed_key);
	return value;
}

#[ink(message)]
pub fn fetch_packed_arr2(&mut self) -> [u32; 32] {
	// it will cause error in packed mode
	// {
	//     self.packed_key += 1;
	//     let value = pull_packed_root(&self.packed_key);
	//     return value;
	// }

	{
		let value: PackedStruct = pull_packed_root(&self.packed_key);
		return value.arr_2;
	}
}

从编程角度,只要上层的编程模型设计正确,对上层编程,隐藏底层细节其实是比较好的设计。在这里存取PackedStruct中的任一数据,都可以用rust的结构体语法,不用去考虑storage cell等繁琐的细节。

当然,如果是spread layout,我们可以使用Key运算的方式,手动让key指向我们想要定位的地方,见如下代码:

#[ink(message)]
pub fn fetch_spread(&mut self) -> SpreadStruct {
	ink_env::debug_println!("{:?}", self.spread_key);
	let value = pull_spread_root(&self.spread_key);
	return value;
}

#[ink(message)]
pub fn fetch_spread_arr1(&mut self) -> [u8; 32] {
	ink_env::debug_println!("{:?}", self.spread_key);
	let value: [u8; 32] = pull_spread_root(&self.spread_key);
	return value;
}

#[ink(message)]
pub fn fetch_spread_arr2(&mut self) -> [u32; 32] {
	self.spread_key += 32;
	ink_env::debug_println!("{:?}", self.spread_key);
	let value = pull_spread_root(&self.spread_key);
	return value;
}

Note:

  1. 取SpreadStruct中的array_1可以用其首地址Key,获取SpreadStruct实例对象,然后.arr_1或者像代码那样
    而SpreadStruct中的array_2,我们可以用key算术运算的方式,让Key指向保存该位置,获取内容解析出arr_2的内容
    当然,这个例子只是为展示底层运作的一点点细节,非特别情况,不推荐大家对Key做运算。

  2. 在代码中,PackedStruct结构体声明中:

#[derive(Debug, Encode, Decode, SpreadLayout, PackedLayout)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct PackedStruct {
	arr_1: [u8; 32],
	arr_2: [u32; 32],
}

scale_info::TypeInfo 是需要的,只要加了SpreadLayout或者PackedLayout
说来奇怪,对于Wasm mode下,scale_info::TypeInfo是不需要的;但在std mode下,就需要这个,所以加了检测feature是不是std,否则会报错说找不到scale_info crate/module
感觉是cargo contract build会同时为wasm mode和std mode都会语法检查(ref: github.com/use-ink/ink/issues/640

@jackalchenxu
Copy link
Owner Author

ink! doc FAQ

ink! 与 Substrate/Polkadot的关系?

Substrate是一个开发框架,可以让开发者基于Substrate框架开发出Dapp;
Polkadot是区块链layer0,包含很多模组,其中模组之一contracts-pallet,可初始化合约并执行合约(合约执行环境)
ink!是智能合约开发语言

也有其他的合约开发方法:

  • solang 编译器:solidity合约被solang编译为Wasm
  • evm-pallet: 提供solidity合约在polkadot下的执行环境
  • actor-pallet:提供actor合约开发语言的执行环境

合约如何与运行时/runtime交互?

参见 Chain Extension一节

How can I use ink! with a Substrate chain with a custom chain config?

Please see the env_types argument for the contract macro. It allows you to specify your environment a la #[ink::contract(env = MyEnvironment)].

合约开始的#![cfg_attr(not(feature = "std"), "no_std")] 是干什么的?

告知rust compiler,合约是在如下哪一种模式被编译:

  • Wasm mode = "no_std"
    合约被编译后会部署到链上,就应该选择此模式,编译后会生成*.wasm文件;
    选择此模式,无法使用rust的std库的模块(取而代之是使用core库)
  • Off-chain mode/链下模式 = "std"
    在链下环境中测试该合约就应该选择该模式,也就是#[cfg(test)] 跑测试就在此模式

为什么链上Wasm不支持合约使用std库?

我们说的std库实际上包含3个部分:

  1. core库:rust最基本的功能,说rust适用于嵌入式系统,也是core库 porting到各种嵌入式系统上
  2. alloc库:内存分配器的实现,rust中各种常见的Box,String,Vec等等,都依赖于此
  3. std库:依赖于所在操作系统提供的底层功能,包含输入,输出,网络,文件等等。

Wasm(即 wasm32-unknown-unknown)编译target不支持std库

为什么合约编译时使用nightly版本?

因为合约编译在处理内存分配部分(no_std alloc),依赖几个unstable nightly特性,在Rust 2021版本上。
只要这几个特性被Rust新版本编译器中规定为stable,那我们就可以换用stable版本。

Security Issues

overflow:利用Rust本身的语法来保证
re-entrance attaack:the Substrate team is well aware of the associated problems and already through about possible future additions to eliminate re-entrancy attacks.

Storage Vs. Memory

有被#[ink(storage)]修饰的结构体或变量,其存放位置就是storage上,对应于链上数据,生命周期为'static。
其余的都是memory上,在离开其变量的作用域后就会失效,无法访问。

打印信息到console

ink_env::debug_println! or debug_print!
还需要在如下环境中:

  1. Enable the feature pallet-contracts/unstable-interface in the target runtime.
    For substrate-contracts-node this is done by default here.

  2. Enable the feature ink-debug for the ink_env crate.
    cargo-contract does this automatically for you (for versions >= 0.13.0), except if you compile a contract in --release mode.

  3. Set the log level of your node to runtime::contracts=debug.
    For example, to have only errors and debug output show up for the substrate-contracts-node:
    substrate-contracts-node --dev -lerror,runtime::contracts=debug

如何在合约中使用hash函数?

在contracts-pallet中提供crypto hash,可参见https://docs.rs/ink_env/3.3.1/ink_env/hash/trait.CryptoHash.html

  • self.env().hash_bytes()
  • self.env().hash_encoded()

如果你想要的hash功能不在其中,则可利用Chain Extension,

打算在ink!中支持浮点数吗?

浮点数在区块链领域最大的问题是在于其不确定性(non-deterministic)。
目前所有合约都不支持浮点数,而换用整形数1来作为token最小单元。
For example, 1 Bitcoin is equivalent to the smallest unit of 100,000,000 Satoshi and all Bitcoin implementations internally persist account balances in Satoshi, not as a decimal number of Bitcoin

在合约storage中,可以使用rust中的Vec数据结构吗,可以使用string吗?

可以,使用ink_prelude::vec::Vec;ink::prelude::string::String。
不过不太推荐,原因:

  • Vec<>中的每一项元素都独占一个storage cell,这样显得太浪费空间,对应的是费用高
  • 有Eager Loading的问题(即只想使用Vec<>中的某一项,但载入时会把Vec<>整个内容都载入进来)
    String也是类似。
    想一想,是否有其他数据结构可以用,而不是非得用像Vec,String这样的;合约Storage只应该保存必要的信息。

遇到ContractTrapped错误,是因为什么原因?

合约中的代码,有遇到Panic的情况,比如assert!, assert_eq,xxx.unwrap()之类的,也可能是算术运算溢出,当然也有
可能是编译器啥的bug(https://github.com/rust-lang/rust/issues/78744)

scale::Encode和scale::Decode trait是干什么的?

数据存储在Storage,从Storage取出数据;或者是Dapp调用合约中的函数,传入参数和返回参数,都是要经过scale编码的。
没有做scale编码,通常的错误类似:the trait "WrapperTypeEncode" is not implemented for "Foo"
要解决这个错误也很简单,给Struct Foo加上derive macro
#[derive(scale::Encode, scale::Decode)]
struct Foo { … }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant