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
- AVS Consumer: Requests a "Hello, {name}" message to be generated and signed.
- AVS: Takes the request and emits an event for operators to handle.
- Operators: Picks up the request, generates the message, signs it, and submits it back to the AVS.
- 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
Start Docker:
Ensure Docker is running on your system.Deploy Contracts:
Open a terminal and run:
make start-chain-with-contracts-deployed
This command builds the contracts, starts an Anvil chain, deploys the contracts, and keeps the chain running.
- Start the Operator: Open a new terminal tab and run:
make start-operator
This will compile the AVS software and start monitoring for new tasks.
- (Optional) Spam Tasks: To test the AVS with random names, open another terminal tab and run:
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:
- 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
{
// ...
}
This section imports necessary libraries and defines the contract, inheriting from ECDSAServiceManagerBase
, IHelloWorldServiceManager
, and Pausable
.
- Storage Variables:
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.
- Constructor:
constructor(
address _avsDirectory,
address _stakeRegistry,
address _delegationManager
)
ECDSAServiceManagerBase(
_avsDirectory,
_stakeRegistry,
address(0),
_delegationManager
)
{}
Initializes the contract with the necessary addresses.
- 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;
}
This function creates a new task, assigns it a unique index, and stores its hash on-chain.
- 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);
}
This function allows operators to respond to tasks, verifying their identity and the integrity of their response.
- Helper Function:
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.
- 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);
Configures the provider and wallet using environment variables.
- 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");
};
Registers the operator with both the delegation manager and the AVS.
- 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...");
};
Monitors for new tasks and responds with a signed message.
- Main Function:
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.
- 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);
- 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)}`;
}
- 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();
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)