Rust智能合约开发框架pallet-contracts的使用,Substrate区块链wasm合约部署与执行

Contracts Module

Contracts模块为运行时提供了部署和执行WebAssembly智能合约的功能。

概述

该模块基于[frame_support::traits::fungible]特性扩展账户,使其具有智能合约功能。它可以与基于[frame_support::traits::fungible]实现账户的其他模块一起使用。这些"智能合约账户"能够实例化智能合约并调用其他合约和非合约账户。

智能合约代码只存储一次,之后可以通过其code_hash检索。这意味着可以从相同的代码实例化多个智能合约,而无需每次都复制代码。

当调用智能合约时,其关联代码通过代码哈希检索并执行。这个调用可以改变智能合约账户的存储条目,实例化新的智能合约,或调用其他智能合约。

最后,当账户被删除时,其关联的智能合约账户代码和存储也将被删除。

权重

发送方必须在每次调用时指定[Weight]限制,因为智能合约调用的所有指令都需要权重。无论执行结果如何,未使用的权重都会在调用后返还。

如果达到权重限制,那么所有调用和状态更改(包括余额转移)仅在当前调用合约级别回滚。例如,如果合约A调用B且B在调用中途用完权重,那么B的所有调用都会回滚。假设合约A正确处理了错误,A的其他调用和状态更改仍然会保留。

一个ref_time Weight被定义为在运行时参考机器上一皮秒的执行时间。

回滚行为

合约调用失败不会级联。当子调用发生故障时,它们不会"冒泡",调用只会在特定合约级别回滚。例如,如果合约A调用合约B,而B失败,A可以决定如何处理该故障,要么继续要么回滚A的更改。

链下执行

一般来说,合约执行需要是确定性的,以便所有节点在执行时得出相同的结论。为此,我们禁止任何可能导致不确定性的指令。最值得注意的是任何浮点算术。也就是说,有时合约是在链下执行的,因此不受共识约束。如果代码仅由单个节点执行并隐式被其他参与者信任就是这种情况。可信执行环境浮现在脑海中。为此,我们允许在以下约束下执行不确定代码用于链下用途:

  1. 永远不能从不确定代码实例化合约。执行代码的唯一方法是使用确定性合约的委托调用。
  2. 想要使用此功能的代码需要依赖pallet-contracts并使用[bare_call()]直接。这确保默认情况下pallet-contracts不会暴露任何不确定性。

如何使用

不确定代码可以通过向[upload_code()]传递Determinism::Relaxed部署到链上。然后确定性合约可以委托调用它,前提是使用[bare_call()]并传递[Determinism::Relaxed]。当合约从链上交易调用时,永远不要使用此参数。

接口

可调度函数

这些在[参考文档]中有记录。

暴露给合约的接口

每个合约都是一个WebAssembly模块,如下所示:

(module
    ;; 当合约实例化时由pallet-contracts调用
    ;; 无参数和空返回类型
    (func (export "deploy"))

    ;; 当合约被调用时由pallet-contracts调用
    ;; 无参数和空返回类型
    (func (export "call"))

    ;; 如果合约使用内存则必须导入。内存是可选的
    ;; 允许的最大内存大小取决于pallet-contracts配置
    (import "env" "memory" (memory 1 1))

    ;; 这是许多可以导入并由pallet-contracts实现的函数之一
    ;; 此函数用于将结果缓冲区和标志复制回调用者
    (import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
)

所有可导入函数的文档可以在[这里]找到。

用法

该模块执行WebAssembly智能合约。这些理论上可以用任何编译为Wasm的语言编写。但是,使用专门针对此模块的语言会使事情变得容易得多。一种这样的语言是[ink!]。它允许用Rust编程语言编写基于WebAssembly的智能合约。

调试

合约在被RPC调用时可以通过[debug_message]API向客户端发出消息。这在[ink!]中通过[ink_env::debug_message()]暴露。

这些消息被收集到内部缓冲区并发送给RPC客户端。由各个客户端决定是否以及如何向用户呈现这些消息。

此缓冲区也作为调试消息打印。为了在节点控制台上看到这些消息,需要将runtime::contracts目标的日志级别提高到至少debug级别。然而,由于区块生产产生的噪音,这些消息很容易被忽略。在Substrate存储库的根目录下使用以下命令行是观察它们的良好起点:

cargo run --release -- --dev -lerror,runtime::contracts=debug

这将runtime::contracts的日志级别提高到debug,所有其他目标提高到error以防止它们向控制台发送垃圾邮件。

--dev:使用开发链规范 --tmp:使用临时存储链数据(链状态在退出时删除)

主机函数追踪

对于合约作者来说,查看调用哪些主机函数、使用哪些参数以及结果是什么,是一个有用的调试工具。

为了在节点控制台上看到这些消息,需要将runtime::contracts::strace目标的日志级别提高到trace级别。

示例:

cargo run --release -- --dev -lerror,runtime::contracts::strace=trace,runtime::contracts=debug

不稳定接口

出于在开发新合约接口时采用迭代方法的愿望,此托盘包含不稳定接口的概念。类似于rust nightly编译器,它允许我们添加新接口但标记为不稳定,以便合约语言可以试验它们并在我们稳定这些接口之前提供反馈。

为了访问[runtime.rs]中标记为#[unstable]的接口,需要将pallet_contracts::Config::UnsafeUnstableInterface设置为ConstU32<true>任何生产运行时显然永远不应该编译此功能:除了可能更改或删除外,这些接口可能没有适当的权重关联,因此被认为是不安全的

新接口通常作为不稳定接口添加,并可能在晋升为稳定接口之前经历几次迭代。

许可证:Apache-2.0

完整示例代码

下面是一个使用pallet-contracts部署和执行Wasm智能合约的完整示例:

// 在runtime/lib.rs中配置合约托盘
impl pallet_contracts::Config for Runtime {
    type Time = Timestamp;
    type Randomness = RandomnessCollectiveFlip;
    type Currency = Balances;
    type Event = Event;
    type Call = Call;
    type CallFilter = ();
    type WeightPrice = ();
    type WeightInfo = ();
    type ChainExtension = ();
    type DeletionQueueDepth = ();
    type DeletionWeightLimit = ();
    type UnsafeUnstableInterface = ConstU32<0>;
}

// 合约代码(Wasm二进制)
const CONTRACT_CODE: &[u8] = include_bytes!("./contract.wasm");

// 部署合约
let result = Contracts::upload_code(
    RuntimeOrigin::signed(ALICE),
    CONTRACT_CODE.to_vec(),
    None,
    Determinism::Deterministic
);

// 实例化合约
let result = Contracts::instantiate(
    RuntimeOrigin::signed(ALICE),
    100_000, // 初始余额
    Weight::from_parts(100_000_000, 0), // 权重限制
    None, // 代码哈希(如果之前上传过)
    vec![], // 输入数据
    vec![], // 盐
);

// 调用合约
let result = Contracts::call(
    RuntimeOrigin::signed(ALICE),
    contract_account_id,
    0, // 转账金额
    Weight::from_parts(100_000_000, 0), // 权重限制
    None, // 目标哈希
    vec![], // 输入数据
);
// 一个简单的ink!合约示例
#[ink::contract]
mod flipper {
    #[ink(storage)]
    pub struct Flipper {
        value: bool,
    }

    impl Flipper {
        #[ink(constructor)]
        pub fn new(init_value: bool) -> Self {
            Self { value: init_value }
        }

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

        #[ink(message)]
        pub fn get(&self) -> bool {
            self.value
        }
    }
}

1 回复

Rust智能合约开发框架pallet-contracts的使用:Substrate区块链wasm合约部署与执行

简介

pallet-contracts是Substrate区块链框架中的一个智能合约模块,它允许在基于Substrate的区块链上部署和执行WebAssembly(WASM)智能合约。这个模块提供了一个沙盒环境来运行合约,并实现了燃料(gas)计量系统来限制执行资源。

主要特性

  1. 支持WebAssembly智能合约
  2. 燃料计量系统防止无限循环和资源耗尽
  3. 沙盒执行环境确保安全性
  4. 与Substrate框架深度集成
  5. 支持合约间调用

完整示例Demo

下面是一个完整的pallet-contracts使用示例,包含合约编写、部署和调用的完整流程:

1. 准备环境

首先安装必要的工具链:

# 安装Rust工具链
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 安装cargo-contract工具
cargo install cargo-contract --force

# 安装Substrate节点
cargo install --git https://github.com/paritytech/substrate.git --force --locked substrate

2. 创建ink!合约项目

cargo contract new flipper
cd flipper

3. 编写合约代码

修改lib.rs文件:

#![cfg_attr(not(feature = "std"), no_std)]

use ink_lang as ink;

#[ink::contract]
mod flipper {
    #[ink(storage)]
    pub struct Flipper {
        value: bool,
    }

    impl Flipper {
        /// 构造函数,初始化合约状态
        #[ink(constructor)]
        pub fn new(init_value: bool) -> Self {
            Self { value: init_value }
        }

        /// 翻转布尔值
        #[ink(message)]
        pub fn flip(&mut self) {
            self.value = !self.value;
        }

        /// 获取当前值
        #[ink(message)]
        pub fn get(&self) -> bool {
            self.value
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn it_works() {
            let mut flipper = Flipper::new(false);
            assert_eq!(flipper.get(), false);
            flipper.flip();
            assert_eq!(flipper.get(), true);
        }
    }
}

4. 编译合约

cargo contract build

编译完成后会在target/ink目录下生成:

  • flipper.wasm - WASM字节码
  • flipper.json - 合约元数据

5. 启动Substrate节点

substrate --dev --tmp

6. 部署合约

使用Polkadot.js Apps与合约交互:

  1. 打开Polkadot.js Apps
  2. 导航到"Developer" -> “Contracts”
  3. 点击"Upload & deploy code"
  4. 上传flipper.wasmflipper.json
  5. 设置初始值(如false)和部署参数

部署JavaScript代码示例:

const tx = api.tx.contracts.instantiate(
  1000000,  // 初始资金
  500000,   // 燃料限制
  codeHash, // 合约代码哈希
  // 初始化数据(false)
  '0x0061736d01000000014100',
  // 随机盐值
  '0x12345678'
);

await tx.signAndSend(alice, (result) => {
  console.log('部署结果:', result);
});

7. 调用合约方法

调用flip方法:

const call = api.tx.contracts.call(
  contractAddress,  // 合约地址
  0,               // 转账金额
  200000,          // 燃料限制
  // flip方法选择器
  '0x633aa551',
  // 无参数
  '0x'
);

await call.signAndSend(alice, (result) => {
  console.log('调用结果:', result);
});

8. 查询合约状态

调用get方法查询状态:

const call = api.tx.contracts.call(
  contractAddress,
  0,
  200000,
  // get方法选择器
  '0x2f865bd9',
  '0x'
);

await call.signAndSend(alice, (result) => {
  console.log('当前值:', result);
});

进阶示例:合约间调用

创建一个调用其他合约的示例:

#[ink::contract]
mod caller {
    use ink_env::call;
    
    #[ink(storage)]
    pub struct Caller {
        target: AccountId,
    }

    impl Caller {
        #[ink(constructor)]
        pub fn new(target: AccountId) -> Self {
            Self { target }
        }
        
        #[ink(message)]
        pub fn call_target(&self) -> bool {
            call::build_call::<call::DefaultEnvironment>()
                .call(self.target)
                .gas_limit(5000)
                .exec_input(
                    call::ExecutionInput::new(
                        call::Selector::new(ink_env::blake2x256(b"get"))
                    )
                )
                .returns::<bool>()
                .invoke()
        }
    }
}

注意事项

  1. 在执行合约方法时注意燃料限制,复杂操作需要更多燃料
  2. 存储操作会消耗较多燃料,应优化存储使用
  3. 合约代码应尽量精简以减少部署成本
  4. 使用ink!宏时确保正确标注属性(#[ink(…)])

总结

通过这个完整示例,我们展示了如何使用pallet-contracts框架开发、部署和调用WASM智能合约。该框架结合了Rust的安全性和WASM的高性能,为Substrate区块链提供了强大的智能合约功能。开发者可以使用熟悉的Rust语法构建去中心化应用,同时享受Substrate框架提供的各种优势。

回到顶部