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 se va a firmar, según el tipo de datos (ver abajo)
3data-typeuint1, 2, 3 o 4
4chain-iduintEl chain ID de EVM. Obligatorio para transacciones; recomendado para todo lo demás
5derivation-path304(crypto-keypath)Ruta completa, p. ej. m/44'/60'/0'/0/0, con la huella digital maestra
6addressbytes (20)La dirección del firmante como bytes sin procesar, no como texto 0x
7origintextoSe muestra al usuario, p. ej. "BitTool Wallet"

Tipos de datos​

data-typesign-dataOrigen típico
1 transacciónLa 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 donde 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 sin procesar del mensaje, sin el prefijo "\x19Ethereum Signed Message:\n" y sin aplicarles 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 en los bytes de la transacción, en la clave 4 y (para datos tipados) en 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 sin procesar de 32 bytes. Las solicitudes para otros chain IDs se rechazan.

Reglas de EIP-712​

BitTool Signer calcula el hash de los datos tipados exactamente como se declaran 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 pueden ser números JSON solo 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 faltantes 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 (single, batch, transfer-from), SafeTx de Safe y órdenes de marketplaces de NFT (Seaport, LooksRare, Blur). Todo lo demás se muestra campo por campo y se marca como parcialmente decodificado.

Mensajes personales​

Cualquier secuencia de 1 a 8,192 bytes. El UTF-8 válido se muestra como texto; cualquier otra cosa, como hex, marcada como opaca. Un mensaje de 32 bytes se marca como de alto riesgo, porque probablemente es 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; es tu billetera la que ensambla la transacción. La parte v depende del tipo de datos:

data-typevLongitud
1 transacción legacychainId × 2 + 35 + yParity (EIP-155), big-endian, con la menor cantidad de bytes necesaria65 bytes o más
4 transacción tipadayParity (0 o 1)65
2, 3 mensajes27 + yParity65

Así que 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 es 86263 o 86264, tres bytes, y la firma mide 67 bytes. El código que siempre toma el byte 64 como v falla con cualquier chain ID de 110 o más.

Para terminar:

  1. Verifica 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 emparejada.
  3. Para transacciones, inserta r, s y la paridad en la transacción sin firmar, serialízala y verifica el hash de la transacción antes de transmitirla.

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ía 0.01 ETH en Ethereum, nonce 12, comisión máxima de 30 gwei, comisión de prioridad de 1 gwei.

Solicitud (tu billetera → 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 billetera)

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 billetera

signedTxHex
0x02f872010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c001a06d11ee93f7876a78ea19f4f9c8e551e4522b6deb8349a6572d076b4e67ceb441a06db3f690b339bf658a5c8d2ad2f75cdf3252f07f940a821f65bb6cb91e9782a1
txHash
0xd7e69271cce78ffdf924b78126b93fb3cdf4f797f7e216bcb6702c8d97b47ea3

Propietarios de Safe (multisig)​

Para usar BitTool Signer como uno de los propietarios de una Safe, firma el SafeTx como EIP-712 (tipo de datos 2) desde la dirección emparejada del propietario; la clave 6 es la dirección del propietario, no la de la Safe. BitTool Signer decodifica CALL/DELEGATECALL, MultiSend, cambios de propietarios/módulos/guards y 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 trabajo de tu billetera.