Solana (sol-sign-request)
sol-sign-request
| 키 | 필드 | 타입 | 내용 |
|---|---|---|---|
| 1 | request-id | 37(bytes) | 정확히 무작위 16바이트 |
| 2 | sign-data | bytes | sign-type 1: 직렬화된 메시지 · sign-type 2: 텍스트 메시지 바이트 |
| 3 | derivation-path | 304(crypto-keypath) | 세 가지 프리셋 경로 중 하나와 마스터 지문 |
| 4 | address | bytes (32) | 서명자의 공개 키. 텍스트가 아니라 Base58로 디코딩한 주소 |
| 5 | origin | text | 사용자에게 표시되는 값 |
| 6 | sign-type | uint | 1 트랜잭션 · 2 메시지 |
키 순서가 eth-, tron-sign-request와 다르다는 점에 주의하세요. 경로가 키 3, 타입이 키 6입니다.
@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)입니다. 트랜잭션을 완성하려면 다음을 수행하세요.
- 요청 ID를 확인하고, 예상한 공개 키로 정확한 메시지 바이트에 대해 서명을 검증합니다.
- 메시지의 필수 서명자 중 서명자의 인덱스를 찾아 그 슬롯에 서명을 넣습니다. 항상 슬롯 0인 것은 아닙니다.
- 다른 슬롯에 이미 있는 서명은 유지합니다. 모든 필수 슬롯이 채워지면 직렬화해 브로드캐스트합니다.
브로드캐스트하기 전에 blockhash가 만료되면 새 메시지와 새 요청을 만드세요. 서명을 다른 blockhash를 가진 메시지로 옮길 수는 없습니다.
코드
@solana/web3.js v1과 Keystone의 @keystonehq/bc-ur-registry-sol을 사용한 예제입니다.
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 };
}
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는 합성 데이터입니다.
- SOL 전송
- SPL 토큰 (USDC)
- 메시지
legacy 메시지로 0.25 SOL(250,000,000 lamports)을 보냅니다. 수수료 지불자 겸 서명자, 수신자, System Program으로 구성됩니다.
요청 (내 지갑 → BitTool Signer)
{
"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 보기
UR:SOL-SIGN-REQUEST/1-2/LPADAOCSZOCYLRESBDUEHDKBOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDMTADAEADAXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLKBLKAYLTHNRSUECAUTTKEYSELBCXNDLFFWWYGMPKWNEHZSSNLOTIWDDWJNBDAMWZAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAESFGAWZEETBWS
UR:SOL-SIGN-REQUEST/2-2/LPAOAOCSZOCYLRESBDUEHDKBBAMOLKTDVLLTFRQDFXZTMDTNEOCHNSOLBSGTRSFGSASRJTMEDTNTGOTYVARHADAOAOAEADBNAOAEAEAELAPRVABAAEAEAEAEAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMADAEWYHKLNHH
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02589601000103f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f77e8c088760bfde1dddcf32c17f209b8242ee52aaf131facd88d0ea2c6d0b06f20000000000000000000000000000000000000000000000000000000000000000cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b901020200010c0200000080b2e60e0000000003d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740601
응답 (BitTool Signer → 내 지갑)
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b"
},
"signature": "99b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c"
}
QR 텍스트와 원본 CBOR 보기
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBDAOHDFZNLRHOXDLSBMNWDTYIMVDGSHYKKCXMKSAFLSACYQZTSMEPEJOOEJNLSMOMKJNJSGWTYGHFTYAWNCLSRGOSGEYYLFLWFCLTLDLSRLPJZYAQZPLIOLAFEOTFGFDTADKMWBNDTZCBGGE
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0b02584099b9a42fcb8eead46ae74c5e792098c247c21ab4d791af70a26d8392986d714fd4543af8f121c355ca32f747f321d52fc3856cf8b4ae678045a34648d924940c
지갑이 조립한 결과
45G7cB2HaLBrWEuZxiWR2dFYu52Cje6d2vv5GnCaZ7YSc6ifhFVdFTGpJSk1PU7tSTPHjg3H4uufoe3XPmTRQiJP
AZm5pC/LjurUaudMXnkgmMJHwhq015GvcKJtg5KYbXFP1FQ6+PEhw1XKMvdH8yHVL8OFbPi0rmeARaNGSNkklAwBAAED8DYnYkanW53jNJ7UKxXiMvZRj8IPX81PHWToH5vSWPd+jAiHYL/eHd3PMsF/IJuCQu5SqvEx+s2I0OosbQsG8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzEkOkozS44c7s0P8ldozF5ymD02/RsLDbpEpnVXU5rkBAgIAAQwCAAAAgLLmDgAAAAA=
TransferChecked(소수점 6자리)로 서명자의 연결 토큰 계정 5N3f1tj9v1vc5TUZ8S7mCAnVmjVKrfnzXWhxLaxyZAgt에서 수신자의 FGETo8T8wMcN2wCjav8VK6eh3dLk63evNDPxzLSJra8B로 12.5 USDC를 보냅니다.
요청 (내 지갑 → BitTool Signer)
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c"
},
"2 sign-data": "01000205f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f740d2f27c461f29e9feba1ab8acd39496a9c65b0ae62a713f01f941a993bf55edd3ea8cf5acaca8cd05207512175c43cef54a5dd99ede20a16b55253738f397dcc6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d6106ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b9010404010302000a0c20bcbe000000000006",
"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 보기
UR:SOL-SIGN-REQUEST/1-2/LPADAOCFADFRCYGEKNLGGDHDNNOLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBNAOHDTBADAEAOAHWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLFZTDWZKEFGCTDTWLZERDCYROPSTEMWMTPTSWHPBKVADRJSFHADYTFPPTMURSGOWETEWDLKYKPSPSPDSNAHCXKPBGCHHHFXTOYKGEHLTANNUECXOYJEGODAEMETWFMSUOSWZSKNWFRNUYPMFTFSIHWFIMPYSOJYEHPARKVESATDYNVTVEKEOLAOAXFEDLHLHSAMUTSEPTJPLD
UR:SOL-SIGN-REQUEST/2-2/LPAOAOCFADFRCYGEKNLGGDHDNNYNVYTSIHOYMUTASBVYFGTOWMKKPSCEQZLPWEHEHPEMMEFTLKYKLPKBZMAEPTSFGABAMOLKTDVLLTFRQDFXZTMDTNEOCHNSOLBSGTRSFGSASRJTMEDTNTGOTYVARHADAAAAADAXAOAEBKBNCXRFRNAEAEAEAEAEAMAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMADAEHTNETOQZ
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c0258d601000205f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f740d2f27c461f29e9feba1ab8acd39496a9c65b0ae62a713f01f941a993bf55edd3ea8cf5acaca8cd05207512175c43cef54a5dd99ede20a16b55253738f397dcc6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d6106ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9cc490e928cd2e3873bb343fc95da33179ca60f4dbf46c2c36e91299d55d4e6b9010404010302000a0c20bcbe00000000000603d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740601
응답 (BitTool Signer → 내 지갑)
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c"
},
"signature": "3ad36b0673be3dbb71ec3b8fd346e2a96692f9ffb7b0e55ccdffe2d43f7395a447487e05522b62fed93351603483e1e2278e2193185e39761ae62d901bd1880e"
}
QR 텍스트와 원본 CBOR 보기
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBNAOHDFZFTTEJEAMJKRNFSRKJSWPFRMYTEFGVOPTIYMOYTZMRLPFVWHHSNZMVOTYFHJKMDOXFLFDKBAHGMDNIDZETAEOGYHNEELSVYVODIMNCLMUCSHYESKOCYVADPMHCWTTLOBABBPLAXRD
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0c0258403ad36b0673be3dbb71ec3b8fd346e2a96692f9ffb7b0e55ccdffe2d43f7395a447487e05522b62fed93351603483e1e2278e2193185e39761ae62d901bd1880e
지갑이 조립한 결과
2BDTaXgjMPuTPcFZ7v87D5Vdcn9uFky9RixC5HbiStvCDGjGasBZuymph2oEEg2TmftbyB8EgTPdtHGgE1Rjmrvh
ATrTawZzvj27cew7j9NG4qlmkvn/t7DlXM3/4tQ/c5WkR0h+BVIrYv7ZM1FgNIPh4ieOIZMYXjl2GuYtkBvRiA4BAAIF8DYnYkanW53jNJ7UKxXiMvZRj8IPX81PHWToH5vSWPdA0vJ8Rh8p6f66Gris05SWqcZbCuYqcT8B+UGpk79V7dPqjPWsrKjNBSB1EhdcQ871Sl3Znt4goWtVJTc485fcxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWEG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqcxJDpKM0uOHO7ND/JXaMxecpg9Nv0bCw26RKZ1V1Oa5AQQEAQMCAAoMILy+AAAAAAAG
로그인 메시지입니다. signatureBase58은 dApp이 signMessage에서 기대하는 형식입니다.
요청 (내 지갑 → BitTool Signer)
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d"
},
"2 sign-data": "5369676e20696e20746f204578616d706c6520417070207769746820536f6c616e612e204e6f6e63653a2037516d327850",
"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": 2
}
QR 텍스트와 원본 CBOR 보기
UR:SOL-SIGN-REQUEST/OLADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBTAOHDEHGUINIOJTCXINJTCXJYJLCXFEKSHSJNJOJZIHCXFPJOJOCXKTINJYISCXGUJLJZHSJTHSDMCXGLJLJTIAIHFTCXEMGYJNEYKSGDAXTAADDYOEADLOCSDWYKCFADYKYKAEYKAEYKAOCYJKSKTNBKAAHDCXWTENDIIDFGOSHPNTVLEENNTYDNBZVOEYYNGYMYSABSHESNGWCAIEVSCTNDTDHDYLAHJTFWINJYGHJLJLJZCXHGHSJZJZIHJYAMAORFCXVWPK
a601d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d0258315369676e20696e20746f204578616d706c6520417070207769746820536f6c616e612e204e6f6e63653a2037516d32785003d90130a20188182cf51901f5f500f500f5021a73c5da0a045820f036276246a75b9de3349ed42b15e232f6518fc20f5fcd4f1d64e81f9bd258f7056e426974546f6f6c2057616c6c65740602
응답 (BitTool Signer → 내 지갑)
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d"
},
"signature": "0fad9739ab61e6c9de54b5aa59a00568fd4c12f4e7ac8d6624595c0df3f2f390b7a19b73f9ea08650115a672a81b2e8eb16810a5766519064cbd7f4fc9bb3b01"
}
QR 텍스트와 원본 CBOR 보기
UR:SOL-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTBTAOHDFZBSPMMSESPYHSVASOUEGHREPKHKNBAHISZCGSBGWKVDPSLGIYDKHKHHBTWFWZWFMHRLOYNDJKYTWDAYIHADBZOLJPPDCWDMMNPAISBEONKOIHCFAMGSRYLBGWSORKFRADOXRPNYAT
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0d0258400fad9739ab61e6c9de54b5aa59a00568fd4c12f4e7ac8d6624595c0df3f2f390b7a19b73f9ea08650115a672a81b2e8eb16810a5766519064cbd7f4fc9bb3b01
지갑이 조립한 결과
KBTsdycDs2SLRqv9vaEaq7fKYeSWTRnohspq2ngCgAZhR38CYqB2GqykyQkphhpkYYsCoeNEU5YWiu2hdM2ajwe
서명자가 여럿인 트랜잭션
트랜잭션에 서명자가 여럿 필요한 경우(SPL 멀티시그, Squads 제안 승인, 송신자가 아닌 수수료 지불자) BitTool Signer 키마다 해당 키의 경로와 주소를 넣은 요청을 하나씩 보내세요. 응답 하나가 슬롯 하나를 채웁니다. Squads는 프로그램 수준의 멀티시그입니다. 서명 기기는 제안 내용이 아니라 제안을 승인하는 명령어를 보게 되며, 검토 화면에서도 그렇게 안내합니다.