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 上链 + 密码学承诺」:
- 用户仍然「像签名一样」在钱包里 确认一笔交易(对多签而言,往往是 创建提案 的那笔交易的签名)。
- Memo 里放的不是明文长句,而是
commitment:由 随机数randomR与 业务消息摘要 共同决定,保证 不可被第三方预先伪造、且可与后续校验对齐。 - 交易上链后,我们得到 Solana 交易签名
txHash(Base58 字符串,对应 64 字节 的签名)。 - 前端把
randomR+txHash(64 字节) 组合成signatureHex,交给后端或业务系统,作为「用户完成授权动作」的凭证。 - 服务端通过 RPC
getTransaction拉取链上交易,解析 Memo、校验 commitment、校验账户关系(见第 6 节)。
核心点:
randomR是我们自己引入的 128-bit/256-bit 级别随机数(实现为 32 字节),用来把「这次登录尝试」变成 唯一承诺,避免只用固定句子时可能遇到的重放 / 拼接问题,并与链上 Memo 内容一一对应。
3. 密码学步骤(与实现一致)
以下与 app/components/squadx-connect-panel.tsx 中 signMessageWithSquadX 的逻辑一致(使用 viem:keccak256、concat、toHex 等)。
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按 字节 拼接randomR与messageHash的 hex 解码结果(与代码一致)。- 链上 Memo 的内容 = 该
commitment(通常为 32 字节的哈希,以 hex 或钱包展示为准)。
这样,任何人若没有正确的
randomR,无法针对同一messageHash构造出相同commitment;后端可根据用户提交的randomR复算并比对链上 Memo。
3.5 发送 Memo 交易
- 使用 Solana Memo Program(
MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr)创建一条 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 字节 二进制结构:
| 偏移 | 长度 | 含义 |
|---|---|---|
| 0 | 4 | PREFIX |
| 4 | 32 | randomR |
| 36 | 64 | txHashBytes64(整条链上交易的签名字节) |
signatureBytes = PREFIX || randomR || txHashBytes64 // 4 + 32 + 64 = 100 字节
signatureHex = toHex(signatureBytes) // 0x 开头,200 hex 字符 + 0x
解析函数 decryptSignatureHex(同名在组件内)即按上表拆回 prefix、randomR(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 方案一致) |
randomR | 0x + 64 hex |
messageHash | keccak256(toHex(utf8(message))) |
commitment | keccak256(concat(randomRHex, messageHash)) |
memoTxHash / txSignature | 用于 getTransaction 的签名(约定是提案或执行) |
signatureHex | 100 字节结构的 hex,或后端只收 randomR + txSignature 自行拼 |
后端 必须 用 RPC 拉取 txSignature 对应交易,验 Memo、账户、Squads 程序参与关系(见下节)。
6. 服务端校验(概念)
app/lib/verifier.ts 中的 verifySquadsLogin 展示了链上侧验证思路(需与你们 RPC、jsonParsed 能力对齐):
getTransaction(txSignature, { maxSupportedTransactionVersion: 0, commitment: 'confirmed' })。- 检查交易成功(
meta.err为空)。 - 在 账户列表 中验证:用户地址、Memo Program、Squads Program 等是否出现。
- 在 Squads 指令 中验证用户地址参与方式是否符合预期。
- 在 inner instructions 中找到 Memo,解析出 memo 内容,与
commitment或解析后的明文策略一致。 - 校验 Memo 账户与 Squads 指令账户的 包含关系,防止「把 Memo 挂到无关指令上」的拼接攻击(具体逻辑以代码为准)。
安全提醒:仅客户端自证不够;以服务端链上校验为准。客户端传来的
randomR、txHash若被篡改,应在服务端复算commitment并与链上 Memo 比对失败。
7. 安全与产品注意点(摘要)
- 重放:
commitment应 单次使用;后端应对txSignature或commitment做 幂等 / 已使用标记。 - 时效:可结合区块时间或业务
nonce。 - 多签流程:明确使用 提案 还是 执行 签名。
- RPC:生产环境使用可靠节点;
getTransaction对历史交易可能需要searchTransactionHistory等(视节点而定)。 - 文案一致性:
message与后端计算的messageHash必须同源,避免用户看到的是 A、哈希按 B 计算。
8. 代码索引(便于跳转)
| 内容 | 位置 |
|---|---|
随机数、commitment、Memo 发送、signatureHex 组装 | app/components/squadx-connect-panel.tsx(signMessageWithSquadX、buildSignature、txHashToBytes64) |
signatureHex 解析 | 同文件 decryptSignatureHex |
| 多签执行轮询 | app/lib/sqds.ts(waitForMultisigExecution) |
| 发送后钱包特殊处理 | app/lib/transaction.ts(withWalletSupport) |
| 登录链上验证示例 | app/lib/verifier.ts(verifySquadsLogin) |
9. 小结
- 原因:SquadX 多签场景下,不能依赖与单签相同的 标准 off-chain
signMessage完成我们的业务证明。 - 做法:用 Memo 交易 承载
commitment = hash(randomR, messageHash),再用PREFIX || randomR || txSignatureBytes(64)形成可传输的signatureHex。 - 多签:区分 提案 tx 与 execute tx;需要执行完成时,用
waitForMultisigExecution或产品层按钮异步获取 执行签名 再绑定。 - 验证:服务端
getTransaction+ Memo / 账户 / Squads 关系校验,并 复算 commitment。