Hello World AVS: Dev Entrypoint
Tags: crypto, blockchain, ethereum, solidity
Published: 2024-06-01 21:45:22

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbkqwk4e1yzwrh35e1li.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/za7pqq9pirgdgq0ca0gl.png)

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

2. **Deploy Contracts**:
   Open a terminal and run:
   ```sh
   make start-chain-with-contracts-deployed
   ```
   This command builds the contracts, starts an Anvil chain, deploys the contracts, and keeps the chain running.

3. **Start the Operator**:
   Open a new terminal tab and run:
   ```sh
   make start-operator
   ```
   This will compile the AVS software and start monitoring for new tasks.

4. (Optional) **Spam Tasks**:
   To test the AVS with random names, open another terminal tab and run:
   ```sh
   make spam-tasks
   ```

### 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**:
   ```solidity
   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
   {
       // ...
   }
   ```

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

2. **Storage Variables**:
   ```solidity
   uint32 public latestTaskNum;
   mapping(uint32 => bytes32) public allTaskHashes;
   mapping(address => mapping(uint32 => bytes)) public allTaskResponses;
   ```

   - `latestTaskNum`: Keeps track of the latest task index.
   - `allTaskHashes`: Maps task indices to task hashes.
   - `allTaskResponses`: Maps operator addresses and task indices to responses.

3. **Constructor**:
   ```solidity
   constructor(
       address _avsDirectory,
       address _stakeRegistry,
       address _delegationManager
   )
       ECDSAServiceManagerBase(
           _avsDirectory,
           _stakeRegistry,
           address(0),
           _delegationManager
       )
   {}
   ```

   Initializes the contract with the necessary addresses.

4. **Creating a New Task**:
   ```solidity
   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;
   }
   ```

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

5. **Responding to a Task**:
   ```solidity
   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);
   }
   ```

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

6. **Helper Function**:
   ```solidity
   function operatorHasMinimumWeight(address operator) public view returns (bool) {
       return ECDSAStakeRegistry(stakeRegistry).getOperatorWeight(operator) >= ECDSAStakeRegistry(stakeRegistry).minimumWeight();
   }
   ```

   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**:
   ```javascript
   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);
   ```

   Configures the provider and wallet using environment variables.

2. **Registering an Operator**:
   ```javascript
   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");
   };
   ```

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

3. **Monitoring and Responding to Tasks**:
   ```javascript
   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...");
   };
   ```

   Monitors for new tasks and responds with a signed message.

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

   main().catch(console.error);
   ```

   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**:
   ```javascript
   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);
   ```

2. **Generating Random Names**:
   ```javascript
   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)}`;
   }
   ```

3. **Creating New Tasks**:
   ```javascript
   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();
   ```

### 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!