T2 · Solana / SVM Engineer
在一条把代码与数据彻底分开的链上,写出能被别人集成的程序。
面向 Rust 与高性能链方向。
本页里的「T2」指这条 Track。课程章节一律写成「第 T10 章」并带链接,两者不是一回事——章节 T2 讲的是密码学基础。
这条 Track 适合谁
你从 EVM 过来,一直在用 EVM 的脑子套 Solana。 你会下意识找「合约的存储在哪」,找不到就开始别扭。你知道有 PDA 这个东西,但说不清它和一个普通账户在链上到底差在哪一个字节。
你写得动 Rust,但写不出安全的账户校验。 借用检查器你早就打通了,宏也能看懂。可你的指令函数里,账户校验要么全靠框架的属性宏(你不确定它到底检查了什么),要么靠自己手写几个 require(你不确定漏了什么)。
你在 Devnet 跑得很顺,一上主网就开始出怪事。 交易偶发失败、账户创建的租金不够、同一笔请求被重复执行、账户空间不够又不知道怎么扩。这些在本地验证器上一次都没出现过。
你能写程序,但交不出别人能用的东西。 你的项目只有一个测试脚本,别人要集成就得读你的 Rust 源码去猜账户顺序。你没写过一个客户端 SDK。
分界线是这一句:你交付的不是一个程序,是一个别人不读你源码也能正确调用的程序。
前置
Part B 全 36 章读完最好。要现在就开始,知识图谱里通向这条 Track 的最短路径是:
- 第 T1–T3 章
- 第 T4 章
- 第 T5 章
- 第 T6 章
- 第 T10 章
- 第 T11 章
| 必须先读 | 图谱里的理由 |
|---|---|
| 第 T1 章 到 第 T3 章 | 状态、签名、交易生命周期。这三章不分链,是所有后续内容的地基 |
| 第 T4 章 · RPC 是什么 | 第 T5 章的直接前置。Solana 的高吞吐会把 RPC 的限速与数据完整性问题放大,这一章必须在前面 |
| 第 T5 章 · EVM Account Model | 图谱里第 T6 章的直接前置。这不是笔误:你要先有一个「账户即状态索引」的参照系,才看得出 Solana 把什么东西拆开了 |
| 第 T6 章 · Solana Account Model | 这条 Track 的真正起点。程序不存数据、交易必须提前声明它会碰哪些账户,两句话决定了后面一切 |
| 第 T10 章 · Solana Program | 图谱里依赖第 T6 章。PDA 与 CPI 在这里第一次成为你要自己写的东西 |
| 第 T11 章 · Token Program | 图谱里依赖第 T10 章。共享程序加各自账户这个模型,是 Solana 生态可组合性的来源 |
| 第 T13 章 → 第 T17 章 | 只有你要做 Indexer 那个 topic 时才需要。图谱里第 T17 章的依赖链一路回到第 T9 章,绕不过去 |
你会学什么
- Rust
- Anchor
- PDA
- CPI
- Token-2022
- Indexer
- DeFi
七个 topic 分成四组。
一、Rust 与框架:语言不是难点,账户校验才是
Rust 这一关的重点不在所有权,而在你能不能读懂框架的属性宏展开成了什么检查。框架帮你生成的校验通常包括:账户是不是签名者、是不是可写、owner 是不是预期的程序、PDA 的种子能不能推导出这个地址、账户空间够不够。
这些检查任何一条没生成,就得你自己补。这条 Track 要求你能对着一个指令说出:这里一共做了几项校验、每一项防的是什么、以及少了哪一项会被怎样利用。
不用框架手写一遍指令入口,是这条 Track 里性价比最高的一次练习。写过一次,你就再也不会把属性宏当黑盒。
二、PDA 与 CPI:程序的地址空间和它的对外调用
PDA 要真正吃透三件事:种子的设计(谁决定、是否可被他人抢先创建)、bump 的规范取值、以及「这个 PDA 由哪个程序签名」意味着什么。绝大多数 Solana 的严重问题都可以追到一句「这个账户我以为只有我的程序能写」。
CPI 这边是对称的问题:你调别人时要确认调的确实是那个程序,别人调你时要确认权限没有被顺带传递。跨程序调用的深度限制和账户传递规则,在设计接口时就要考虑,不能等到跑不通再改。
三、Token-2022 与生态集成:你的程序要和谁共处
扩展能力带来的不是新功能列表,而是新的假设失效:转账可能触发钩子、可能收取手续费、可能被冻结、元数据可能就在 mint 账户里。你的程序如果假设「转账就是余额加减」,接上带扩展的代币就会算错账。
这一组的核心训练是:列出你的程序对代币做了哪些隐含假设,然后逐条问「哪个扩展会让它不成立」。
四、Indexer 与 DeFi:让链上发生的事变成可查询的事
Solana 的数据形态和 EVM 不一样:没有 EVM 那种日志模型,你要从交易的指令、内层指令和账户变化里还原语义。第 T17 章讲的重组、断点续传、幂等三个问题在这里一个都不少,只是载体换了。
DeFi 这一项的目标不是自己做一个协议(那是 Track T4),而是能正确地集成:读得懂主流程序的账户布局,算得对一次兑换的预期产出,处理得了交易在链上失败的情况。
毕业作品
一个部署在主网的 Anchor 程序,附客户端 SDK。
和毕业项目分工明确:毕业项目要一个横向完整的系统,这条 Track 只要一个程序,但要求它能被别人正确使用。「附客户端 SDK」这五个字是这份作品的全部重量所在——它把「我写完了」变成「别人用得上」。
别人能不能跑起来。 clone 之后,一条命令装依赖,一条命令在本地验证器上跑完全部测试。README 写清楚需要的工具链版本——这条链的工具链版本敏感度比多数人预期的高,不写版本等于不能复现。
再给一段不超过 20 行的示例代码,展示 SDK 最主要的那个调用。
SDK 让调用方不需要知道账户顺序。 调用方传业务参数,SDK 负责推导 PDA、组装账户列表、处理关联账户是否已存在、估算所需租金。
验收方式很直接:让一个没读过你 Rust 代码的人,只看 SDK 的类型定义,完成一次成功调用。 他要是得回来问你「第三个账户传什么」,这一条不算过。
账户校验有一份清单,并且每一条都有一个会失败的测试。 逐条列出这个程序做了哪些校验,每条配一个「故意传错账户,交易必须失败」的用例。
只测成功路径的测试套件,证明不了任何安全性质。
失败与重试路径是设计过的,不是撞出来的。 至少覆盖:账户已存在、租金不足、账户空间不够、同一请求被重复提交、交易过期需要重发。
每一种都要说清 SDK 这一侧怎么表现——是重试、是报错、还是幂等地返回成功。
主网部署要小额,而且要有升级与停机的说明。 用你自己的钱,金额控制在出问题也不心疼的范围。写清楚升级权限在谁手里、你打算什么时候交出或销毁它、以及出事时怎么让程序停下来。
只交 Devnet 部署加一份「为什么还不该上主网」,在评审里同样成立。
怎样判断自己达标了
因为运行时要在执行之前就知道哪些交易之间没有冲突,从而并行执行它们。账户列表就是这个冲突判断的依据:读写集不相交的交易可以同时跑。
这个设计的代价直接落在开发者身上:你在客户端就必须能推导出这次调用会触碰的全部账户,包括 PDA 和关联账户。这正是「附客户端 SDK」在这条 Track 里是硬要求的原因——账户推导是程序的一部分,只是它运行在链下。
PDA 是一个由程序 id 和一组种子推导出来、并且故意落在椭圆曲线之外的地址。落在曲线外意味着它没有对应的私钥,谁都无法用常规方式为它签名。
推导过程会尝试一个递减的 bump 值,直到得到一个不在曲线上的地址。运行时允许派生它的那个程序在 CPI 时带上种子和 bump,从而以这个地址的身份「签名」。
关键推论有两条:PDA 的控制权完全由种子设计决定;以及 bump 必须用规范值,接受任意 bump 会让同一组种子对应到多个地址。
第一,不检查 owner。攻击者传进来一个由自己的程序创建、但数据布局伪装成你的结构的账户,你按你的结构解读它,读到的全是攻击者写的值。
第二,不检查签名者。把「谁发起的」和「操作谁的账户」这两件事分开了,任何人都能替别人执行操作。
第三,不校验账户之间的关系。比如一个用户仓位账户和一个金库账户都传进来了,各自类型都对,但它们并不属于同一个市场——你把 A 市场的仓位拿到 B 市场去结算。
第三种最难发现,因为每个账户单独看都合法。防它的办法是把关系本身编进 PDA 的种子里。
带转账手续费的扩展:你转出 100,对方收到的少于 100,中间的差额进了手续费账户。按你传入的数量记账,账立刻就歪了。
带转账钩子的扩展:转账会调用另一个程序,它可能失败,也可能改变你没预期的状态。这把重入的风险带进了「我只是转个账」的路径。
可冻结、有默认冻结状态的扩展:一次完全合法的转账也可能被拒绝,你的流程必须能容忍这种失败。
正确做法和 EVM 那边一致:以账户余额在转账前后的实际变化为准,不信你传进去的数字。
没有标准答案,检查三件事:
- 你的账户顺序、种子规则、错误码是不是都写进了 SDK 的类型里,而不是留在注释或者你的脑子里。
- 你有没有一条不需要读源码的路径让集成方知道「这次调用失败是因为什么」。
- 你的升级权限如果被用了,集成方会不会在毫无察觉的情况下被换掉逻辑。答不上这一条,说明你还没把升级权限当成资产来管。
常见误区
「Devnet 跑通了就等于能上主网」
Devnet 的拥塞模式、RPC 行为、账户租金环境都和主网不同。主网上你会遇到:交易因为过期而消失、优先费不够导致长时间不进块、RPC 返回的确认状态和你以为的不一样。
正确做法:把「交易可能永远不确认」当成默认情况来设计客户端。发送后必须有明确的查询与重发策略,而不是发完就当成功。
「用了框架就不用管账户校验了」
框架生成的是你声明的检查,不是你需要的检查。少写一个属性,就少一项校验,而编译器不会提醒你。
正确做法:把账户校验清单当成一份独立的文档来写,逐条对照代码确认它真的存在。每一条都配一个故意传错账户的失败用例——这份用例集就是你的校验清单的可执行版本。
「PDA 就是个地址,随便设个种子就行」
种子设计等于权限设计。种子里少放一个维度,两个本该隔离的对象就会共用同一个账户;种子里放了用户可控且不受约束的值,别人就能抢先占掉你将来要用的地址。
正确做法:先写出「这个 PDA 代表什么,谁有资格让它存在」,再从这句话反推种子。把对象之间的归属关系也编进种子,让「拿错账户」在地址推导阶段就不成立。
「Solana 上没有重入,所以不用管调用顺序」
没有 EVM 那种回调式重入,不等于没有顺序问题。CPI 会把执行权交出去,转账钩子会触发别人的代码,而你的账户此刻可能正处在中间状态。
正确做法和第 T28 章那条肌肉记忆完全一样:先把自己的账记清楚,最后才和外界打交道。 换了一条链,这条规则一个字都不用改。
接下来
- 带着自己的程序重读:第 T6 章、第 T10 章、第 T11 章。
- 要做 Indexer 那个 topic,先补第 T16 章与第 T17 章,再看 Track T3 · Onchain Data Engineer。
- 常见组合:加 Track T4 · DeFi Engineer 做交易类程序,加 Track T5 · Security Engineer 把账户校验做成专业能力。
- 跨到非技术侧:Track N4 · Crypto BD & Ecosystem。你的程序要被别人集成,而「被集成」这件事有一半不是技术问题——SDK 解决的是后一半,前一半在那条 Track 里。
- 想把程序扩成完整系统,去毕业项目。