본문으로 건너뛰기

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()는 주소를 Base58이 아니라 32바이트 키의 hex 문자열로 받습니다. 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는 제어 문자가 없는 1–1,024바이트의 UTF-8 텍스트입니다. 트랜잭션 헤더로 해석될 수 있는 바이트는 거부되므로, 트랜잭션을 "메시지"로 위장할 수 없습니다. 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
}
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 제안 승인, 송신자가 아닌 수수료 지불자) BitTool Signer 키마다 해당 키의 경로와 주소를 넣은 요청을 하나씩 보내세요. 응답 하나가 슬롯 하나를 채웁니다. Squads는 프로그램 수준의 멀티시그입니다. 서명 기기는 제안 내용이 아니라 제안을 승인하는 명령어를 보게 되며, 검토 화면에서도 그렇게 안내합니다.