DEV Community

Gajesh Naik
Gajesh Naik

Posted on

Hello World AVS: Dev Entrypoint

Welcome to the second post in our series on EigenLayer. Today, we'll dive into a practical example with the "Hello World" AVS (Actively Validated Service). This guide will help you understand the basic components and get started with your own AVS on EigenLayer.

What is the Hello World AVS?

The "Hello World" AVS is a simple implementation designed to demonstrate the core mechanics of how AVSs work within the EigenLayer framework. This example walks you through the process of requesting, generating, and validating a simple "Hello World" message.

Key Components of Hello World AVS

Image description

  1. AVS Consumer: Requests a "Hello, {name}" message to be generated and signed.
  2. AVS: Takes the request and emits an event for operators to handle.
  3. Operators: Picks up the request, generates the message, signs it, and submits it back to the AVS.
  4. Validation: Ensures the operator is registered and has the necessary stake, then accepts the submission.

Setting Up Your Environment

Before you start, ensure you have the following dependencies installed:

  • npm: Node package manager
  • Foundry: Ethereum development toolchain
  • Docker: Containerization platform

Quick Start Guide

Image description

  1. Start Docker:
    Ensure Docker is running on your system.

  2. Deploy Contracts:
    Open a terminal and run:

   make start-chain-with-contracts-deployed
Enter fullscreen mode Exit fullscreen mode

This command builds the contracts, starts an Anvil chain, deploys the contracts, and keeps the chain running.

  1. Start the Operator: Open a new terminal tab and run:
   make start-operator
Enter fullscreen mode Exit fullscreen mode

This will compile the AVS software and start monitoring for new tasks.

  1. (Optional) Spam Tasks: To test the AVS with random names, open another terminal tab and run:
   make spam-tasks
Enter fullscreen mode Exit fullscreen mode

Hello World AVS: Code Walkthrough

In this section, we'll break down the code behind the "Hello World" Actively Validated Service (AVS) to understand its functionality and how it leverages EigenLayer.

Smart Contract: HelloWorldServiceManager

The HelloWorldServiceManager contract is the primary entry point for procuring services from the HelloWorld AVS. Here's a step-by-step explanation:

  1. Imports and Contract Declaration:
   import "@eigenlayer/contracts/libraries/BytesLib.sol";
   import "@eigenlayer/contracts/core/DelegationManager.sol";
   import "@eigenlayer-middleware/src/unaudited/ECDSAServiceManagerBase.sol";
   import "@eigenlayer-middleware/src/unaudited/ECDSAStakeRegistry.sol";
   import "@openzeppelin-upgrades/contracts/utils/cryptography/ECDSAUpgradeable.sol";
   import "@eigenlayer/contracts/permissions/Pausable.sol";
   import {IRegistryCoordinator} from "@eigenlayer-middleware/src/interfaces/IRegistryCoordinator.sol";
   import "./IHelloWorldServiceManager.sol";

   contract HelloWorldServiceManager is 
       ECDSAServiceManagerBase,
       IHelloWorldServiceManager,
       Pausable
   {
       // ...
   }
Enter fullscreen mode Exit fullscreen mode

This section imports necessary libraries and defines the contract, inheriting from ECDSAServiceManagerBase, IHelloWorldServiceManager, and Pausable.

  1. Storage Variables:
   uint32 public latestTaskNum;
   mapping(uint32 => bytes32) public allTaskHashes;
   mapping(address => mapping(uint32 => bytes)) public allTaskResponses;
Enter fullscreen mode Exit fullscreen mode
  • latestTaskNum: Keeps track of the latest task index.
  • allTaskHashes: Maps task indices to task hashes.
  • allTaskResponses: Maps operator addresses and task indices to responses.
  1. Constructor:
   constructor(
       address _avsDirectory,
       address _stakeRegistry,
       address _delegationManager
   )
       ECDSAServiceManagerBase(
           _avsDirectory,
           _stakeRegistry,
           address(0),
           _delegationManager
       )
   {}
Enter fullscreen mode Exit fullscreen mode

Initializes the contract with the necessary addresses.

  1. Creating a New Task:
   function createNewTask(
       string memory name
   ) external {
       Task memory newTask;
       newTask.name = name;
       newTask.taskCreatedBlock = uint32(block.number);

       allTaskHashes[latestTaskNum] = keccak256(abi.encode(newTask));
       emit NewTaskCreated(latestTaskNum, newTask);
       latestTaskNum = latestTaskNum + 1;
   }
Enter fullscreen mode Exit fullscreen mode

This function creates a new task, assigns it a unique index, and stores its hash on-chain.

  1. Responding to a Task:
   function respondToTask(
       Task calldata task,
       uint32 referenceTaskIndex,
       bytes calldata signature
   ) external onlyOperator {
       require(
           operatorHasMinimumWeight(msg.sender),
           "Operator does not have match the weight requirements"
       );
       require(
           keccak256(abi.encode(task)) ==
               allTaskHashes[referenceTaskIndex],
           "supplied task does not match the one recorded in the contract"
       );
       require(
           allTaskResponses[msg.sender][referenceTaskIndex].length == 0,
           "Operator has already responded to the task"
       );

       bytes32 messageHash = keccak256(abi.encodePacked("Hello, ", task.name));
       bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash();

       address signer = ethSignedMessageHash.recover(signature);

       require(signer == msg.sender, "Message signer is not operator");

       allTaskResponses[msg.sender][referenceTaskIndex] = signature;

       emit TaskResponded(referenceTaskIndex, task, msg.sender);
   }
Enter fullscreen mode Exit fullscreen mode

This function allows operators to respond to tasks, verifying their identity and the integrity of their response.

  1. Helper Function:
   function operatorHasMinimumWeight(address operator) public view returns (bool) {
       return ECDSAStakeRegistry(stakeRegistry).getOperatorWeight(operator) >= ECDSAStakeRegistry(stakeRegistry).minimumWeight();
   }
Enter fullscreen mode Exit fullscreen mode

Checks if an operator meets the minimum weight requirement.

Operator Client Code

The operator client code interacts with the smart contract to register operators, monitor tasks, and respond to them.

  1. Setup:
   import { ethers } from "ethers";
   import * as dotenv from "dotenv";
   import { delegationABI, contractABI, registryABI, avsDirectoryABI } from "./abis";
   dotenv.config();

   const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
   const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
Enter fullscreen mode Exit fullscreen mode

Configures the provider and wallet using environment variables.

  1. Registering an Operator:
   const registerOperator = async () => {
       const tx1 = await delegationManager.registerAsOperator({ ... });
       await tx1.wait();
       console.log("Operator registered on EL successfully");

       const salt = ethers.utils.hexlify(ethers.utils.randomBytes(32));
       const expiry = Math.floor(Date.now() / 1000) + 3600;

       const digestHash = await avsDirectory.calculateOperatorAVSRegistrationDigestHash(wallet.address, contract.address, salt, expiry);
       const signingKey = new ethers.utils.SigningKey(process.env.PRIVATE_KEY);
       const signature = signingKey.signDigest(digestHash);

       const operatorSignature = { expiry, salt, signature: ethers.utils.joinSignature(signature) };

       const tx2 = await registryContract.registerOperatorWithSignature(wallet.address, operatorSignature);
       await tx2.wait();
       console.log("Operator registered on AVS successfully");
   };
Enter fullscreen mode Exit fullscreen mode

Registers the operator with both the delegation manager and the AVS.

  1. Monitoring and Responding to Tasks:
   const signAndRespondToTask = async (taskIndex, taskCreatedBlock, taskName) => {
       const message = `Hello, ${taskName}`;
       const messageHash = ethers.utils.solidityKeccak256(["string"], [message]);
       const messageBytes = ethers.utils.arrayify(messageHash);
       const signature = await wallet.signMessage(messageBytes);

       const tx = await contract.respondToTask({ name: taskName, taskCreatedBlock }, taskIndex, signature);
       await tx.wait();
       console.log(`Responded to task.`);
   };

   const monitorNewTasks = async () => {
       await contract.createNewTask("EigenWorld");

       contract.on("NewTaskCreated", async (taskIndex, task) => {
           console.log(`New task detected: Hello, ${task.name}`);
           await signAndRespondToTask(taskIndex, task.taskCreatedBlock, task.name);
       });

       console.log("Monitoring for new tasks...");
   };
Enter fullscreen mode Exit fullscreen mode

Monitors for new tasks and responds with a signed message.

  1. Main Function:
   const main = async () => {
       await registerOperator();
       monitorNewTasks().catch(console.error);
   };

   main().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Initializes the process by registering the operator and starting task monitoring.

Task Spammer

A simple script to create tasks at regular intervals for testing purposes.

  1. Setup:
   import { ethers } from 'ethers';

   const provider = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:8545`);
   const wallet = new ethers.Wallet('your-private-key', provider);
   const contractAddress = 'your-contract-address';
   const contractABI = [ ... ];
   const contract = new ethers.Contract(contractAddress, contractABI, wallet);
Enter fullscreen mode Exit fullscreen mode
  1. Generating Random Names:
   function generateRandomName() {
       const adjectives = ['Quick', 'Lazy', 'Sleepy', 'Noisy', 'Hungry'];
       const nouns = ['Fox', 'Dog', 'Cat', 'Mouse', 'Bear'];
       return `${adjectives[Math.floor(Math.random() * adjectives.length)]}${nouns[Math.floor(Math.random() * nouns.length)]}${Math.floor(Math.random() * 1000)}`;
   }
Enter fullscreen mode Exit fullscreen mode
  1. Creating New Tasks:
   async function createNewTask(taskName) {
       try {
           const tx = await contract.createNewTask(taskName);
           const receipt = await tx.wait();
           console.log(`Transaction successful with hash: ${receipt.transactionHash}`);
       } catch (error) {
           console.error('Error sending transaction:', error);
       }
   }

   function startCreatingTasks() {
       setInterval(() => {
           const randomName = generateRandomName();
           console.log(`Creating new task with name: ${randomName}`);
           createNewTask(randomName);
       }, 15000);
   }

   startCreatingTasks();
Enter fullscreen mode Exit fullscreen mode

Conclusion

This post delved into the "Hello World" AVS example, exploring the smart contract and operator client code in detail. This foundational understanding sets the stage for more advanced applications and custom AVS development.

Stay tuned as we continue this series, where we will cover more AVS examples and provide comprehensive guides to building with AVSs on EigenLayer. Exciting innovations await as we leverage the full potential of this powerful protocol!

Top comments (0)