跳至主要内容

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 chain ID。交易必填;其他所有類型也建議提供
5derivation-path304(crypto-keypath)完整路徑,例如 m/44'/60'/0'/0/0,並附上主指紋
6addressbytes(20)簽名者地址的原始位元組,而非 0x 文字
7origintext向使用者顯示,例如 "BitTool Wallet"

資料類型​

data-typesign-data常見來源
1 交易帶有 EIP-155 chain ID 的 未簽名 legacy 交易 RLP:rlp([nonce, gasPrice, gas, to, value, data, chainId, 0, 0])在使用 legacy gas 的鏈上執行 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 位元組的摘要。

網路​

交易位元組中的 chain ID、鍵 4 的值,以及(對 typed data 而言)domain.chainId 必須全部一致,且必須是下列其中之一:

Chain ID網路Chain ID網路
1Ethereum10OP Mainnet
56BNB Chain137Polygon PoS
42161Arbitrum One100Gnosis
8453Base43114Avalanche C-Chain

不支援的項目​

Blob 交易(type 3)、EIP-7702 交易(type 4)、沒有 chain ID 的無保護 legacy 交易,以及對原始 32 位元組雜湊執行的 eth_sign。其他 chain ID 的請求會被拒絕。

EIP-712 規則​

BitTool Signer 會完全依照宣告的方式對 typed data 計算雜湊,並嚴格檢查各個值,因為「顯示的是一種值、雜湊的卻是另一種」是典型的網路釣魚手法:

  • bool 值必須是 JSON 的 true/false,不能是字串。
  • 整數只有在不超過 253 − 1 時才能使用 JSON 數字;更大的值必須是不含前導零的十進位字串,或 0x hex 字串。
  • address 值為 0x + 40 位 hex 數字,大小寫混合的地址必須具有有效的 EIP-55 校驗碼;bytesN 必須正好是 N 個位元組。
  • message 必須恰好具有其類型所宣告的欄位:不可缺少,也不可多出。
  • EIP712Domain 只能包含 name、version、chainId、verifyingContract 與 salt;domain separator 會依宣告的順序計算。

已知的結構會得到完整審核:ERC-2612 / DAI Permit、Permit2(single、batch、transfer-from)、Safe SafeTx,以及 NFT 市集訂單(Seaport、LooksRare、Blur)。其他內容則會逐欄位顯示,並標示為部分解碼。

個人訊息​

長度為 1 到 8,192 位元組的任意位元組。有效的 UTF-8 會以文字顯示;其他內容則以 hex 顯示,並標示為不透明內容。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 legacy 交易chainId × 2 + 35 + yParity(EIP-155),big-endian,使用所需的最少位元組65 位元組以上
4 型別化交易yParity(0 或 1)65
2、3 訊息27 + yParity65

因此,請將位元組索引 64 到結尾的部分讀取為 big-endian 無號整數 v。例如在 Avalanche(chain ID 43114)上,legacy 的 v 是 86263 或 86264,佔三個位元組,簽名長度為 67 位元組。總是只取位元組索引 64 作為 v 的程式碼,在 chain ID 為 110 以上的任何鏈上都會出錯。

完成步驟:

  1. 檢查請求 ID。
  2. 從你預期的摘要(交易雜湊、EIP-191 雜湊或 EIP-712 雜湊)還原簽名者,並與配對的地址比較。
  3. 對於交易,將 r、s 與 parity 填入未簽名的交易並序列化,在廣播前檢查交易雜湊。

程式碼​

使用 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(hex)
a701d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0102583102ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c00304040105d90130a2018a182cf5183cf500f500f400f4021a73c5da0a06549858effd232b4033e47d90003d41ec34ecaeda94076e426974546f6f6c2057616c6c6574

回應(BitTool Signer → 你的錢包)

解碼後的回應 · ur:eth-signature
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a01"
},
"signature": "6d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb4416db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a101"
}
顯示 QR 文字與原始 CBOR
QR 文字 · ur:eth-signature
UR:ETH-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDFPJNBYWYMUYLLTIMKSWDCFWKYTSPVWGYVEGMDNJNWMLSGAOLHGDPATJEGLIOTOQZFPJNQDYNMHQDESRSIHLEHHLGDRTDYLHHUREYGMWTLBMWBKLFCTIHRKJZRHCKMSLFOYADCWJNLUAY
UR CBOR(hex)
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a010258416d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb4416db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a101

由你的錢包組裝

signedTxHex
0x02f872010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c001a06d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb441a06db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a1
txHash
0xd7e69271cce78ffdf924b78126b93fb3cdf4f797f7e216bcb6702c8d97b47ea3

Safe(多重簽名)擁有者​

若要將 BitTool Signer 作為 Safe 的其中一位擁有者,請從該擁有者的配對地址以 EIP-712(資料類型 2)簽署 SafeTx;鍵 6 是 擁有者 的地址,而非 Safe 的地址。BitTool Signer 會解碼 CALL/DELEGATECALL、MultiSend、擁有者/模組/guard 變更以及 gas 退款參數,並針對危險項目提高風險等級。收集、排序與提交擁有者簽名,則是你的錢包的工作。