【在Solana中使用固定PDA账号实现管理员权限验证和全局状态变量保存】

一、前言

我们知道,在Solana区块链中,智能合约(可执行程序)本身并不能直接保存数据,所有的相关数据保存在owner为该程序的账号中。这一点和我们常见的EVM区块链(例如以太坊)是不同的。因此,我们需要将全局变量和管理员权限也保存在一个账号里。

在Solana中,我们可以很方便的创建一个随机账号用来保存这些信息,但由于Solana中所有账号信息必须由用户在客户端输入,因此用户可以输入一个伪造的保存这些信息的账号,这在合约中是无法辨识的,除非我们升级合约并将保存账号硬编码写死在合约中,显然,这会增加我们合约的复杂度。我们得另外想个办法。

在Solana中还有另一个类账号,叫程序派生账号,也叫PDA(program derived account),它并不适用于ed25519 椭圆算法,同时也不是由私钥控制的,而是由程序控制的。给定一个种子,就可以计算出相应的PDA账号。因此,我们可以用固定种子的PDA账号来保存这些全局信息,因为固定种子的PDA账号是唯一的。将用户输入的账号和计算得到的PDA账号相比较就能判断用户输入的是否正确的账号。

二、什么是Anchor

Anchor is a framework for Solana’s Sealevel (opens new window)runtime providing several convenient developer tools.

Anchor是为了解决Solana上合约开发效率低下(需要手动检查账号,手动编码和解码数据)而开发的一个应用框架。它的功能很强大,也集成了很多功能。例如,如果使用原生的Solana程序开发方式 ,用户需要统一程序入口,再根据某个输入参数(一个枚举值)的判定,调用不同的处理函数。使用Anchor框架后,你可以直接调用相应的处理函数。这有点类似Fabric超级账本1.4.0和2.0的区别。

Anchor同时还提供了其它强大的功能(其实为各种宏,其本质应该为语法糖,因为Solana区块链并没有任何变化),可以大大的简化我们的开发工作。例如PDA账号的初始化,如果手动处理,则需要创建账号,调用系统程序来分配空间,然后再向其转入足够数量的sol以免除租金。但在Anchor中,直接使用一个宏进行了全部操作。

我们这里也正是使用Anchor来实现管理员权限和全局状态变量的保存。

三、详细流程

这里为了最真实的贴近实际,我们采用边写边操作的方式,尽量减小写出的文章和实际操作之间的差别。

3.1、准备工作

Solana开发必须准备相应的开发环境,这里不再细述了,我只将我本机使用的环境列出来:

  • Rust 1.59.0-nightly
  • Solana 1.9.1
  • Node.js v14.18.1
  • Yarn 1.22.5
  • Anchor 0.17.0

3.2、新建项目

运行anchor init global-state,等待运行完毕,运行code global-state,使用vscode打开该目录,我们使用vscode作为编辑器。

3.3、编辑lib.rs

本节参考anchor本身的测试用例misc,见: misc

找到programs/global-state/src目录,可以看到仅有一个lib.rs,这就是我们今天简单示例所使用的文件。

lib.rs替换为如下内容。

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

const CONSTRAINT_SEED:&[u8] = b"fixed-seed";

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

    pub fn init_state(
        ctx: Context<InitState>,
        _bump: u8,
        value: u16,
    ) -> ProgramResult {
    
    
        msg!("In init state");
        ctx.accounts.my_pda.data = value;
        ctx.accounts.my_pda.authority = ctx.accounts.authority.key();
        msg!("The initial authority is {} and the initial data is {}.", ctx.accounts.my_pda.authority, ctx.accounts.my_pda.data);
        Ok(())
    }

    pub fn change_state(
        ctx: Context<ChangeState>,
        value: u16,
    ) -> ProgramResult {
    
    
        msg!("In change state");
        let pda = &mut ctx.accounts.my_pda;
        let old_value = pda.data;
        pda.data = value;
        msg!("The authority is {}.", ctx.accounts.authority.key());
        msg!("State's data has changed from {} to {}.", old_value, value);
        Ok(())
    }

    pub fn read_state(
        ctx: Context<ReadState>,
    ) -> ProgramResult {
    
    
        msg!("In read state");
        let data = ctx.accounts.my_pda.data;
        let authority = ctx.accounts.my_pda.authority;
        msg!("The authority is {} and the data is {}.", authority, data);
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(bump: u8, value: u16)] //这里必须列出指令中所有参数,未使用的也要列出
pub struct InitState<'info> {
    
    
    #[account(
        init,
        seeds = [CONSTRAINT_SEED.as_ref()],
        bump = bump,
        payer = my_payer,
    )]
    pub my_pda: Account<'info, MyState>,
    pub authority: AccountInfo<'info>,
    pub my_payer: AccountInfo<'info>,
    pub system_program: AccountInfo<'info>,
}


#[derive(Accounts)]
pub struct ChangeState<'info> {
    
    
    #[account(mut, has_one = authority, seeds = [CONSTRAINT_SEED.as_ref()],bump)]
    pub my_pda: Account<'info, MyState>,
    pub authority: Signer<'info>,
}

#[derive(Accounts)]
pub struct ReadState<'info> {
    
    
    #[account(seeds = [CONSTRAINT_SEED.as_ref()],bump)]
    pub my_pda: Account<'info, MyState>,
}



#[account]
#[derive(Default)]
pub struct MyState {
    
    
    pub authority: Pubkey,
    pub data: u16,
}

编辑完成之后运行anchor build来编译程序。

3.4、运行单元测试

首先编辑tests/global-state.js,内容如下:

const assert = require("assert");
const anchor = require("@project-serum/anchor");
const PublicKey = anchor.web3.PublicKey;

describe('global-state', () => {
    
    

  // Use a local provider.
  const provider = anchor.Provider.local();
  // Configure the client to use the local cluster.
  anchor.setProvider(provider);
  const program = anchor.workspace.GlobalState;
  const myAccount = anchor.web3.Keypair.generate();
  const myAccount2 = anchor.web3.Keypair.generate();

  it("Creates and initializes an pda account", async () => {
    
    
    const [myPda, nonce] = await PublicKey.findProgramAddress(
      [
        Buffer.from(anchor.utils.bytes.utf8.encode("fixed-seed")),
      ],
      program.programId
    );

    await program.rpc.initState(nonce, new anchor.BN(12), {
    
    
      accounts: {
    
    
        myPda,
        authority: myAccount.publicKey,
        myPayer: program.provider.wallet.publicKey,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        systemProgram: anchor.web3.SystemProgram.programId,
      },
    });

    const myPdaAccount = await program.account.myState.fetch(myPda);
    assert.ok(myPdaAccount.data === 12);
    assert.ok(myPdaAccount.authority.toBase58() === myAccount.publicKey.toBase58());

    _myPdaAccount = myPda;
  });

  it("Init twice should be reverted", async () => {
    
    
    const [myPda2, nonce2] = await PublicKey.findProgramAddress(
      [
        Buffer.from(anchor.utils.bytes.utf8.encode("fixed-seed")),
      ],
      program.programId
    );
    await assert.rejects(
      program.rpc.initState( nonce2, new anchor.BN(13), {
    
    
        accounts: {
    
    
          myPda:myPda2,
          authority: myAccount2.publicKey,
          myPayer: provider.wallet.publicKey,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
          systemProgram: anchor.web3.SystemProgram.programId,
        },
      }),
      (err) => {
    
    
        console.log()
        console.log("reason:",(err.logs)[2])
        return true;
      }
    )
  });

  it("Authority can change pda's data", async () => {
    
    
    const myPda = _myPdaAccount;
    await program.rpc.changeState(16,{
    
    
      accounts: {
    
    
        myPda,
        authority: myAccount.publicKey,
      },
      signers: [myAccount],
    });
    const myPdaAccount = await program.account.myState.fetch(myPda);
    assert.ok(myPdaAccount.data === 16);
  });

  it("It should be reverted while unmatched authority change pda's data", async () => {
    
    
    const myPda = _myPdaAccount;
    await assert.rejects(
      program.rpc.changeState(26,{
    
    
        accounts: {
    
    
          myPda,
          authority: myAccount.publicKey,
        },
        signers: [provider.wallet.payer],
      }),
      (err) => {
    
    
        console.log()
        console.log(err)
        return true
      });
    const myPdaAccount = await program.account.myState.fetch(myPda);
    assert.ok(myPdaAccount.data === 16);
  });

  it("Read state will be always success", async () => {
    
    
    const myPda = _myPdaAccount;
    await program.rpc.readState({
    
    
      accounts: {
    
    
        myPda,
      },
    });
    const myPdaAccount = await program.account.myState.fetch(myPda);
    assert.ok(myPdaAccount.data === 16);
  });
});

然后运行测试脚本:anchor test

可以看到如下输出:

  5 passing (1s)

说明我们的测试通过了。

3.5、查询测试日志

在合约中,我们使用了msg!宏来输出相关日志,我们可以在.anchor/program-logs/program-id.global_state.log中查看相应的日志输出。

Streaming transaction logs mentioning Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS. Confirmed commitment
Transaction executed in slot 2:
  Signature: 2mwGoymncBjzuNB33ioExLeGpsfS6pVQT5qmXtP8eRw7EfJXu1vEp536QrUiaLyDDRJ1rvawrcaeCYFQtttrkTGK
  Status: Ok
  Log Messages:
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]
    Program 11111111111111111111111111111111 invoke [2]
    Program 11111111111111111111111111111111 success
    Program log: In init state
    Program log: The initial authority is CAihAsajGTa8hMrwDQn2TdW8ETj9AxsbpAVF5nj4HKWm and the initial data is 12.
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 23933 of 200000 compute units
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS success
Transaction executed in slot 3:
  Signature: 3yQ2RLTUUvTdvCwi7AnL88VKjFTrMikRe6iuS5V7iAgyim5GHDwHKDxzBJFfJE5wMfGjoztCUucPejCRY5o6JUv6
  Status: Ok
  Log Messages:
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]
    Program log: In change state
    Program log: The authority is CAihAsajGTa8hMrwDQn2TdW8ETj9AxsbpAVF5nj4HKWm.
    Program log: State's data has changed from 12 to 16.
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 18870 of 200000 compute units
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS success
Transaction executed in slot 4:
  Signature: 4QjEHQQ7WAJqLe6nPAfZYUvddZ8gvNyKMxZh7FrXVLfeSWmU6tYZprUgMdvUi4stxH4m69UQ99bFdgpDd1JfVZKN
  Status: Ok
  Log Messages:
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]
    Program log: In read state
    Program log: The authority is CAihAsajGTa8hMrwDQn2TdW8ETj9AxsbpAVF5nj4HKWm and the data is 16.
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 17163 of 200000 compute units
    Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS success

从日志中可以看出,我们的管理员权限初始化为了 CAihAsajGTa8hMrwDQn2TdW8ETj9AxsbpAVF5nj4HKWm,只有它才能修改相应的全局变量(MyState.data)的值,并且最后我读取该固定PDA账号中保存的管理员,正是CAihAsajGTa8hMrwDQn2TdW8ETj9AxsbpAVF5nj4HKWm

4、补充说明

这里我们已经实现了一个功能比较简单的管理员权限和全局状态变量,相比于Solidity,使用Solana进行相关实现是比较复杂的。这里我们使用msg!宏来输出相关日志(类似hardhat中的console.log),它一般只应用于开发环境中,正式版本中一般移除msg!而使用Event(事件)来发送通知,从而使客户端能更好的追踪。

另外,我们在change_stateread_state函数中并没有定义bump这个参数,因此也未提供给my_pda账号验证。这是因为bump并不参与PDA地址的计算,它和地址一样,根据特定的算法计算得到,它只是用来验证该地址是否和该bump适配。在这里我们也可以在函数参数中提供bump并参与my_pda的验证,如果我们输入了一个错误的bump,会运行出错。因此,这里bump仅用来验证输入是否正确,与地址计算无关,所以可以略过。

一般情况下bump为255,但不是绝对的。

猜你喜欢

转载自blog.csdn.net/weixin_39430411/article/details/122655081