跳到主要内容

Solana(sol-sign-request)

ur:sol-sign-requestur:sol-signatureEd25519legacy + v0 messages

sol-sign-request​

键字段类型内容
1request-id37(bytes)恰好 16 个随机字节
2sign-databytessign-type 1:序列化后的消息 · sign-type 2:文本消息的字节
3derivation-path304(crypto-keypath)三个预设路径之一,并带主指纹
4addressbytes (32)签名者的公钥:Base58 解码后的地址,而不是文本
5origintext向用户显示
6sign-typeuint1 交易 · 2 消息

注意,键的顺序与 eth- 和 tron-sign-request 不同:路径是键 3,类型是键 6。

Keystone 的辅助函数需要十六进制

@keystonehq/bc-ur-registry-sol 中的 SolSignRequest.constructSOLRequest() 接受的地址是 32 字节密钥的十六进制字符串,而不是 Base58。传入 Base58 地址会生成错误的键 4,导致请求无法匹配。

sign-type 1:交易​

sign-data 是交易的消息部分,即签名所覆盖的字节:在 @solana/web3.js v1 中为 transaction.serializeMessage(),对于 VersionedTransaction 则为 message.serialize()。它不是完整的交易(完整交易以签名数量和签名槽位开头),也不是哈希。

规则详情
消息版本Legacy 和 v0
地址查找表不支持。签名器无法离线验证查找表的内容。编译 v0 消息时只使用静态密钥;如果结果过大,就不要发送。
签名者键 4 中的密钥必须是消息的必需签名者之一(可以是任意槽位,不限于手续费支付方)。
大小1–64 条指令;签名后的交易必须符合 1,232 字节的数据包上限。
Durable nonce允许,但会在审核中特别标出(这类交易会无限期保持有效)。

已解码的程序:System(transfer、create account、nonce、assign、allocate)、SPL Token 和 Token-2022(transfer、transferChecked、approve、mint、burn、close、set authority、sync native)、Associated Token Account(含地址检查)、Compute Budget、Memo、Stake(delegate、split、withdraw、deactivate)以及常见的 NFT 程序(Metaplex Token Metadata、Bubblegum、Core)。Token-2022 扩展和 Squads 提案指令为部分解码。其他程序显示为原始指令,需要用户明确同意。

sign-type 2:消息​

sign-data 为 UTF-8 文本,1–1,024 字节,不含控制字符。可能被解读为交易头的字节会被拒绝,因此交易永远无法冒充“消息”。BitTool Signer 直接用 Ed25519 对消息字节签名(不加前缀、不做哈希),这正是 Solana 钱包适配器中的 signMessage 所期望的方式。

sol-signature​

sol-signature = {
1: 37(request-id bytes),
2: signature ; 64 bytes, Ed25519
}

这是一个分离式签名(detached signature)。完成交易的步骤:

  1. 检查请求 ID,并用预期的公钥、针对确切的消息字节验证签名。
  2. 在消息的必需签名者中找到该签名者的索引,并将签名放入对应的槽位。它不一定是槽位 0。
  3. 保留其他槽位中已有的签名;所有必需槽位都填满后,再序列化并广播。

如果 blockhash 在广播前过期,请构建新的消息和新的请求。签名不能挪用到 blockhash 不同的消息上。

代码​

使用 @solana/web3.js v1 和 Keystone 的 @keystonehq/bc-ur-registry-sol:

examples/solana.mjs
import { randomUUID } from 'node:crypto';
import { PublicKey, Transaction, SystemProgram } from '@solana/web3.js';
import { SolSignRequest, SignType, SolSignature } from '@keystonehq/bc-ur-registry-sol';
import { URDecoder } from '@ngraveio/bc-ur';
import { ed25519 } from '@noble/curves/ed25519.js';

export function buildSolTransfer({ from, to, lamports, recentBlockhash }) {
const tx = new Transaction({ feePayer: new PublicKey(from), recentBlockhash });
tx.add(SystemProgram.transfer({ fromPubkey: new PublicKey(from), toPubkey: new PublicKey(to), lamports }));
return tx;
}

export function buildSolRequest(tx, { path, masterFingerprint, address }) {
const requestId = randomUUID();
const request = SolSignRequest.constructSOLRequest(
tx.serializeMessage(), // message bytes, not the whole transaction
path, // "m/44'/501'/0'/0'"
masterFingerprint, // "73c5da0a"
SignType.Transaction, // 1 (2 = text message)
requestId,
new PublicKey(address).toBuffer().toString('hex'), // this library wants hex, not Base58
'BitTool Wallet',
);
const encoder = request.toUREncoder(400);
const frames = [];
for (let i = 0; i < encoder.fragmentsLength; i++) frames.push(encoder.nextPart().toUpperCase());
return { requestId, frames };
}
examples/solana.mjs
export function attachSolSignature(frames, tx, { requestId, address }) {
const decoder = new URDecoder();
for (const frame of frames) decoder.receivePart(frame);
if (!decoder.isSuccess()) throw new Error('Incomplete or invalid QR');
const ur = decoder.resultUR();
if (ur.type !== 'sol-signature') throw new Error(`Expected sol-signature, got ${ur.type}`);
const response = SolSignature.fromCBOR(ur.cbor);
if (Buffer.from(response.getRequestId()).toString('hex') !== requestId.replace(/-/g, '')) throw new Error('Signature is for another request');

const signature = Buffer.from(response.getSignature()); // 64-byte Ed25519 signature
const signer = new PublicKey(address);
if (!ed25519.verify(signature, tx.serializeMessage(), signer.toBytes())) throw new Error('Bad signature');
tx.addSignature(signer, signature); // goes into this key's own slot
return tx.serialize(); // throws if another required signature is missing
}

测试向量​

全部来自 standard 预设 m/44'/501'/0'/0'(HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk)。blockhash 为合成数据。

使用 legacy 消息发送 0.25 SOL(250,000,000 lamports):手续费支付方兼签名者、收款方、System Program。

请求(你的钱包 → BitTool Signer)

sol-sign-request2 帧
UR:SOL-SIGN-REQUEST/1-2/LPADAOCSZOCYLRESBDUEHDKBOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDMTADAEADAXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLKBLKAYLTHNRSUECAUTTKEYSELBCXNDLFFWWYGMPKWNEHZSSNLOTIWDDWJNBDAMWZAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAESFGAWZEETBWS
1 / 2
CBOR 字段
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b"
},
"2 sign-data": "01000103f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f77e8c088760bfde1dddcf32c17f209b8242ee52aaf131facd88d0ea2c6d0b06f20000000000000000000000000000000000000000000000000000000000000000cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b901020200010c0200000080b2e60e00000000",
"3 derivation-path": {
"crypto-keypath (tag 304)": {
"1 components": [
44,
true,
501,
true,
0,
true,
0,
true
],
"2 source-fingerprint": 1942346250
}
},
"4 address": "f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7",
"5 origin": "BitTool Wallet",
"6 sign-type": 1
}
显示二维码文本和原始 CBOR
二维码文本 · ur:sol-sign-request
UR:SOL-SIGN-REQUEST/1-2/LPADAOCSZOCYLRESBDUEHDKBOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDMTADAEADAXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLKBLKAYLTHNRSUECAUTTKEYSELBCXNDLFFWWYGMPKWNEHZSSNLOTIWDDWJNBDAMWZAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAESFGAWZEETBWS
UR:SOL-SIGN-REQUEST/2-2/LPAOAOCSZOCYLRESBDUEHDKBBAMOLKTDVLLTFRQDFXZTMDTNEOCHNSOLBSGTRSFGSASRJTMEDTNTGOTYVARHADAOAOAEADBNAOAEAEAELAPRVABAAEAEAEAEAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMADAEWYHKLNHH
UR CBOR(十六进制)
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02589601000103f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f77e8c088760bfde1dddcf32c17f209b8242ee52aaf131facd88d0ea2c6d0b06f20000000000000000000000000000000000000000000000000000000000000000cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b901020200010c0200000080b2e60e0000000003d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740601

响应(BitTool Signer → 你的钱包)

解码后的响应 · ur:sol-signature
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b"
},
"signature": "99b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c"
}
显示二维码文本和原始 CBOR
二维码文本 · ur:sol-signature
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDFZNLRHOXDLSBMNWDTYIMVDGSHYKKCXMKSAFLSACYQZTSMEPEJOOEJNLSMOMKJNJSGWTYGHFTYAWNCLSRGOSGEYYLFLWFCLTLDLSRLPJZYAQZPLIOLAFEOTFGFDTADKMWBNDTZCBGGE
UR CBOR(十六进制)
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02584099b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c

由你的钱包组装

signatureBase58
45G7cB2HaLBrWEuZxiWR2dFYu52Cje6d2vv5GnCaZ7YSc6ifhFVdFTGpJSk1PU7tSTPHjg3H4uufoe3XPmTRQiJP
signedTransactionBase64
AZm5pC/LjurUaudMXnkgmMJHwhq015GvcKJtg5KYbXFP1FQ6+PEhw1XKMvdH8yHVL8OFbPi0rmeARaNGSNkklAwBAAED8DYnYkanW53jNJ7UKxXiMvZRj8IPX81PHWToH5vSWPd+jAiHYL/eHd3PMsF/IJuCQu5SqvEx+s2I0OosbQsG8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzEkOkozS44c7s0P8ldozF5ymD02/RsLDbpEpnVXU5rkBAgIAAQwCAAAAgLLmDgAAAAA=

多签名者交易​

当一笔交易需要多个签名者时(SPL 多签、Squads 提案审批、手续费支付方不是发送方),请为每个 BitTool Signer 密钥分别发送一个请求,每个请求都带上该密钥的路径和地址。每个响应填充一个槽位。Squads 是程序层面的多签:签名器看到的是批准提案的指令,而不是提案的内容,并会在审核中说明这一点。