跳到主要内容

通过二维码签名

配对完成后,每次签名都是一次往返:你的钱包显示请求二维码,BitTool Signer 显示响应二维码。

链请求(你 → 签名器)响应(签名器 → 你)页面
Bitcoinur:crypto-psbt(也支持 ur:psbt、BBQr、Base64/十六进制文本)相同类型,附加了签名Bitcoin
EVMur:eth-sign-requestur:eth-signatureEVM
TRONur:tron-sign-requestur:tron-signatureTRON
Solanaur:sol-sign-requestur:sol-signatureSolana

这些都是公开的 Keystone UR 注册表类型。钱包级配对格式仅用于导入账户;不存在 BitTool 专有的签名封装格式,也不应自行发明。

请求的组成部分​

EVM、TRON 和 Solana 的请求都是使用整数键的 CBOR map,直接作为 UR 正文传输(只有一层 CBOR,与钱包级配对不同)。它们共用三个组成部分。

请求 ID:37(bytes)​

一个随机标识符,即用 CBOR 标签 37 包裹的字节串。BitTool Signer 会将其原样复制到响应中,方便你把响应与待处理的请求对应起来。

  • 为每个请求生成 16 个随机字节(UUID 的字节,而不是其文本形式)。
  • sol-sign-request 和 tron-sign-request 要求恰好 16 字节。为兼容使用更长不透明 ID 的钱包,eth-sign-request 也接受 1–64 字节。
  • 按字节比较 ID。ID 匹配并不代表签名有效;你仍然需要验证签名。

派生路径:304(crypto-keypath)​

需要签名的密钥的完整路径,以及主指纹:

304({
1: [44, true, 60, true, 0, true, 0, false, 0, false], ; m/44'/60'/0'/0/0: index, hardened, index, hardened …
2: 1942346250 ; master fingerprint 0x73c5da0a as a big-endian uint32
})
  • 路径分量按 index, hardened? 交替排列。不要再给硬化索引加上 0x80000000。
  • 使用具体的叶子路径(…/0/5),不要使用账户路径或 * 通配符。
  • 始终包含指纹(键 2)。缺少指纹时匹配能力会减弱,部分请求甚至完全无法匹配。

一个使用 cborg 构建该结构的辅助函数:

examples/keypath.mjs
import { Tagged } from 'cborg';

/** crypto-keypath (tag 304): "m/44'/195'/0'/0/0" + "73c5da0a" →
* 304({1: [44, true, 195, true, 0, true, 0, false, 0, false], 2: 1942346250}) */
export function keypath(path, masterFingerprint) {
const components = path.replace(/^m\//, '').split('/')
.flatMap((part) => [Number.parseInt(part, 10), part.endsWith("'")]);
return new Tagged(304, new Map([[1, components], [2, Number.parseInt(masterFingerprint, 16)]]));
}

Origin:文本​

向用户显示的简短标签,表示请求的来源,例如 "BitTool Wallet"。最多 256 个字符。它仅用于显示,不经过认证:BitTool Signer 从不把它当作任何证明。

不允许额外字段​

只接受各类型定义的键:eth-sign-request 为 1–7,sol- 和 tron-sign-request 为 1–6。任何未知的键都会使整个请求无效,因此不要添加你自己的元数据。

动态二维码​

请求经常超出单个二维码的容量。请将其编码为多部分 UR,并循环播放各帧:

  • 将 UR 文本转为大写:UR:ETH-SIGN-REQUEST/… 可以使用二维码的字母数字模式,所需比特数比字节模式少约 30%。
  • 每个分片 200–400 字节、每秒 3–5 帧,在手机上扫描较为可靠。
  • 持续调用编码器的 nextPart(),而不是重复播放第一轮。第一轮之后,bc-ur 编码器会生成混合的“喷泉码”分片,即使漏扫了部分帧,扫描器也能完成解码。

BitTool Signer 使用 180 字节的分片编码自己的响应,因此较短的签名是单个静态二维码,而已签名的 PSBT 可能是动态二维码。你的扫描器必须能处理这两种情况。

响应​

  • 先检查 UR 类型:eth-sign-request 的响应必须是 eth-signature,依此类推。Bitcoin 以你发送时所用的相同类型返回。
  • 检查请求 ID(Bitcoin:检查 PSBT 内的未签名交易没有被改动)。
  • 使用预期的密钥、针对你请求签名的确切字节,对签名进行密码学验证,然后再组装并广播。

不存在拒绝响应。如果用户在 BitTool Signer 中取消,不会显示任何内容。你的界面应允许用户返回、重新显示二维码或重新开始,并且绝不能无限期地等待回复。

保存待处理记录​

保存每个请求,直到它得到响应或被放弃:

pendingId, walletId, chain/network, signer path + key/address,
requestId, request type, exact bytes to sign, digest,
unsigned transaction, created at, expiry (nonce / blockhash / TRON expiration),
status: awaitingSignature → signatureVerified → broadcast → confirmed | failed | cancelled

如果交易中有任何内容发生变化(手续费、nonce、blockhash、金额),请创建一个使用新 ID 的新请求。绝不要把一个签名用于不同的字节;对于已取消或已过期的请求,收到的响应应予以拒绝。

BitTool Signer 如何找到密钥​

你无需告诉 BitTool Signer 使用哪个钱包;它会把请求中的路径、指纹以及地址或公钥,与手机上每个钱包的公钥进行比对,自行确定。必须恰好有一个钱包匹配。完整规则参见签名器会检查什么。实践中:在每个请求中都发送完整路径、指纹和地址(或公钥),BitTool Signer 就能直接找到密钥。