Saltar al contenido principal

EVM (eth-sign-request)

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

eth-sign-request​

ClaveCampoTipoContenido
1request-id37(bytes)16 bytes aleatorios (se aceptan de 1 a 64)
2sign-databytesLo que hay que firmar, según el tipo de datos (abajo)
3data-typeuint1, 2, 3 o 4
4chain-iduintEl chain ID de EVM. Obligatorio para transacciones; recomendado para todo
5derivation-path304(crypto-keypath)Ruta completa, p. ej. m/44'/60'/0'/0/0, con la huella maestra
6addressbytes (20)La dirección del firmante como bytes sin procesar, no como texto 0x
7origintextSe muestra al usuario, p. ej. "BitTool Wallet"

Tipos de datos​

data-typesign-dataOrigen habitual
1 transacciónEl RLP de la transacción legacy sin firmar con un chain ID EIP-155: rlp([nonce, gasPrice, gas, to, value, data, chainId, 0, 0])eth_sendTransaction en cadenas en las que usas gas legacy
4 transacción tipadaEl sobre EIP-2718 sin firmar: 0x02 ‖ rlp([...]) (EIP-1559) o 0x01 ‖ rlp([...]) (EIP-2930)eth_sendTransaction, la mayoría de los envíos modernos
3 mensaje personalLos bytes del mensaje sin procesar, sin el prefijo "\x19Ethereum Signed Message:\n" y sin hashpersonal_sign
2 datos tipadosEl JSON EIP-712 en UTF-8: {types, primaryType, domain, message}eth_signTypedData_v4

Envía bytes, nunca un hash. BitTool Signer analiza lo que firma y se niega a firmar a ciegas digests de 32 bytes.

Redes​

El chain ID de los bytes de la transacción, el de la clave 4 y (para datos tipados) el de domain.chainId deben coincidir, y deben ser uno de estos:

Chain IDRedChain IDRed
1Ethereum10OP Mainnet
56BNB Chain137Polygon PoS
42161Arbitrum One100Gnosis
8453Base43114Avalanche C-Chain

No compatible​

Transacciones blob (tipo 3), transacciones EIP-7702 (tipo 4), transacciones legacy no protegidas sin chain ID y eth_sign de un hash de 32 bytes sin procesar. Las solicitudes para otros chain IDs se rechazan.

Reglas de EIP-712​

BitTool Signer calcula el hash de los datos tipados exactamente según están declarados y es estricto con los valores, porque un valor que se muestra de una forma pero se hashea de otra es un truco clásico de phishing:

  • Los valores bool deben ser true/false de JSON, no cadenas.
  • Los enteros solo pueden ser números JSON hasta 253 − 1; los valores mayores deben ser cadenas decimales sin ceros a la izquierda, o cadenas hex 0x.
  • Los valores address son 0x + 40 dígitos hex, y las direcciones con mayúsculas y minúsculas mezcladas deben tener un checksum EIP-55 válido; bytesN debe tener exactamente N bytes.
  • El message debe tener exactamente los campos que declara su tipo: ni campos que falten ni campos adicionales.
  • EIP712Domain solo puede contener name, version, chainId, verifyingContract y salt; para el separador de dominio se usa el orden declarado.

Las estructuras conocidas reciben una revisión completa: Permit de ERC-2612 / DAI, Permit2 (simple, por lotes, transfer-from), SafeTx de Safe y órdenes de marketplaces de NFT (Seaport, LooksRare, Blur). Todo lo demás se muestra campo a campo y se marca como parcialmente decodificado.

Mensajes personales​

Cualquier secuencia de bytes de 1 a 8192 de longitud. El UTF-8 válido se muestra como texto; todo lo demás, como hex, marcado como opaco. Un mensaje de 32 bytes se marca como de alto riesgo, porque probablemente sea un hash que el usuario no puede leer.

eth-signature​

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

La firma no es una transacción firmada; esa la ensambla tu cartera. La parte v depende del tipo de datos:

data-typevLongitud
1 transacción legacychainId × 2 + 35 + yParity (EIP-155), big-endian, con tan pocos bytes como haga falta65 bytes o más
4 transacción tipadayParity (0 o 1)65
2, 3 mensajes27 + yParity65

Por tanto, lee v como un entero big-endian sin signo desde el byte 64 hasta el final. Por ejemplo, en Avalanche (chain ID 43114) un v legacy vale 86263 o 86264, ocupa tres bytes, y la firma tiene 67 bytes. El código que siempre toma el byte 64 como v falla con cualquier chain ID igual o superior a 110.

Para terminar:

  1. Comprueba el ID de solicitud.
  2. Recupera el firmante a partir del digest que esperas (hash de la transacción, hash EIP-191 o hash EIP-712) y compáralo con la dirección vinculada.
  3. Para las transacciones, inserta r, s y la paridad en la transacción sin firmar, serialízala y comprueba el hash de la transacción antes de difundirla.

Código​

Con ethers y @keystonehq/bc-ur-registry-eth, de Keystone:

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
}

Vectores de prueba​

Todos desde m/44'/60'/0'/0/0 (0x9858EfFD232B4033E47d90003D41EC34EcaEda94) de la frase de prueba.

Envío de 0.01 ETH en Ethereum, nonce 12, comisión máxima de 30 gwei y comisión de prioridad de 1 gwei.

Solicitud (tu cartera → BitTool Signer)

eth-sign-request1 fotogramas
UR:ETH-SIGN-REQUEST/OSADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDEHAOWSADBNLRFRNYSGAELPAMZTCNPSAELFGMAYMWAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEUEPMLTCNLNWZJLSEAEAELARTAXAAAAADAHTAADDYOEADLECSDWYKCSFNYKAEYKAEWKAEWKAOCYJKSKTNBKAMGHMKHDWSZCCNDNFZEOVEKIMHAEFSFPWPEEWPPLTNMWATJTFWINJYGHJLJLJZCXHGHSJZJZIHJYKEBDYTMH
Campos 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"
}
Mostrar el texto del QR y el CBOR sin procesar
Texto del QR · ur:eth-sign-request
UR:ETH-SIGN-REQUEST/OSADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDEHAOWSADBNLRFRNYSGAELPAMZTCNPSAELFGMAYMWAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEUEPMLTCNLNWZJLSEAEAELARTAXAAAAADAHTAADDYOEADLECSDWYKCSFNYKAEYKAEWKAEWKAOCYJKSKTNBKAMGHMKHDWSZCCNDNFZEOVEKIMHAEFSFPWPEEWPPLTNMWATJTFWINJYGHJLJLJZCXHGHSJZJZIHJYKEBDYTMH
CBOR del UR (hex)
a701d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0102583102ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c00304040105d90130a2018a182cf5183cf500f500f400f4021a73c5da0a06549858effd232b4033e47d90003d41ec34ecaeda94076e426974546f6f6c2057616c6c6574

Respuesta (BitTool Signer → tu cartera)

Respuesta decodificada · ur:eth-signature
{
"request-id": {
"uuid": "5f1e9b2c7d3a4e6f8a0b1c2d3e4f5a01"
},
"signature": "6d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb4416db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a101"
}
Mostrar el texto del QR y el CBOR sin procesar
Texto del QR · ur:eth-signature
UR:ETH-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDFPJNBYWYMUYLLTIMKSWDCFWKYTSPVWGYVEGMDNJNWMLSGAOLHGDPATJEGLIOTOQZFPJNQDYNMHQDESRSIHLEHHLGDRTDYLHHUREYGMWTLBMWBKLFCTIHRKJZRHCKMSLFOYADCWJNLUAY
CBOR del UR (hex)
a201d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a010258416d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb4416db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a101

Ensamblado por tu cartera

signedTxHex
0x02f872010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c001a06d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb441a06db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a1
txHash
0xd7e69271cce78ffdf924b78126b93fb3cdf4f797f7e216bcb6702c8d97b47ea3

Propietarios de Safe (multifirma)​

Para usar BitTool Signer como uno de los propietarios de un Safe, firma la SafeTx como EIP-712 (tipo de datos 2) desde la dirección vinculada del propietario; la clave 6 es la dirección del propietario, no la del Safe. BitTool Signer decodifica CALL/DELEGATECALL, MultiSend, los cambios de propietarios, módulos y guards, y los parámetros de reembolso de gas, y eleva el nivel de riesgo de los que son peligrosos. Reunir, ordenar y enviar las firmas de los propietarios es tarea de tu cartera.