本文详细介绍了如何在Solana平台上构建程序,其功能与Solidity合约类似,并探讨了Solana如何处理算术溢出问题。文章通过具体示例展示了如何在Rust中实现函数,处理数据类型,以及进行单元测试,同时强调了计算成本及浮点操作的性能限制。
今天我们将学习如何创建一个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测试验证器:
solana-test-validator
在另一个终端运行Solana日志:
solana logs
通过运行测试确保新搭建的程序正常工作
anchor test --skip-local-validator
在我们进行任何数学计算之前,让我们将初始化函数更改为接收两个整数。以太坊使用uint256作为“标准”整数大小。在Solana上,它是u64——这相当于Solidity中的uint64。
默认的初始化函数将如下所示:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
在lib.rs中修改initialize()
函数,如下所示。
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64) -> Result<()> {
msg!("你发送了 {} 和 {}", a, b);
Ok(())
}
现在我们需要更改./tests/day2.ts
中的测试。
it("初始化成功!", async () => {
// 在这里添加你的测试。
const tx = await program.methods
.initialize(new anchor.BN(777), new anchor.BN(888)).rpc();
console.log("你的交易签名", 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: 你发送了 777 和 888
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 消耗了 1116 的 200000 计算单位
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 成功
现在让我们演示如何将字符串作为参数传递。
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64,
message: String) -> Result<()> {
msg!("你说了 {:?}", message);
msg!("你发送了 {} 和 {}", a, b);
Ok(())
}
并更改测试。
it("初始化成功!", async () => {
// 在这里添加你的测试。
const tx = await program.methods
.initialize(
new anchor.BN(777), new anchor.BN(888), "hello").rpc();
console.log("你的交易签名", tx);
});
当我们运行测试时,会看到新的日志。
接下来,我们添加一个函数(和测试)来演示如何传递数字数组。在Rust中,“向量”或Vec
就是Solidity所称的“数组”。
pub fn initialize(ctx: Context<Initialize>,
a: u64,
b: u64,
message: String) -> Result<()> {
msg!("你说了 {:?}", message);
msg!("你发送了 {} 和 {}", a, b);
Ok(())
}
// 添加这个函数
pub fn array(ctx: Context<Initialize>,
arr: Vec<u64>) -> Result<()> {
msg!("你的数组 {:?}", arr);
Ok(())
}
我们将单元测试更新如下
it("初始化成功!", async () => {
// 在这里添加你的测试。
const tx = await program.methods.initialize(new anchor.BN(777), new anchor.BN(888), "hello").rpc();
console.log("你的交易签名", tx);
});
// 添加这个测试
it("数组测试", async () => {
const tx = await program.methods.array([new anchor.BN(777), new anchor.BN(888)]).rpc();
console.log("你的交易签名", tx);
});
然后我们再次运行测试并查看日志,以查看数组输出:
Transaction executed in slot 368489:
Signature: 3TBzE3NddEY8KREv1FSXnieoyT6G6iNxF1n4hJHCeeWhAsUward3MEKm9WJHV4PMjPxeN2jRSRC9Rq8FUKjXoBQR
Status: Ok
Log Messages:
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY invoke [1]
Program log: Instruction: Initialize
Program log: 你说了 [777, 888]
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 消耗了 1587 的 200000 计算单位
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 成功
提示:如果你在Anchor测试中遇到问题,请尝试在Google中搜索与该错误相关的“Solana web3 js”。Anchor使用的TypeScript库是Solana Web3 JS库。
Solana有一些有限的原生支持浮点运算。
然而,最好避免浮点运算,因为它们计算消耗很大(稍后我们将看到这一点的例子)。请注意,Solidity对浮点运算没有原生支持。
关于使用浮点数的限制请阅读这里。
算术溢出在Solidity 0.8.0版本之前是一种常见的攻击途径,该版本默认内建了溢出保护。在Solidity 0.8.0或更高版本中,溢出检查是默认进行的。由于这些检查会消耗gas,因此有时开发人员会故意通过“unchecked”块来禁用它们。
overflow-checks = true
如果在Cargo.toml文件中将键overflow-checks
设置为true
,则Rust将在编译器级别添加溢出检查。请查看下一步Cargo.toml的截图:
如果Cargo.toml文件是这样配置的,你就不需要担心溢出。
然而,添加溢出检查会增加交易的计算成本(我们稍后将对此进行重访)。因此在一些计算成本问题影响下,你可能希望将overflow-checks
设置为false
。要战略性地检查溢出,可以使用Rust中的checked_*
运算符。
checked_*
运算符。让我们看看如何在Rust自身的算术运算中应用溢出检查。考虑下面的Rust代码片段。
+
运算符进行算术运算,它会静默溢出。.checked_add
,如果发生溢出则会抛出错误。请注意,我们还可以对其他操作使用.checked_*
,如checked_sub
和checked_mul
。let x: u64 = y + z; // 将静默溢出
let xSafe: u64 = y.checked_add(z).unwrap(); // 如果发生溢出,将panic
// checked_sub、checked_mul等也可用
练习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
。会发生什么?
你是否应该将overflow-checks = true
保持在Cargo.toml文件中用于你的Anchor项目?通常是的。但是如果你正在进行一些复杂计算,你可能想将overflow-checks
设置为false
,并在关键节点上战略性地防御溢出以节省计算成本,接下来我们将演示这一点。
在以太坊中,交易运行直到消耗交易指定的“gas限额”。Solana称“gas”为“计算单位”。默认情况下,交易限制为200,000计算单位。如果消耗超过200,000计算单位,该交易会回滚。
与以太坊相比,Solana确实便宜使用,但这并不意味着你在以太坊开发中的优化技能毫无用处。让我们测量一下我们的数学函数需要多少计算单位。
Solana日志终端还显示所使用的计算单位。我们为checked和unchecked减法提供了基准测试。
禁用溢出保护时消耗824计算单位:
启用溢出保护时消耗872计算单位:
正如你所看到的,进行一个简单的数学运算将使用近1000个单位。由于我们有20万单位,我们每笔交易只能够执行几百个简单的算术操作。因此,尽管Solana上的交易通常比以太坊便宜,但我们仍然受到相对较小的计算单位上限的限制,并且无法在Solana链上执行复杂的计算任务,比如流体动力学模拟。
我们稍后会重访交易成本。
在Solidity中,如果我们想将x
提高到y
的幂,我们这样写:
uint256 result = x ** y;
Rust不使用这种语法。相反地,它使用.pow
。
let x: u64 = 2; // 基数的数据类型必须明确
let y = 3; // 指数的数据类型可以推断
let result = x.pow(y);
如果你担心溢出,还有.checked_pow
。
使用Rust进行智能合约开发的一个好处是,我们不必导入像Solmate或Solady这样的库来进行数学运算。Rust是一个相当复杂的语言,内置了许多操作,如果我们需要一些代码,我们可以在Solana生态系统外寻找一个Rust crate(这是Rust中库的称谓)来完成这个任务。
让我们计算50的立方根。浮点数的立方根函数已内置在Rust语言中,使用函数cbrt()
。
// 注意我们将`a`更改为f32(浮点32)
// 因为`cbrt()`对u64不可用
pub fn initialize(ctx: Context<Initialize>, a: f32) -> Result<()> {
msg!("你说了 {:?}", 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: 试图开始函数,参数为50
Program log: 结果 = 3.6840315
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 消耗了 4860 的 200000 计算单位
Program 8o3ehd3XnyDocd9hG1uz5trbmSRB7gaLaE9BCXDpEnMY 成功
练习4:构建一个实现+、-、×和÷运算的计算器,并包含sqrt和log10运算。
最初发布于2024年2月9日
- 原文链接: rareskills.io/post/rust-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!