Los precompilados de Ethereum se comportan como contratos inteligentes integrados en el protocolo de Ethereum. Los nueve precompilados residen en las direcciones 0x01 a 0x09.
La utilidad de los precompilados se divide en cuatro categorías
- Recuperación de firmas digitales de curva elíptica
- Métodos de hash para interactuar con Bitcoin y zcash
- Copia de memoria
- Métodos para habilitar matemáticas de curva elíptica para pruebas de conocimiento cero
Estas operaciones se consideraron lo suficientemente deseables como para tener mecanismos eficientes en términos de gas para realizarlas. Implementar estos algoritmos en Solidity sería considerablemente menos eficiente en cuanto a gas.
Los precompilados no se ejecutan dentro de un contrato inteligente, son parte de la especificación del cliente de Ethereum. Puedes ver una lista de ellos aquí en el Geth Client. Debido a que son una especificación del protocolo, están enumerados en el Ethereum Yellow Paper (en el Apéndice E).
Llamar a Contratos Inteligentes Precompilados con Solidity
La mayoría de los precompilados no tienen un contenedor (wrapper) de Solidity (siendo ecRecover la única excepción). Necesitarás llamar a la dirección directamente con addressOfPrecompile.staticcall(…) o usar assembly.
Aunque ninguno de los contratos precompilados cambia el estado, la función de Solidity que los llama no puede ser pure porque el compilador de Solidity no tiene forma de inferir que una staticcall no cambiará el estado.
Dirección 0x01: ecRecover
ECRecover es el precompilado para recuperar una dirección a partir de un hash y una firma digital para ese hash, es decir, determinar quién lo firmó si la firma es válida. (Aprende más sobre cómo usar firmas digitales en Solidity en nuestro tutorial).
Ejemplo:
function recoverSignature(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
address r = ecrecover(hash, v, r, s);
require(r != address(0), "signature is invalid");
}
Atención: ecrecover no revierte cuando la firma no se valida contra el hash. Devuelve la dirección cero (zero address). Siempre debes verificar esto explícitamente o, mejor aún, usar la biblioteca de Openzeppelin que lo maneja por ti. ¡Hay varias cosas que pueden salir mal con las firmas si no sabes lo que estás haciendo!
Dirección 0x02 y 0x03: SHA-256 y RIPEMD-160
Ambos de estos precompilados generarán un hash de los bytes suministrados en la calldata. Aquí hay un ejemplo con SHA-256. En aras de la simplicidad, aplicaremos un hash a un uint256
function hashSha256(uint256 numberToHash) public view returns (bytes32 h) {
(bool ok, bytes memory out) = address(2).staticcall(abi.encode(numberToHash));
require(ok);
h = abi.decode(out, (bytes32));
}
Y aquí está con RIPEMD-160
function hashRIPEMD160(bytes calldata data) public view returns (bytes20 h) {
(bool ok, bytes memory out) = address(3).staticcall(data);
require(ok);
h = bytes20(abi.decode(out, (bytes32)) << 96);
}
Aunque RIPEMD-160 devuelve 20 bytes, la EVM solo puede trabajar en incrementos de 32 bytes, razón por la cual se utilizan el desplazamiento de bits (bitshifting) y la conversión de tipos (casting) en el código de ejemplo anterior.
¿Por qué Ethereum soporta SHA-256 y RIPEMD-160? Bitcoin hace un uso intensivo de SHA-256 de la misma manera que Ethereum hace un uso intensivo de keccak256. Sin embargo, las direcciones de Bitcoin usan RIPEMD-160 para aplicar un hash a la clave pública y hacer que la dirección pública sea más compacta. Esto es comparable a cómo Ethereum toma los últimos 20 bytes (160 bits, como RIPEMD) del keccak256 de la clave pública ECDSA.
Uso de Yul Assembly
Debido a que el tamaño de retorno se conoce de antemano, no hay necesidad de usar el opcode returndatasize. En Yul (y en el opcode) staticcall toma seis argumentos:
- args
- gas a enviar
- dónde en la memoria buscar los datos a hashear
- tamaño de los datos a hashear (32 bytes)
- dónde escribir la salida
- tamaño de la salida
En el código a continuación, escribimos el uint256 en la memoria y luego lo pasamos a la dirección 2 para aplicar el hash.
function hashSha256Yul(uint256 numberToHash) public view returns (bytes32) {
assembly {
mstore(0, numberToHash) // store number in the zeroth memory word
let ok := staticcall(gas(), 2, 0, 32, 0, 32)
if iszero(ok) {
revert(0,0)
}
return(0, 32)
}
}
Dirección 0x04: Identity
El precompilado identity copia una región de la memoria a otra. Ethereum no tiene un opcode ‘‘memcopy’’ (un opcode para copiar una región en la memoria a otra). Normalmente, tendrías que usar MLOAD para cargar una palabra (word) de memoria en la pila (stack) y luego usar MSTORE para copiarla, y tendrías que hacer la copia palabra por palabra. Con el precompilado identity, puedes copiar un conjunto contiguo de palabras de 32 bytes de una sola vez, en lugar de un byte a la vez.
Dirección 0x05: Modexp
ECDSA no soporta cifrado público. Si una aplicación tiene un caso de uso para esto, entonces se debe usar el clásico cifrado RSA. A alto nivel, RSA funciona tomando un mensaje y elevándolo a la potencia de la clave pública del destinatario, módulo algún número muy grande. El número resultante es el mensaje cifrado. Dado que esto limita severamente la longitud del mensaje, el intercambio típico de mensajes funciona cifrando una clave simétrica como AES-256 y enviándola al destinatario. Luego, el destinatario puede usar la clave AES-256 para descifrar el mensaje.
Firmar mensajes con RSA funciona a la inversa. El remitente eleva el hash del mensaje a la potencia de su clave privada, módulo el número grande (que es de conocimiento público). El resultado es la firma del mensaje. El receptor puede verificar la firma elevando la firma a la potencia de la clave pública, módulo el número grande, y comprobando si el resultado es el hash del mensaje.
Ethereum no tiene una infraestructura de clave pública para RSA. Sin embargo, una dirección de Ethereum podría probar la propiedad de una clave pública RSA firmando con RSA su dirección de Ethereum. Ten en cuenta que esto no funciona a la inversa. Firmar una clave pública RSA con ECDSA no es seguro porque cualquiera puede firmar con ECDSA una cadena arbitraria, incluyendo claves públicas RSA.
Puedes ver una aplicación de RSA con Solidity en nuestro otro artículo sobre el tema.
Aquí hay un ejemplo del uso de modExp con uint256 en Solidity:
function modExp(uint256 base, uint256 exp, uint256 mod) public view returns (uint256) {
bytes memory precompileData = abi.encode(32, 32, 32, base, exp, mod);
(bool ok, bytes memory data) = address(5).staticcall(precompileData);
require(ok, "expMod failed");
return abi.decode(data, (uint256));
}
Dirección 0x06, 0x07 y 0x08: ecAdd, ecMul y ecPairing (EIP-196 y EIP-197)
Estos precompilados se utilizan para hacer que la criptografía de pruebas de conocimiento cero sea más eficiente. De hecho, puedes ver que los tres precompilados se utilizan en el verificador de pruebas de conocimiento cero de Tornado Cash:
Suma de curva elíptica: staticcall a address(6)
Multiplicación de curva elíptica: staticcall a address(7)
Emparejamiento de curva elíptica (Pairing): static call a address(8)
Estas operaciones solo soportan las curvas elípticas BN-128 Barreto-Naehrig. Estas no son las mismas que las curvas elípticas utilizadas para firmas digitales.
ecAdd y ecMul se añadieron en EIP-196 EIP-196 y ecPairing se añadió en EIP-197.
Puedes aprender cómo funcionan estos precompilados en nuestros otros tutoriales:
Curvas elípticas en campos finitos
Emparejamientos bilineales
Costos de Gas para ecAdd, ecMul y ecPairing
Los costos de gas para estos precompilados se redujeron de sus especificaciones originales con la introducción de EIP-1108. Los usuarios deben consultar ese EIP para obtener información actualizada sobre sus costos de gas en lugar de las especificaciones de los respectivos EIP.
Dirección 0x09: Blake2 (EIP-152)
El hash Blake2 es el hash preferido de zcash. Al igual que SHA-256 y RIPEMD-160, Blake2 se añadió para permitir a Ethereum validar afirmaciones sobre transacciones en esa blockchain. Este precompilado se añadió en EIP-152 y hay algo de código de ejemplo disponible en la propuesta.
Dirección 0xa: Precompilado de evaluación de puntos (EIP-4844)
El hardfork de Decun añadió un precompilado en la dirección 10 (dirección 0xa) precompilado en la dirección 10 (dirección 0xa) para verificar compromisos KZG. Es decir, dado un compromiso blob y una prueba de conocimiento cero, el precompilado revierte si la prueba es inválida.
Precompilados en otras cadenas
Los desarrolladores de contratos inteligentes deben tener cuidado al copiar código de Solidity a otras cadenas compatibles con la EVM, ya que los precompilados en esas cadenas podrían no coincidir con los que tiene Ethereum. Por ejemplo, ecrecover y los otros precompilados criptográficos no son compatibles en zksync. (La razón técnica de esto es que la mayoría de los algoritmos de criptografía no son amigables con SNARK, son costosos de verificar desde la perspectiva de las pruebas de conocimiento cero).
Aprende Más
Este material es parte de nuestro bootcamp de Solidity. También puedes aprender Solidity gratis con nuestro curso de Solidity gratuito.
Publicado originalmente el 16 de abril de 2023