メインコンテンツまでスキップ

EVM(eth-sign-request)

ur:eth-sign-requestur:eth-signatureEIP-155EIP-1559EIP-712

eth-sign-request​

キーフィールド型内容
1request-id37(bytes)16 バイトのランダム値(1〜64 バイトも可)
2sign-databytes署名対象。データタイプによって異なる(後述)
3data-typeuint1、2、3、4 のいずれか
4chain-iduintEVM のチェーン ID。トランザクションでは必須、それ以外でも推奨
5derivation-path304(crypto-keypath)フルパス(例:m/44'/60'/0'/0/0)とマスターフィンガープリント
6addressbytes (20)署名者のアドレスの生バイト列。0x で始まるテキストではない
7origintextユーザーに表示される(例:"BitTool Wallet")

データタイプ​

data-typesign-data主な呼び出し元
1 トランザクションEIP-155 のチェーン ID を含む 未署名のレガシートランザクションの RLP:rlp([nonce, gasPrice, gas, to, value, data, chainId, 0, 0])レガシーのガス設定を使うチェーンでの eth_sendTransaction
4 型付きトランザクション未署名の EIP-2718 エンベロープ:0x02 ‖ rlp([...])(EIP-1559)または 0x01 ‖ rlp([...])(EIP-2930)eth_sendTransaction、現在の送金の大半
3 パーソナルメッセージメッセージの生バイト列。"\x19Ethereum Signed Message:\n" プレフィックスを付けず、ハッシュもしないpersonal_sign
2 型付きデータUTF-8 の EIP-712 JSON:{types, primaryType, domain, message}eth_signTypedData_v4

ハッシュではなく、必ずバイト列を送ってください。BitTool Signer は署名する内容をパースしており、中身の分からない 32 バイトのダイジェストへのブラインド署名は拒否します。

ネットワーク​

トランザクションのバイト列内のチェーン ID、キー 4、そして(型付きデータの場合は)domain.chainId はすべて一致し、かつ次のいずれかでなければなりません。

チェーン IDネットワークチェーン IDネットワーク
1Ethereum10OP Mainnet
56BNB Chain137Polygon PoS
42161Arbitrum One100Gnosis
8453Base43114Avalanche C-Chain

非対応​

Blob トランザクション(タイプ 3)、EIP-7702 トランザクション(タイプ 4)、チェーン ID のない保護されていないレガシートランザクション、生の 32 バイトハッシュに対する eth_sign には対応していません。その他のチェーン ID のリクエストは拒否されます。

EIP-712 のルール​

BitTool Signer は型付きデータを宣言どおりにハッシュし、値を厳密に扱います。表示と異なる形でハッシュされる値は、典型的なフィッシングの手口だからです。

  • bool の値は文字列ではなく、JSON の true/false でなければなりません。
  • 整数を JSON の数値で表せるのは 253 − 1 までです。それより大きい値は、先頭にゼロのない 10 進文字列、または 0x の 16 進文字列にしてください。
  • address の値は 0x + 16 進 40 桁で、大文字小文字が混在するアドレスは有効な EIP-55 チェックサムを持つ必要があります。bytesN はちょうど N バイトでなければなりません。
  • message は、その型が宣言するフィールドを過不足なく持つ必要があります。欠けているフィールドも余分なフィールドも不可です。
  • EIP712Domain に含められるのは name、version、chainId、verifyingContract、salt のみです。ドメインセパレーターには宣言された順序が使われます。

既知の構造は完全に解析して表示します。ERC-2612 / DAI の Permit、Permit2(single、batch、transfer-from)、Safe の SafeTx、NFT マーケットプレイスの注文(Seaport、LooksRare、Blur)です。それ以外はフィールドごとに表示され、部分的にデコードされたものとして示されます。

パーソナルメッセージ​

1〜8,192 バイトの任意のバイト列です。有効な UTF-8 はテキストとして、それ以外は不透明なデータとして 16 進で表示されます。32 バイトのメッセージは、ユーザーが読めないハッシュである可能性が高いため、高リスクとして警告されます。

eth-signature​

eth-signature = {
1: 37(request-id bytes), ; copied from the request
2: signature ; r (32) ‖ s (32) ‖ v (1 or more bytes)
}

この署名は署名済みトランザクションでは ありません。署名済みトランザクションはウォレット側で組み立てます。v の部分はデータタイプによって異なります。

data-typev長さ
1 レガシートランザクションchainId × 2 + 35 + yParity(EIP-155)、ビッグエンディアン、必要最小限のバイト数65 バイト以上
4 型付きトランザクションyParity(0 または 1)65
2、3 メッセージ27 + yParity65

したがって、v はバイト 64 から末尾までを符号なしビッグエンディアン整数として読み取ってください。たとえば Avalanche(チェーン ID 43114)では、レガシーの v は 86263 または 86264 の 3 バイトで、署名全体は 67 バイトになります。常にバイト 64 だけを v とみなすコードは、チェーン ID が 110 以上のチェーンで動かなくなります。

仕上げの手順は次のとおりです。

  1. リクエスト ID を確認します。
  2. 期待するダイジェスト(トランザクションハッシュ、EIP-191 ハッシュ、または EIP-712 ハッシュ)から署名者を復元し、ペアリング済みのアドレスと比較します。
  3. トランザクションの場合は、r、s、パリティを未署名トランザクションに挿入してシリアライズし、ブロードキャストする前にトランザクションハッシュを確認します。

コード​

ethers と Keystone の @keystonehq/bc-ur-registry-eth を使う例です。

examples/evm.mjs
import { randomUUID } from 'node:crypto';
import { Transaction, Signature, getBytes } from 'ethers';
import { EthSignRequest, DataType, ETHSignature } from '@keystonehq/bc-ur-registry-eth';
import { URDecoder } from '@ngraveio/bc-ur';

export function buildEthRequest(tx, { path, masterFingerprint, address }) {
const unsigned = Transaction.from(tx); // no signature yet
const requestId = randomUUID();
const request = EthSignRequest.constructETHRequest(
Buffer.from(getBytes(unsigned.unsignedSerialized)), // the bytes to sign, not a hash
unsigned.type === 0 ? DataType.transaction : DataType.typedTransaction, // 1 or 4
path, // "m/44'/60'/0'/0/0"
masterFingerprint, // "73c5da0a"
requestId,
Number(unsigned.chainId),
address,
'BitTool Wallet', // origin, shown to the user
);
const encoder = request.toUREncoder(400); // fragment length in bytes
const frames = [];
for (let i = 0; i < encoder.fragmentsLength; i++) frames.push(encoder.nextPart().toUpperCase());
return { unsigned, requestId, frames };
}
examples/evm.mjs
export function readEthSignature(frames, { unsigned, 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 !== 'eth-signature') throw new Error(`Expected eth-signature, got ${ur.type}`);

const response = ETHSignature.fromCBOR(ur.cbor);
const id = response.getRequestId();
if (!id || Buffer.from(id).toString('hex') !== requestId.replace(/-/g, '')) throw new Error('Signature is for another request');

const sig = Buffer.from(response.getSignature()); // r (32) ‖ s (32) ‖ v (1+ bytes)
const r = '0x' + sig.subarray(0, 32).toString('hex');
const s = '0x' + sig.subarray(32, 64).toString('hex');
const v = BigInt('0x' + sig.subarray(64).toString('hex'));
const yParity = unsigned.type === 0 ? Number(v - unsigned.chainId * 2n - 35n) : Number(v);
if (yParity !== 0 && yParity !== 1) throw new Error('Unexpected v');

const signed = unsigned.clone();
signed.signature = Signature.from({ r, s, yParity });
if (signed.from !== address) throw new Error('Signed by an unexpected key');
return signed.serialized; // ready for eth_sendRawTransaction
}

テストベクター​

すべてテストフレーズの m/44'/60'/0'/0/0(0x9858EfFD232B4033E47d90003D41EC34EcaEda94)から生成しています。

Ethereum で 0.01 ETH を送金します。nonce 12、最大手数料 30 gwei、優先手数料 1 gwei です。

リクエスト(ウォレット → BitTool Signer)

eth-sign-request1 フレーム
UR:ETH-SIGN-REQUEST/OSADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDEHAOWSADBNLRFRNYSGAELPAMZTCNPSAELFGMAYMWAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEUEPMLTCNLNWZJLSEAEAELARTAXAAAAADAHTAADDYOEADLECSDWYKCSFNYKAEYKAEWKAEWKAOCYJKSKTNBKAMGHMKHDWSZCCNDNFZEOVEKIMHAEFSFPWPEEWPPLTNMWATJTFWINJYGHJLJLJZCXHGHSJZJZIHJYKEBDYTMH
CBOR フィールド
{
"1 request-id": {
"uuid (tag 37)": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a01"
},
"2 sign-data": "02ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c0",
"3 data-type": 4,
"4 chain-id": 1,
"5 derivation-path": {
"crypto-keypath (tag 304)": {
"1 components": [
44,
true,
60,
true,
0,
true,
0,
false,
0,
false
],
"2 source-fingerprint": 1942346250
}
},
"6 address": "9858effd232b4033e47d90003d41ec34ecaeda94",
"7 origin": "BitTool Wallet"
}
QR テキストと生の CBOR を表示
QR テキスト · ur:eth-sign-request
UR:ETH-SIGN-REQUEST/OSADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDEHAOWSADBNLRFRNYSGAELPAMZTCNPSAELFGMAYMWAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEUEPMLTCNLNWZJLSEAEAELARTAXAAAAADAHTAADDYOEADLECSDWYKCSFNYKAEYKAEWKAEWKAOCYJKSKTNBKAMGHMKHDWSZCCNDNFZEOVEKIMHAEFSFPWPEEWPPLTNMWATJTFWINJYGHJLJLJZCXHGHSJZJZIHJYKEBDYTMH
UR CBOR(16 進)
a701d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0102583102ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c00304040105d90130a2018a182cf5183cf500f500f400f4021a73c5da0a06549858effd232b4033e47d90003d41ec34ecaeda94076e426974546f6f6c2057616c6c6574

レスポンス(BitTool Signer → ウォレット)

デコードしたレスポンス · ur:eth-signature
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a01"
},
"signature": "6d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb4416db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a101"
}
QR テキストと生の CBOR を表示
QR テキスト · ur:eth-signature
UR:ETH-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDFPJNBYWYMUYLLTIMKSWDCFWKYTSPVWGYVEGMDNJNWMLSGAOLHGDPATJEGLIOTOQZFPJNQDYNMHQDESRSIHLEHHLGDRTDYLHHUREYGMWTLBMWBKLFCTIHRKJZRHCKMSLFOYADCWJNLUAY
UR CBOR(16 進)
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a010258416d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb4416db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a101

ウォレット側で組み立てた結果

signedTxHex
0x02f872010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c001a06d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb441a06db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a1
txHash
0xd7e69271cce78ffdf924b78126b93fb3cdf4f797f7e216bcb6702c8d97b47ea3

Safe(マルチシグ)のオーナー​

BitTool Signer を Safe のオーナーの 1 人として使うには、オーナーのペアリング済みアドレスから SafeTx を EIP-712(データタイプ 2)として署名します。キー 6 は Safe のアドレスではなく、オーナー のアドレスです。BitTool Signer は CALL/DELEGATECALL、MultiSend、オーナー・モジュール・ガードの変更、ガス払い戻しのパラメーターをデコードし、危険なものについてはリスクレベルを引き上げます。オーナーの署名を集めてソートし、送信するのはウォレット側の役割です。