Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者

本文详细比较了Solidity和Solana智能合约中的msg.sendertx.origin概念,并提供了在Solana中如何实现类似功能的代码示例。文章还介绍了如何在Solana中处理多个签名者以及如何实现onlyOwner功能。

tx.origin msg.sender onlyOwner在Solana中

在Solidity中,msg.sender 是一个全局变量,表示调用或发起智能合约函数调用的地址。全局变量 tx.origin 是签署交易的钱包。

在Solana中,没有与 msg.sender 相当的概念。

tx.origin 有类似的概念,但你需要注意的是,Solana交易可以有多个签名者,因此我们可以将其视为具有“多个 tx.origins”。

要在Solana中获取 “tx.origin” 地址,你需要通过将签名者账户添加到函数上下文中并在调用函数时传递调用者的账户来进行设置。

让我们看看如何在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;

        // 函数逻辑....

        msg!("签名者1: {:?}", *the_signer1.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer1: Signer<'info>,
}

在上面的代码片段中,Signer<'info> 用于验证 Initialize<'info> 账户结构中的 signer1 账户已签署该交易。

initialize 函数中,signer1 账户从上下文中可变引用并被赋值给 the_signer1 变量。

然后最后,我们使用 msg! 宏记录 signer1 的公钥(地址),并传入 *the_signer1.key,这会解引用并访问通过 the_signer1 指向的实际值的 key 字段或方法。

接下来是为上述程序编写测试:

describe("Day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  it("由单个签名者签名", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.initialize().accounts({
      signer1: program.provider.publicKey
    }).rpc();

    console.log("签名者1: ", program.provider.publicKey.toBase58());
  });
});

在测试中,我们将我们的钱包账户作为签名者传递给 signer1 账户,然后调用初始化函数。之后,我们将钱包账户记录到控制台,以验证其与程序中的一致性。

练习: 你注意到了在运行测试后 shell_1(命令终端)和 shell_3(日志终端)中的输出有什么吗?

多个签名者

在Solana中,我们也可以有多个签名者签署一个交易,你可以把它看作是将一堆签名捆绑在一起并发送到一个交易中。一个用例是在一个交易中进行多签交易。

要做到这一点,我们只需在程序的账户结构中添加更多的 Signer 结构,然后确保在调用函数时传递必要的账户:

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!("签名者1: {:?}", *the_signer1.key);
        msg!("签名者2: {:?}", *the_signer2.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    pub signer1: Signer<'info>,
    pub signer2: Signer<'info>,
}

上面的示例与单个签名者示例基本相同,有一个显著的区别。在这种情况下,我们向 Initialize 结构添加了另一个签名者账户(signer2),并在 initialize 函数中记录了两个签名者的公钥。

调用 initialize 函数与多个签名者是不同的,下面的测试显示如何调用多个签名者的函数:

describe("Day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  // 生成一个签名者来调用我们的函数
  let myKeypair = anchor.web3.Keypair.generate();

  it("由多个签名者签名", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signer1: program.provider.publicKey,
        signer2: myKeypair.publicKey,
      })
      .signers([myKeypair])
      .rpc();

    console.log("签名者1: ", program.provider.publicKey.toBase58());
    console.log("签名者2: ", myKeypair.publicKey.toBase58());
  });
});

那么上面的测试有什么不同呢?首先是 signers() 方法,它接收一个签名者数组作为参数来签署交易。但我们在数组中只有一个签名者,而不是两个。Anchor 会自动将提供者中的钱包账户作为签名者,因此我们不需要将其再次添加到签名者数组中。

生成随机地址进行测试

第二个变化是 myKeypair 变量,它存储了通过 anchor.web3 模块随机生成的 Keypair(一个公钥及其对应的私钥以访问账户)。在测试中,我们将 Keypair(存储在 myKeypair 变量中的)公钥分配给 signer2 账户,因此它作为参数传递给 .signers([myKeypair]) 方法。

多次运行测试,你会注意到 signer1 的公钥没有变化,但 signer2 的公钥发生了变化。这是因为分配给 signer1 账户的钱包账户(在测试中)来自提供者,它也是你本地计算机上的Solana钱包账户,而分配给 signer2 的账户则是每次运行 anchor test —skip-local-validator 都是随机生成的。

练习: 创建另一个函数(你可以随意命名)要求三个签名者(提供者钱包账户和两个随机生成的账户),并为其编写测试。

onlyOwner

这是在Solidity中常用的模式,限制函数的访问仅限于合约的所有者。使用 Anchor 的 #[access_control] 属性,我们也可以实现 only owner 模式,即限制我们Solana程序中函数的访问到一个 PubKey(所有者的地址)。

以下是如何在Solana中实现“onlyOwner”功能的示例:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

// 注意:替换为你的钱包的公钥
const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";

#[program]
pub mod day14 {
    use super::*;

    #[access_control(check(&ctx))]
    pub fn initialize(ctx: Context<OnlyOwner>) -> Result<()> {
        // 函数逻辑...

        msg!("嗨,我是所有者。");
        Ok(())
    }
}

fn check(ctx: &Context<OnlyOwner>) -> Result<()> {
    // 检查签名者是否为所有者
    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>,
}

// 自定义错误代码的枚举
#[error_code]
pub enum OnlyOwnerError {
    #[msg("只有所有者可以调用此函数!")]
    NotOwner,
}

在上面的代码中,OWNER 变量存储与我本地Solana钱包关联的公钥(地址)。在测试之前,请确保替换 OWNER 变量为你的钱包的公钥。你可以通过运行 solana address 命令轻松检索到你的公钥。

#[access_control] 属性在运行主要指令之前执行给定的访问控制方法。当调用 initialize 函数时,访问控制方法 (check) 会在 initialize 函数之前执行。check 方法接受引用上下文作为参数,然后检查交易的签名者是否等于 OWNER 变量的值。require_keys_eq! 宏确保两个公钥值相等,如果为真,则执行 initialize 函数,否则它会以 NotOwner 自定义错误进行回滚。

测试 onlyOwner 功能——正常情况

在下面的测试中,我们调用 initialize 函数并使用所有者的密钥对交易进行签名:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Day14 } from "../target/types/day14";

describe("day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  it("由所有者调用", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: program.provider.publicKey,
      })
      .rpc();

    console.log("交易哈希:", tx);
  });
});

我们调用 initialize 函数,并将钱包账户(本地Solana钱包账户)通过提供者传递给 signerAccount,以验证该钱包账户确实签署了交易。还要记住,Anchor 会自动使用提供者中的钱包账户秘密签署任何交易。

运行测试 anchor test --skip-local-validator,如果一切正确,测试应该通过:Anchor测试通过

测试签名者不是所有者——攻击情况

使用不是所有者的不同密钥对 initialize 函数进行调用和签署交易将抛出错误,因为该函数调用仅限于所有者:

describe("day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  let Keypair = anchor.web3.Keypair.generate();

  it("NOT由所有者调用", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: Keypair.publicKey,
      })
      .signers([Keypair])
      .rpc();

    console.log("交易哈希:", tx);
  });
});

这里我们生成了一个随机的密钥对,并用其来签署交易。让我们再次运行测试:anchor测试因签名者错误而失败 正如预期的那样,我们得到了一个错误,因为签名者的公钥不等于所有者的公钥。

修改所有者

要在程序中更改所有者,必须将赋值给所有者的公钥存储在链上。但是,关于Solana中“存储”的讨论将在未来的教程中涵盖。

所有者可以重新部署字节码。

练习:升级上述程序以拥有新所有者。

深入学习

本教程是我们Solana课程的第14章。

最初于2024年2月21日发布

  • 原文链接: rareskills.io/post/msg-s...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/