DEV Community

Ahmed Castro
Ahmed Castro

Posted on • Edited on

AI Agents Inruggeables con ZKML

Lo que antes era especulación ahora es una realidad: la IA y blockchain son tecnologías con gran sinergia. Recientemente, los AI Agents han estado participando activamente en redes sociales y on-chain, realizando tareas como dar tips a usuarios, realizar swaps on-chain, y más.

Sin embargo, todavía existe un problema crítico: en el meta actual de los AI agents no es posible verificar la validez de la computación. Esto significa que el creador de un agent podría alterar o hacerse pasar por el modelo, de modo que los usuarios no puedan saber qué acciones realizó la IA o el creador. En este tutorial, exploraremos cómo Zero Knowledge, abreviada como ZK, puede ofrecer la experiencia más segura y fluida para todos en el espacio de los AI agents.

¿Porqué Zero Knowledge?

Zero Knowledge ofrece dos funcionalidades distintas: privacidad y escalabilidad.

Para este caso, nos centraremos en la segunda. Verificar directamente el cálculo de un modelo de Machine Learning (ML) on-chain es imposible por métodos normales, ya que resulta demasiado costoso y, en la mayoría de los casos, ni siquiera cabe en un bloque de Ethereum. En lugar de eso, realizaremos el cálculo del modelo de ML off-chain y utilizaremos ZK para generar una prueba de ese cálculo. Esta prueba luego puede ser verificada de manera económica en on-chain por un contrato inteligente en Solidity, garantizando confianza sin necesidad ejecutar la respuesta del IA on-chain.

Construyendo una AI Agent que dá tips verificables

En este artículo, crearemos un agente llamado Tipper que será capaz de enviar tips, propinas, únicamebte sí es capaz de demostrar que su modelo decidió hacerlo. decidir a quién enviar un tip basado en datos proporcionados, acomo una lista de comentarios en Farcaster, por ejemplo. Vamos a necesitar crear un contrato inteligente que actúe como una barrera de seguridad (guardrail), garantizando que el agente opere de manera autónoma sin ser controlado por su creador. Para lograr esto, utilizaremos EZKL, una herramienta para generar pruebas verificables de cálculos de ML.

En este tutorial, aprenderás a

  1. Crear un contrato verificador ZKML
  2. Generar una prueba ZK válida
  3. Lanzar un token de propinas sencillo
  4. Enviar propinas a usuarios cuando se envíe una prueba ZK válida

EZKL: Herramienta de ZKML para desarrolladores

EZKL ofrece Notebooks de Google Colab con el ambiente de desarrollo que necesitaremos. Usemos este Demo Sencillo.

Notebook

Paso 0: Genera un modelo de AI

Existen muchas posibilidades en cuanto a lo que un agente es capaz de hacer con IA. Por ejemplo, puede decidir quién merece una propina basándose en quién tiene los mejores comentarios en Farcaster, o comprar un token en función de las condiciones del mercado, o también mintear un NFT según tus habilidades de canto.

En este ejemplo, lo mantendremos simple, generaremos un modelo que simplemente decida de manera determinista quién merece una propina de 1000 tokens a una dirección proporcionada.

Comencemos reemplazando el primer bloque proporcionado en Google Colab con lo siguiente. Además, reemplaza tip_recipient_address con una billetera de tu elección; esta billetera recibirá los tokens que el agente de IA decida enviar como propina.

# Verifica si el notebook está en Colab
try:
    # Instala ezkl y onnx si estás en Colab
    import google.colab
    import subprocess
    import sys
    subprocess.check_call([sys.executable, "-m", "pip", "install", "ezkl"])
    subprocess.check_call([sys.executable, "-m", "pip", "install", "onnx"])
except:
    pass # Si no, usa tu setup de ezkl local

# Importa las librería necesarias
from torch import nn
import torch
import ezkl
import os

# Define el address que recibirá el tip
tip_recipient_address = 0x707e55a12557E89915D121932F83dEeEf09E5d70

# Convierte la dirección al formato que EZKL espera
def address_to_byte_tensor(address):
    address_hex = f"{address:040x}"
    return torch.tensor([int(address_hex[i:i+2], 16) for i in range(0, len(address_hex), 2)], dtype=torch.uint8)

tip_recipient_tensor = address_to_byte_tensor(tip_recipient_address)
print(f"Tip Recipient Tensor: {tip_recipient_tensor}")

# Define un modelo que genera como salida el address que recibirá el tip
class FixedHexAddressModel(nn.Module):
    def __init__(self, address_tensor):
        super(FixedHexAddressModel, self).__init__()
        self.output_value = address_tensor

    def forward(self, x):
        # Ignora los inputs y siempre devuelve una dirección fija
        batch_size = x.size(0)
        return self.output_value.repeat(batch_size, 1)  # Repeat for batch size

# Instancia el modelo
circuit = FixedHexAddressModel(tip_recipient_tensor)

# Input ejemplo
dummy_input = torch.ones(1, 1)  # Dummy input (not used by the model)

# Genera el output
output = circuit(dummy_input)
print(f"Output (Hex Address): {output}")

# Exporta el model en el formato de Onnx
torch.onnx.export(
    circuit,
    dummy_input,
    "fixed_hex_address_model.onnx",
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}
)

print("Model saved as 'fixed_hex_address_model.onnx'!")
Enter fullscreen mode Exit fullscreen mode

Paso 1: Crea un Contrato Verificador

A continuación, ejecuta todos los demás snippets en Google Colab. Esto generará un circuito ZK que se puede verificar off-chain. Ahora queremos poder verificar on-chain, para hacerlo, ejecuta el siguiente snippet. Esto imprimirá un contrato verificador de Solidity completamente funcional.

# Define el archivo donde se generará el ABI y el código Solidity
abi_path = 'test.abi'
sol_code_path = 'test.sol'

# Crea el código Solidity del verificador basado en el circuito previamente generado
res = await ezkl.create_evm_verifier(
        vk_path,

        settings_path,
        sol_code_path,
        abi_path,
    )

assert res == True

# Imprime el código Solidity en la consola
with open(sol_code_path, 'r') as sol_file:
    sol_content = sol_file.read()
    print(sol_content)
Enter fullscreen mode Exit fullscreen mode

El siguiente paso es lanzar este contrato en la cadena de tu elección. Ten en cuenta que deben habilitarse las optimizaciones del compilador para compilar correctamente el contrato. Esto es necesario porque el contrato generado es bastante grande y requiere optimizaciónes.

Paso 2: Genera una prueba ZK

A continuación, genera la prueba ZK y encodéala en el formato que espera el contrato verificador.

Ejecuta el siguiente snippet y la prueba será impresa la pantalla.

# Define el archivo dónde se generará el calldata en formato JSON
calldata = 'calldata.proof'

# Encodea la prueba en el formato esperado por el contrato verificador
res = ezkl.encode_evm_calldata(
        proof_path,
        calldata,
    )

# Opcionalmente, imprime los datos JSON
# print(res)

# Convierte la prueba de formato JSON a bytes hexadecimales
calldata_hex = "0x" + ''.join(f"{byte:02x}" for byte in res)
print(calldata_hex)
Enter fullscreen mode Exit fullscreen mode

EZKL genera pruebas ZK como calldata de bajo nivel que se envían al contrato verificador. Esto significa que la prueba debe enviarse utilizando el opcode CALL en EVM en lugar de cómo normalmente llamamos a otro contrato en Solidity. Más sobre esto en el Paso 4.

Paso 3: Lanza el Token de Propinas

Lanza un token ERC20 sencillo con el que el agente de IA podrá interactuar. Despliega el siguiente contrato que te minteará algunos tokens.

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// Contrato ERC20 sencillo creado con el wizzard de OpenZeppelin
// Simple ERC20 token contract created from the OpenZeppelin Wizzard
contract MyToken is ERC20 {
    constructor() ERC20("Tip Token", "TIPT") {
        // Mint 21M tokens to the deployer
        _mint(msg.sender, 21_000_000 ether);
    }
}
Enter fullscreen mode Exit fullscreen mode

Paso 4: Construye el contrato Tipper

El contrato Tipper asegura que los tips, o propinas, solo se transfieran cuando se envíe una prueba ZK válida. Lanza el siguiente contrato reemplazando HALO2_VERIFIER y TIP_TOKEN con las direcciones de los contratos que acabas de desplegar. Luego, transfiere al menos 1000 Tip Tokens a este contrato.

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

// Interface that contains the function that verifies the proof on 
// the Halo2 verifier generated automatically by EZKL
// SPDX-License-Identifier: GPL-3.0

// Interfaz que contiene la función que verifica la prueba en el verificador Halo2 generado automáticamente por EZKL
interface Halo2Verifier {
    function verifyProof(bytes calldata proof, uint256[] calldata instances) external view;
}


// En este ejemplo, solo usaremos la función ERC20 transfer del Token Tip para enviar 1000 tokens cuando se envíe una prueba de tipping valida
interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
}

// El contrato Tipper actúa como barrera de seguridad, o guardrail, para el agente de IA, solo permite realizar una acción si se envía una prueba ZKML válida
contract Tipper {
    // Reemplaza las siguientes dos direcciones con los contratos que acabas de lanzar
    address HALO2_VERIFIER = 0x84fBBc680F4aB4240e8cF6C488aEca03bfF91d2E;
    address TIP_TOKEN = 0x84fBBc680F4aB4240e8cF6C488aEca03bfF91d2E;

    // Envía el tip al usuario solo si se envía una prueba ZKML válida
    function verifyAndSendTip(bytes memory proofCalldata) public {
        // La verificación se realiza pasando los bytes de calldata previamente generados al contrato verificador de Halo2
        (bool success, bytes memory data) = HALO2_VERIFIER.call(proofCalldata);
        // El verificador de EZKL devuelve 1 si la prueba es válida
        if(success && data.length == 32 && uint8(data[31]) == 1)
            // Transfiere 1000 tokens al usuario
            // Nota que los parámetros sensibles como el destinatario deben ser generados por la prueba ZK para prevenir ataques de frontrunning: una vez que la transacción está en la mempool, si información es enviada como parametros normales, alguien más podría pagar extra gas para que se procese su transacción robando tu transacción. Por eso encodeados y decodeamos el address del destinatario en la prueba 
            IERC20(TIP_TOKEN).transfer(extractTipRecipientFromProof(proofCalldata), 1_000 ether);
        else 
            revert("Could not verify proof");
    }

    // Las pruebas ZK tienen parámetros públicos integrados en la prueba
    // En este caso, extraemos la dirección del destinatario del tip de la prueba y la convertimos al formato de dirección de Solidity
    function extractTipRecipientFromProof(bytes memory proofCalldata) internal pure returns (address) {
        bytes memory lastBytes = new bytes(20);
        for (uint i = 0; i < 20; i++) {
            lastBytes[20-1-i] = proofCalldata[proofCalldata.length - (1+i*32)];
        }
        return address(bytes20(abi.encodePacked(lastBytes)));
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora llama a verifyAndSendTip enviando la prueba como parámetro en el formato de calldata, y una vez que la transacción se confirme, se otorgarán 1000 tokens al address que recibe la propina.

Próximos pasos

Al integrar la tecnología ZK, podemos crear agentes de IA seguros y autónomos que operen de manera transparente on-chain. Los próximos pasos implican profundizar en el aprendizaje ZK donde, dependiendo lo que construyas, ocuparás conocer sobre nullificadores, relayers y firmas de wallets de Ethereum. Además necesitarás entrenar un modelo de IA capaz de realizar tareas novedosas e interesantes. Para comenzar con ZK, te recomiendo mi curso completo de ZK, y para comenzar con el entrenamiento de modelos de ML, te recomiendo este curso con ejercicios interactivos.

Top comments (0)