Ethereum: de historia a labs
Artículo 11 de 48
23%
completado al llegar aquí
Sistemas distribuidosNota de investigación12 min de lectura
ABI, calldata y eventos: el idioma público de los contratos
Cómo se calcula un selector de 4 bytes, cómo se codifican argumentos en palabras de 32 bytes, qué guardan topics y data de un evento y cómo leer un revert.
Ruta Ethereum: 0/48
Prerequisitos recomendados
Si alguno de estos conceptos todavía está medio nebuloso, empieza por aquí. Te ahorra tropiezos y un par de cejas fruncidas.
Un usuario abre su wallet para aprobar un pago y ve un campo de datos que empieza por 0xa9059cbb seguido de 128 caracteres hexadecimales. ¿Qué está autorizando? Responder exige conocer la ABI (Application Binary Interface), el formato que define cómo se convierten llamadas, resultados, eventos y errores en bytes. Este capítulo la recorre con un ejemplo concreto: una transferencia ERC-20.
La EVM no conoce funciones ni tipos: recibe bytes. Que un contrato tenga una función transfer es una convención entre el compilador y quien lo llama, y esa convención está escrita en la especificación ABI de Solidity. [1]
Brújula de lectura
Actor, evidencia y estado
Actor
Frontend, wallet e indexadorCodifican llamadas y decodifican resultados y eventos con el mismo esquema.
Evidencia
Calldata, logs y datos de revertBytes públicos que solo tienen sentido con la ABI correcta.
Estado
Ninguno por sí mismoLa ABI describe la frontera; el estado lo cambia el código que recibe los bytes.
Preguntas guía
- ¿Cómo sabe un contrato qué función quiere ejecutar quien lo llama?
- ¿Por qué cada argumento ocupa 32 bytes aunque sea una dirección de 20?
- ¿Qué parte de un evento se puede filtrar y qué parte solo se puede leer?
- ¿Qué información trae una transacción que revierte y cuánto se puede confiar en ella?
Conexiones dentro de la serie
- Antes: EVM: stack, memory, storage y calldata explica que calldata es de solo lectura y que los logs quedan fuera del estado.
- Después: JSON-RPC: hablar con Ethereum sin magia muestra cómo enviar calldata y pedir logs a un nodo.
- Más adelante: ERC-20: simple por fuera, tramposo por dentro, Approvals, permit y phishing de firmas e Indexadores: eventos, reorgs e idempotencia.
El selector: cuatro bytes de un hash
Los primeros cuatro bytes de la calldata eligen la función. Son los cuatro primeros bytes del Keccak-256 de la firma canónica: el nombre de la función y la lista de tipos de sus parámetros entre paréntesis, separados por comas y sin espacios. El tipo de retorno no forma parte de la firma. [1]
Para transfer(address,uint256) el hash empieza por a9059cbb, que es exactamente el prefijo que vio el usuario. [2] La firma usa tipos canónicos: uint se escribe uint256. Un espacio de más o un alias cambia el hash y produce un selector que ningún contrato reconoce.
Cuatro bytes son valores posibles, así que dos funciones distintas pueden compartir selector. La documentación de ethereum.org lo muestra con el mismo 0xa9059cbb: las bases de datos públicas de firmas registran varias funciones con ese selector, y solo el código fuente verificado del contrato dice cuál es la real. [2] Una herramienta que traduce selectores a nombres está adivinando, no verificando.
Traza de codificación: palabras de 32 bytes, cabeza y cola
Después del selector vienen los argumentos, codificados en palabras de 32 bytes. [1] Para la llamada transfer(0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 10^18) la calldata completa son 68 bytes:
a9059cbb selector
00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8 to (address)
0000000000000000000000000000000000000000000000000de0b6b3a7640000 amount = 10^18Las direcciones y los enteros sin signo se rellenan con ceros a la izquierda hasta 32 bytes; los tipos bytesN de tamaño fijo se rellenan a la derecha. [1] [2] Así, 0x0de0b6b3a7640000 es en hexadecimal: un token con 18 decimales lo mostraría como 1,0.
Los tipos dinámicos, como string, bytes o arrays sin tamaño fijo, no caben en una palabra. La ABI los resuelve en dos zonas. En la cabeza, cada argumento ocupa una palabra: los estáticos van completos y los dinámicos dejan un desplazamiento en bytes. En la cola, cada dinámico se escribe con su longitud seguida de su contenido. La especificación desarrolla el ejemplo completo de f(uint256,uint32[],bytes10,bytes), cuyo selector es 0x8be65246. [1]
La codificación no se describe a sí misma. Los mismos 64 bytes de argumentos se pueden leer como (address,uint256) o como (uint256,uint256) sin ningún error: quien decodifica necesita la ABI correcta, y un decodificador con la ABI equivocada produce números válidos pero falsos.
Anatomía de una llamada transfer y de su evento Transfer
Cómo leer esta figura
Fila superior: una barra de calldata dividida en tres tramos. El primero, a9059cbb, es el selector de 4 bytes tomado del keccak de la firma. El segundo es la palabra 1, una dirección rellenada con ceros a la izquierda que termina en 70997970 y 17dc79c8. El tercero es la palabra 2, un uint256 igual a 10 a la 18, 0de0b6b3a7640000, también rellenado a la izquierda. Fila inferior: cinco cajas del log del evento Transfer: address, el contrato emisor; topics 0, que empieza por ddf252ad y es el keccak de la firma del evento; topics 1, from, y topics 2, to, parámetros indexed que se pueden filtrar; y data, value, con los parámetros no indexados codificados con ABI.
Eventos: topics para buscar, data para leer
Cuando el token ejecuta la transferencia, ERC-20 exige emitir Transfer(address indexed _from, address indexed _to, uint256 _value). [3] Ese evento se convierte en un log con tres partes: la dirección del contrato que lo emitió, hasta cuatro topics de 32 bytes y un campo de datos. [1]
Las reglas de codificación son estas: [1]
topics[0]es el Keccak-256 de la firma del evento, salvo que el evento se declareanonymous. ParaTransfer(address,address,uint256)vale0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.- Los parámetros marcados
indexedocupan los topics siguientes: hasta tres en un evento normal y cuatro en uno anónimo. - Los parámetros no indexados se codifican con ABI en
data.
Los topics existen para que un nodo pueda filtrar sin decodificar: "dame todos los Transfer hacia esta dirección" es una búsqueda por topics[0] y topics[2]. El costo de esa comodidad aparece con los tipos dinámicos. Si un string o un array se marca indexed, el topic guarda su Keccak-256 y no el valor, de modo que se puede buscar un valor conocido pero no recuperar uno desconocido. [1]
Recuerda la frontera del capítulo anterior: los logs no son estado y ningún contrato puede leerlos. [4] Un evento informa a observadores externos; no reemplaza la consulta al storage cuando se necesita el valor vigente.
Errores: el revert también habla ABI
Cuando una llamada revierte, puede devolver datos que explican el fallo, codificados igual que una llamada a función. [1] Solidity usa tres formas: [5]
Error(string), con selector0x08c379a0, pararequire(cond, "mensaje")yrevert("mensaje").Panic(uint256), con selector0x4e487b71, para fallos que indican un bug. Algunos códigos:0x01por unassertfalso,0x11por desbordamiento aritmético fuera de un bloqueunchecked,0x12por división entre cero y0x32por índice de array fuera de rango.- Errores personalizados, como
InsufficientBalance(uint256,uint256), cuyo selector0xcf479181se calcula igual que el de una función.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Ilustrativo: un token mínimo que muestra un evento y un error personalizado.
contract MiniToken {
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
error InsufficientBalance(uint256 available, uint256 required);
function transfer(address to, uint256 amount) external returns (bool) {
uint256 available = balanceOf[msg.sender];
if (available < amount) revert InsufficientBalance(available, amount);
balanceOf[msg.sender] = available - amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}La especificación incluye una advertencia que conviene copiar en cualquier guía interna: nunca confíes en los datos de error. Se propagan a través de llamadas anidadas, así que un contrato puede recibir un error que no definió ninguno de los contratos a los que llamó, y cualquier contrato puede fabricar datos que imitan la firma de un error ajeno. [1]
Riesgo operativo: decodificar sin confiar a ciegas
Leer bien la calldata cambia decisiones de seguridad. Un transfer mueve tokens una vez. Un approve(spender, value), con selector 0x095ea7b3, autoriza a spender a retirar de tu cuenta varias veces hasta value. [3] Los dos se parecen en la pantalla de una wallet que muestra solo bytes, pero el segundo deja un permiso abierto después de la firma. Una política de firma debe distinguirlos por selector y mostrar el spender y el monto antes de aprobar.
La ABI es pública y cualquiera puede imitarla. De ahí salen los riesgos más comunes en producción:
Controles para quien decodifica calldata y eventos
0 de 5 revisados
Los dos últimos puntos tienen respaldo directo. El receipt de una transacción con status 0 no trae logs, y la interfaz JSON-RPC marca un log con removed: true cuando una reorganización de la cadena lo elimina. [6] La descripción de ERC-7730 y del firmado a ciegas está en la documentación de transacciones de ethereum.org. [2]
Laboratorio de decodificación con cast
Estos comandos no necesitan nodo: trabajan solo con la ABI y Keccak-256.
# Selector de una función y topic de un evento
cast sig "transfer(address,uint256)" # 0xa9059cbb
cast sig-event "Transfer(address,address,uint256)" # 0xddf252ad...b3ef
# Codificar la llamada completa y volver a decodificarla
cast calldata "transfer(address,uint256)" 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 1000000000000000000
cast decode-calldata "transfer(address,uint256)" 0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000000de0b6b3a7640000
# Selector de un error personalizado
cast sig "InsufficientBalance(uint256,uint256)" # 0xcf479181Qué observar: cast calldata debe producir los 68 bytes de la traza anterior. Prueba a decodificar los mismos bytes con "approve(address,uint256)": cast los aceptará, porque la ABI no se describe a sí misma, aunque el selector real sea el de transfer. Esa es la razón por la que la ABI se toma del contrato verificado y no de una suposición.
Bibliografía académica
- [1]
Solidity Team, "Contract ABI specification," Solidity documentation.
Selector, codificación de argumentos, eventos y errores.
- [2]
ethereum.org, "Transactions," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/transactions/
Campos de una transacción, tipos y ciclo de vida.
- [3]
F. Vogelsteller and V. Buterin, "ERC-20: Token standard," Ethereum Improvement Proposals, 2015.
Funciones transfer y evento Transfer.
- [4]
Solidity Team, "Contracts: events," Solidity documentation.
Logs inaccesibles desde contratos y hasta tres parámetros indexed.
- [5]
Solidity Team, "Expressions and control structures: error handling," Solidity documentation.
Error(string), Panic(uint256) y códigos de pánico.
- [6]
ethereum.org, "JSON-RPC API," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/apis/json-rpc/
Codificación hex de cantidades y datos, parámetro de bloque y ejemplos curl.
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.
Series relacionadas
HotStuff: de cero a estado del arte
Ruta progresiva para entender consenso BFT moderno: quórums, sincronía parcial, locks, taxonomía one/two/three-chain, QC/highQC, multipipeline, view-change, responsiveness, AggQC, Fast-HotStuff, sync y frontera DAG-BFT.
Substrate: del runtime al consenso propio
Serie de 20 capítulos, de neófito a experto, sobre Substrate (Polkadot SDK): runtime en Wasm, FRAME, extrinsics, estado, pesos, actualizaciones sin fork, Aura, BABE, GRANDPA, sincronización, parachains, XCM, Sassafras, BEEFY y cómo conectar un consenso BFT propio a un nodo.
Ruta práctica: activos digitales en la operación
Nueve módulos, de menor a mayor dependencia, para decidir cómo usar custodia, wallets, llaves, firmas multifirma y MPC, AML/KYT y stablecoins con Fireblocks, BitGo, Kaleido, Circle y Tether.
Labs para convertir lectura en práctica
Prueba de trabajo y reajuste de dificultad
Laboratorio determinista para medir el costo de la prueba de trabajo y ver cómo el reajuste por altura de bloque estabiliza el tiempo entre bloques.
Consensus benchmark
Banco reproducible para comparar consenso BFT y documentar los trade-offs de HotStuff-2.
Ethereum local: Anvil, wallet y primera transacción
Laboratorio base para levantar una red local, crear cuentas, firmar transacciones y observar receipts sin gastar ETH real.
RPC y Cast: leer Ethereum con lupa
Ejercicios con JSON-RPC y Cast para consultar bloques, nonces, gas, receipts, logs y errores reproducibles.
Sigue leyendo
Ideas conectadas con este artículo
Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.
Indexadores: eventos, reorgs e idempotencia
Cómo consumir logs, manejar confirmations, backfills, replays y reorgs sin inventar exactly-once.
Ethereum: mapa de lectura antes de tocar Solidity
Ruta completa para aprender Ethereum desde historia y arquitectura hasta EVM, proof-of-stake, L2, seguridad, DeFi y labs reproducibles.
Antes de Ethereum: Bitcoin, scripts y la máquina mundial
Historia inicial de Ethereum: qué problema vio el whitepaper, por qué los contratos generales importaban y qué cambió frente a Bitcoin.
Frontier, Homestead y The DAO: la red aprendió con golpes
De mainnet inicial a The DAO: primeros forks, tensiones sociales y por qué la historia de Ethereum no es solo técnica.