In our first article, we introduced the concept of Keeper Networks and how integrating EigenLayer and Actively Validated Services (AVS) can overcome their existing limitations. Now, in our second article, we will delve into the technical implementation of Keeper Networks 2.0, providing a comprehensive guide for developers and blockchain enthusiasts.
This blog delves into the technical nuances of AVS, offering a comprehensive overview of its architecture and operational framework. Whether you're a developer seeking to integrate AVS into your projects or a tech enthusiast eager to understand its inner workings, this guide provides a comprehensive overview of the technology driving the future of decentralized networks.
Before we dive into the technical implementation, let’s briefly look at what we’ll be executing. In this article, we'll create a keeper network using EigenLayer and the Active Validator Set (AVS). As part of our demo, we'll integrate a simple oracle to show how external data can be utilized within a blockchain application.
Oracles act as intermediaries, fetching and verifying external information so smart contracts can respond to real-world events. In our demonstration, we’ll implement a basic price oracle that supplies our smart contract with up-to-date bitcoin prices. This will showcase the enhanced functionality our keeper network can achieve by integrating reliable external data sources.
Now, let's move on to the step-by-step implementation.
Before diving into the technical implementation of Keeper Networks' actively validated services (AVS), ensure you have the following prerequisites set up:
Foundry: A powerful development toolkit for writing, testing, and deploying smart contracts. Make sure to install and configure Foundry for a robust development environment.
zap-pretty: A tool that enhances your development workflow by providing formatted output for easier readability and debugging. Integrate zap-pretty into your setup to streamline your coding process.
Docker: Ensure Docker is installed and running on your desktop. Docker is essential for running containerized services, which AVS components rely on for consistent and isolated execution.
With these tools in place, you'll be well-prepared to explore and implement the technical insights and code snippets discussed in this blog.
Command: make build-contracts
Usage: Builds all contracts
Command: make deploy-eigenlayer-contracts-to-anvil-and-save-state
Usage: This command deploys all the necessary EigenLayer contracts to an Anvil (a local Ethereum testnet) instance and saves the deployment state for further use. This ensures the contracts are available for interaction during development and testing.
Command: make deploy-keeper-network-contracts-to-anvil-and-save-state
Usage: This command deploys the Keeper Network contracts, including the Actively Validated Services (AVS), to the Anvil testnet and saves the deployment state.
Command: make start-anvil-chain-with-el-and-avs-deployed
Usage: This command starts the Anvil (a local Ethereum test net) chain with the EigenLayer (EL) and Actively Validated Services (AVS) contracts already deployed. It initializes the local blockchain environment with the necessary contracts in place, allowing for immediate interaction and testing of the Keeper Network functionalities.
Command: make start-task-manager
Usage: Starts Task Manager, which listens to the events from the createJob smart contract and sends them to the operator.
Command: make start-keeper
Usage: Starts the keeper(operator) and executes the job sent by the task manager.
Let's see some important functions which are majorly contributing in the working of this AVS.
This will set up a job that updates the latest Bitcoin price to a smart contract every 15 seconds via a keeper,
Open your terminal and execute the following command to create a new job:
make create-a-new-job
It will list a new job using CreateJob.s.sol.
string memory jobType = "Example Job Type";
string memory jobDescription = "This is an example job description.";
string memory gitlink = "https://gist.githubusercontent.com/nipunshah412/7d21fc1cdd74a25f940139133f58307f/raw/fbba4d005d695e911f9071b84de11a3f3c8a4fe7/BTCPriceOracle.js";
string memory status = "Open";
bytes memory quorumNumbers = abi.encodePacked(uint8(1), uint8(2), uint8(3)); // Example quorum numbers
uint32 quorumThresholdPercentage = 70; // Example quorum threshold percentage
uint32 timeframe = 100; // Example timeframe
keeperNetworkTaskManager.createJob(
jobType,
jobDescription,
gitlink,
status,
quorumNumbers,
quorumThresholdPercentage,
timeframe
);
And call the smart contract function.
/**
* @notice Create a new job
* @param jobType The type of job
* @param jobDescription The description of the job
* @param gitlink The git link associated with the job
* @param status The status of the job
* @param quorumNumbers The quorum numbers for the job
* @param quorumThresholdPercentage The quorum threshold percentage
* @param timeframe The timeframe for the job
*/
function createJob(
string calldata jobType,
string calldata jobDescription,
string calldata gitlink,
string calldata status,
bytes calldata quorumNumbers,
uint32 quorumThresholdPercentage,
uint32 timeframe
) external override whenNotPaused {
require(stakes[msg.sender] >= MINIMUM_STAKE, "Must stake minimum 1 ETH to create a job");
jobCounter++;
jobs[jobCounter] = Job(
jobCounter,
jobType,
jobDescription,
status,
quorumNumbers,
quorumThresholdPercentage,
timeframe,
gitlink,
block.number
);
emit JobCreated(jobCounter, jobType, jobDescription, gitlink);
}
Task Manager will listen to the events from the createJob smart contract and send them to the operator(Keeper).
Below is the function for listening to the events.
func (tm *TaskManager) ListenForEvents() {
query := ethereum.FilterQuery{
Addresses: []common.Address{tm.contractAddr},
Topics: [][]common.Hash{{tm.jobCreatedSig}},
}
logs := make(chan types.Log)
ctx := context.Background()
sub, err := tm.client.SubscribeFilterLogs(ctx, query, logs)
if err != nil {
log.Fatalf("Failed to subscribe to filter logs: %v", err)
}
for {
select {
case err := <-sub.Err():
log.Fatalf("Subscription error: %v", err)
case vLog := <-logs:
log.Printf("Received JobCreated event log: %+v\n", vLog)
tm.AllocateTask(vLog)
}
}
}
After listening it will send the task to the operator(keeper).
Send an HTTP POST request with the JSON data to localhost.
func sendTaskToOperator(job JobCreatedEvent) error {
taskJSON, err := json.Marshal(job)
if err != nil {
return err
}
resp, err := http.Post("http://localhost:8081/executeTask", "application/json", strings.NewReader(string(taskJSON)))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to send task: %s", resp.Status)
}
return nil
}
Keeper will execute job sent by the task manager.
The executeTaskHandler function handles HTTP requests for executing tasks.
It decodes the incoming JSON request to extract job details, logs the received task, and then calls the executeJob function to perform the actual job execution.
Finally, it sends an HTTP 200 OK response.
func executeTaskHandler(w http.ResponseWriter, r *http.Request) {
var job JobCreatedEvent
if err := json.NewDecoder(r.Body).Decode(&job); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("Received task: %+v\n", job)
// Perform task execution logic here
executeJob(job)
w.WriteHeader(http.StatusOK)
}
The executeJob function fetches a script from a specified URL for a given job.
It logs the job details, sends an HTTP GET request to retrieve the script, and checks for errors and the HTTP response status.
If successful, it reads the script from the response body and calls executeScript to run the script.
func executeJob(job JobCreatedEvent) {
log.Printf("Executing job %d: fetching script from %s", job.JobID, job.JobURL)
response, err := http.Get(job.JobURL)
if err != nil {
log.Printf("Error fetching script for job %d: %v", job.JobID, err)
return
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
log.Printf("Unexpected status code %d for job %d", response.StatusCode, job.JobID)
return
}
// Use io.ReadAll to read response body
script, err := io.ReadAll(response.Body)
if err != nil {
log.Printf("Error reading script for job %d: %v", job.JobID, err)
return
}
executeScript(script)
}
For executing a particular job,keeper needs a script so it will call executeScript function.
func executeScript(script []byte) {
log.Println("Executing script...")
cmd := exec.Command("node", "-e", string(script))
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Error executing script: %v", err)
return
}
log.Printf("Script output:\n%s", output)
}
And that's it! You've now set up and deployed Keeper Network's Actively Validated Services, created a job, and set up the task manager and operator to handle and execute tasks. In the next part of this tutorial, we'll cover adding the aggregator, metrics, challenger, and service manager components required for the platform. Thank you for reading, and happy coding!