본문으로 건너뛰기

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를 포함한 서명되지 않은 legacy 트랜잭션 RLP: rlp([nonce, gasPrice, gas, to, value, data, chainId, 0, 0])legacy 가스를 사용하는 체인의 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 트랜잭션(type 3), EIP-7702 트랜잭션(type 4), 체인 ID가 없는 비보호 legacy 트랜잭션, 원시 32바이트 해시에 대한 eth_sign은 지원하지 않습니다. 다른 체인 ID에 대한 요청은 거부됩니다.

EIP-712 규칙​

BitTool Signer는 타입 데이터를 선언된 그대로 해시하며 값에 엄격합니다. 화면에는 한 가지로 보이면서 해시는 다르게 계산되는 값은 전형적인 피싱 수법이기 때문입니다.

  • bool 값은 문자열이 아닌 JSON true/false여야 합니다.
  • 정수는 253 − 1까지만 JSON 숫자로 쓸 수 있습니다. 그보다 큰 값은 앞자리 0이 없는 10진수 문자열이나 0x 16진수 문자열이어야 합니다.
  • address 값은 0x + 16진수 40자리이며, 대소문자가 섞인 주소는 유효한 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). 그 밖의 구조는 필드별로 보여 주고 부분 해석(partially decoded)으로 표시합니다.

개인 메시지​

1바이트부터 8,192바이트까지의 임의 바이트입니다. 유효한 UTF-8은 텍스트로 보여 주고, 그 밖의 데이터는 hex로 보여 주며 해석 불가(opaque)로 표시합니다. 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), 빅엔디언, 필요한 최소 바이트 수65바이트 이상
4 타입 트랜잭션yParity (0 또는 1)65
2, 3 메시지27 + yParity65

따라서 v는 64번째 바이트부터 끝까지를 부호 없는 빅엔디언 정수로 읽어야 합니다. 예를 들어 Avalanche(체인 ID 43114)에서 legacy v는 86263 또는 86264로 3바이트이며, 서명 길이는 67바이트입니다. 항상 64번째 바이트만 v로 읽는 코드는 체인 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의 소유자 중 하나로 사용하려면, 소유자의 페어링된 주소에서 SafeTx를 EIP-712(데이터 타입 2)로 서명하세요. 키 6에는 Safe의 주소가 아니라 소유자의 주소를 넣습니다. BitTool Signer는 CALL/DELEGATECALL, MultiSend, 소유자/모듈/가드 변경, 가스 환급 파라미터를 해석하며, 위험한 항목은 위험 수준을 높여 표시합니다. 소유자 서명을 수집하고 정렬해 제출하는 일은 지갑이 담당합니다.