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的更改。
链下执行
一般来说,合约执行需要是确定性的,以便所有节点在执行时得出相同的结论。为此,我们禁止任何可能导致不确定性的指令。最值得注意的是任何浮点算术。也就是说,有时合约是在链下执行的,因此不受共识约束。如果代码仅由单个节点执行并隐式被其他参与者信任就是这种情况。可信执行环境浮现在脑海中。为此,我们允许在以下约束下执行不确定代码用于链下用途:
- 永远不能从不确定代码实例化合约。执行代码的唯一方法是使用确定性合约的委托调用。
- 想要使用此功能的代码需要依赖
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
}
}
}
Rust智能合约开发框架pallet-contracts的使用:Substrate区块链wasm合约部署与执行
简介
pallet-contracts
是Substrate区块链框架中的一个智能合约模块,它允许在基于Substrate的区块链上部署和执行WebAssembly(WASM)智能合约。这个模块提供了一个沙盒环境来运行合约,并实现了燃料(gas)计量系统来限制执行资源。
主要特性
- 支持WebAssembly智能合约
- 燃料计量系统防止无限循环和资源耗尽
- 沙盒执行环境确保安全性
- 与Substrate框架深度集成
- 支持合约间调用
完整示例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与合约交互:
- 打开Polkadot.js Apps
- 导航到"Developer" -> “Contracts”
- 点击"Upload & deploy code"
- 上传
flipper.wasm
和flipper.json
- 设置初始值(如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()
}
}
}
注意事项
- 在执行合约方法时注意燃料限制,复杂操作需要更多燃料
- 存储操作会消耗较多燃料,应优化存储使用
- 合约代码应尽量精简以减少部署成本
- 使用ink!宏时确保正确标注属性(#[ink(…)])
总结
通过这个完整示例,我们展示了如何使用pallet-contracts框架开发、部署和调用WASM智能合约。该框架结合了Rust的安全性和WASM的高性能,为Substrate区块链提供了强大的智能合约功能。开发者可以使用熟悉的Rust语法构建去中心化应用,同时享受Substrate框架提供的各种优势。