第 8 章 · 01 Solana 账户与密钥模型


文档摘要

第 8 章 · 01 Solana 账户与密钥模型 本节摘要:本章是全教程技术密度最高的一章,讲 AutoHedge 唯一真实动钱的代码。本节先打地基——讲清 Solana 的账户模型与密钥模型,这是理解后续 Jupiter Ultra Swap API 和签名流程的前提。核心概念:Solana 上一切皆账户(包括代币、程序、钱包),每个账户有个 32 字节的地址(公钥);钱包由一个 Keypair(公私钥对) 控制,私钥是 base58 编码的字符串,公钥可由私钥唯一派生;代币(如 SOL、USDC)在链上有唯一的 mint 地址;金额用 lamports 等最小单位记录(1 SOL = 10^9 lamports)。

第 8 章 · 01 Solana 账户与密钥模型

本节摘要:本章是全教程技术密度最高的一章,讲 AutoHedge 唯一真实动钱的代码。本节先打地基——讲清 Solana 的账户模型密钥模型,这是理解后续 Jupiter Ultra Swap API 和签名流程的前提。核心概念:Solana 上一切皆账户(包括代币、程序、钱包),每个账户有个 32 字节的地址(公钥);钱包由一个 Keypair(公私钥对) 控制,私钥是 base58 编码的字符串,公钥可由私钥唯一派生;代币(如 SOL、USDC)在链上有唯一的 mint 地址;金额用 lamports 等最小单位记录(1 SOL = 10^9 lamports)。本节会对照 ultra_tools.py 里的 _get_keypair_get_wallet_pubkey,讲清这套模型怎么落到代码里,为后两节的交易与签名打底。

内容来源:原项目源码 autohedge/tools/ultra_tools.py 的密钥相关函数 + Solana 官方文档,精读并套用体系化模板。

⚠️ 风险提示:本节涉及私钥。Solana 私钥一旦泄露,钱包资产立即清零,没有任何冻结/找回机制。务必用测试钱包、测试网,绝不在主网用大额资产试这套未经验证的代码。

学习目标

阅读完本节,你应当能够:

  1. 说清 Solana **「一切皆账户」**模型,以及账户的地址/数据/owner 三要素。
  2. 区分 Keypair、公钥、私钥、地址四者关系,理解「公钥即地址」。
  3. 知道私钥为什么是 base58 编码、为什么这么长、为什么不能泄露。
  4. 记住 SOL 与 USDC 的 mint 地址及 wrapped SOL 的概念。
  5. 理解 lamports / decimals 最小单位制,能做「人类单位 ↔ 最小单位」换算。
  6. 看懂 _get_keypair / _get_wallet_pubkey 如何把私钥变成可签名的 Keypair。

一、Solana 的「一切皆账户」

与以太坊「合约是一种特殊地址」不同,Solana 的设计更极端——链上一切实体都是账户(Account)。一个账户就是一条数据记录,包含:

  • 地址(public key):32 字节的公钥,base58 编码后是一串字符(如 BQ72nSv9f3PRyRKCBnHLVrerrv37CYTHm5h3s9VSGQDV)。
  • 数据(lamports 与 data):lamports 字段存这个账户里有多少 SOL(最小单位);data 字段存任意字节(可以是代币余额、程序代码、状态等)。
  • owner:这个账户归哪个程序(智能合约)管。只有 owner 程序能修改账户的 data 和扣 lamports。

举几个例子帮助理解:

实体 在 Solana 上是什么
你的钱包 一个系统账户,owner 是系统程序,持有 SOL
你持有的 USDC 一个代币账户,owner 是 SPL Token 程序,data 里存余额
USDC 这个代币本身 一个 mint 账户,owner 是 SPL Token 程序,定义代币的 decimals/supply
Jupiter 聚合器 一个 程序账户,data 里是可执行的字节码

💡 核心心法:Solana 没有独立的「合约账户」概念——程序本身也是个账户(可执行账户),代币、钱包也都是账户。统一模型降低了认知负担,但要记住:只有账户的 owner 程序能改它的数据。这就是为什么转 USDC 要调 SPL Token 程序(它的 owner),不能直接改你的代币账户余额。

二、Keypair:公私钥对

控制一个钱包账户的,是一对密钥——Keypair。solders 库里 Keypair 对象同时持有公钥和私钥:

  • 私钥(private key):32 字节的随机数,用于签名。这是钱包的「控制权凭证」——谁有私钥,谁能花这个钱包的钱。
  • 公钥(public key):由私钥单向派生出来(Ed25519 曲线),32 字节,用于验签和地址。公钥可以公开,知道它无法反推私钥。
  • 地址:就是公钥的 base58 文本表示。所以「公钥」和「地址」在 Solana 里基本是同义词。

_get_keypair(ultra_tools.py 第 39-67 行)做的就是「从私钥恢复 Keypair」:

def _get_keypair() -> Keypair: raw = os.getenv("SOLANA_PRIVATE_KEY") if not raw or not raw.strip(): raise ValueError( "SOLANA_PRIVATE_KEY is required in .env to sign transactions" ) try: return Keypair.from_base58_string(raw.strip()) except Exception as e: raise ValueError(f"Invalid SOLANA_PRIVATE_KEY in .env: {e}") from e

逐行:

  • 从环境变量 SOLANA_PRIVATE_KEY 读私钥(注意这个变量名,下一节会点和 .env.example 的不一致)。
  • 空值直接报错——没私钥就别往下走。
  • Keypair.from_base58_string():把 base58 文本私钥解析成 Keypair 对象。这一步同时恢复了私钥和由它派生的公钥。
  • 解析失败(格式错)报友好错误。

💡 关键心法:Keypair 是「公私钥对」的封装——给一个 base58 私钥,它能在本地算出对应的公钥,不需要额外存公钥。所以 .env 里只要存私钥,公钥/地址随时能派生。_get_wallet_pubkey 就是这样做的。

三、base58 编码:私钥为什么长那样

Solana 的私钥/公钥都用 base58 编码成文本。base58 是比特币发明的编码方式,类似 base64,但去掉了 0、O、I、l 这几个容易混淆的字符,适合人眼辨识和手抄。特点:

  • 32 字节的私钥,base58 编码后约 64 个字符(一长串)。
  • 32 字节的公钥,base58 编码后约 43-44 个字符(钱包地址就这么长)。

对比以太坊:以太坊私钥是 64 位十六进制(0-9a-f),地址是 40 位 hex 加 0x 前缀。Solana 用 base58 是为了更短、更不易抄错。

⚠️ 风险提示:AutoHedge 把这串 base58 私钥明文存在 .env 里。这意味着:一旦 .env 被提交到 git、被日志打印、被进程内存 dump,私钥就泄露了。生产环境绝不能这样存,要用 HSM、KMS、硬件钱包或至少系统钥匙串(第 10 章详讲)。

四、公钥即地址:_get_wallet_pubkey

_get_wallet_pubkey(第 70-82 行)演示了「公钥从私钥派生、公钥即地址」:

def _get_wallet_pubkey() -> str: return str(_get_keypair().pubkey())

就这么一行:

  • _get_keypair() 拿到 Keypair(含私钥+公钥)。
  • .pubkey() 取出公钥对象。
  • str(...) 把公钥转成 base58 文本,就是钱包地址。

这行的意义:钱包地址不是独立存的,而是从私钥算出来的。所以 get_order 要告诉 Jupiter「谁是付款方」时,不需要额外配置地址,直接从私钥派生即可(第 131 行 wallet_pubkey = _get_wallet_pubkey())。

五、mint 地址:代币的唯一身份证

第 7 章讲过,Solana 上代币用 mint 地址唯一标识。本节需要记住两个最常用的 mint:

代币 mint 地址 decimals 说明
Wrapped SOL(wSOL) So11111111111111111111111111111111111111112 9 包装在 SPL Token 里的原生 SOL
USDC(Solana 版) EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 6 Circle 发行的美元稳定币

几点说明:

  1. 为什么 SOL 要「wrapped」:原生 SOL 本身是 lamports(记在账户的 lamports 字段),不在 SPL Token 体系里。要在 DeFi(如 Jupiter 兑换)里用 SOL,得先 wrap 成 wSOL(一个 SPL Token),mint 就是上面那个 So111...11112。Jupiter 通常自动处理 wrap/unwrap,所以代码里直接传 wrapped SOL 的 mint 即可。
  2. mint 不是 symbol:get_order 的参数必须是完整 mint 地址,不能写 "SOL""USDC"。第 7 章的 search 工具就是用来从 symbol 反查 mint 的。
  3. mint 不可变:一个代币的 mint 地址在部署那一刻就固定了,任何人无法伪造相同 mint 的假币(只能造同名 symbol 的假币)。这就是 mint 作为「身份证」的意义。

💡 记忆窍门:wrapped SOL 的 mint So111...11112So 开头(像 SOL 的前两个字母)、中间全是 1、以 2 结尾,是 Solana 故意设计的「好看地址」;USDC 的 mint 没规律,只能硬记或查表。实际代码里会从 search 工具或常量拿,不用死记。

六、lamports 与 decimals:最小单位制

Solana 的金额用最小单位记录,不是人类单位:

  • SOL 的最小单位是 lamports:1 SOL = 1,000,000,000 lamports(10^9)。账户里存的「SOL 余额」实际是 lamports 数。
  • 代币的最小单位由 decimals 决定:USDC 的 decimals=6,意味着 1 USDC = 1,000,000 个最小单位。一个 decimals=9 的代币,最小单位就是 10^-9。

get_orderamount 参数(下一节详讲)传的就是最小单位数。例如要兑换 0.01 SOL:

0.01 SOL = 0.01 × 10^9 lamports = 10,000,000 lamports

所以代码里 amount 字符串传 "10000000",不是 "0.01"。ultra_tools.py 的 docstring 示例也是这么写的:

>>> get_order("So11111111111111111111111111111111111111112", ... "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ... "10000000") # ← 0.01 SOL 的 lamports 表示

⚠️ 常见坑:把 0.01 直接当 amount 传,链上会理解成「0.01 lamports」(几乎是 0),交易可能成功但成交量为零。务必先按 decimals 换算成最小单位。第 7 章查价时返回的 decimals 字段,就是用来做这个换算的。

七、账户模型如何影响交易结构

理解了账户模型,就能看懂 Solana 交易为什么这么「重」。一笔交易(Transaction)的本质是:一条消息,告诉链上一组指令;每个指令是「让某个程序修改某些账户的数据」

交易(Transaction) └─ 消息(Message) ├─ payer(付款方,付 gas 的账户) ├─ 最近 blockhash(防重放) └─ 指令列表(Instructions) └─ 每条指令: ├─ program_id(调哪个程序,如 SPL Token) ├─ accounts(这条指令要碰的所有账户地址) └─ data(给程序的参数,字节流)

关键点:每条指令要显式列出它要读写的所有账户。一次 Jupiter 兑换涉及 input mint、output mint、付款方、收款方、各代币账户、ATL 表......动辄几十个账户。这就是为什么 Solana 用 Versioned Transaction + Address Lookup Table——把常用账户放进查找表压缩,legacy 格式装不下。

💡 与以太坊对比:以太坊交易是「从 A 调合约 B 的方法 m,带参数 p」——简洁;Solana 交易是「显式列出所有要碰的账户 + 交给程序解释」——冗长但并行友好。这种差异决定了 Solana 交易签名前必须由程序(这里是 Jupiter)预先组装好完整账户列表,这正是 get_order 要做的事(下一节讲)。

八、本节概念在代码里的映射

把概念和 ultra_tools.py 的代码对应一遍,加深印象:

概念 代码位置 体现
Keypair _get_keypair Keypair.from_base58_string(raw)
公钥即地址 _get_wallet_pubkey str(keypair.pubkey())
私钥明文存 .env _get_keypair os.getenv("SOLANA_PRIVATE_KEY")
mint 地址 get_order 入参 input_mint / output_mint
最小单位 amount get_order 入参 amount(lamports for SOL)
账户列表在交易里 第 03 节详讲 VersionedTransaction.message

本节要点回顾

  1. 一切皆账户:钱包、代币、mint、程序都是 Account;只有账户的 owner 程序能改它的数据——这就是转 USDC 要调 SPL Token 程序的原因。
  2. Keypair:Keypair.from_base58_string(私钥) 恢复出含公私钥的对象;公钥由私钥单向派生,Ed25519 无法反推。
  3. 公钥即地址:钱包地址 = 公钥的 base58 文本;_get_wallet_pubkey 就是从私钥 str(keypair.pubkey()) 一行算出,无需额外存。
  4. base58 私钥:约 64 字符,去掉了易混字符;.env 明文存储是生产隐患。
  5. 两个常用 mint:Wrapped SOL = So11111111111111111111111111111111111111112(decimals=9),USDC = EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v(decimals=6)。
  6. lamports/decimals:金额用最小单位;1 SOL = 10^9 lamports;get_order 的 amount 传 lamports 不是 SOL 数。
  7. 交易结构:消息含 payer + blockhash + 指令;每条指令显式列所有账户——这就是 Jupiter 要预先组装交易、且要用 Versioned Transaction 的原因。

下一节,我们看这套账户模型之上的 Jupiter Ultra Swap API——get_order 怎么组装出一笔未签名交易,routePlan 怎么定路由,feeBps 是什么,以及一个本项目特有的「私钥变量名不一致」的坑。


发布者: 作者: 灏天文库 转发
评论区 (0)
U