跳至主要内容

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 的輔助函式需要 hex

@keystonehq/bc-ur-registry-sol 中的 SolSignRequest.constructSOLRequest() 所接受的地址,是 32 個金鑰位元組的 hex 字串,而非 Base58。傳入 Base58 地址會產生錯誤的鍵 4,請求也將無法比對成功。

sign-type 1:交易​

sign-data 是交易中的 訊息 部分,也就是簽名所涵蓋的位元組:在 @solana/web3.js v1 中為 transaction.serializeMessage(),對 VersionedTransaction 則為 message.serialize()。它不是完整的交易(完整交易以簽名數量與簽名位置開頭),也不是雜湊。

規則細節
訊息版本Legacy 與 v0
Address lookup table不支援。 簽名器無法在離線狀態下驗證查找表的內容。編譯 v0 訊息時只使用靜態金鑰;如果結果太大,就不要送出。
簽名者鍵 4 中的金鑰必須是該訊息的 必要簽名者 之一(任何位置皆可,不限於 fee payer)。
大小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 wallet adapter 中 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):fee payer 兼簽名者、收款人、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
}
顯示 QR 文字與原始 CBOR
QR 文字 · ur:sol-sign-request
UR:SOL-SIGN-REQUEST/1-2/LPADAOCSZOCYLRESBDUEHDKBOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDMTADAEADAXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLKBLKAYLTHNRSUECAUTTKEYSELBCXNDLFFWWYGMPKWNEHZSSNLOTIWDDWJNBDAMWZAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAESFGAWZEETBWS
UR:SOL-SIGN-REQUEST/2-2/LPAOAOCSZOCYLRESBDUEHDKBBAMOLKTDVLLTFRQDFXZTMDTNEOCHNSOLBSGTRSFGSASRJTMEDTNTGOTYVARHADAOAOAEADBNAOAEAEAELAPRVABAAEAEAEAEAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMADAEWYHKLNHH
UR CBOR(hex)
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02589601000103f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f77e8c088760bfde1dddcf32c17f209b8242ee52aaf131facd88d0ea2c6d0b06f20000000000000000000000000000000000000000000000000000000000000000cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b901020200010c0200000080b2e60e0000000003d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740601

回應(BitTool Signer → 你的錢包)

解碼後的回應 · ur:sol-signature
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b"
},
"signature": "99b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c"
}
顯示 QR 文字與原始 CBOR
QR 文字 · ur:sol-signature
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDFZNLRHOXDLSBMNWDTYIMVDGSHYKKCXMKSAFLSACYQZTSMEPEJOOEJNLSMOMKJNJSGWTYGHFTYAWNCLSRGOSGEYYLFLWFCLTLDLSRLPJZYAQZPLIOLAFEOTFGFDTADKMWBNDTZCBGGE
UR CBOR(hex)
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02584099b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c

由你的錢包組裝

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

多簽名者交易​

當一筆交易需要多位簽名者(SPL 多重簽名、Squads 提案核准、fee payer 不是傳送者)時,請針對每把 BitTool Signer 金鑰各送出一個請求,每個請求都帶有該金鑰的路徑與地址。每個回應會填入一個位置。Squads 是程式層級的多重簽名:簽名器看到的是核准提案的指令,而非提案的內容,並會在審核畫面中說明這一點。