初接触 Solana 的开发者经常对 “owner”(所有者)和 “authority”(权限控制者)之间的区别感到困惑。本文试图尽可能简明扼要地消除这一困惑。
Owner 与 Authority
只有程序(program)才能向账户(account)写入数据——具体来说,只能向它们拥有(own)的账户写入数据。程序不能向任意账户写入数据。
当然,程序不能自发地向账户写入数据。它们需要接收来自钱包的指令才能执行此操作。然而,程序通常只会接受来自拥有特权的钱包对特定账户的写入指令:即 authority。
账户的 owner 是一个程序。authority 是一个钱包。authority 向程序发送一笔交易,随后该程序就可以向该账户写入数据。
Solana 中的所有账户都有以下字段,这些字段大多是不言自明的:
- Public Key
- lamport balance
- owner
- executable(一个布尔型标志)
- rent_epoch(对于免租金账户可以忽略)
- data
我们可以通过在终端中运行 solana account <our wallet address> 来查看这些字段(需在后台运行 Solana 验证节点):

注意一个有趣的现象:我们并不是自己钱包的 owner! 地址 111...111 是系统程序(system program)。
为什么是由系统程序拥有钱包,而不是钱包自己拥有自己?
只有账户的 owner 才能修改其中的数据。
这意味着我们无法直接修改自己的余额。只有系统程序才能做到这一点。要将 SOL 从我们的账户中转出,我们需要向系统程序发送一笔带有签名的交易。系统程序会验证我们是否拥有该账户的私钥,然后代为修改余额。
这是你在 Solana 中会经常看到的一种模式:只有账户的 owner 才能修改账户中的数据。如果程序看到来自预先指定的地址(即 authority)的有效签名,它就会修改账户中的数据。
authority 是一个地址,如果程序看到来自该地址的有效签名,就会接受其指令。authority 不能直接修改账户。它需要通过拥有该目标账户的程序来间接完成修改。

然而,owner 始终是一个程序,如果交易的签名有效,该程序就会代表其他人修改该账户。
例如,我们在使用不同签名者修改账户的教程中看到过这种情况。
练习:创建一个初始化存储账户的程序。你需要随时准备好该程序和存储账户的地址。考虑在测试中添加以下代码:
console.log(`program: ${program.programId.toBase58()}`);
console.log(`storage account: ${myStorage.toBase58()}`);
然后对初始化好的账户运行 solana account <storage account>。你应该会看到 owner 是该程序。
以下是运行该练习的截图:

当我们查看存储账户的元数据时,可以看到该程序是 owner。
因为程序拥有该存储账户,所以它能够向其中写入数据。用户不能直接向存储账户写入数据,他们需要签署一笔交易,请求程序代为写入数据。
Solana 中的 owner 与 Solidity 中的 owner 截然不同
在 Solidity 中,我们通常将 owner 称为对智能合约拥有管理员权限的特殊地址。“owner” 并不是以太坊运行时(runtime)级别的概念,它是应用于 Solidity 合约的一种设计模式。而 Solana 中的 owner 则要底层得多。在以太坊中,智能合约只能写入自己的存储槽(storage slots)。想象一下,如果我们有一种机制能够允许某个以太坊智能合约写入其他存储槽。用 Solana 的术语来说,它将成为这些存储槽的 owner。
Authority 可以指部署合约的人,也可以指能够为特定账户发送写入交易的人
authority 可以是程序级别的一个构造。在我们的 Anchor 签名者 教程中,我们编写了一个程序,Alice 可以从自己的账户中扣除积分并转移给其他人。为了确保只有 Alice 才能为该账户发送扣除交易,我们在该账户中存储了她的地址:
#[account]
pub struct Player {
points: u32,
authority: Pubkey
}
Solana 使用类似的机制来记录谁部署了程序。在我们的 Anchor 部署教程中,我们提到部署程序的钱包也能够升级该程序。
“升级” 程序等同于向其写入新数据——即新的字节码(bytecode)。只有程序的 owner 才能对其进行写入操作(正如我们很快会看到的,这个程序是 BPFLoaderUpgradeable)。
因此,Solana 是如何知道要将升级权限授予部署特定程序的钱包的呢?
通过命令行查看程序的 authority
在部署程序之前,让我们通过在终端中运行 solana address 来查看 Anchor 使用的是哪个钱包:

请注意我们的地址是 5jmi...rrTj。现在让我们创建一个程序。
确保 solana-test-validator 和 solana logs 正在后台运行,然后部署该 Solana 程序:
anchor init owner_authority
cd owner_authority
anchor build
anchor test --skip-local-validator
查看日志时,我们可以看到刚刚部署的程序的地址:

记住,在 Solana 上一切皆为账户,包括程序。现在让我们使用命令 solana account 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg 来检查这个账户。我们会得到以下结果:

注意,这里并没有 authority 字段,因为“authority”并不是 Solana 账户包含的字段。如果你向上滚动到本文的开头,你会看到控制台中的键与我们在文章顶部列出的字段相匹配。
在这里,“owner”是 BPFLoaderUpgradeable111...111,它是所有 Solana 程序的 owner。
现在让我们运行 solana program show 6Ye7CgrwJxH3b4EeWKh54NM8e6ZekPcqREgkrn7Yy3Tg,其中 6Ye7...y3TG 是我们程序的地址:

在上方绿色方框中,我们看到了自己的钱包地址——这是用于部署程序的钱包,也就是我们之前用 solana address 打印出来的地址:

但这引出了一个重要的问题……
Solana 是在哪里存储该程序的“authority”(目前即我们的钱包)的?
它不是账户中的一个字段,因此必定存在于某个 Solana 账户的 data 字段中。“authority” 存储在该程序字节码存放的 ProgramData Address 中:

钱包(即 authority)的十六进制编码
在继续往下看之前,将 ProgramData Address 的 base58 编码转换为十六进制表示会很有帮助。实现此功能的代码在文章末尾提供,但现在我们请读者先接受我们的 Solana 钱包地址 5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj 的十六进制表示为:
4663b48dfe92ac464658e512f74a8ee0ffa99fffe89fb90e8d0101a0c3c7767a
查看存储可执行文件的 ProgramData Address 账户中的数据
我们可以使用 solana account 来查看 ProgramData Address 账户,但我们会将其输出到一个临时文件中,以避免向终端转储过多的数据。
solana account FkYygT7X7qjifdxfBVWXTHpj87THJGmtmKUyU4SamfQm > tempfile
head -n 10 tempfile
上述命令的输出显示,我们的钱包(以十六进制形式)被嵌入在了 data 中。请注意观察,带有黄色下划线的十六进制代码与我们钱包(即 authority)的十六进制编码完全匹配:

程序的字节码存储在一个独立的账户中,而不是在程序自身的地址中
虽然从上面的一系列命令中应该能够暗示出这一点,但仍值得明确说明。尽管程序是一个被标记为 executable(可执行)的账户,但其字节码并不存储在它自身的 data 字段中,而是存储在另一个账户中(这多少有些令人困惑,该账户不可执行,只是单纯存储字节码)。
练习:你能找到程序将存储字节码的账户地址存在了哪里吗?本文的附录中包含可能有用的代码。
总结
只有程序的 owner 才能更改其数据。Solana 程序的 owner 是 BPFLoaderUpgradeable 系统程序,因此在默认情况下,部署该程序的钱包无法更改存储在账户中的数据(字节码)。
为了实现程序升级,Solana 运行时将部署者的钱包地址嵌入到了程序的字节码中。它将该字段称为“authority”。
当部署钱包尝试升级字节码时,Solana 运行时会检查交易的签名者是否是这个 authority。如果交易签名者与 authority 匹配,那么 BPFLoaderUpgradeable 就会代表 authority 更新程序的字节码。
附录:将 base 58 转换为十六进制
下面的 Python 代码将实现这种转换。该代码由聊天机器人生成,因此仅供说明参考:
def decode_base58(bc, length):
base58_digits = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
n = 0
for char in bc:
n = n * 58 + base58_digits.index(char)
return n.to_bytes(length, 'big')
def find_correct_length_for_decoding(base58_string):
for length in range(25, 50): # Trying lengths from 25 to 50
try:
decoded_bytes = decode_base58(base58_string, length)
return decoded_bytes.hex()
except OverflowError:
continue
return None
# Base58 string to convert
base58_string = "5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj"
# Convert and get the hexadecimal string
hex_string = find_correct_length_for_decoding(base58_string)
print(hex_string)
通过 RareSkills 了解更多
查看我们的 Solana 开发课程来学习更多 Solana 主题!关于其他区块链主题,请查看我们的 区块链训练营。
首发于 2024 年 3 月 11 日