在本教程中,我们将深入了解 anchor 的幕后机制,看看 Solana 程序是如何部署的。
让我们来看看当我们运行 anchor init deploy_tutorial 时,anchor 为我们创建的测试文件:
describe("deploy_tutorial", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.DeployTutorial as Program<DeployTutorial>;
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize().rpc();
console.log("Your transaction signature", tx);
});
});
它生成的初始程序对你来说应该很熟悉:
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod deploy_tutorial {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
上面的程序是在何时何地部署的?
这个合约唯一可能被部署的地方,似乎就是测试文件中的这一行代码:
const program = anchor.workspace.DeployTutorial as Program<DeployTutorial>;
但这说不通,因为我们通常会预期这是一个异步(async)函数。
其实,Anchor 是在后台默默地部署该程序的。
Solana 程序没有构造函数
对于那些来自其他面向对象语言的开发者来说,这可能看起来很不寻常。Rust 并没有对象(objects)或类(classes)。
在以太坊智能合约中,构造函数可以配置存储,或者设置字节码和不可变变量。
那么“部署步骤”究竟在哪里呢?
(如果你仍在运行 Solana 验证节点和 Solana 日志,我们建议你重启并清空这两个终端)
让我们进行常规的设置。创建一个名为 program-deploy 的新 Anchor 项目,并确保验证节点和日志在其他 shell 窗口中运行。
不要运行 anchor test,而是在终端中运行以下命令:
anchor deploy

在上面日志的截图中,我们可以看到程序被部署的时刻。
现在有趣的部分来了。再次运行 anchor deploy:

程序被部署到了同一个地址,但这一次它是被升级的,而不是被全新部署的。
程序 ID 没有改变,程序被覆盖了。
Solana 程序默认是可变的
对于那些认为不可变性是理所当然的以太坊开发者来说,这可能会让他们感到震惊。
如果作者可以随意更改程序,那程序还有什么意义呢?其实,将 Solana 程序设置为不可变是可行的。这里的预设理念是:作者会首先部署可变版本,随着时间的推移且没有发现任何漏洞后,他们再将其重新部署为不可变版本。
在功能上,这与由管理员控制的代理合约(proxy)没有区别,即所有者后来将所有权放弃并移交至零地址。但可以说,Solana 的模型要干净得多,因为以太坊的代理合约可能会出现很多问题。
另一个推论是:Solana 没有 delegatecall,因为它不需要。
在 Solidity 合约中,delegatecall 的主要用途是通过向新的实现合约发出 delegatecall 来升级代理合约的功能。然而,由于 Solana 中程序的字节码是可以直接升级的,因此不需要向实现合约进行 delegatecall 委托调用。
又一个推论:Solana 并没有 Solidity 所定义的那种不可变变量(即只能在构造函数中设置的变量)。
在不重新部署程序的情况下运行测试
因为 anchor 默认会重新部署程序,让我们演示一下如何在不重新部署的情况下运行测试。
将测试文件修改如下:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import fs from 'fs'
let idl = JSON.parse(fs.readFileSync('target/idl/deployed.json', 'utf-8'))
describe("deployed", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
// Change this to be your programID
const programID = "6p29sM4hEK8ZFT5AvsGJQG5nKUtHBKs13iVL6juo5Uqj";
const program = new Program(idl, programID, anchor.getProvider());
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize().rpc();
console.log("Your transaction signature", tx);
});
});
在运行测试之前,我们建议清空 Solana 日志终端,并重启 solana-test-validator。
现在,使用以下命令运行测试:
anchor test --skip-local-validator --skip-deploy
现在查看日志终端:

我们看到 initialize 指令被执行了,但程序既没有被部署也没有被升级,因为我们在 anchor test 中使用了 --skip-deploy 参数。
练习:为了验证程序的字节码确实发生了变化,部署两个打印不同 msg! 值的合约。具体来说:
- 更新 lib.rs 中的
initialize函数,包含一个将字符串写入日志的msg!语句。 - 运行
anchor deploy - 运行
anchor test --skip-local-validator --skip-deploy - 检查日志以查看记录的消息
- 重复步骤 1 - 4,但修改
msg!中的字符串 - 验证程序 ID 是否没有改变
你应该会观察到消息字符串改变了,但程序 ID 保持不变。
总结
- Solana 没有构造函数,程序只是“直接被部署”的
- Solana 没有不可变变量
- Solana 没有 delegatecall,程序可以“直接被更新”
通过 RareSkills 了解更多
本教程是我们免费 Solana 课程 的一部分。
原发布于 2024 年 2 月 12 日