Ethereum: de historia a labs

Artículo 7 de 48

15%

completado al llegar aquí

Sistemas distribuidosNota de investigación21 min de lectura

Cuentas, claves y direcciones sin humo

Cómo una clave secp256k1 se convierte en dirección con Keccak-256, qué valida el checksum EIP-55 y qué cambia EIP-7702 para quien custodia claves.

Ruta Ethereum: 0/48

Un cliente pide retirar fondos a 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf. Antes de firmar, el equipo de custodia debe poder responder tres preguntas: qué clave controla esa dirección, si el texto está bien escrito y si detrás hay una persona, un contrato o una cuenta que delegó su código. Este capítulo responde las tres con la especificación en la mano.

Ethereum tiene dos tipos de cuenta. Una cuenta de propiedad externa (EOA, externally owned account) la controla quien tenga su clave privada. Una cuenta de contrato la controla su código: no tiene clave privada y solo actúa cuando recibe un mensaje.

Brújula de lectura

Actor, evidencia y estado

Antes de avanzar, ubica quién actúa, qué prueba queda y qué parte del sistema cambia.

Actor

Titular de la clave privada

Quien conoce k firma; ningún servidor de Ethereum guarda esa clave.

Evidencia

Firma ECDSA (v, r, s)

De la firma y el hash se recupera la clave pública y, con ella, la dirección.

Estado

nonce, balance, storageRoot, codeHash

Los cuatro campos de cada cuenta en el estado global.

Preguntas guía

  • ¿Qué operación matemática convierte una clave privada en una dirección y por qué no se puede invertir?
  • ¿Qué detecta exactamente el checksum EIP-55 y qué no detecta?
  • ¿Por qué una transacción no lleva un campo from y aun así la red sabe quién la envió?
  • ¿Qué cambia para la custodia cuando una EOA puede delegar su código a un contrato?

Conexiones dentro de la serie

Los cuatro campos de una cuenta

Piensa en el estado de Ethereum como un mapa gigante cuya clave es una dirección de 20 bytes. Cada valor es una cuenta con cuatro campos.

  • nonce: en una EOA, cuántas transacciones envió; en un contrato, uno más que los contratos que creó (desde EIP-161 el nonce de un contrato empieza en 1). Solo se puede ejecutar una transacción por cada nonce, y eso impide reenviar una transacción ya firmada.
  • balance: los wei que posee la cuenta. Un ether son 101810^{18} wei.
  • storageRoot: la raíz de un árbol de Merkle que resume el almacenamiento persistente del contrato.
  • codeHash: el hash Keccak-256 del código. En una EOA es el hash de la cadena vacía, salvo que haya delegado su código con EIP-7702.

El yellow paper usa la misma definición formal: una cuenta "vacía" es la que tiene codeHash igual a KEC(())\mathrm{KEC}(()), nonce cero y balance cero. La regla práctica es directa: para saber si una dirección tiene código, no mires la interfaz de la wallet; consulta codeHash o el código en un bloque concreto.

Lectura crítica

Qué objeto tengo delante

Recomendado

EOA sin código

codeHash igual al hash vacío. Solo la clave privada autoriza.

Ideal para
Cuentas operativas con política de firma clara.
Ojo con
Perder la clave es perder la cuenta; no hay recuperación en el protocolo.

Contrato

Tiene código desplegado y no tiene clave privada.

Ideal para
Multisig, vaults, reglas de gasto programables.
Ojo con
La seguridad depende del código y de quién puede actualizarlo.
Precaución

EOA delegada (EIP-7702)

Su código es 0xef0100 seguido de la dirección de un contrato delegado.

Ideal para
Batching, patrocinio de gas, sub-claves con permisos menores.
Ojo con
La clave original sigue mandando y el contrato delegado puede mover fondos.
La misma cadena hexadecimal de 20 bytes puede identificar objetos con capacidades distintas.

Traza de derivación: de la clave privada a la dirección

Ethereum firma con ECDSA sobre la curva secp256k1, la misma curva que usa Bitcoin. Una clave privada es un entero kk elegido al azar en el rango [1,n−1][1, n-1], donde nn es el orden del grupo de la curva:

n=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n = \texttt{0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141}

Que kk ocupe 32 bytes no significa que cualquier valor de 32 bytes sea una clave válida. Un generador correcto rechaza el cero y cualquier valor mayor o igual que nn. La probabilidad de caer fuera del rango al azar es despreciable, pero un importador de claves debe validar el rango igual.

La clave pública es el punto K=k⋅GK = k \cdot G, donde GG es el punto generador de la curva. Multiplicar es barato; volver de KK a kk es el problema del logaritmo discreto, que se considera inviable con los recursos actuales. El yellow paper representa KK como 64 bytes: las coordenadas xx e yy concatenadas, sin el byte de prefijo 0x04 que usan otros formatos.

La dirección es el tramo final de un hash:

A(k)=B96..255(KEC(ECDSAPUBKEY(k)))A(k) = \mathcal{B}_{96..255}\big(\mathrm{KEC}(\mathrm{ECDSAPUBKEY}(k))\big)

En palabras: se calcula Keccak-256 de los 64 bytes de la clave pública y se conservan los últimos 160 bits, es decir, los bytes 12 a 31. El resultado son 20 bytes, que se escriben como 40 caracteres hexadecimales con el prefijo 0x.

Figura 1

De la clave privada a la dirección con checksum

Fuentes:

[2][3]
De la clave privada a la dirección con checksum
Cómo leer esta figura

Fila superior: una caja roja con la clave privada k de 32 bytes, entre 1 y n menos 1, una flecha azul rotulada secp256k1 y fácil hacia la clave pública K igual a k por G, de 64 bytes, y otra flecha rotulada Keccak-256 hacia un hash de 32 bytes. Una flecha roja discontinua vuelve de la clave pública a la privada con el rótulo inviable. Fila inferior: del hash baja una flecha a la dirección de 20 bytes, que son los bytes 12 a 31 del hash, escrita en minúsculas. De ahí una flecha rotulada Keccak-256 del hex y nibble mayor o igual que 8 lleva a la forma EIP-55 con mayúsculas, y una última flecha lleva a la verificación que hace la wallet. Una nota indica que con k igual a 1 la dirección es 0x7E5F4552 hasta 29395Bdf.

Visual original de TrautsLab. La clave pública sale de multiplicar el generador de secp256k1 por la clave privada; la dirección son los últimos 20 bytes de su Keccak-256; EIP-55 vuelve a hashear la dirección para decidir qué letras van en mayúscula.

Un detalle que rompe implementaciones: el Keccak-256 de Ethereum es la versión que ganó el concurso SHA-3, no el estándar SHA-3 final publicado después. Los dos producen salidas distintas para la misma entrada. Si un servicio usa una implementación de SHA3-256 (el estándar final) en lugar de Keccak-256, obtendrá direcciones equivocadas sin ningún error visible. Una prueba rápida: Keccak-256 de la cadena vacía empieza por c5d24601, mientras que SHA3-256 de la cadena vacía empieza por a7ffc6f8.

EIP-55: el checksum vive en las mayúsculas

Una dirección en minúsculas no tiene control de errores: si alguien cambia un carácter, sigue siendo una dirección válida de otra persona. EIP-55 añade un checksum sin cambiar la longitud, usando la diferencia entre mayúsculas y minúsculas de las letras a a f.

El algoritmo tiene cuatro pasos:

  1. Escribe la dirección en hexadecimal en minúsculas, sin 0x.
  2. Calcula Keccak-256 de esos 40 caracteres tratados como texto ASCII, no como bytes.
  3. Recorre la dirección. Para cada carácter que sea letra, mira el carácter en la misma posición del hash.
  4. Si ese carácter del hash vale 8 o más, escribe la letra en mayúscula; si no, en minúscula. Los dígitos no cambian.

Con unos 15 bits de control por dirección en promedio, la probabilidad de que un error de tipeo aleatorio pase el control es 0,0247 %, según la propia especificación. Hay dos límites que un equipo de custodia debe conocer. Una dirección escrita toda en minúsculas o toda en mayúsculas no trae checksum, así que no hay nada que verificar. Y el checksum solo detecta errores de escritura: no protege contra una dirección correcta del atacante pegada en lugar de la tuya.

El siguiente fragmento es ilustrativo y reproduce el cálculo completo con la clave de prueba k=1k = 1, que es pública y nunca debe recibir fondos:

javascript
// Ilustrativo. Requiere: npm i @noble/curves@1.4.0 @noble/hashes@1.4.0
import { secp256k1 } from '@noble/curves/secp256k1';
import { keccak_256 } from '@noble/hashes/sha3';
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
 
const k = new Uint8Array(32); k[31] = 1;              // clave de prueba k = 1
const pub = secp256k1.getPublicKey(k, false);         // 65 bytes: 0x04 || x || y
const addr = bytesToHex(keccak_256(pub.slice(1)).slice(12)); // últimos 20 bytes
 
const h = bytesToHex(keccak_256(utf8ToBytes(addr)));  // hash del texto en minúsculas
let eip55 = '0x';
for (let i = 0; i < 40; i++) {
  eip55 += parseInt(h[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i];
}
console.log(eip55); // 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf

Firmas recuperables: por qué no hay campo from

Una transacción firmada no incluye la dirección del emisor. Incluye una firma (v,r,s)(v, r, s) y la red recupera la clave pública a partir de la firma y del hash de la transacción; de esa clave pública sale la dirección. El yellow paper define el emisor exactamente así, aplicando la misma función de dirección al resultado de ECDSARECOVER\mathrm{ECDSARECOVER}. El valor vv indica la paridad de la coordenada yy para elegir entre los dos puntos candidatos.

Tres reglas cierran huecos de esa recuperación:

  • Firma canónica (low-s). Para toda firma válida (r,s)(r, s) existe otra válida (r,n−s)(r, n - s). Desde Homestead, una transacción con s>n/2s > n/2 es inválida. Así nadie puede cambiar el hash de tu transacción reenviándola con la firma "espejo". El precompilado ecrecover sigue aceptando ambos valores, así que un contrato que verifica firmas debe aplicar esa regla por su cuenta.
  • Chain ID. EIP-155 incluye el identificador de la red en lo que se firma, de modo que una transacción firmada con EIP-155 para mainnet (chain ID 1) no es válida en otra red. Una firma en el formato anterior, sin chain ID, no tiene esa protección.
  • Nonce. Cada transacción consume el siguiente nonce del emisor y no puede repetirse.

Firmar un mensaje no es lo mismo que firmar una transacción. EIP-191 antepone el byte 0x19 para que los datos firmados nunca sean una transacción RLP válida; la variante usada por personal_sign añade "\x19Ethereum Signed Message:\n" y la longitud del mensaje. EIP-712 define cómo firmar datos estructurados con un dominio explícito (nombre, versión, chain ID y contrato verificador, entre otros campos). Para una política de custodia, cada formato de firma es un riesgo distinto y debe tener su propia regla de aprobación.

Direcciones de contrato: CREATE, CREATE2 y colisiones

Un contrato no tiene clave, pero sí dirección. Con el opcode CREATE, la dirección son los últimos 20 bytes de Keccak-256 de la codificación RLP del par (emisor, nonce). Por ejemplo, la primera cuenta de prueba de Anvil, 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, despliega su primer contrato con nonce 0 en 0x5FbDB2315678afecb367f032d93F642f64180aa3. Cualquiera puede calcularlo antes del despliegue.

CREATE2 elimina la dependencia del nonce: la dirección sale de keccak256(0xff ++ emisor ++ salt ++ keccak256(init_code)), tomando los últimos 20 bytes. Eso permite saber hoy dónde vivirá un contrato que se desplegará mañana, y es la base de muchas wallets de contrato.

Las direcciones de 160 bits tienen un límite de seguridad conocido. EIP-3607 estima que encontrar una colisión entre una EOA y un contrato cuesta del orden de 2802^{80} operaciones, factible con un presupuesto muy grande. Por eso la red rechaza transacciones cuyo emisor ya tiene código desplegado (desde Pectra, con una sola excepción: el indicador de delegación de EIP-7702 que veremos abajo): así nadie puede desplegar un contrato atractivo y luego vaciarlo con la clave de una EOA que colisiona en la misma dirección.

Riesgo operativo para custodia: semillas, rutas y delegación

Casi ninguna wallet guarda claves sueltas. Guarda una frase mnemónica (BIP-39): de 12 a 24 palabras de las que se deriva una semilla con PBKDF2, HMAC-SHA512 y 2048 iteraciones, usando la frase como contraseña y "mnemonic" más una passphrase opcional como sal. De esa semilla, BIP-32 deriva un árbol de claves sobre secp256k1.

BIP-44 fija la ruta m / purpose' / coin_type' / account' / change / address_index, y el registro SLIP-44 asigna a Ether el coin type 60. Por eso la ruta habitual de la primera cuenta es m/44'/60'/0'/0/0; es, por ejemplo, la ruta por defecto con la que Anvil deriva sus cuentas de prueba. La comilla marca un nivel endurecido (hardened): su derivación requiere la clave privada del padre.

La advertencia de BIP-32 que más importa a custodia es esta: quien conoce una clave pública extendida (xpub) del padre y cualquier clave privada hija no endurecida puede reconstruir la clave privada del padre y, con ella, todo el subárbol. Una xpub no es "solo pública" y no debe circular como si lo fuera.

Desde Pectra, activado en mainnet el 7 de mayo de 2025, una EOA puede tener código. EIP-7702 crea la transacción tipo 0x04 con una lista de autorizaciones [chain_id, address, nonce, y_parity, r, s]. Cada autorización firmada escribe en la EOA el indicador 0xef0100 || address, y desde entonces cualquier llamada a esa EOA ejecuta el código del contrato delegado.

Tres consecuencias directas para un equipo de custodia, todas tomadas de la especificación:

  • Una autorización con chain_id igual a 0 es válida en cualquier cadena que implemente EIP-7702.
  • La delegación persiste: si la transacción que la contiene revierte, el indicador ya escrito no se deshace. Se elimina delegando a la dirección cero.
  • La clave privada original conserva el control total. Un contrato delegado mal escrito puede permitir que un tercero tome casi el control completo de la cuenta.
Lista de verificación

Controles mínimos antes de firmar con una clave custodiada

0 de 6 revisados

Práctica local: derivar y verificar sin tocar fondos

Este ejercicio usa solo valores públicos de prueba. La mnemónica por defecto de Anvil es test test test test test test test test test test test junk y está definida en el código fuente de Foundry; nunca debe recibir fondos reales.

Bash
# Derivar las cuentas 0 y 1 de la mnemónica de prueba (ruta m/44'/60'/0'/0/i)
cast wallet address --mnemonic "test test test test test test test test test test test junk" --mnemonic-index 0
cast wallet address --mnemonic "test test test test test test test test test test test junk" --mnemonic-index 1
 
# Obtener la dirección a partir de una clave privada de prueba
cast wallet address --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
 
# Aplicar el checksum EIP-55 a una dirección en minúsculas
cast to-check-sum-address 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
 
# Calcular la dirección del primer contrato que desplegaría esa cuenta
cast compute-address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --nonce 0

Resultado esperado: la cuenta 0 es 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, la cuenta 1 es 0x70997970C51812dc3A010C7d01b50e0d17dc79C8, el checksum devuelve 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf y la dirección de contrato es 0x5FbDB2315678afecb367f032d93F642f64180aa3. Comprobado con Foundry 1.5.1. Estos valores los recalculamos de forma independiente con el fragmento JavaScript anterior. Si tu herramienta da otros, revisa la ruta de derivación o la función de hash antes de seguir.

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    ethereum.org, "Ethereum accounts," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/accounts/

    official docsID: ETH-ACCOUNTS-DOCSAbrir fuente (abre en otra pestaña)

    Tipos de cuenta, los cuatro campos de una cuenta y derivación de la dirección.

  2. [2]

    G. Wood, "Ethereum: A secure decentralised generalised transaction ledger," Yellow Paper, Shanghai version efc5f9a, 2025.

    formal specID: ETH-YELLOWPAPERAbrir fuente (abre en otra pestaña)

    Especificación formal de estado, transacciones, firma (apéndice F), gas y EVM.

  3. [3]

    V. Buterin and A. Van de Sande, "EIP-55: Mixed-case checksum address encoding," Ethereum Improvement Proposals, 2016.

    Checksum de direcciones con mayúsculas y minúsculas.

  4. [4]

    V. Buterin, "EIP-2: Homestead hard-fork changes," Ethereum Improvement Proposals, 2015.

    Firmas con s mayor que n/2 dejan de ser válidas en transacciones.

  5. [5]

    V. Buterin, "EIP-155: Simple replay attack protection," Ethereum Improvement Proposals, 2016.

    Chain ID dentro de la firma para evitar replay entre redes.

  6. [6]

    M. H. Swende and N. Johnson, "EIP-191: Signed data standard," Ethereum Improvement Proposals, 2016.

    Prefijo 0x19 para que un mensaje firmado no sea una transacción válida.

  7. [7]

    R. Bloemen, L. Logvinov and J. Evans, "EIP-712: Typed structured data hashing and signing," Ethereum Improvement Proposals, 2017.

    Firma de datos estructurados con dominio explícito.

  8. [8]

    V. Buterin, "EIP-1014: Skinny CREATE2," Ethereum Improvement Proposals, 2018.

    Dirección de contrato determinista a partir de salt e initcode.

  9. [9]

    D. Feist, D. Khovratovich and M. van der Wijden, "EIP-3607: Reject transactions from senders with deployed code," Ethereum Improvement Proposals, 2021.

    Colisiones de direcciones de 160 bits y rechazo de emisores con código.

  10. [10]

    M. Palatinus, P. Rusnak, A. Voisine and S. Bowe, "BIP-39: Mnemonic code for generating deterministic keys," Bitcoin Improvement Proposals, 2013.

    Frase mnemónica y derivación de la semilla con PBKDF2-HMAC-SHA512.

  11. [11]

    P. Wuille, "BIP-32: Hierarchical deterministic wallets," Bitcoin Improvement Proposals, 2012.

    Derivación jerárquica sobre secp256k1, claves endurecidas y riesgo de xpub.

  12. [12]

    M. Palatinus and P. Rusnak, "BIP-44: Multi-account hierarchy for deterministic wallets," Bitcoin Improvement Proposals, 2014.

    Ruta m / purpose' / coin_type' / account' / change / address_index.

  13. [13]

    SatoshiLabs, "SLIP-0044: Registered coin types for BIP-0044," SatoshiLabs Improvement Proposals.

    Registro de coin types: 60 corresponde a Ether.

  14. [14]

    Foundry contributors, "anvil/src/config.rs," foundry-rs/foundry, GitHub repository.

    source codeID: FOUNDRY-ANVIL-SRCAbrir fuente (abre en otra pestaña)

    Valores por defecto de Anvil: puerto 8545, chain ID 31337 y mnemónica de prueba.

  15. [15]

    T. Beiko, "EIP-7600: Hardfork meta - Pectra," Ethereum Improvement Proposals, 2024.

    EIPs incluidos en Pectra y su activación en mainnet.

  16. [16]

    V. Buterin, S. Wilson, A. Dietrichs and lightclient, "EIP-7702: Set code for EOAs," Ethereum Improvement Proposals, 2024.

    Transacción tipo 0x04 y autorizaciones que delegan código a una EOA.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

Rutas vecinas

Si este tema te abrió hambre, sigue por aquí.

Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.

Los comentarios se cargan al final de la lectura para no meter peso antes de tiempo.