解读Solana数据账户

在处理Solana时,我经常需要检查交易或账户的内容。 通常,数据读取是通过用户界面完成的,从而不需要手动干预, 然而,在某些情况下,使用UI不方便或者不可行。

解读Solana数据账户
一键发币: SUI | SOL | BNB | ETH | BASE | ARB | OP | POLYGON | AVAX | FTM | OK

在处理Solana时,我经常需要检查交易或账户的内容。 通常,数据读取是通过用户界面完成的,从而不需要手动干预。 然而,在某些情况下,使用UI不方便或者不可行,或者有人只是想手动验证某些数据的存在。 在这篇文章中,我将分享一种我使用的方法,并在此过程中探索一些有用的工具。

1、Solana交易和账户数据

首先简单介绍一下Solana交易和账户(不涉及细节,有很多其他资源可以参考)。 为了本文的目的,我们说Solana有一个账户,具有各种属性(如owner和拥有lamports),账户的内容表示为原始二进制数据,结构由所属程序定义。类似地,交易也有一个特定格式,规定了交易中涉及的账户,而调用数据则根据所属程序的要求进行编码。

2、使用Solana CLI获取数据

为了调查,让我们从区块链获取一些数据。

在这个文章中,我使用了一个名为simple-admin的示例合约程序(分支v1)。 这是一个简单的程序,允许创建一个管理员账户,并且一旦创建, 就可以请求打印包含在交易日志中的消息。 该程序部署在开发网地址

我们将使用solana account命令来获取使用simple-admin程序创建的账户的数据。 我们执行了指令CreateSimpleAccount,初始化了Solana账户在开发网地址 DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ。 默认情况下,solana account命令以十六进制格式显示数据。 但是,通过使用--output选项,我们可以以base64格式检索数据,这是我们想要使用的格式。

# DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ是一个simple-admin合约的管理账户
solana -ud account DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ --output json

使用 --output json

{
  "pubkey": "DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ",
  "account": {
    "lamports": 1224960,
    "data": [
      "pZvg+dv0TqnyG8J8NTj5iMEN0Cv0IG7X57+QlCAJzwcIXnvdhqAXFQMAAAAAAAAA",
      "base64"
    ],
    "owner": "sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS",
    "executable": false,
    "rentEpoch": 0
  }
}

同样,我们可以获取有关交易的数据。

对于这个例子,我选择了一个与simple-admin程序交互的交易之一。 它是一个包含指令PrintAdmin的交易,该指令接受一条消息并将其打印到交易日志中。 通过使用带有-v开关的solana confirm命令,我们可以获得交易的所有数据 (如果没有-v开关,则只显示交易状态确认)。 此外,当我们包括--output json选项时,数据将以base58格式提供。

# simple-admin合约调用PrintAdmin指令的交易
solana confirm -ud 55E5mPX87Ms55chvKdGUg2XCGrJ1Qp4Pw7ER4z4fCgSqtXQ3JhZGXP7mpohTxPEm8S87Q5PNW7x7MSqx9GDATMiF --output json -v

使用 --output json(缩短)

{
  "confirmationStatus": "finalized",
  "transaction": {
    ...
    "message": {
      "header": {
        "numRequiredSignatures": 2,
        "numReadonlySignedAccounts": 1,
        "numReadonlyUnsignedAccounts": 1
      },
      "accountKeys": [
        "CUuLjSEx7q3AB3sRGn3sMJBsSNTmULwowMGUh6NdsxQD"
        "HJ6DPqQhAYRw8YyEuVXV8mwzzNAexxEYVC3aQutWxWn8",
        "DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ",
        "sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS"
      ],
      "recentBlockhash": "GJ8fngsibguWjKTAYWeDnqKd4P7mYntDhdEBQpUjyHM6",
      "instructions": [
        {
          "programIdIndex": 3,
          "accounts": [
            2,
            1
          ],
          "data": "7oAbCvAjeJqUZ7RpVyY51x5Sa"
        }
      ]
    }
    ...
  },
}

3、将数据转换为Uint8数组

在处理数据时,我发现将其内容以uint8二进制数据数组的形式查看很有帮助。 在这种格式下,字节表示为无符号整数。如果你曾经打印过私钥文件的内容,比如使用以下命令生成的文件 solana-keygen new --silent --no-bip39-passphrase --outfile /tmp/random.keypair; cat /tmp/random.keypair,你可能会认出这种格式。

为了简化数据转换过程,我编写了一些方便的Python脚本。 这些脚本使你可以轻松地将数据从一个源转移到另一个源。 你可以在我的代码片段中找到这些脚本:

这些脚本旨在简化转换过程。 请随意探索并利用它们满足自己的数据转换需求。

#!~/.pyenv/shims/python3

# Usage:
# tobase58.py [3,0,0,0]
# Output:
# 5Sxr3

import base58
import sys
import json

if len(sys.argv) != 2:
  print(f"Expecting one argument of json array listing uint items. The argument was not provided. Arguments: {sys.argv}")
  exit(1)

json_string = sys.argv[1]
json_string = json_string.strip()

if not json_string.startswith("["):
  print(f"Expecting the provided argument is a json array and starts with '[', but it is: {json_string}")
  exit(2)

json_data = json.loads(json_string)
byte_array = bytearray(json_data)

print(base58.b58encode(byte_array).decode('utf-8'))

当处理账户数据时,你可以选择使用 --output-file /path/to/file 参数,而不是使用 solana account --output 参数。这样,你可以直接将账户数据以二进制形式保存到文件中。

要在 Bash 中以 uint8 数组的形式处理这些二进制数据,可以使用 od 命令。以下是如何使用它的示例:

# loading data from binary format as u8 array
decimal_array=($(od -v -An -t u1 < '/path/to/file'))
# printing data
echo "${decimal_array[@]}"

# show first 8 bytes
echo "${decimal_array[@]:0:8}"
# printing only last 32 bytes of the loaded data
echo "${decimal_array[@]:(-32):32}"

# convert solana account data to hex
solana account -um <address> -o /tmp/acc
cat /tmp/acc | od -An -v -tx1 | tr -d ' \n'

4、数据解释

simple-admin 程序是用 Anchor 编写的的,我们知道数据使用了 borsh 编码。Anchor 使用数据的前 8 字节作为判别符(即账户的 Rust 标识符的 sha256 哈希)。

我们有合约的源代码。SimpleAccount 的数据结构在代码中声明性地定义了。我们可以看到它包括一个 Pubkey 字段,我们知道其长度为 32 字节,以及一个长度为 8 字节的 u64 数字。

通过检查这段代码,我们可以了解数据结构,并理解具体的长度。

pub struct SimpleAccount {
    pub admin: Pubkey,
    pub print_call_count: u64,
}

使用上述的 CLI 命令,我们得到了 base64 格式的数据。我们可以使用脚本将其转换为 uint 数组。

solana -ud account DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ --output json

# ... taking account data base64 string ...
frombase64.py pZvg+dv0TqnyG8J8NTj5iMEN0Cv0IG7X57+QlCAJzwcIXnvdhqAXFQMAAAAAAAAA
> [165,155,224,249,219,244,78,169,242,27,194,124,53,56,249,136,193,13,208,43,244,32,110,215,231,191,144,148,32,9,207,7,8,94,123,221,134,160,23,21,3,0,0,0,0,0,0,0]
solana -ud account DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ --output json

我们观察到数组由 48 个字节组成。最初的 8 个字节表示 Anchor 描述符,接着是 32 个字节表示 Pubkey,剩余的 8 个字节表示一个数值。值得注意的是,这个数值被编码为无符号整数(uint),Solana 使用小端序编码。

# reading bytes at index 8 (9th byte) in length of 32 bytes
arraybyindex.sh [165,155,224,249,219,244,78,169,242,27,194,124,53,56,249,136,193,13,208,43,244,32,110,215,231,191,144,148,32,9,207,7,8,94,123,221,134,160,23,21,3,0,0,0,0,0,0,0] 8 32
# > [242,27,194,124,53,56,249,136,193,13,208,43,244,32,110,215,231,191,144,148,32,9,207,7,8,94,123,221,134,160,23,21]
# printing the 32 bytes in base58 format (Pubkey)
tobase58.py [242,27,194,124,53,56,249,136,193,13,208,43,244,32,110,215,231,191,144,148,32,9,207,7,8,94,123,221,134,160,23,21]
HJ6DPqQhAYRw8YyEuVXV8mwzzNAexxEYVC3aQutWxWn8

# reading bytes at index 40 (41st byte) in length of 8 bytes
arraybyindex.sh [165,155,224,249,219,244,78,169,242,27,194,124,53,56,249,136,193,13,208,43,244,32,110,215,231,191,144,148,32,9,207,7,8,94,123,221,134,160,23,21,3,0,0,0,0,0,0,0] 40 8
# > [3,0,0,0,0,0,0,0]
# printing the 8 bytes as integer
toout.py [3,0,0,0,0,0,0,0] int
# > 3

到目前为止,我们忽略了前 8 个字节,它们作为 Anchor 判别符。这个值是账户名称的哈希值,可以视为内部 Anchor 详情。但如果我们希望验证我们操作的是正确的账户,Anchor 使用判别符来确认加载数据的完整性,当有源代码时,我们可以使用 anchor expand CLI 命令获取扩展的源代码。

git clone https://github.com/ochaloup/simple-admin.git -b v1
cd simple-admin
anchor expand
# > Expanded simple-admin into file .anchor/expanded-macros/simple-admin/simple-admin-0.1.0.rs
cat .anchor/expanded-macros/simple-admin/simple-admin-0.1.0.rs | grep -i Discriminator
# > impl anchor_lang::Discriminator for SimpleAccount {
# >     const DISCRIMINATOR: [u8; 8] = [165, 155, 224, 249, 219, 244, 78, 169];

4.1 交易数据结构

对于 Solana 确认命令的交易输出,我们可以通过日志消息读取到许多上下文信息,通常会显示指令名称,并告知发生了什么。但让我们专注于基本信息和调用数据。我们可以看到使用了一条指令,还可以进一步查看相关账户。

在检查 Solana 确认命令的交易输出时,我们可以从日志消息中提取有价值的信息。日志消息通常提供有关指令名称的详细信息。然而,为了我们的目的,我们专注于调用数据。我们可以观察到使用了一条指令,并可以进一步查看相关账户。

注意事项:为了了解交易格式,推荐阅读深入文章《深入理解 Solana 交易》
solana confirm -ud 55E5mPX87Ms55chvKdGUg2XCGrJ1Qp4Pw7ER4z4fCgSqtXQ3JhZGXP7mpohTxPEm8S87Q5PNW7x7MSqx9GDATMiF --output json -v
# > ...
# > "accountKeys": [
# >   "CUuLjSEx7q3AB3sRGn3sMJBsSNTmULwowMGUh6NdsxQD"
# >   "HJ6DPqQhAYRw8YyEuVXV8mwzzNAexxEYVC3aQutWxWn8",
# >   "DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ",
# >   "sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS"
# > ],
# > "instructions": [
# >   {
# >     "programIdIndex": 3,
# >     "accounts": [
# >       2,
# >       1
# >     ],
# >     "data": "7oAbCvAjeJqUZ7RpVyY51x5Sa"
# >   }
# > ]
# >

我们以与上述账户相同的方式将数据转换为 uint8 数组格式。

frombase58.py 7oAbCvAjeJqUZ7RpVyY51x5Sa
# > [163,217,65,81,53,230,29,28,6,0,0,0,104,101,108,108,111,51]

我们知道 Anchor 程序使用前 8 个字节作为判别符,这里表示为 [163, 217, 65, 81, 53, 230, 29, 28]。与 Ethereum 不同的是,Solana 采用单入口点方法处理程序。在这种模式下,程序本身负责确定执行哪个操作或函数。判别符在此过程中起到关键作用。这 8 个字节作为一个决定因素,允许部署的程序选择合适的的函数进行执行。Anchor 判别符是从操作/函数名称的源代码中计算出的哈希值。

由于我们有程序的源代码,我们可以使用 anchor expand 命令来找出将要执行的操作。

cd simple-admin
vim .anchor/expanded-macros/simple-admin/simple-admin-*.rs

impl anchor_lang::Discriminator for PrintAdmin {
    const DISCRIMINATOR: [u8; 8] = [163, 217, 65, 81, 53, 230, 29, 28];
}

现在,让我们检查涉及的账户。从 Solana 确认调用的输出来看,索引 3 的账户被用作程序地址,而索引 2 和 1 的账户作为指令输入账户。通过检查 accountKeys,我们知道索引从 0 开始。值得注意的是,索引 0 的公钥负责支付交易费用,即它是 feePayer。通过检查这些值,我们可以确定 sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS 对应于程序 ID,提供的两个账户为 DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJHJ6DPqQhAYRw8YyEuVXV8mwzzNAexxEYVC3aQutWxWn8

有了代码在手,我们可以检查 PrintAdmin 指令的源代码。

pub struct PrintAdmin<'info> {
    pub simple_admin_account: Account<'info, SimpleAccount>,
    pub admin: Signer<'info>,
}

pub fn process(&mut self, PrintAdminParams { message }: PrintAdminParams) -> Result<()> {
  // ...
}

我们可以看到指令 中传递的第一个账户地址 DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ 用作存储数据账户(pub simple_admin_account: Account<'info, SimpleAccount>)。第二个账户地址 HJ6DPqQhAYRw8YyEuVXV8mwzzNAexxEYVC3aQutWxWn8 是管理员钱包地址,我们意识到这个密钥需要签名交易。只有当交易由这个密钥签名后,处理才会被允许 (pub admin: Signer<'info>)。

现在来看调用数据。我们知道前 8 个字节是操作的判别符。其余部分是 PrintAdminParams#message。消息是一个字符串。由于是 Solana Rust,我们知道它将是 UTF-8 字符串。借助 Python 脚本,我们可以将消息转换为可读形式。

solana confirm -ud 55E5mPX87Ms55chvKdGUg2XCGrJ1Qp4Pw7ER4z4fCgSqtXQ3JhZGXP7mpohTxPEm8S87Q5PNW7x7MSqx9GDATMiF --output json -v

# ... call data ...
frombase58.py 7oAbCvAjeJqUZ7RpVyY51x5Sa
# > [163,217,65,81,53,230,29,28,6,0,0,0,104,101,108,108,111,51]

# getting all data from index 8 to the end of the list
arraybyindex.sh [163,217,65,81,53,230,29,28,6,0,0,0,104,101,108,108,111,51] 8
# > [6,0,0,0,104,101,108,108,111,51]

toout.py [6,0,0,0,104,101,108,108,111,51] string
# > hello3

我们可以看到交易日志应包含消息 hello3(即 Solana Rust 宏 msg!(...)。让我们在交易列表中确认。

# get info about the examined transaction
solana confirm -ud 55E5mPX87Ms55chvKdGUg2XCGrJ1Qp4Pw7ER4z4fCgSqtXQ3JhZGXP7mpohTxPEm8S87Q5PNW7x7MSqx9GDATMiF --output json -v

# > ...
# > "logMessages": [
# >   "Program sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS invoke [1]",
# >   "Program log: Instruction: PrintAdmin",
# >   "Program log: hello3",
# >   "Program data: 4nBLdtV6DJ/yG8J8NTj5iMEN0Cv0IG7X57+QlCAJzwcIXnvdhqAXFQYAAABoZWxsbzM=",
# >   "Program sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS consumed 4266 of 200000 compute units",
# >   "Program sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS success"
# > ],
# > ...

我们之前提到过,我们能够访问源代码。但是如何知道代码存放的位置呢?这就是为什么在程序中包含security-txt元数据被认为是良好实践的原因。通过发布它,你可以增强程序的安全性和透明度。一旦元数据发布,你可以方便地使用Solana Explorer来定位你的程序。这使得访问和审查相关信息变得更加容易。

5、数据解释:查看非Anchor程序账户

由于Solana账户内的数据格式没有严格的规范,Borsh编码并不是唯一的选择。另一种在Solana账户中编码二进制数据的常用策略是使用bincode。Bincode在Solana程序库中经常被使用。一个很好的例子就是SPL Token程序。

为了进一步说明,我们将进行一个快速检查。我们将创建一个新的铸造并铸造一个代币到我们将要检查的ATA钱包地址。

# creating a new mint of the token (-ud signifies we work on devnet)
spl-token -ud create-token --decimals 0
# > Creating token FqQXsU826gjPFXkgYXpVyuaDkgVbmvULz2MktNm1p7n6 under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
# > Address:  FqQXsU826gjPFXkgYXpVyuaDkgVbmvULz2MktNm1p7n6
# > Decimals:  0

# creating token ATA account of the Solana localhost wallet (by default ~/.config/solana/id.json)
spl-token -ud create-account FqQXsU826gjPFXkgYXpVyuaDkgVbmvULz2MktNm1p7n6

# mint 100 tokens to wallet ATA address
spl-token -ud mint FqQXsU826gjPFXkgYXpVyuaDkgVbmvULz2MktNm1p7n6 100
# > Minting 100 tokens
# >   Token: FqQXsU826gjPFXkgYXpVyuaDkgVbmvULz2MktNm1p7n6
# >   Recipient: JCX5iiNKRhkSVsqjspSgJxT5KmJ7Pqfoqr2Gt5snz8sP

solana account -ud JCX5iiNKRhkSVsqjspSgJxT5KmJ7Pqfoqr2Gt5snz8sP --output json
# > Output below:
{
  "pubkey": "JCX5iiNKRhkSVsqjspSgJxT5KmJ7Pqfoqr2Gt5snz8sP",
  "account": {
    "lamports": 2039280,
    "data": [
      "3GoaknTR+oDWqFG297b0/v2Vu8SDp7+L82vTdUdUB6eqlmtWff4bdZUd8oayhnUR5sMO/i+gRTg93gti4R0UbmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
      "base64"
    ],
    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    "executable": false,
    "rentEpoch": 0
  }
}

现在我们可以将数据转换为uint数组的格式,并检查数据长度(应该是165)是否匹配。

ARR=`frombase64.py '3GoaknTR+oDWqFG297b0/v2Vu8SDp7+L82vTdUdUB6eqlmtWff4bdZUd8oayhnUR5sMO/i+gRTg93gti4R0UbmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'`
echo $ARR
# > [220,106,26,146,116,209,250,128,214,168,81,182,247,182,244,254,253,149,187,196,131,167,191,139,243,107,211,117,71,84,7,167,170,150,107,86,125,254,27,117,149,29,242,134,178,134,117,17,230,195,14,254,47,160,69,56,61,222,11,98,225,29,20,110,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

echo "$ARR" | sed 's/[^,]//g' | wc -c
# > 165

Token程序在检查账户类型时并不使用任何判别符,我们可以看到数据的第一部分属于代币铸造的公钥和代币的所有者。

arraybyindex.sh $ARR 0 32
# > [220,106,26,146,116,209,250,128,214,168,81,182,247,182,244,254,253,149,187,196,131,167,191,139,243,107,211,117,71,84,7,167]
tobase58.py '[220,106,26,146,116,209,250,128,214,168,81,182,247,182,244,254,253,149,187,196,131,167,191,139,243,107,211,117,71,84,7,167]'
# > FqQXsU826gjPFXkgYXpVyuaDkgVbmvULz2MktNm1p7n6
arraybyindex.sh $ARR 32 32
# > [170,150,107,86,125,254,27,117,149,29,242,134,178,134,117,17,230,195,14,254,47,160,69,56,61,222,11,98,225,29,20,110]
tobase58.py '[170,150,107,86,125,254,27,117,149,29,242,134,178,134,117,17,230,195,14,254,47,160,69,56,61,222,11,98,225,29,20,110]'
# > CUuLjSEx7q3AB3sRGn3sMJBsSNTmULwowMGUh6NdsxQ

注意:原生Solana程序经常使用serde中的bincode库来编码数据。虽然borsh和bincode的base64数据格式不完全兼容,但它们可以互相转换。最近我在Anchor项目中发起了一次讨论,维护者acheroncrypto管理了从bincode到borsh base64格式的编码转换。

5、更改账户以进行测试

更改账户数据的目的之一可能是为测试加载账户。例如,你可能想要在本地主机启动Solana测试验证器,并处理在主网上无法执行的测试动作。假设我们想要铸造一个MNDE代币,这个代币已经被以最大数量铸造,并且不再有铸造权限。我们可以从主网上加载该账户,更改数据解释,然后在测试验证器启动时传递修改后的账户。

# loading account data from mainnet
solana account -um MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey --output json --output-file ./mnde-mint.json
{
  "pubkey": "MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey",
  "account": {
    "lamports": 1991461600,
    "data": [
      "AAAAAAU7Cq81B+5iPAIrQjwKE7gsOycDnU85ZkE/v2q2Xh0rrPJWkUi24A0JAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
      "base64"
    ],
    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    "executable": false,
    "rentEpoch": 361,
    "space": 82
  }
}
Wrote account to ./mnde-mint.json

注意:我们也可以反向操作。我们可以生成一个带有所需参数的新铸造账户,然后仅根据需要更改公钥。这是一个展示如何更改授权字段的例子,这个方法可以用于任意程序。有些账户在模拟当前状态时比简单地铸造一个代币要复杂得多。
我们对这个账户的数据感兴趣。数据的解释可以从源代码或Sec3文章《理解SPL代币铸造》中读取。

frombase64.py 'AAAAAAU7Cq81B+5iPAIrQjwKE7gsOycDnU85ZkE/v2q2Xh0rrPJWkUi24A0JAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='
[0,0,0,0,5,59,10,175,53,7,238,98,60,2,43,66,60,10,19,184,44,59,39,3,157,79,57,102,65,63,191,106,182,94,29,43,172,242,86,145,72,182,224,13,9,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

我们可以看到账户的前几个字节属于铸造权限 mint_authority: COption<Pubkey>,这是我们想要更改的部分。COption 编码为4个字节,其中 [1,0,0,0] 表示已设置,然后是32个字节的公钥。让我们把所有这些信息汇总起来,并加载 solana-test-validator

solana-keygen new --no-bip39-passphrase -o ./mnde-mint-authority.keypair
solana-keygen pubkey ./mnde-mint-authority.keypair
> 9q3UhZFX5jAFZfn16Z3bsy8PedHwJVGHhd33CatVkcsN

frombase58.py 9q3UhZFX5jAFZfn16Z3bsy8PedHwJVGHhd33CatVkcsN
> [131,44,44,247,56,178,214,183,144,210,96,63,240,40,135,179,251,160,128,101,14,63,1,5,151,61,4,190,76,95,32,109]

# we know we want 4 bytes + 32 bytes to get off
arraybyindex.sh [0,0,0,0,5,59,10,175,53,7,238,98,60,2,43,66,60,10,19,184,44,59,39,3,157,79,57,102,65,63,191,106,182,94,29,43,172,242,86,145,72,182,224,13,9,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 36
> [172,242,86,145,72,182,224,13,9,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

# merging all together [1,0,0,0] + [mint pubkey] + [rest]
tobase64.py tobase64.py [1,0,0,0,131,44,44,247,56,178,214,183,144,210,96,63,240,40,135,179,251,160,128,101,14,63,1,5,151,61,4,190,76,95,32,109,172,242,86,145,72,182,224,13,9,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
> AQAAAIMsLPc4sta3kNJgP/Aoh7P7oIBlDj8BBZc9BL5MXyBtrPJWkUi24A0JAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==

# change ./mnde-mint.json "data" field
vim ./mnde-mint.json
> ...

solana-test-validator --reset --ledger /tmp/test-ledger --account MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey ./mnde-mint.json

# test to mint (in different console)
spl-token -ul create-account  MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey
> Creating account HDBG6v77dUiNS9yjPKRaRJhDRLzhpL54aBK5PZFUAhXj
spl-token -ul mint --mint-authority ./mnde-mint-authority.keypair  MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey 1000 HDBG6v77dUiNS9yjPKRaRJhDRLzhpL54aBK5PZFUAhXj
Minting 1000 tokens

6、RPC调用 getProgramAccounts

在我们的测试程序 "SimpleAdmin" 的情况下,让我们查找所有执行了三次的管理员账户(SimpleAccount)。为了实现这一点,我们需要搜索在devnet上部署的 "simple-admin" 程序拥有的Solana账户,该程序的地址为sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS,其中的print_call_count字段值等于3。

为了实现这一点,我们将调用devnet RPC服务器(假设我们已经在该服务器上创建了一个账户实例)。在这个例子中,我们只需要确定符合标准(print_call_count等于3)的账户数量。我们不需要下载整个程序数据。因此,我们将dataSize参数设置为0。

根据提供的筛选条件,我们知道每个"SimpleAccount"的数据由8个字节的Anchor判别符、32个字节的公钥以及8个字节的我们要匹配的计数器组成。我们使用的偏移值为40(计算为8+32)。为了指定要匹配的数据,我们传递字节数组[3,0,0,0,0,0,0,0](u64)的base58格式,该格式表示为W723RTUpoZ。

tobase58.py '[3,0,0,0,0,0,0,0]'
# > W723RTUpoZ

curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS",
      {
        "encoding": "base64",
        "dataSlice": {
          "offset": 0,
          "length": 0
        },
        "filters": [
          {
            "memcmp": {
              "offset": 40,
              "bytes": "W723RTUpoZ",
              "encoding": "base58"
            }
          }
        ]
      }
    ]
  }
'
# > {"jsonrpc":"2.0","result":[{"account":{"data":["","base64"],"executable":false,"lamports":1224960,"owner":"sa3HiPEaDZk5JyU1CCmmRbcWnBc9U4TzHq42RWVUNQS","rentEpoch":0},"pubkey":"DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ"}],"id":1}

7、工具

7.1 Borsh账户解码

我们讨论了手动检查Solana账户和交易调用数据的二进制数据结构的一种潜在方法。虽然这种方式通常可以检查账户,但手动操作既繁琐又低效。Solana周围的一些工具使这个过程变得更加简单。

当我们讨论borsh格式的账户(大多数是使用Anchor编写的)时,有一个网站https://borsh.m2.xyz 可以提供帮助。当我们了解账户的结构时,它可以帮助我们解码账户。就像我们上面使用Python或Bash脚本手动操作一样,我们可以通过点击这个网站来完成。这里有一个关于检查账户DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ的示例。我们可以填写字段类型和大小,偏移量会自动为我们计算。

7.2 交易的UI

一个用于构建交易的用户友好工具可以在 https://bettercallsol.dev/ 找到。该工具允许通过图形界面与各种参数进行交互,轻松创建交易。定义好交易后,你可以选择运行它、模拟它或将它传递给集成系统进行进一步处理。更多详细信息,请参阅题为《使用Better Call Sol进行Solana交易》的博客文章。

在本文的上下文中,我尝试为“Simple Admin”合约定义一个交易,特别是“PrintAdmin”操作。你可以在附带的截图中查看表单及其参数。

7.3 Anchor IDL

最灵活的方法是有一个可以由工具解析的IDL定义,为我们提供对底层二进制结构的动态理解。这使我们能够了解账户名称,并在区块链浏览器中解析交易和账户。上述UI工具也利用IDL来提供关于账户名称的信息。

使用Anchor框架开发程序时,IDL会自动生成。然而,对于非Anchor程序,可以使用Metaplex基金会提供的Solita框架。关于Solana中IDL的标准化方法,目前在Solana论坛上有讨论。

为了便于Anchor IDL数据的解析,程序需要在链上发布IDL。一旦发布,你可以在 https://explorer.solana.com/ 的Anchor Program IDL框中看到它。

对于我们的“Simple Admin”合约,可以在Explorer中查看IDL,并且可以在GitHub上与合约并列发布的TypeScript格式。

Explorer使以人类可读的格式访问交易细节变得容易。例如,你可以查看具有ID 55E5mPX87Ms55chvKdGUg2XCGrJ1Qp4Pw7ER4z4fCgSqtXQ3JhZGXP7mpohTxPEm8S87Q5PNW7x7MSqx9GDATMiF的交易

或者查看简单账户 DabBrrPf3JcNgKqcYNhn3tVcfPwDpsQ6HdAifNWi1ebJ

其他Solana区块链检查器也以类似的方式解析IDL数据。例如,你可以在Solana.fm上看到类似的功能。

8、结束语

我们讨论了一种手动检查Solana二进制格式数据结构的方法。为此,我们使用了Python和Bash脚本。我们还讨论了可以帮助我们的工具。


原文链接:Decoding Solana data accounts

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。
通过 NowPayments 打赏