Solana 中的 Tx.origin、msg.sender 和 onlyOwner:识别调用者
在 Solidity 中,msg.sender是一个全局变量,表示调用或发起智能合约函数调用的地址。全局变量tx.origin是签署交易的钱包。
在 Solana 中,没有与 等效的东西msg.sender。
有一个等效的tx.origin,但是你应该知道 Solana 交易可以有多个签名者,所以我们可以将其视为具有“多个 tx.origins”。
要在 Solana 中获取“ tx.origin”地址,需要通过将 Signer 帐户添加到函数上下文来设置它,并在调用函数时将调用者的帐户传递给它。
让我们看一个如何在 Solana 中访问交易签名者地址的示例:
use anchor_lang::prelude::*;
declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");
#[program]
pub mod day14 {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
// Function logic....
msg!("The signer1: {:?}", *the_signer1.key);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub signer1: Signer<'info>,
}
从上面的代码片段来看,Signer<'info>用于验证账户结构signer1中的账户Initialize<'info>是否签署了交易。
在initialize函数中,signer1帐户从上下文中可变地引用并分配给the_signer1变量。
最后,我们使用宏并传入来记录signer1的公钥(地址),它取消引用并访问指向的实际值上的字段或方法。msg!*the_signer1.keykeythe_signer1
接下来就是针对上面的程序编写测试:
describe("Day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
it("Is signed by a single signer", async () => {
// Add your test here.
const tx = await program.methods.initialize().accounts({
signer1: program.provider.publicKey
}).rpc();
console.log("The signer1: ", program.provider.publicKey.toBase58());
});
});
测试中,我们将钱包账户作为 signer 传入账户signer1,然后调用初始化函数,最后在控制台上打印钱包账户,验证和程序中的一致。
练习:运行测试后,你从shell_1(命令终端)和shell_3 (日志终端)的输出中注意到了什么?
多名签名者
在 Solana 中,我们还可以让多个签名者签署一笔交易,你可以将其视为批量处理一堆签名并将其发送至一笔交易中。一个用例是在一笔交易中执行多重签名交易。
为此,我们只需在程序中的帐户结构中添加更多签名者结构,然后确保在调用函数时传递必要的帐户:
use anchor_lang::prelude::*;
declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");
#[program]
pub mod day14 {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
let the_signer2: &mut Signer = &mut ctx.accounts.signer2;
msg!("The signer1: {:?}", *the_signer1.key);
msg!("The signer2: {:?}", *the_signer2.key);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
pub signer1: Signer<'info>,
pub signer2: Signer<'info>,
}
上述示例与单个签名者示例有些相似,但有一个明显的区别。在本例中,我们signer2在结构中添加了另一个签名者帐户()Initialize,并在初始化函数中记录了两个签名者的公钥。
与单个签名者相比,使用多个签名者调用初始化函数有所不同。以下测试显示了如何使用多个签名者调用函数:
describe("Day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
// generate a signer to call our function
let myKeypair = anchor.web3.Keypair.generate();
it("Is signed by multiple signers", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
signer1: program.provider.publicKey,
signer2: myKeypair.publicKey,
})
.signers([myKeypair])
.rpc();
console.log("The signer1: ", program.provider.publicKey.toBase58());
console.log("The signer2: ", myKeypair.publicKey.toBase58());
});
});
那么上述测试有什么不同?首先是signers()方法,它接受一个签名者数组,这些签名者将对交易进行签名作为参数。但是数组中只有一个签名者,而不是两个。Anchor 会自动将提供商中的钱包账户作为签名者传递,因此我们不需要再次将其添加到签名者数组中。
生成随机地址进行测试
第二个变化是变量,它存储模块随机生成的myKeypair密钥对(用于访问帐户的公钥和相应的私钥anchor.web3) 。在测试中,我们将密钥对(存储在myKeypair变量中)的公钥分配给signer2帐户,这就是为什么它作为参数传递到.signers([myKeypair])方法中的原因。
多次运行测试,你会注意到signer1pubkey 没有改变,但signer2pubkey 发生了变化。这是因为分配给该signer1账户的钱包账户(在测试中)来自提供商,这也是你本地机器中的 Solana 钱包账户,并且分配给的账户signer2是每次运行时随机生成的anchor test —skip-local-validator。
练习:创建另一个需要三个签名者(提供者钱包账户和两个随机生成的账户)的函数(您可以随意称呼它),并为其编写测试。
唯一所有者
这是 Solidity 中常用的模式,用于将函数的访问权限限制为合约所有者。使用#[access_control]Anchor 中的属性,我们还可以实现唯一所有者模式,即将 Solana 程序中函数的访问权限限制为 PubKey(所有者的地址)。
以下是如何在 Solana 中实现“onlyOwner”功能的示例:
use anchor_lang::prelude::*;
declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");
// NOTE: Replace with your wallet's public key
const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";
#[program]
pub mod day14 {
use super::*;
#[access_control(check(&ctx))]
pub fn initialize(ctx: Context<OnlyOwner>) -> Result<()> {
// Function logic...
msg!("Holla, I'm the owner.");
Ok(())
}
}
fn check(ctx: &Context<OnlyOwner>) -> Result<()> {
// Check if signer === owner
require_keys_eq!(
ctx.accounts.signer_account.key(),
OWNER.parse::<Pubkey>().unwrap(),
OnlyOwnerError::NotOwner
);
Ok(())
}
#[derive(Accounts)]
pub struct OnlyOwner<'info> {
signer_account: Signer<'info>,
}
// An enum for custom error codes
#[error_code]
pub enum OnlyOwnerError {
#[msg("Only owner can call this function!")]
NotOwner,
}
在上述代码的上下文中,OWNER变量存储与我的本地 Solana 钱包关联的公钥(地址)。在测试之前,请务必将 OWNER 变量替换为您的钱包的公钥。您可以通过运行solana address命令轻松检索您的公钥。
该#[access_control]属性在运行主指令之前执行给定的访问控制方法。调用初始化函数时,访问控制方法 ( check) 先于初始化函数执行。该check方法接受引用的上下文作为参数,然后检查交易的签名者是否等于OWNER变量的值。require_keys_eq!宏确保两个公钥值相等,如果相等,则执行初始化函数,否则,将使用自定义错误进行恢复NotOwner。
测试 onlyOwner 功能 — 令人满意的情况
在下面的测试中,我们调用初始化函数并使用所有者的密钥对签署交易:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Day14 } from "../target/types/day14";
describe("day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
it("Is called by the owner", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
signerAccount: program.provider.publicKey,
})
.rpc();
console.log("Transaction hash:", tx);
});
});
我们调用了初始化函数,并将提供程序中的钱包账户(本地 Solana 钱包账户signerAccount)传递给具有结构体的Signer<'info>,以验证钱包账户是否确实签署了交易。 另请记住,Anchor 使用提供程序中的钱包账户秘密签署任何交易。
运行测试anchor test --skip-local-validator,如果一切正确完成,测试应该通过:
测试签名者是否不是所有者——攻击案例
使用非所有者的其他密钥对来调用初始化函数并签署交易将引发错误,因为函数调用仅限于所有者:
describe("day14", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Day14 as Program<Day14>;
let Keypair = anchor.web3.Keypair.generate();
it("Is NOT called by the owner", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
signerAccount: Keypair.publicKey,
})
.signers([Keypair])
.rpc();
console.log("Transaction hash:", tx);
});
});
这里我们生成了一个随机密钥对并用它来签署交易。让我们再次运行测试:
正如预期的那样,我们得到了一个错误,因为签名者的公钥不等于所有者的公钥。
修改所有者
要在程序中更改所有者,需要将分配给所有者的公钥存储在链上。不过,有关 Solana 中“存储”的讨论将在未来的教程中介绍。
所有者可以重新部署字节码。
练习:升级类似上述程序,以获得新的所有者。