WEB3DEV Español

Cover image for Cómo firmar Datos EIP-712 Estructurados con MetaMask
Hector
Hector

Posted on

Cómo firmar Datos EIP-712 Estructurados con MetaMask

Image description

MetaMask es una de las criptobilleteras más usadas, pero ofrece mucho más que eso. Con su ayuda, tenemos la posibilidad de firmar digitalmente datos estructurados los cuales pueden ser utilizados en muchas otras formas. Una opción es usar MetaMask para autenticar a nuestros usuarios. En este caso, identificamos nuestros usuarios con una dirección Ethereum en vez de una clave. El usuario proporciona la titularidad de la dirección Ethereum firmando digitalmente datos estructurados y luego, la firma es validada desde el lado del servidor. Como MetaMask almacena de forma segura nuestra clave privada e incluso ofrece la opción para usar billeteras hardware, este tipo de autenticación es mucho más segura que las soluciones tradicionales basadas en claves tradicionales.

También está la opción para firmar ciertas llamadas API con MetaMask. Por ejemplo, en el caso del servicio financiero, además de la autenticación de MetaMask, podemos firmar transacciones financieras individuales de forma separada, haciéndolas mucho más seguras.

Las firmas de MetaMask pueden ser validadas en la blockchain con la ayuda de un contrato inteligente. Una área de uso para esto son las metatransacciones. Con las metatransacciones, podemos simplificar el uso de las aplicaciones blockchain para los usuarios. En el caso de las metatransacciones, las tarifas de las transacciones son pagadas por un servidor de retransmisión en vez del usuario, así que el usuario no tiene que tener criptomonedas. El usuario simplemente junta la transacción, la firma con MetaMask y lo envía al servidor de retransmisión, el cual lo reenvía al contrato inteligente. El contrato inteligente valida la firma digital y ejecuta la transacción.

Luego de la teoría, veamos la práctica.

El estándar EIP-712 define cómo firmar los paquetes de datos estructurados en un formato leíble para el usuario. En una estructura conforme EIP-712, como se muestra en MetaMask (puede ser probado en esta URL) se ve así:

Image description

La transacción de arriba fue generado usando el siguiente código simple:

async function main() {
     if (!window.ethereum || !window.ethereum.isMetaMask) {
       console.log("Please install MetaMask")
       return
     }

     const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
     const chainId = await window.ethereum.request({ method: 'eth_chainId' });
     const eip712domain_type_definition = {
       "EIP712Domain": [
         {
           "name": "name",
           "type": "string"
         },
         {
           "name": "version",
           "type": "string"
         },
         {
           "name": "chainId",
           "type": "uint256"
         },
         {
           "name": "verifyingContract",
           "type": "address"
         }
       ]
     }
     const karma_request_domain = {
       "name": "Karma Request",
       "version": "1",
       "chainId": chainId,
       "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
     }

     document.getElementById('transfer_request')?.addEventListener("click", async function () {
       const transfer_request = {
         "types": {
           ...eip712domain_type_definition,
           "TransferRequest": [
             {
               "name": "to",
               "type": "address"
             },
             {
               "name": "amount",
               "type": "uint256"
             }
           ]
         },
         "primaryType": "TransferRequest",
         "domain": karma_request_domain,
         "message": {
           "to": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
           "amount": 1234
         }
       }
       let signature = await window.ethereum.request({
         "method": "eth_signTypedData_v4",
         "params": [
           accounts[0],
           transfer_request
         ]
       })
       alert("Signature: " + signature)
     })
   }
   main()
Enter fullscreen mode Exit fullscreen mode

El eip712domain_type_definition es una descripción de estructura general, el cual contiene los metadatos. El nombre del campo es el nombre de la estructura, el campo de la versión es la definición de la versión de la estructura y los campos chainID y verifyingContract determinan cuál contrato para los que está destinado el mensaje. El contrato ejecutador verifica estos metadatos para poder asegurar que la transacción firmada sólo sea ejecutada en el contrato objetivo.

karma_request_domain contiene el valor específico de los metadatos definidos por la estructura EIP712Domain.

La estructura real que enviamos a MetaMask para la firma está contenida en la variable transfer_request. Estos tipos de bloques contienen el tipo de definiciones. Aquí, el primer elemento es la definición mandatoria de EIP712Domain, el cual describe los metadatos. Esto es seguido por la definición de la estructura real, que en nuestro caso es TransferRequest. Esta es la estructura que aparecerá en MetaMask para el usuario. El dominio del bloque contiene valores específicos de los metadatos, mientras que el mensaje contiene la estructura específica que queremos firmar con el usuario.

La firma puede ser validada fácilmente en el lado del servidor, usando eth-sigutil:

import { recoverTypedSignature } from '@metamask/eth-sig-util'

const address = recoverTypedSignature({
   data: typedData,
   signature: signature,
   version: SignTypedDataVersion.V4
}))
Enter fullscreen mode Exit fullscreen mode

La función recoverTypedSignature tiene tres parámetros. El primer parámetro son los datos estructurados, el segundo es la firma y el último es la versión de la firma. El valor del retorno de la función se recupera con la dirección Ethereum.

Ahora, veamos cómo la firma puede validarse en la cadena por un contrato inteligente. El código es desde mi repositorio de dinero karma (puede ler más sobre el dinero karma aquí). El siguiente código TypeScript envía la meta transacción al dinero karma del contrato inteligente:

const types = {
           "TransferRequest": [
               {
                   "name": "from",
                   "type": "address"
               },
               {
                   "name": "to",
                   "type": "address"
               },
               {
                   "name": "amount",
                   "type": "uint256"
               },
               {
                   "name": "fee",
                   "type": "uint256"
               },
               {
                   "name": "nonce",
                   "type": "uint256"
               }
           ]
       }

let nonce = await contract.connect(MINER).getNonce(ALICE.address)
const message = {
     "from": ALICE.address,
     "to": JOHN.address,
     "amount": 10,
     "fee": 1,
     "nonce": nonce
}

const signature = await ALICE.signTypedData(karma_request_domain,
  types, message)
await contract.connect(MINER).metaTransfer(ALICE.address, JOHN.address,
  10, 1, nonce, signature)
assert.equal(await contract.balanceOf(ALICE.address), ethers.toBigInt(11))
Enter fullscreen mode Exit fullscreen mode

La variable types define la estructura de la transacción. “From” es la dirección del remitente mientras “to” es la dirección del receptor. La cantidad representa la cantidad de los tokens que serán transferidos. La tasa es la “amount” de tokens que ofrecemos al nodo relay en intercambio por ejecutar nuestra transacción y cubrir el costo en la divisa nativa de la cadena. El servidor “nonce” sirve como un contador para garantizar la singularidad de la transacción. Sin este campo, una transacción puede ejecutarse múltiple veces. Sin embargo, gracias al nonce, una transacción firmada sólo puede ejecutarse una vez.

La función signTypedData proporcionada por ethers.js hace fácil firmar las estructuras EIP-712. Hace la misma cosa como el código presentado anteriormente pero con un uso simple.

metaTransfer es el método del contrato karma para ejecutar la meta-transacción.

Veamos cómo funciona:

function metaTransfer(
       address from,
       address to,
       uint256 amount,
       uint256 fee,
       uint256 nonce,
       bytes calldata signature
   ) public virtual returns (bool) {
       uint256 currentNonce = _useNonce(from, nonce);
       (address recoveredAddress, ECDSA.RecoverError err) = ECDSA.tryRecover(
           _hashTypedDataV4(
               keccak256(
                   abi.encode(
                       TRANSFER_REQUEST_TYPEHASH,
                       from,
                       to,
                       amount,
                       fee,
                       currentNonce
                   )
               )
           ),
           signature
       );

       require(
           err == ECDSA.RecoverError.NoError && recoveredAddress == from,
           "Signature error"
       );

       _transfer(recoveredAddress, to, amount);
       _transfer(recoveredAddress, msg.sender, fee);
       return true;
   }
Enter fullscreen mode Exit fullscreen mode

Para poder validar la firma, primero debemos generar el hash de la estructura. Los pasos exactos para hacer esto son descritos en detalles en el estándar EIP-712, el cual incluye la muestra del contrato inteligente y el código de muestra havascript.

En resumen, la esencia es que combinamos el TYPEHASH (el cual es el hash de la descripción de la estructura) con los campos de la estructura usando abi.encode. Luego produce el hash keccak256. El hash pasa al método _hashTypedDataV4, heredado desde el contrato OpenZeppelin EIP712 en el contrato Karma. Esta función añade los metadatos a nuestra estructura y genera el hash final, haciendo que la validación de la estructura sea simple y transparente. La función más externa es ECDSA.tryRecover, el cual intenta recuperar la dirección del firmante desde el hash y la firma. Si coincide la dirección del parámetro “from”, la firma es válida. Al final del código, la transacción real se ejecuta y el nodo relay, que realiza la transacción, recibe la tasa.

EIP-712 es un estándar general para estructuras de firmas, haciendo que sea uno de los muchos usos para implementar metatransacciones. Como la firma puede ser validada, no sólo con contratos inteligentes, también puede ser muy útil en aplicaciones que no son blockchain. Por ejemplo, puede ser usado para la autenticación del lado del servidor, donde el usuario se identifica con su propia clave privada. Tal sistema puede proporcionar un alto nivel de seguridad, típicamente asociado con criptomonedas, permitiendo la posibilidad de usar la aplicación web con una llave hardware. Además, las llamadas individuales API también pueden ser firmadas con la ayuda de MetaMask.

Espero que este vistazo rápido del estándar EIP-712 haya inspirado a muchos y seas capaz de utilizarlo en ambos: en los proyectos basados, o no, en la blockchain.

Este artículo es una traducción de Laszlo Fazekas, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)