
x402 Solana exact 支付方案详解协议流程、字段契约与 Facilitator 验证规则【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402本文基于 specs/schemes/exact/scheme_exact_svm.md 规格文档系统讲解 x402 协议中面向 SolanaSVM链的exact支付方案客户端如何构建部分签名交易、PaymentRequirements/PaymentPayload/SettlementResponse的完整字段契约、Facilitator 必须执行的六类安全校验以及重复结算竞态的缓存缓解方案。读完本文你可以理解该方案客户端发起、Facilitator 代付 Gas的设计原理并能对照仓库中的 Go/Python/TypeScript 参考实现完成接入或审计。1. exact 方案在 Solana 上的定位x402 是一个构建在 HTTP 之上的互联网支付协议详见 specs/x402-specification-v2.md。当资源服务器对受保护接口返回 402 Payment Required 信号后买卖双方需要协商支付机制scheme 网络network。exact是 x402 的核心方案之一通用定义见 specs/schemes/exact/scheme_exact.md表示支付一笔指定数量的代币。在 Solana 网络上的特殊性在于交易由客户端发起并部分签名client-driven链上交易的手续费fee由Facilitator 作为feePayer代付客户端无需持有 SOL只需持有 SPL 代币如 USDC收款地址不是普通钱包地址的任意形式而是由payTo派生的Associated Token AccountATA因此卖方无需为每笔支付准备唯一充值地址。仓库中该方案的参考实现分布在三个语言目录Go 位于 go/mechanisms/svm/Python 位于 python/x402/mechanisms/svm/TypeScript 位于 typescript/packages/mechanisms/svm/。本文以 Go 实现为主要源码证据。2. 协议完整流程规格文档将 SVM 上的exact流程定义为客户端驱动的四阶段。完整继承原文档的 14 步如下请求阶段Client向Resource Server发起资源请求Resource Server返回PaymentRequired信号。关键点extra字段中携带feePayer即代付交易手续费的身份公钥地址通常是 Facilitator 的地址Client构建一笔交易将指定数量的资产转账到资源服务器钱包地址对应的 ATAClient用自己的钱包签名该交易。由于 Facilitator 的feePayer签名仍缺失此时得到的是一个部分签名交易Client将部分签名交易序列化并以 Base64 编码Client携带包含该 Base64 字符串的PaymentPayload向资源服务器发起新请求。验证阶段7.Resource Server收到请求后将PaymentPayload与PaymentRequirements转发给Facilitator的/verify端点 8.Facilitator解码、反序列化这笔拟议交易 9.Facilitator检查交易合法性确保其中只包含预期的支付指令 10.Facilitator向资源服务器返回VerifyResponse。结算阶段11. 验证成功后Resource Server将 payload 转发给 Facilitator 的/settle端点 12.Facilitator以feePayer身份补上最终签名将完整签名交易提交到 Solana 网络 13. 链上结算成功后Facilitator向资源服务器返回SettlementResponse 14.Resource Server在响应中向Client放行资源。Go 参考实现中验证与结算分别对应 go/mechanisms/svm/exact/facilitator/scheme.go 的Verify与 Settle。其中Settle的内部顺序是先完整跑一遍Verify→ 检查重复结算缓存 → 解码交易 → 校验交易首账户AccountKeys[0]即链上 feePayer与extra.feePayer一致 → 签名 →SendTransaction→ConfirmTransaction。从源码结构看参考实现还比规格多出一步防御性检查Verify的最后会用 Facilitator 私钥签名并调用SimulateTransaction做 RPC 模拟scheme.go 第 224-241 行以提前捕获余额不足、账户无效等问题避免在/settle阶段才暴露失败。3.PaymentRequirements字段契约在标准 x402PaymentRequirements字段之外SVMexact要求extra中包含feePayer与可选的memo。规格文档给出的完整示例{ scheme: exact, network: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, amount: 1000, asset: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, payTo: 2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4, maxTimeoutSeconds: 60, extra: { feePayer: EwWqGE4ZFKLofuestmU4LDdK7XM1N4ALgdZccwYugwGd, memo: pi_3abc123def456 } }各字段语义继承原文档并补充实现细节字段必需说明scheme是固定为exactnetwork是CAIP-2 标识如主网solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpamount是以代币最小单位base unit表示的十进制字符串必须与交易内TransferChecked的 amount 一致asset是代币 mint 的公钥地址base58例如主网 USDCEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vpayTo是收款方钱包公钥实际收款 ATA 由(payTo, asset)按所选代币程序派生maxTimeoutSeconds是结算时限extra.feePayer是代付交易手续费的账户公钥通常为 Facilitator 的公钥extra.memo否卖方自定义 UTF-8 字符串写入交易的 Memo 指令存在时客户端必须用它替代随机 nonce。上限 256 字节。让卖方能把发票号等支付引用挂在链上交易里便于对账且无需唯一收款地址网络标识与资产常量Go 参考实现把三个网络的 CAIP-2 标识、RPC 地址与默认 USDC mint 集中在 go/mechanisms/svm/constants.go网络CAIP-2默认 USDC mintMainnetsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vDevnetsolana:EtWTRABZaYq6iMfeYKouRu166VU2xqa14zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDUTestnetsolana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z同 Devnet同时该文件维护了 V1 网络名solana/solana-devnet/solana-testnet到 CAIP-2 的映射utils.go 的 NormalizeNetwork因此旧版网络名可被自动归一化。extra.feePayer的生成方式从源码结构看Facilitator 支持持有多个签名地址用于负载均衡与密钥轮换FacilitatorSvmSigner.GetAddresses接口见 go/mechanisms/svm/types.go。/kinds端点返回 requirements 时参考实现会随机选取其中一个地址作为feePayerscheme.go 的 GetExtra以分散多签名器的负载。相应地Verify会先校验请求中的feePayer必须是本 Facilitator 管理的地址之一否则返回fee_payer_not_managed类错误scheme.go 第 104-120 行。4. 客户端侧部分签名交易是怎么构建的客户端的工作对应流程第 3~6 步。Go 参考实现 go/mechanisms/svm/exact/client/scheme.go 的CreatePaymentPayload完整演示了规格中构建转账交易的细节识别代币程序通过 RPC 读取 mint 账户的owner判断该 token 属于spl-token还是token-2022第 83-87 行。这与 Facilitator 侧TransferChecked 程序必须是二者之一的校验相呼应派生 ATA用FindAssociatedTokenAddress分别派生客户端的 source ATA 与收款方payTo的 destination ATA第 95-105 行组装指令序列SetComputeUnitLimit默认 20000 CU→SetComputeUnitPrice默认 1 microlamports→TransferChecked→Memo。计算预算默认值及理由见 constants.go 第 16-28 行20000 CU 覆盖 transfer约 6200 CU memo约 8500 CU 预算指令约 300 CU加余量Memo 取值规则若extra.memo存在且非空则直接用作 Memo 数据超过MaxMemoBytes 256字节则报错否则生成 16 字节随机数并 hex 编码保证 UTF-8 合规作为 nonce用于保证并发同参交易在链上可区分第 166-184 行设置 feePayer 与版本交易 feePayer 设为extra.feePayer并将消息版本显式设为V0versioned transaction注释说明这是为了让 TypeScript、Python、Go 各语言的 Facilitator 都能正确反序列化该交易第 199-202 行部分签名与编码仅用客户端密钥签名然后MarshalBinary Base64 标准编码go/mechanisms/svm/utils.go 的 EncodeTransaction放入payload.transaction。注意此处的签名模型客户端签名是authority转账批准者签名而feePayer签名缺失——这正是交易成为部分签名的原因Facilitator 补签后才能上链。5.PaymentPayload结构payload字段是一个仅含transaction键的对象{ transaction: AAAAAAAAAAAAA...AAAAAAAAAAAAA }transaction为 Base64 编码、序列化后的部分签名versioned Solana 交易。完整的PaymentPayload对象继承规格原文示例{ x402Version: 2, resource: { url: https://example.com/weather, description: Access to protected content, mimeType: application/json }, accepted: { scheme: exact, network: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, amount: 1000, asset: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, payTo: 2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4, maxTimeoutSeconds: 60, extra: { feePayer: EwWqGE4ZFKLofuestmU4LDdK7XM1N4ALgdZccwYugwGd, memo: pi_3abc123def456 } }, payload: { transaction: AAAAAAAAAAAAA...AAAAAAAAAAAAA } }从源码结构看参考实现把 payload 定义为ExactSvmPayload{ Transaction string }go/mechanisms/svm/types.goV1/V2 复用同一结构反序列化入口PayloadFromMap会强制要求transaction非空否则报missing transaction field in payloadtypes.go 第 85-103 行。accepted字段是客户端对PaymentRequirements的确认Facilitator 在验证时会校验 payload 与 requirements 的scheme、network一致facilitator/scheme.go 第 85-93 行。6.SettlementResponse结算成功后的响应结构{ success: true, transaction: base58 encoded transaction signature, network: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, payer: base58 encoded public address of the transaction fee payer }transaction链上交易签名base58是卖方核对链上结算的最终凭证payer规格定义为交易 feePayer 的 base58 公钥地址。需要说明的是从 Go 参考实现的源码结构看SettleResponse.Payer实际取自Verify阶段通过GetTokenPayerFromTransaction提取的TransferChecked 指令的 authority代币转出方utils.go 第 174-209 行即客户端钱包地址实现该方案时应以所选语言参考实现的实际返回为准并把transaction签名作为链上核验的第一依据。7. Facilitator 验证规则MUST这是整个方案的安全核心。规格规定 Facilitator 在代付并签名之前必须通过以下全部检查Go 参考实现逐条对应7.1 指令布局反编译后的交易必须包含3~6 条指令顺序固定Compute Budget: Set Compute Unit LimitCompute Budget: Set Compute Unit PriceSPL Token 或 Token-2022 的 TransferChecked可选Lighthouse 或 Memo 程序指令可选Lighthouse 或 Memo 程序指令可选Memo 程序指令允许出现的可选程序只有两个LighthouseL2TExMFKdjpN9kozasaurP8sbXoAN1qA3S95与 SPL MemoMemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr常量见 constants.go 第 30-39 行。为什么必须容忍 Lighthouse 指令Phantom 钱包会注入 1 条 Lighthouse 指令Solflare 注入 2 条二者都是钱包内置的用户保护机制。因此参考实现把 3~6 条指令的窗口写成前 3 条固定 后续仅允许 Lighthouse/Memofacilitator/scheme.go 第 134-202 行出现其他任何程序即拒绝。Memo 指令的双重作用保证同一参数并发支付的交易在链上唯一随机 nonce 至少 16 字节hex 编码以满足 UTF-8当PaymentRequirements携带extra.memo时Facilitator必须验证恰好存在一条 Memo 指令且其数据与extra.memo的 UTF-8 编码完全相等。参考实现的对应逻辑在 facilitator/scheme.go 第 204-221 行Memo 数量不等于 1 返回memo_count错误数据不匹配返回memo_mismatch错误错误码定义见 facilitator/errors.go。7.2 FeePayerFacilitator自身安全这是防止付费方被骗转走自己资金的关键三条款配置的 feePayer 地址不得出现在交易任何指令的accounts中防止交易借 feePayer 账户做其他写入feePayer不得是TransferChecked 指令的authority即不能批准 Facilitator 自己的代币流出feePayer不得是转出资金的source账户。参考实现把前两条落实为遍历 Facilitator 全部签名地址若 TransferChecked 的 authority第 4 个账户或 mint 侧账户属于签名器之一即拒绝facilitator/scheme.go 第 427-434 行同时Settle阶段还校验交易AccountKeys[0]链上真实 feePayer必须等于extra.feePayer第 299-304 行防止用 A 地址的要求搭 B 地址的费。7.3 计算预算合法性指令 1、2 的程序必须是ComputeBudget且判别子discriminator分别为2SetLimit与3SetPrice——参考实现对inst.Data[0]做了字节级校验第 331-355 行 与 第 357-392 行计算单元价格必须设上限以防止 Gas 滥用。规格给出的参考实现上限为≤ 5 lamports/CU对应常量MaxComputeUnitPriceMicrolamports 5_000_0005 lamports 5,000,000 microlamportsconstants.go 第 19-21 行超限返回compute_price_too_high类错误。7.4 转账意图与目的地TransferChecked 的程序必须是spl-token或token-2022mint 必须等于PaymentRequirements.asset目的地必须等于(owner payTo, mint asset)在所选代币程序下的 ATA 派生值——参考实现用FindAssociatedTokenAddress(payTo, mint)计算期望值后与交易内 destination 逐字节比对第 436-461 行杜绝付给任意地址的攻击面。7.5 账户存在性sourceATA 必须已存在destination ATA 的判定规则是当且仅当交易中不存在Create ATA 指令时它必须已存在若存在 Create ATA 指令则执行前允许 destination ATA 尚未存在首次转账时由链上自动创建。7.6 金额规格要求 TransferChecked 的amount与PaymentRequirements.amount严格相等。Go 参考实现中以不足即拒绝的方式落地*transferChecked.Amount requiredAmount时返回amount_insufficient第 463-471 行。从源码结构看这是下界检查与规格的精确相等要求在超额支付边缘情形上存在口径差异实现方若严格按 MUST 语义审计应以规格为准收紧为相等判断。规格最后强调这些检查对安全至关重要实现可以引入更严格限制例如更低的计算价格上限但不得放松上述约束。8. 重复结算竞态与缓解方案RECOMMENDED8.1 漏洞原理结算流程存在一个竞态窗口若同一笔支付交易在第一笔提交尚未上链确认前被多次并发提交到/settle每次调用都可能返回成功——Solana 的交易去重保证转账只在链上执行一次但 RPC 对重复提交返回的仍是 successFacilitator 于是可能对每个调用方都回success。恶意客户端可借此付一次款、开多个资源。8.2 推荐缓解规格建议商家和/或 Facilitator 维护一个短期、进程内的交易载荷缓存验证通过后从交易载荷如 Base64 交易字符串本身派生缓存键键已存在 → 以duplicate_settlement错误拒绝结算键不存在 → 插入缓存后继续签名与提交超过 120 秒的条目被淘汰——约为 Solana blockhash 生命周期~60–90 秒的两倍。窗口过后 blockhash 必然过期该交易无论如何都无法再上链缓存条目失去意义。该方案不需要外部存储或持久状态只依赖进程内 map 时间淘汰在保持 Facilitator 无状态设计的同时封死重复结算攻击面。仓库实现对照Go 侧完整实现为 go/mechanisms/svm/settlement_cache.go——SettlementCache用互斥锁保护map[string]time.TimeIsDuplicate在加锁后先prune删除超过SettlementTTL的条目再判断键是否存在存在则返回 true否则记录当前时间戳并返回 false第 27-38 行。TTL 常量SettlementTTL 120 * time.Second及其注释与规格完全一致constants.go 第 50-52 行。调用点位于Settle中以solanaPayload.Transaction为键命中即返回duplicate_settlement错误facilitator/scheme.go 第 276-280 行错误常量见 errors.go 第 37 行。两个值得注意的工程细节V1/V2 共享缓存NewExactSvmScheme允许注入外部SettlementCache使 V1 与 V2 协议版本的 Facilitator 实例共享同一去重状态——经 V1 提交的交易在 V2 端同样被拦截facilitator/scheme.go 第 25-39 行测试佐证Go 端在 facilitator/duplicate_tx_test.go 与 client/duplicate_tx_test.go 中分别验证了 Facilitator 去重行为与duplicate_settlement错误码第 79 行断言ErrDuplicateSettlement duplicate_settlementPython 参考实现同样提供 python/x402/mechanisms/svm/settlement_cache.pyTypeScript 端对应 typescript/packages/mechanisms/svm/test/unit/duplicateTx.test.ts说明该缓解方案已是多语言实现的一致基线。9. 接入检查清单把规格与参考实现合起来看实现 SVMexact方案时的核对清单卖方Resource Serverrequirements 的extra必须携带由 Facilitator 签发的feePayer如需对账可加 ≤256 字节的memo如发票号客户端按ComputeLimit → ComputePrice → TransferChecked → Memo组装 V0 versioned 交易feePayer 指向extra.feePayer仅签 authority 签名后 Base64 编码上送Memo 优先使用extra.memo缺省时用 ≥16 字节随机数 hex 编码Facilitator/verify执行第 7 节全部 MUST 检查3~6 指令布局、feePayer 安全三条款、ComputeBudget 判别子 2/3 与价格上限、代币程序与 ATA 目的地、账户存在性、金额一致/settle前走重复结算缓存键为交易 Base64 字符串TTL 120 秒补 feePayer 签名后提交并按签名轮询确认网络与资产CAIP-2 标识与默认 USDC mint 以 constants.go 的 NetworkConfigs 为准V1 网络名会自动归一化到 CAIP-2。10. 关键文件索引内容路径规格文档本文主体specs/schemes/exact/scheme_exact_svm.mdexact 方案通用规格specs/schemes/exact/scheme_exact.md网络/程序/限额常量go/mechanisms/svm/constants.goGo Facilitator 验证与结算go/mechanisms/svm/exact/facilitator/scheme.goGo 客户端 payload 构建go/mechanisms/svm/exact/client/scheme.go交易编解码与 payer 提取工具go/mechanisms/svm/utils.go重复结算缓存go/mechanisms/svm/settlement_cache.goSigner 接口定义go/mechanisms/svm/types.goPython 重复结算缓存python/x402/mechanisms/svm/settlement_cache.pyTypeScript 单元/集成测试typescript/packages/mechanisms/svm/【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考