Solana智能合约开发:调试程序

Solong
11 min readDec 30, 2020

前面Solana智能合约开发:HelloWorld 合约 我们的合约写好了,也可以正常编译了,那么怎么确定我们的合约能正常工作呢?

在前面的文章Solana开发教程:搭建开发环境 我们介绍了solana1.4.x提供了一个localnet.sh的脚步可以搭建一个本地的solana节点,然后在上面发布合约。 但是从solana1.5.0以后,官方又为我们增加了一个”solana-test-validator”命令,方便我们启动自己的测试节点。

有了测试节点后,我们可以将合约发布到我们自己的本地测试节点上,然后通过观察节点输出的日志,来进行合约内容的 调试。

启动测试节点

首先我们去到solana的github 下载最新的1.5.0版本,然后在本地解压,并设置bin目录到Path路径。或者直接在bin目录下执行

ubuntu@VM-0-3-ubuntu:~/solana/localtest$ solana-test-validator -h
solana-test-validator 1.5.0 (src:981294cb; feat:2255750762)
Test Validator
USAGE:
solana-test-validator [FLAGS] [OPTIONS] --ledger <DIR>
FLAGS:
-h, --help Prints help information
--log Log mode: stream the validator log
-q, --quiet Quiet mode: suppress normal output
-r, --reset Reset the ledger to genesis if it exists. By default the validator will resume an existing ledger
(if present)
-V, --version Prints version information
OPTIONS:
--bpf-program <ADDRESS BPF_PROGRAM.SO>... Add a BPF program to the genesis configuration
-C, --config <PATH>
Configuration file to use [default: /home/ubuntu/.config/solana/cli/config.yml]
-l, --ledger <DIR> Use DIR as ledger location [default: test-ledger]
--mint <PUBKEY>
Address of the mint account that will receive tokens created at genesis [default: client keypair]
--rpc-port <PORT>
Use this port for JSON RPC and the next port for the RPC websocket [default: 8899]

这些选项中,通过 — log可以打印节点运行日志;通过-C指定使用的config文件, 默认为”~/.config/solana/cli/config.yml”;通过-l指定节点区块信息存储目录,不指定时为当前目录; — rpc-port指定了RPC的端口,默认为8899以及websocket的8900。

这里我们以这样的命令来启动本地测试节点:

nohup  solana-test-validator  --log  > solana.log 2>&1 &

这里首先通过”nohup” 加上最后的”&”,让节点程序在后台运行。然后通过” — log > solana.log”选项, 让节点打印出日志。并通过2>&1将错误日志也重定向到文件中。

之后我们通过 :

tail -f solana.log

就可以查看滚动的节点日志了。如果再加上”grep”和我们日志的中的关键字,就可以过滤出我们自己合约中的关键 日志了。

发布合约

在编译好我们的合约后,solana会提示我们:

To deploy this program:
$ solana deploy /xxx/onchain_program/target/deploy/helloworld.so

但是如果这时我们执行,可能会报错,因为这里还没有发布程序的账号,并且这个账号是需要有SOL代币的,用于 发布合约的消耗。因此我们首先创建账号

solana-keychan new

输入密码后,记好你的公钥,如果不记得,可以用:

solana-keygen pubkey ~/.config/solana/id.json
4n3CDb6jtrbsChMVUSbnARknv1S6wCTN1bRWqopfH35B

来找回,然后申请空投:

solana airdrop 10 4n3CDb6jtrbsChMVUSbnARknv1S6wCTN1bRWqopfH35B
Requesting airdrop of 10 SOL from 127.0.0.1:9900
10 SOL

空投10个SOL到这个账号,接着我们就可以用上面提示的命令发布程序了:

solana deploy /xxx/onchain_program/target/deploy/helloworld.so
{"programId":"AffdN6VX2wjyDYCHbZa2wPGcpyRzNPPn2ostTw1sChZ9"}

空投前,记得设置RPC节点为本地节点:

solana config set --url http://localhost:8899

DApp RPC交互

要想测试合约,我们必须要通过API的RPC接口来和节点进行交互。这里我们选择了JS的API solana-web3.js, solana同时也提供 了Rust的SDK,也可以用Rust写个命令行客户端。

JS的demo可以参见:helloworld_demo

这是一个react的项目,pull下来后,执行:

yarn install
yarn start

就可以运行了。

这里我们主要来看”hello”指令是怎么执行的。在App.js里面:

let messageNeeded = await this.connection.getMinimumBalanceForRentExemption(Layout.messagSpace);const trxi0 =  SystemProgram.createAccount({
fromPubkey: this.playerAccount.publicKey,
newAccountPubkey: this.messageAccount.publicKey,
lamports: messageNeeded,
space: Layout.messagSpace,
programId: this.programID,
});
console.log("message:", this.messageAccount.publicKey.toBase58());
let trxi = HelloWorld.createHelloInstruction(
this.playerAccount.publicKey,
this.messageAccount.publicKey,
this.programID,
"hello world!",
);
const transaction = new Transaction();
transaction.add(trxi0);
transaction.add(trxi);
let signers= [this.playerAccount, this.messageAccount];
sendAndConfirmTransaction(this.connection, transaction, signers, {
skipPreflight: false,
commitment: 'recent',
preflightCommitment: 'recent',
}).then(()=>{
console.log("done hello");
}).catch((e)=>{
console.log("error:", e);
})

首先通过connection.getMinimumBalanceForRentExemptio来查询,创建一个我们存的Message大小的 Account需要多少花费,然后通过系统的创建账号指令,创建一个Instruction,也就是上面的trxi0。

接着我们通过HelloWorld.js里面封装的createHelloInstruction创建我们前面文章中需要的instruction:

static createHelloInstruction(
playerAccountKey,
messageAccountKey,
programID,
message,
) {
const dataLayout = BufferLayout.struct([
BufferLayout.u8("i"),
BufferLayout.blob(message.length,"message"),
]);

const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
i:0, // hello
message: Buffer.from(message, 'utf8'),
},
data,
);

let keys = [
{pubkey: playerAccountKey, isSigner: true, isWritable: true},
{pubkey: messageAccountKey, isSigner: true, isWritable: true},
];
const trxi = new TransactionInstruction({
keys,
programId: programID,
data,
});
return trxi;
}

这里将keys里面设置了用户的账号和上面创建的message的账号,data则是按照前面文中的:

+-----------------------------------+
Hello: | 0 | message |
+-----------------------------------+

填入的内容。

最后创建transaction并进行发送。

这是我们通过上面的tail命令:

tail -f solana.log |grep hello

然后在界面上触发hello按钮,观看打印:

[2020-12-29T14:56:58.304430806Z DEBUG solana_runtime::message_processor] Program log: hello-world: HelloWorld msg:hello world!
[2020-12-29T14:56:58.304451252Z DEBUG solana_runtime::message_processor] Program log: before unpack hello
[2020-12-29T14:56:58.304475950Z DEBUG solana_runtime::message_processor] Program log: after unpack hello
[2020-12-29T14:56:58.304484573Z DEBUG solana_runtime::message_processor] Program log: before pack hello
[2020-12-29T14:56:58.304577373Z DEBUG solana_runtime::message_processor] Program log: pack into slice msg:hello world!
[2020-12-29T14:56:58.304603157Z DEBUG solana_runtime::message_processor] Program log: after pack hello

这样就可以通过日志来进行相应的调试了

总结

当前,Solana还没法像一般程序开发一样下断点进行调试,但是我们可以通过打日志的方式,对合约执行逻辑进行调试,但是需要注意的是,在发布的合约时,要精简一下日志,因为打日志是有 资源消耗的,并且尽量少使用,不使用format!,这个宏会消耗大量的计算单元。

除此之外,还可以通过rust的单元测试来对我们的一般逻辑进行测试,这个时候是可以通过断点以及普通的rust程序 调试过程来进行调试的。

参考

  1. solana1.5.0
  2. solana-web3.js

--

--