Rust智能合约实战:把第一个 ink! 计数器跑起来
开场三问:到底解决啥、为何用 Rust、你卡在何处?
问题 1:这活儿是干嘛的? 你想把业务规则写进链上,让它像自助售货机一样自动执行,资产和权限都透明可追溯。
问题 2:为什么非 Rust 不可? Polkadot 这类链要求代码既高性能又安全。Rust 的所有权系统就像房产证,谁拿钥匙谁负责,不会有人半夜来把你家墙拆了。
问题 3:新手最容易栽在哪? 大多卡在工具链:cargo-contract 是啥、为什么要装 nightly、命令一串看懵。今天我们用最小计数器,带你从空目录一路走到链上部署。
快速类比:把 ink! 当成小区门禁里的规则芯片
想象你的合约是小区大门的控制板:谁能进、什么时候开门、扣谁物业费都写在这块芯片里。Substrate runtime 就是物业,规定“芯片必须是 Wasm 尺寸,代码不能乱伸手”。Rust 负责把口语化的门禁规则翻译成安全、确定的 Wasm 二进制。
原理速写:五个钉子先钉牢
- ink! 合约最终编译成
wasm32-unknown-unknown,在链上的 sandbox 中运行。 #[ink::contract]宏把一个模块标记为合约,内部消息函数必须显式声明。- 构造函数(
#[ink::constructor])只在部署时跑一次,消息(#[ink::message])才是链上可调用入口。 - 状态写在
#[ink(storage)]结构体里,读写都要走框架提供的 API,避免原地 new/Box。 - 每条链上调用都会计量 weight/gas;循环、日志、序列化都要考虑“物业费”。
环境准备:稳定版 + 夜ly 组合拳
示例在 macOS 14.6 (ARM)、Rust 1.80.1 stable、nightly 2025-10-20、cargo-contract 4.0.0 验证。直接跑:
rustup default stable
rustup update nightly
rustup target add wasm32-unknown-unknown
cargo install cargo-contract --force
为什么要 nightly?当前 cargo-contract 的部分子命令仍默认调用 nightly toolchain(未来可能改动)。实战时也可以用 cargo +nightly 显式指定。
可选工具:wasm-opt(Binaryen)压缩包体、contracts-node 或 canvas-node 做本地链、polkadot.js 扩展配合前端操作。
实战步骤:ink! 计数器六步通关
第 1 步:脚手架开局
cargo contract new counter
cd counter
目录里会有 Cargo.toml 和 lib.rs 两个核心文件,其他辅助文件此阶段可以忽略。
第 2 步:检查 Cargo.toml
[package]
name = "counter"
version = "0.1.0"
edition = "2021"
[lib]
name = "counter"
path = "lib.rs"
crate-type = ["cdylib", "rlib"]
[dependencies]
ink = { version = "5", default-features = false, features = ["std"] }
[dev-dependencies]
ink = { version = "5", default-features = false, features = ["std", "ink-as-dependency"] }
[features]
default = ["std"]
std = [
"ink/std",
]
重点:crate-type 里要包含 cdylib,否则构建不出 Wasm;ink/std 让本地测试可用标准库。
第 3 步:撰写合约代码
#![cfg_attr(not(feature = "std"), no_std)]
#[ink::contract]
mod counter {
#[ink(storage)]
pub struct Counter {
value: i32,
}
impl Counter {
#[ink(constructor)]
pub fn new(init_value: i32) -> Self {
Self { value: init_value }
}
#[ink(message)]
pub fn increment(&mut self) {
self.value = self.value.saturating_add(1);
}
#[ink(message)]
pub fn decrement(&mut self) {
self.value = self.value.saturating_sub(1);
}
#[ink(message)]
pub fn get(&self) -> i32 {
self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[ink::test]
fn counter_moves_both_directions() {
let mut counter = Counter::new(5);
counter.increment();
counter.decrement();
assert_eq!(counter.get(), 5);
}
}
}
用 saturating_add / saturating_sub 是为了防止数值上下溢,省得链上被恶意用户拉爆。
第 4 步:了解构造与消息
部署时调用构造函数:
cargo contract instantiate \
--constructor new \
--args 10
链上读写:
cargo contract call --message increment
cargo contract call --message decrement
cargo contract call --message get
调试阶段可以切换到 --dry-run,配合本地节点验证 weight。
第 5 步:编写与运行测试
cargo +nightly test
#[ink::test] 会为每个测试提供内存中的链环境,你能直接断言 storage 变化;别忘了加 +nightly,否则某些宏解析失败。
第 6 步:构建、部署与观测
cargo +nightly contract build --release
产物都在 target/ink/:.wasm、.contract、metadata.json。之后:
- 启动本地 Contracts Node(或使用 Canvas)。
cargo contract upload上传代码。cargo contract instantiate初始化实例。- 用
polkadot.js或 CLI 调increment、decrement、get确认效果。
日志里能看到最终 weight、Gas 费用,记下来方便后续调优。
核心语法对照表
| 元素 | 作用 |
|---|---|
#[ink::contract] | 声明一个模块为智能合约 |
#[ink(storage)] | 标记可持久化的状态结构 |
#[ink(constructor)] | 部署时执行一次的初始化入口 |
#[ink(message)] | 链上可调用的公共方法 |
cargo-contract | 管理构建、测试、上传、实例化的 CLI |
WASM | 上传到 Substrate runtime 的二进制产物 |
性能与权衡:Wasm 的甜点与代价
- 优点:Wasm 模块小、易验证、跨节点 deterministic;Rust 提供内存安全和零成本抽象。
- 代价:链上环境没有完整标准库、必须通过 host 函数访问外部资源;Nightly 工具链略增 CI 配置复杂度。
- 建议:上线前用
wasm-opt -Oz target/ink/counter.wasm压缩体积,一般能省下 5%~12% 的 Gas;同时保留未压缩版本,便于调试。
常见坑预警
- 忘了装 nightly 或没加
cargo +nightly,导致宏扩展报错。 - 漏写
#![cfg_attr(not(feature = "std"), no_std)],链上加载直接 panic。 - 构造函数返回
Result却没处理错误分支,实际部署失败却看不出原因。 - 直接
value += 1,忽略溢出风险,被恶意调用后变负数。 - 只在本地跑
cargo test,没用#[ink::test],导致没有 storage 模拟。 - 部署到主网前没锁定
ink版本,团队同伴cargo update后合约字节码变了。
总结与下一步
- ink! 合约像门禁芯片:构造函数设定初始状态,消息函数负责对外互动。
cargo-contract是你的瑞士军刀,从生成项目到部署全都打包好了。- 有了计数器模板,你可以安心拆解更复杂的业务状态机。
下一步行动:
- 给
increment/decrement加入访问控制,试试caller校验。 - 写一个
reset消息,把计数器重置到指定值,练习错误处理。 - 参考 CosmWasm 的 counter 示例,对比入口设计和测试方式,挑一个方向继续深入。