今天,我们将学习如何创建一个 Solana 程序,来实现与下方 Solidity 合约相同的功能。我们还将学习 Solana 是如何处理溢出等算术问题的。
contract Day2 {
event Result(uint256);
event Who(string, address);
function doSomeMath(uint256 a, uint256 b) public {
uint256 result = a + b;
emit Result(result);
}
function sayHelloToMe() public {
emit Who("Hello World", msg.sender);
}
}
让我们启动一个新项目
anchor init day2
cd day2
anchor build
anchor keys sync
确保你正在一个终端中运行 Solana 测试验证器(test validator):
solana-test-validator
并在另一个终端中查看 Solana 日志:
solana logs
运行测试,确保新生成的脚手架程序能够正常工作
anchor test --skip-local-validator
提供函数参数
在进行任何数学运算之前,让我们修改 initialize 函数,使其接收两个整数。Ethereum 使用 uint256 作为“标准”整数大小。而在 Solana 上,它是 u64——这等同于 Solidity 中的 uint64。
传递无符号整数
默认的 initialize 函数如下所示:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
将 lib.rs 中的 initialize() 函数修改如下。
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64) -> Result<()> {
msg!("You sent {} and {}", a, b);
Ok(())
}
现在我们需要修改 ./tests/day2.ts 中的测试。
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize(new anchor.BN(777), new anchor.BN(888)).rpc();
console.log("Your transaction signature", tx);
});
现在重新运行 anchor test --skip-local-validator。
查看日志时,我们应该能看到类似下面的内容
Transaction executed in slot 367357:
Signature: 54iJFbtEE61T9X2WCLbMe8Dq2YYBzCLYE4qW2DqTsA4gZRgootcubLgHc1MHYncbP63sxNxEY8tJfgfgsdt1Ch4g
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: You sent 777 and 888
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY consumed 1116 of 200000 compute units
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY success
传递字符串
现在让我们演示如何将字符串作为参数传递。
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64,
message: String) -> Result<()> {
msg!("You said {:?}", message);
msg!("You sent {} and {}", a, b);
Ok(())
}
然后修改测试代码。
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize(
new anchor.BN(777), new anchor.BN(888), "hello").rpc();
console.log("Your transaction signature", tx);
});
运行测试时,我们可以看到新的日志。
数字数组
接下来,我们添加一个函数(及测试)来演示如何传递数字数组。在 Rust 中,“向量(vector)”或 Vec 就是 Solidity 中所谓的“数组(array)”。
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64,
message: String) -> Result<()> {
msg!("You said {:?}", message);
msg!("You sent {} and {}", a, b);
Ok(())
}
// added this function
pub fn array(ctx: Context<Initialize>,
arr: Vec<u64>) -> Result<()> {
msg!("Your array {:?}", arr);
Ok(())
}
接着我们按如下方式更新单元测试
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize(new anchor.BN(777), new anchor.BN(888), "hello").rpc();
console.log("Your transaction signature", tx);
});
// added this test
it("Array test", async () => {
const tx = await program.methods.array([new anchor.BN(777), new anchor.BN(888)]).rpc();
console.log("Your transaction signature", tx);
});
我们再次运行测试并查看日志以查看数组输出:
Transaction executed in slot 368489:
Signature: 3TBzE3NddEY8KREv1FSXnieoyT6G6iNxF1n4hJHCeeWhAsUward3MEKm9WJHV4PMjPxeN2jRSRC9Rq8FUKjXoBQR
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: You said [777, 888]
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY consumed 1587 of 200000 compute units
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY success
提示:如果你在进行 Anchor 测试时遇到问题卡住了,请尝试在 Google 上搜索与你的错误相关的“Solana web3 js”。Anchor 使用的 Typescript 库正是 Solana Web3 JS 库。
Solana 中的数学运算
浮点数数学运算
Solana 对浮点运算有一些原生支持,尽管是有限的。
然而,最好避免使用浮点运算,因为它们的计算强度非常大(稍后我们将看到一个具体的例子)。请注意,Solidity 对浮点运算 没有 任何原生支持。
在这里了解更多关于使用浮点数的限制。
算术溢出
在 Solidity 0.8.0 版本将溢出保护默认内置到语言中之前,算术溢出曾经是一个常见的攻击向量。在 Solidity 0.8.0 或更高版本中,溢出检查默认开启。由于这些检查会消耗 gas,有时开发者会使用 “unchecked” 块来策略性地禁用它们。
Solana 是如何防御算术溢出的?
方法 1:在 Cargo.toml 中设置 overflow-checks = true
如果在 Cargo.toml 文件中将键 overflow-checks 设置为 true,那么 Rust 将在编译器级别添加溢出检查。请看下面的 Cargo.toml 截图:

如果 Cargo.toml 文件按照这种方式配置,你就不需要担心溢出的问题。
然而,添加溢出检查会增加交易的计算成本(我们稍后会再讨论这个问题)。因此,在一些对计算成本敏感的情况下,你可能希望将 overflow-checks 设置为 false。为了有策略地检查溢出,你可以使用 Rust 中的 checked_* 运算符。
方法 2:使用 checked_* 运算符。
让我们来看看在 Rust 内部如何将溢出检查应用于算术运算。考虑下面的 Rust 代码片段。
- 在第 1 行,我们使用常规的
+运算符进行算术运算,这会发生静默溢出。 - 在第 2 行,我们使用
.checked_add,如果发生溢出,它将抛出一个错误。注意,我们还可以将.checked_*用于其他运算,例如checked_sub和checked_mul。
let x: u64 = y + z; // will silently overflow
let xSafe: u64 = y.checked_add(z).unwrap(); // will panic if overflow happens
// checked_sub, checked_mul, etc are also available
练习 1:设置 overflow-checks = true,创建一个测试用例,通过执行 0 - 1 使 u64 发生下溢。你需要将这些数字作为参数传递,否则代码将无法编译。发生了什么?
当测试运行时,你会看到交易失败(并显示下面那条相当晦涩的错误信息)。这是因为 Anchor 开启了溢出保护:

练习 2:现在将 overflow-checks 更改为 false,然后再次运行测试。你应该会看到一个下溢值 18446744073709551615。
练习 3:在 Cargo.toml 中禁用溢出保护的情况下,执行 let result = a.checked_sub(b).unwrap();,并使 a = 0 和 b = 1。发生了什么?
你是否应该直接在 Anchor 项目的 Cargo.toml 文件中保留 overflow-checks = true 呢?通常来说,是的。但是如果你正在进行一些密集的计算,你可能希望将 overflow-checks 设置为 false,并在关键节点有策略地防御溢出,以节省计算成本,接下来我们将对此进行演示。
Solana 计算单元基础入门
在 Ethereum 中,交易会一直运行,直到消耗完交易所指定的“gas limit”。Solana 将“gas”称为“计算单元(compute unit)”。默认情况下,一笔交易被限制为 200,000 个计算单元。如果消耗超过 200,000 个计算单元,交易将会被回退(revert)。
确定 Solana 中交易的计算成本
与 Ethereum 相比,Solana 的使用成本确实很低,但这并不意味着你在 Ethereum 开发中锻炼出的“极致优化(optimizoooor)”技巧就毫无用武之地了。让我们来测量一下我们的数学函数需要消耗多少计算单元。
Solana 日志终端也会显示使用了多少计算单元。我们在下面提供了开启检查和未开启检查时执行减法操作的基准测试结果。
在禁用溢出保护的情况下,消耗 824 个计算单元:

在开启溢出保护的情况下,消耗 872 个计算单元:

如你所见,仅仅执行一个简单的数学运算就占用了将近 1000 个计算单元。由于我们只有 20 万个单元的额度,在每笔交易的 gas 限制内,我们只能执行几百次简单的算术运算。因此,尽管 Solana 上的交易通常比 Ethereum 上更便宜,但我们仍然受限于一个相对较小的计算单元上限,无法在 Solana 链上执行像流体动力学模拟这样计算密集型的任务。
我们将在稍后重新讨论交易成本的问题。
幂运算不使用与 Solidity 相同的语法
在 Solidity 中,如果我们想求 x 的 y 次方,我们会这样做:
uint256 result = x ** y;
Rust 不使用这种语法。相反,它使用 .pow:
let x: u64 = 2; // it is important that the base's data type is explicit
let y = 3; // the exponent data type can be inferred
let result = x.pow(y);
如果你担心发生溢出,也有 .checked_pow 可以使用。
浮点数
使用 Rust 编写智能合约的一个好处是,我们不需要像在 Solidity 中那样导入像 Solmate 或 Solady 这样的库来进行数学运算。Rust 是一门相当成熟的语言,内置了许多运算操作,如果我们需要某些功能代码,我们可以在 Solana 生态系统之外寻找 Rust crate(在 Rust 中库被称为 crate)来完成这项工作。
让我们计算 50 的立方根。针对浮点数的立方根函数已通过 cbrt() 函数内置于 Rust 语言中。
// note that we changed `a` to f32 (float 32)
// because `cbrt()` is not available for u64
pub fn initialize(ctx: Context<Initialize>, a: f32) -> Result<()> {
msg!("You said {:?}", a.cbrt());
Ok(());
}
还记得我们在前面的章节中提到的浮点运算可能在计算成本上非常昂贵吗?在这里,我们可以看到立方根运算的消耗是无符号整数简单算术运算的 5 倍以上:
Transaction executed in slot unspecified:
Signature: VfvySG5vvVSAnsYLCsvB9N6PsuGwL39kKd1fMsyvuB7y5DUHURwQVHU9rv3Xkz5NJqGHLSXoWoW92zJb5VKYCEF
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: attempting to begin the function with 50
Program log: Result = 3.6840315
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY consumed 4860 of 200000 compute units
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY success
练习 4:构建一个能执行 +、-、x、÷,以及 sqrt(平方根)和 log10(以 10 为底的对数)运算的计算器。
原载于 2024 年 2 月 9 日