Solana SquadX off-chain 登录 /「类签名」

0 阅读7分钟

Solana SquadX off-chain 登录 /「类签名」

本文档说明:在 SquadX(Squads 多签)场景下,官方能力不足以覆盖「标准链下 signMessage」时,我们如何用 链上 Memo + 随机数 randomR + 交易签名绑定 完成可验证的「授权证明」,以及后续 服务端校验 的思路。


1. 背景:为什么不用「标准 off-chain signMessage」?

1.1 常见钱包(如 Phantom)的做法

许多 Solana 钱包提供 signMessage(或 Wallet Standard 的 signMessage),用户签的是 链下字节,服务端用公钥验 Ed25519 即可。这是 Web3 里最典型的「证明你控制该地址」的方式。

1.2 SquadX / 多签场景的限制

SquadX 面向 Squads 多签 工作流,与「单地址直接签一条链下消息」的模型不一致:

  • 官方文档与产品能力主要围绕 交易提案(proposal)→ 投票 → 执行(execute) 等链上流程。
  • 实际集成中我们发现:不能依赖与单签钱包相同的「一键 off-chain signMessage」路径来完成业务所需的「用户同意条款 / 授权声明」证明(或该路径不可用、不稳定、与多签模型不兼容,官方原链接)。

因此需要 替代方案:用 链上可验证数据 代替链下签名。


2. 总体思路:用 Memo 交易代替 signMessage,并用 randomR 绑定「语义」

我们采用 「Memo Program 上链 + 密码学承诺」

  1. 用户仍然「像签名一样」在钱包里 确认一笔交易(对多签而言,往往是 创建提案 的那笔交易的签名)。
  2. Memo 里放的不是明文长句,而是 commitment:由 随机数 randomR业务消息摘要 共同决定,保证 不可被第三方预先伪造、且可与后续校验对齐。
  3. 交易上链后,我们得到 Solana 交易签名 txHash(Base58 字符串,对应 64 字节 的签名)。
  4. 前端把 randomR + txHash(64 字节) 组合成 signatureHex,交给后端或业务系统,作为「用户完成授权动作」的凭证。
  5. 服务端通过 RPC getTransaction 拉取链上交易,解析 Memo、校验 commitment、校验账户关系(见第 6 节)。

核心点randomR 是我们自己引入的 128-bit/256-bit 级别随机数(实现为 32 字节),用来把「这次登录尝试」变成 唯一承诺,避免只用固定句子时可能遇到的重放 / 拼接问题,并与链上 Memo 内容一一对应。


3. 密码学步骤(与实现一致)

以下与 app/components/squadx-connect-panel.tsxsignMessageWithSquadX 的逻辑一致(使用 viemkeccak256concattoHex 等)。

3.1 业务消息 msg

例如由 getSignMessage(address) 生成,包含地址与条款说明(UTF-8 字符串)。

3.2 生成 randomR

  • 使用 crypto.getRandomValues 生成 32 字节 随机数 randomR
  • randomRHex = toHex(randomR)(带 0x 前缀的 hex)。
  • 可选:按地址写入 localStorage(如 squadx_R:${address}),便于调试或客户端恢复(注意:敏感业务应以服务端会话为准)。

3.3 消息摘要 messageHash

messageHex = toHex(UTF8_BYTES(msg))
messageHash = keccak256(messageHex)

即:对 消息的 UTF-8 字节 做 hex,再 Keccak-256。

3.4 承诺 commitment(写入 Memo 的 payload)

commitment = keccak256(concat([randomRHex, messageHash]))
  • concat字节 拼接 randomRmessageHash 的 hex 解码结果(与代码一致)。
  • 链上 Memo 的内容 = 该 commitment(通常为 32 字节的哈希,以 hex 或钱包展示为准)。

这样,任何人若没有正确的 randomR,无法针对同一 messageHash 构造出相同 commitment;后端可根据用户提交的 randomR 复算并比对链上 Memo。

3.5 发送 Memo 交易

  • 使用 Solana Memo ProgramMemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr)创建一条 Memo 指令,data 为上述 commitment(实现里与 memoPayload 一致)。
  • Fee payer 必须为连接的钱包地址(多签场景下按你们产品要求设置)。
  • 使用较新的 blockhash,并对 Blockhash not found 做重试(用户在钱包里停留过久会导致 blockhash 过期)。

成功后得到 txHash(Base58,64 字节)。

3.6 组装对外暴露的 signatureHex

定义固定 4 字节前缀(与链上协议无关,仅用于识别我们的格式):

PREFIX = 0xFF 0xFF 0xFF 0xFE   // 即 SIGNATURE_PREFIX_BYTES

Solana 交易签名 转为 64 字节(Base58 decode):

txHashBytes64 = base58Decode(txHash)  // 长度 64

最终 100 字节 二进制结构:

偏移长度含义
04PREFIX
432randomR
3664txHashBytes64(整条链上交易的签名字节)
signatureBytes = PREFIX || randomR || txHashBytes64   // 4 + 32 + 64 = 100 字节
signatureHex = toHex(signatureBytes)                  // 0x 开头,200 hex 字符 + 0x

解析函数 decryptSignatureHex(同名在组件内)即按上表拆回 prefixrandomR(hex)、txHash(Base58 字符串)。


4. 多签(SquadX)特别注意:提案签名 vs 执行签名

Squads 多签中,用户在前端点「确认」后,第一笔返回的 txHash 往往是「创建提案」的交易签名,而不是最终 execute 那笔。

  • 若业务只关心「用户发起了带某 Memo 的提案」:使用提案 txHash 绑定即可(需后端校验逻辑与之一致)。
  • 若业务要求「多签已执行」才认登录成功:必须在提案通过后,再取 execute 交易的 signature,并用它替换上面结构里的 txHashBytes64(或单独字段存储执行哈希)。

项目里已提供辅助能力:

  • app/lib/sqds.ts 中的 waitForMultisigExecution:根据 提案交易签名 轮询链上 Proposal 状态,直到 Executed,再解析出 执行交易 signature
  • app/lib/transaction.ts 中的 withWalletSupport:在钱包名为 SquadsX 时,可在发送交易后 自动等待 执行完成并返回执行签名(按产品需要选用)。

与前端同事对齐时务必约定signatureHex 里的 txHash 到底是 提案 还是 执行,避免后端验错交易。


5. 前端交付给后端的最小字段建议

推荐在登录请求里携带(或等价信息):

字段说明
address用户公钥(Base58)
message用户看到的完整 UTF-8 文案(或与标准文案的 hash 方案一致)
randomR0x + 64 hex
messageHashkeccak256(toHex(utf8(message)))
commitmentkeccak256(concat(randomRHex, messageHash))
memoTxHash / txSignature用于 getTransaction 的签名(约定是提案或执行)
signatureHex100 字节结构的 hex,或后端只收 randomR + txSignature 自行拼

后端 必须 用 RPC 拉取 txSignature 对应交易,验 Memo、账户、Squads 程序参与关系(见下节)。


6. 服务端校验(概念)

app/lib/verifier.ts 中的 verifySquadsLogin 展示了链上侧验证思路(需与你们 RPC、jsonParsed 能力对齐):

  1. getTransaction(txSignature, { maxSupportedTransactionVersion: 0, commitment: 'confirmed' })
  2. 检查交易成功(meta.err 为空)。
  3. 账户列表 中验证:用户地址、Memo Program、Squads Program 等是否出现。
  4. Squads 指令 中验证用户地址参与方式是否符合预期。
  5. inner instructions 中找到 Memo,解析出 memo 内容,与 commitment 或解析后的明文策略一致。
  6. 校验 Memo 账户与 Squads 指令账户的 包含关系,防止「把 Memo 挂到无关指令上」的拼接攻击(具体逻辑以代码为准)。

安全提醒:仅客户端自证不够;以服务端链上校验为准。客户端传来的 randomRtxHash 若被篡改,应在服务端复算 commitment 并与链上 Memo 比对失败。


7. 安全与产品注意点(摘要)

  1. 重放commitment单次使用;后端应对 txSignaturecommitment幂等 / 已使用标记
  2. 时效:可结合区块时间或业务 nonce
  3. 多签流程:明确使用 提案 还是 执行 签名。
  4. RPC:生产环境使用可靠节点;getTransaction 对历史交易可能需要 searchTransactionHistory 等(视节点而定)。
  5. 文案一致性message 与后端计算的 messageHash 必须同源,避免用户看到的是 A、哈希按 B 计算。

8. 代码索引(便于跳转)

内容位置
随机数、commitment、Memo 发送、signatureHex 组装app/components/squadx-connect-panel.tsxsignMessageWithSquadXbuildSignaturetxHashToBytes64
signatureHex 解析同文件 decryptSignatureHex
多签执行轮询app/lib/sqds.tswaitForMultisigExecution
发送后钱包特殊处理app/lib/transaction.tswithWalletSupport
登录链上验证示例app/lib/verifier.tsverifySquadsLogin

9. 小结

  • 原因:SquadX 多签场景下,不能依赖与单签相同的 标准 off-chain signMessage 完成我们的业务证明。
  • 做法:用 Memo 交易 承载 commitment = hash(randomR, messageHash),再用 PREFIX || randomR || txSignatureBytes(64) 形成可传输的 signatureHex
  • 多签:区分 提案 txexecute tx;需要执行完成时,用 waitForMultisigExecution 或产品层按钮异步获取 执行签名 再绑定。
  • 验证:服务端 getTransaction + Memo / 账户 / Squads 关系校验,并 复算 commitment