Deconstructing the Incredible Squaring AVS
A comprehensive documentation of EigenLayer's "hello world" AVS.
“Everything that can be built as an AVS, will be built as an AVS”
EigenLayer is a protocol that unlocks innovation at a massive scale by making it easy to bootstrap trust networks. As the range of diverse AVSs continues to expand, there are many exciting opportunities for those interested in this technology.
Whenever someone learns a new tech stack, they always start with a “hello world” example. If you're interested in building your own AVS, consider exploring the Incredible Squaring AVS repository created by EigenLayer. This repository serves as an ideal starting point for designing and developing your own AVS, using a straightforward example where operators simply square a number assigned by the task manager.
In this article, we'll delve into the mechanics of the Incredible Squaring AVS (shortened to ISAVS). If you're new to EigenLayer's functionalities, you might find it helpful to read our previous blog, 'You Could’ve Invented EigenLayer', available on AVS research.
High level overview of how Incredible Squaring works:
Task Initialization: The Task Generator calls the TaskManager contract at ten-second intervals, dispatching a number for squaring, thereby initiating the task cycle.
Event Emission: Upon calling, the TaskManager contract emits an event with relevant information regarding the task.
Task Processing: An AVS subscriber, configured by the operator, listens for this event and proceeds to process the designated task.
Task Execution: The operator computes the square of the provided number and signs the task response by the BLS signature scheme.
Response Dispatch: This signed task response is then transmitted to the Aggregator via an RPC call.
Aggregation Processing: Upon reception, the aggregator's RPC server processes the incoming operator responses through the BLS aggregation service integrated within the EigenSDK.
Signature Aggregation: The aggregator receives the aggregated signature from the BLS aggregation service, subsequently broadcasting these aggregated responses back to the Task Manager contract.
Quorum Verification and Response Finalization: The TaskManager contract, upon executing the RespondToTask function, verifies whether the gathered signatures meet the predefined quorum threshold. Following verification, the task response is emitted via an event.
Before proceeding any further, I’d like to clarify that this architecture includes unique components like aggregator and task generator (integrated in the same file). The aggregator is centralized which could lead to censorship of operator task responses. AVSs should think through how to decentralize this role, such as by using a consensus mechanism. You can check out the unique characteristics of ISAVS here.
The architecture design of your AVS is up to you and you are not forced to follow the patterns implemented in this repository. For example, ISAVS uses BLS aggregation but it isn't necessary and you can use ECDSA signatures instead.
The ISAVS is a demo of a minimum viable AVS with full Eigenlayer integration. By understanding how it works, you can draw inspiration for your own architecture design. If you have any AVS ideas that you wish to explore you can contact the team and we’d love to discuss how that could be implemented.
Moving on, let’s see how to set up the Incredible Squaring AVS locally.
Set Up
This video walkthrough explains how you can set up this project for yourself. Running this repository involves 4 steps:
Installing dependencies
In order to run this example you need to have foundry installed.
curl -L https://foundry.paradigm.xyz | bash
You will also need zap-pretty which is a log prettifier.
go install github.com/maoueh/zap-pretty@latest
After installing the dependencies, you build the contracts using this command.
make build-contracts
Starting a local anvil chain
You can start the anvil chain by using this makefile command:
make start-anvil-chain-with-el-and-avs-deployed
This will start a local anvil chain with this particular saved state.
Setting up the aggregator
In a separate terminal, you can now start running the aggregator with the following command:
make start-aggregator
Setting up the operator
After the aggregator is setup, you can start running the operator with the following command in a separate terminal:
make start-operator
What happens during Aggregator setup?
The `make start-aggregator` command executes the code in ./aggregator/cmd/main.go, where the `aggregatorMain` function serves as the operational entrypoint.
This function does the following things:
Set up a new aggregator
Start the aggregator by calling its method
Located in the aggregator.go file, the `NewAggregator` function is instrumental in configuring the aggregator's core components:
AVS Communication Setup: It configures avsReader and avsWriter (explained later) utilizing the chainIO service, facilitating on-chain interaction.
Service Initialization: It concurrently sets up essential services such as:
Operator Public Key Service: Manages the authentication keys of network operators.
AVS Registry Service: Maintains a register of all active AVS configurations.
BLS Aggregation Service: Processes and aggregates BLS signatures from various operators.
In conjunction with initializing services, the `Start` method within the aggregator struct initiates the aggregator's runtime environment:
RPC Server Deployment: A Go routine is deployed to register and launch an RPC server capable of handling HTTP requests.
Routine Management: The system initiates a default 10-second timer (which spins into a separate goroutine by default), creating a responsive loop that listens for channel responses:
Task Dispatch: On receiving a tick from the timer (every 10 seconds), it triggers the sendNewTask function.
Signature Aggregation: Responses from the blsAggregationService channel (explained later) are collected and processed, with aggregated responses being forwarded to the appropriate contract.
Loop Termination: The loop exits if the Go routine concludes, signalled by ctx.Done().
As you can see, a strong understanding of concurrency patterns is essential. The ISAVS extensively utilizes concepts like goroutines, channels, and context so you should familiarize yourself with these topics to continue reading further. However, an AVS can be built in any language so you may draw parallel architecture designs compatible with other tech stacks.
What happens during Operator setup?
The `make start-operator` command runs the code in `./operator/cmd/main.go`, where the operatorMain function serves as the entry point.
This function does the following things:
Set up a new operator
Start the operator by calling its method
During setup, if the `register_operator_on_startup` flag is set to true in the `config-files/operator.anvil.yaml` file, the command also handles the registration of the operator with EigenLayer.
Implemented in `operator/registration.go`, the `registerOperatorOnStartup` method triggers:
RegisterOperatorWithEigenlayer: Invokes the `RegisterAsOperator` method on eigenlayerWriter.
DepositIntoStrategy: Invokes the `DepositERC20IntoStrategy` method on eigenlayerWriter.
RegisterOperatorWithAvs: Executes the `RegisterOperatorWithAvs` method on avsWriter.
Alternatively, the operator can be manually set up using the command which executes the same commands in `cli/actions`:
make cli-setup-operator
Upon calling the `NewOperatorFromConfig` function from the entry point, the setup process:
Configures the node API using methods provided by the eigenSDK.
Initializes ethRpcClient and ethWsClient using the NewInstrumentedClient method from the SDK.
Reads blsKeyPassword from the environment and configures the blsKeyPair.
Sets up avsReader, avsWriter, and avsSubscriber for interactions with EigenLayer core and middleware contracts.
The `Start` method is invoked at the entry point which verifies if the operator is registered. It then subscribes to new tasks via `avsSubscriber`.
The operator then enters an indefinite loop, listening on the newTaskCreatedChan, which activates whenever the TaskManager contract emits a NewTaskCreated event.
Upon receiving a task, the operator processes the task (squares the number) and signs it using the BLS key pair.
A separate Go routine is then initiated to transmit the signed task response to the aggregator.
With a foundational understanding of the setup process, let’s delve deeper into the critical components that define the Incredible Squaring AVS.
We’ll be talking about the following components:
EL contracts
Middleware Contracts
BLS Signatures
BLS Aggregation service
Contract Interactions
EigenLayer Contracts
At the end of the day, EigenLayer is a set of smart contracts. It locks restaked assets (native or liquid) from stakers which is made subject to slashing on predetermined verifiable conditions.
The core EigenLayer contracts provides several workflows of which the following are of importance:
Stakers can deposit restaked assets either natively through EigenPods or by liquid staking tokens. These deposits (for each particular token) are called Strategies.
After Depositing, stakers can delegate their stake to operators who can perform tasks on their behalf.
Stakers can also undelegate and withdraw their stakes after a withdrawal period.
How to become an AVS operator?
Registration: Initiate by calling the `registerAsOperator` function on the Delegation Manager contract. This action records the address of the operator, marking it as self-delegated and ready for further engagement within the system.
Existing Shares: Operators who already possess EigenPod or Strategy shares can directly use these assets as stake.
Acquiring New Shares: Operators without shares must either:
Persuade a staker to delegate shares to them using the `delegateTo` function on the Delegation Manager Contract.
Independently deposit shares by utilizing the `depositIntoStrategy` function on the Strategy Manager contract.
Opting into an AVS: Post stake delegation, operators must opt into an AVS. Each AVS needs to write its own set of contracts that interact with the core EigenLayer contracts.
AVS Contract Requirements: These AVS contracts must have a Service Manager contract that deals with registering/deregistering of operators from their service. EigenLayer core contracts have an AVS registry that keeps a track of all AVS Service Manager contracts.
Registration with AVS: Operators must call `registerOperatorToAVS` function on the Service Manager Base contract which in turn calls the AVS Registry contract. This sets the address of the operator as registered in the address of the AVS’s Service Manager contract.
How an AVS should design their contracts will depend heavily on the architecture of the AVS and type of task itself. The eigenlayer-middleware contracts are supposed to be building blocks providing Base contracts for all kinds of AVSs.
You can learn more about these design patterns here.
Incredible Squaring Task Manager
The ISAVS Task Manager contract is responsible for managing the workflow of tasks, from their creation to the reception of responses. It provides a structured interface through which tasks are systematically processed and verified.
The contract has three external function:
`createNewTask function` is called by the aggregator (task generator) every 10 seconds with the following inputs:
numberToBeSquared (which is incremented after each function call)
quorumThresholdPercentage
quorumNumbers
After taking these inputs it just emits them as an event and increments the task Number. This is important as the operators are listening to this event in order to start performing their tasks.
`respondToTask` function is called by the aggregator whenever it gets a response from the bls aggregation service channel (a quorum threshold of operators have performed and signed the task which has been aggregated).
Along with the Task and its Response (squared number), the struct (defined in IBLSSignatureChecker.sol of the eigenlayer-middleware contracts) `NonSignerStakesAndSignature` is also passed as an input to this function. This is responsible for providing aggregated signature to the contract.
When this function is called, it checks if:
The task is valid
The task hasn’t been responded to yet
The task is being responded in time (all tasks must be responded within a TASK_RESPONSE_WINDOW_BLOCK which is set in the constructor)
After these checks are made, the `checkSignature` function from the `BLSSignatureChecker.sol` Contract is called. It basically verifies that the aggregated signatures are valid and returns the `QuorumStakeTotals` which contains information regarding staked amount by operators.
Now the Task Manager contract ensures that the total quorum provided by the set of operators is greater than the threshold required for the task to pass. If that is the case, then the Task Response is emitted as an event and the task is completed.
The `raiseAndResolveChallenge` function is supposed to provide operators with a way to challenge their claims if their stake is subject to slashing. This function is not properly implemented as:
The Slasher is still under active research and development
The task is verified on-chain in this example which is obviously not possible for every AVS so they may require a committee to vote.
BLS Signatures
The ISAVS uses BLS signatures to aggregate responses from all operators. What does this mean? Digital signatures are required to prove you’re the holder of a private key. It’s a way of identifying yourself in the world of blockchain.
Identifying a party is very important in proof of stake networks including Ethereum because you need to know who to slash in case someone provides a malicious task response. Normally, when you sign on a Dapp using MetaMask, you give them an ECDSA signature.
The problem with this is that verifying a signature requires work and verifying thousands of signatures would be infeasible for any network. Luckily, there’s a scheme called BLS Signature Aggregation which can aggregate several signatures into one which drastically reduces the time and space required to verify thousands of signatures.
If you’re not a fan of Math then that’s all you need to know about BLS Aggregation but if you like cryptography let’s dive a bit deeper into it.
Unlike ECDSA signatures that operate over the secp256k1 curve, BLS signatures utilize a property known as “pairings” which it does not support. This cryptographic feature allows BLS signatures to efficiently combine multiple individual signatures into one aggregated signature.
A pairing is a mathematical operation defined as:
If you want to learn about pairings in more detail, you can check out this blog: https://www.rareskills.io/post/bilinear-pairing
BLS Aggregation can be done on any pairing friendly function. EigenLayer uses the BN254 curves as their precompiles exists but Ethereum itself uses the BLS12-318 curve. Interestingly, EIP2537 proposes to these curve operations as precompiles so any protocol is free to choose their aggregation scheme.
Doing scalar multiplication on Elliptic curve points is easy (logarithmic complexity) but finding the secret key from the public key is hard (linear complexity). This is called the discrete logarithm problem which makes elliptic curve cryptography possible.
When we wish to obtain a signature, we take a hash of that message and then sign it using our private key. This is what it means mathematically:
m → H(m) [ H( ) is a collision resistant hash function that maps the message m from a point on the curve to another point such that the original message cannot be obtained from the hash ]
After hashing the message we perform a scalar multiplication with the secret key to obtain the signature: 𝜎 = sk*H(m)
Now in order to verify if a signature is signed by a public key, we just verify if the following pairings match:
Now that we have learned how signatures work, we can look into how these can be aggregated. And it’s quite simple, just add the signatures and public keys.
Proof:
If you wish to understand BLS signatures and their benefits in detail, you can check out this resource: https://eth2book.info/capella/part2/building_blocks/signatures/
BLS Aggregation Service
The EigenSDK provides the BLS Aggregation Service which has functions to help aggregate signed task responses.
Here is the interface of the BlsAggregationService:
The functions implemented in the BlsAggregationService are methods on the `BlsAggregatorService` struct which is created during the initialization of the Aggregator by calling the `NewBlsAggregatorService` function.
The service has 2 main functions:
`InitializeNewTask` is called by the task generator when it sends a new task. It makes the `signedTasksRespsCs` channel and launches a new Go routine dedicated to managing the aggregation of task responses.
`ProcessNewSignature` is called by the aggregator when it's RPC server receives the signed task response from operators.It handles the incoming response by directing it into the `signedTasksRespsCs` channel, ensuring it is queued for aggregation.
The GetResponseChannel function returns a channel of type BlsAggregationServiceResponse which delivers the final aggregated response from the service.
The goroutine that was created by the InitializeNewTask calls the function `singleTaskAggregatorGoroutineFunc` which takes in the following inputs:
Task Details like index, created timestamp, etc
Quorum numbers and threshold percentages
Time for the task to expire
Signed task response channel
Initially, the Go routine interacts with the AVS registry service to pull necessary state data for the task. It then establishes mappings for tracking responses and sets a timer to ensure the task completes before reaching its expiration.
A select statement is employed to actively listen for new entries on the `signedTaskRespsCs` channel:
Checks the validity of each received signature.
Adds the verified signature to the aggregate signature.
Updates the registry to note which operators have contributed to this task.
Determines if the received responses meet or exceed the quorum threshold required for task validation.
If the quorum threshold is met, the service prepares the aggregation response and sends it through the `aggregatedResponsesC` channel.
If you recall, the aggregator is listening to this channel after setup and when it receives an `blsAggregationServiceResponse` from this channel, it calls the `sendAggregatedResponseToContract` function which sends this aggregated response to the Task Manager contract by calling the `repsondToTask` function.
If only I had a nickel every time data is transferred between threads using channels. Here’s a diagram to showcase the overall execution of the entire process from Task Creation to Completion.
Contract Interactions
We have seen how the ISAVS works, now let’s look at the reader, writer and subscriber architecture that was implemented as wrappers of bindings that interact with smart contracts.
A binding is an auto generated golang code (created using tools like abigen) that allows a go application to interact with smart contracts, performing actions like reading data, initiating transactions, or listening for events.
ISAVS employs three main components within its ChainIO service:
avsReader: It is set up in the operator and its primary function is to fetch the state of the operator through the `IsOperatorRegistered` function. This function is implemented in the EigenSDK which the avsReader inherits.
avsWriter: It’s set up in the aggregator and its primary function is to call the `createNewTask` and `respondToTask` functions through wrappers `SendNewTaskNumberToSquare` and `SendAggregatedResponse` respectively.
avsSubscriber: It is set up in the operator and listens to the `NewTaskCreated` event. Upon detecting the event, this component receives the task details through the `NewTaskCreatedChan` channel, processes the task, and forwards the completed task response back to the aggregator.
Conclusion
Designing your own AVS can be hard. The Incredible Squaring AVS is a simplified demo of how an AVS can be implemented. Understanding the technicalities of this project can allow you to gain some clarity on your how your own AVS could be developed.
This is just a starting point. With this overview, you can try researching about different AVS architectures as they are made public. The field of AVS design is going to be very excited and I can’t wait to see how this wave of innovation on EigenLayer unfolds!
If you’re building your own AVS and need help, you can reach out to me on twitter.