Layr-Labs/incredible-squaring-avs
GitHub: Layr-Labs/incredible-squaring-avs
一个基于 EigenLayer 的 AVS 中间件演示项目,通过简单的数字平方任务完整展示 AVS 的任务分发、计算、签名聚合、争议削减及奖励分配流程。
Stars: 186 | Forks: 136
# 令人惊叹的平方 AVS
[](https://goreportcard.com/report/github.com/Layr-Labs/incredible-squaring-avs)
请勿在生产环境中使用,仅供 testnet 使用。
这是一个演示简单 AVS middleware 并包含完整 eigenlayer 集成的基础仓库。请参阅此[视频演练](https://www.loom.com/share/50314b3ec0f34e2ba386d45724602d76?sid=9d68d8cb-d2d5-4123-bd06-776de2076de0)。
## 依赖项
你需要 [foundry](https://book.getfoundry.sh/getting-started/installation)、[zap-pretty](https://github.com/maoueh/zap-pretty) 和 docker 才能运行以下示例。
```
curl -L https://foundry.paradigm.xyz | bash
foundryup
go install github.com/maoueh/zap-pretty/cmd/zap-pretty@latest
```
你还需要[安装 docker](https://docs.docker.com/get-docker/),并构建合约:
```
make build-contracts
```
如果你想修改智能合约并生成 go bindings,还需要[安装 abigen](https://geth.ethereum.org/docs/tools/abigen):
```
make bindings
```
## 通过 make 运行
这个简单的会话说明了 AVS 的基本流程。makefile 命令是为单个 operator 硬编码的,但是创建新的 operator 配置文件并手动启动更多 operator 也很容易(请参阅 makefile 调用的实际命令)。
在单独的终端中启动 anvil:
```
anvil
```
在单个命令中部署合约、设置 UAM 权限并创建 quorum:
```
make deploy-all
```
启动 aggregator:
```
make start-aggregator
```
向 eigenlayer 和 incredible-squaring 注册 operator,然后启动该进程:
```
make start-operator
```
默认情况下,`start-operator` 命令也会注册 operator。
要禁用此功能,请在 `config-files/operator.anvil.yaml` 中将 `register_operator_on_startup` 设置为 false。
可以通过运行 `make cli-setup-operator` 手动注册 operator。
operator 在 100 次中会产出 10 次无效结果,正如配置中的 `times_failing` 字段所设置的那样。
这些失败一旦受到挑战,就会导致 slashing。
要查看其实际效果,请使用以下命令启动 challenger:
```
make start-challenger
```
## 通过 docker compose 运行
我们编写了一个 [docker-compose.yml](./docker-compose.yml) 文件,以便在单台机器上运行和测试所有内容。它将启动一个 anvil 实例,加载一个部署了 eigenlayer 和 incredible-squaring 合约的[state](./tests/anvil/avs-and-eigenlayer-deployed-anvil-state.json),启动 aggregator,最后启动一个 operator,以及 prometheus 和 grafana 服务器。grafana 服务器将可以在 访问,用户名和密码均设置为 `admin`。我们创建了一个简单的 [grafana dashboard](./grafana/provisioning/dashboards/AVSs/incredible_squaring.json),可以将其作为入门示例并进行扩展,以包含 AVS 特定的指标。eigen 指标不应添加到此 dashboard 中,因为它们将显示在由 eigenlayer-cli 提供的主 eigenlayer dashboard 上。
## 创建和领取 Distribution
该示例在 Makefile 接口中公开了 3 个脚本:
- 创建一个 distribution root,这意味着创建一个 AVS rewards 提交并提交一个 payment root。
- 创建一个 operator directed distribution root,与上一个类似,但奖励会发放给参与 claim 生成的 operator。注意:在这种情况下,operator 是在脚本文件中硬编码的。
- 领取创建的 distribution,将奖励给予特定的 receiver 账户。注意:在这种情况下,receiver 是在脚本文件中硬编码的(地址 0x01)。
这带来了 2 种可能的工作流:在所有 operator 之间平均分配,以及对每个 operator 使用自定义分配。
### 在所有 operator 之间平均分配
首先,在一个单独的终端中启动 anvil 并部署合约。为此,请按照[运行部分](#to-run)中的说明进行操作
然后,运行命令:
```
make create-avs-distributions-root
```
这会创建一个 claimable root,即 merkle tree 的 root,它存储了每个 earner 在每个 ERC20 reward token 上的累计收益。
要针对该 root 进行领取,请使用:
```
make claim-distributions
```
如果你想查看 claimer 的余额,可以运行以下命令:
```
make claimer-account-token-balance
```
请注意,claimer 地址不是通过参数传递的,因为该地址在脚本中是硬编码的。
### 对每个 operator 使用自定义分配
首先,在一个单独的终端中启动 anvil 并部署合约。为此,请按照[运行部分](#to-run)中的说明进行操作
然后,运行命令:
```
make create-operator-directed-distributions-root
```
这会创建一个 claimable root,它与前一个的不同之处在于,它还会将 claim 分配给脚本中设定的 directed operator(目前为硬编码)。
payment leaves 可在 `contracts/payments.json` 中找到。payment leaves 是每个 earner leaf 的 keccak256 哈希值。一个 earner leaf 由 earner 和 token leaves 的 token root 组成,而每个 token leaf 是对 token 地址和 token 收益进行哈希的结果。
要针对该 root 进行领取,请使用:
```
make claim-distributions
```
如果你想查看 claimer 的余额,可以运行以下命令:
```
make claimer-account-token-balance
```
请注意,claimer 地址不是通过参数传递的,因为该地址在脚本中是硬编码的。
## AVS 任务描述
AVS 的架构包含:
- [Eigenlayer core](https://github.com/Layr-Labs/eigenlayer-contracts/tree/master) 合约
- AVS 合约
- [ServiceManager](contracts/src/IncredibleSquaringServiceManager.sol) 最终将包含 slashing 逻辑,但对于 M2 它只是一个占位符。
- [TaskManager](contracts/src/IncredibleSquaringTaskManager.sol) 包含[任务创建](contracts/src/IncredibleSquaringTaskManager.sol#L83)和[任务响应](contracts/src/IncredibleSquaringTaskManager.sol#L102)逻辑。
- [challenge](contracts/src/IncredibleSquaringTaskManager.sol#L176) 逻辑可以分离到它自己的合约中,但我们决定为这个简单的任务将其包含在 TaskManager 中。
- 一组[registry 合约](https://github.com/Layr-Labs/eigenlayer-middleware) 用于管理选择加入此 AVS 的 operator
- Task Generator
- 在真实场景中,这可能是一个独立的实体,但对于这个简单的演示,aggregator 也充当 task generator
- Aggregator
- 聚合来自 operator 的 BLS 签名,并将聚合后的响应发布到 task manager
- 对于这个简单的演示,aggregator 不是一个 operator,因此不需要向 eigenlayer 或 AVS 合约注册。它的 IP 地址只是被硬编码到 operator 的配置中。
- Operators
- 对 task generator 发送给 task manager 的数字进行平方计算,对其进行签名,并将其发送给 aggregator

1. Task generator(在我们的例子中,与 aggregator 相同)每隔固定的间隔(比如 10 个区块,你可以自由设置自己的间隔)向 IncredibleSquaringTaskManager 合约的 [createNewTask](contracts/src/IncredibleSquaringTaskManager.sol#L83) 函数发布任务。每个任务指定一个整数 `numberToBeSquared`,它希望当前选择加入的 operator 计算其平方 `numberToBeSquared^2`。`createNewTask` 还接受 `quorumNumbers` 和 `quorumThresholdPercentage`,这要求列出的每个 quorum(我们在 incredible-squaring 中只使用 quorumNumber 0)必须达到至少 thresholdPercentage 的 operator 签名。
2. 部署了一个 [registry](https://github.com/Layr-Labs/eigenlayer-middleware/blob/master/src/BLSRegistryCoordinatorWithIndices.sol) 合约,允许任何拥有至少 1 个委托 [mockerc20](contracts/src/ERC20Mock.sol) token 的 eigenlayer operator 选择加入此 AVS,并可以从此 AVS 取消注册。
3. [Operator] 当前选择加入该 AVS 的 operator 需要从 Task 合约中读取任务编号,计算其平方,对计算结果进行签名(在 BN254 曲线上),并将他们的 taskResponse 和签名发送给 aggregator。
4. [Aggregator] aggregator 收集 operator 的签名,并使用 BLS aggregation 将它们聚合起来。如果任何响应通过了 task generator 在发布任务时设置的 [quorumThresholdPercentage](contracts/src/IIncredibleSquaringTaskManager.sol#L36),aggregator 就会将聚合后的响应发布到 Task 合约。
5. 如果在 [response window](contracts/src/IncredibleSquaringTaskManager.sol#L119) 内发送了响应,我们将进入 [Dispute resolution] 期。
- [Off-chain] 启动一个 challenge 窗口,在此期间任何人都可以在 DisputeResolution 合约中[提出争议](contracts/src/IncredibleSquaringTaskManager.sol#L171)(在我们的例子中,这与 TaskManager 合约相同)
- [On-chain] DisputeResolution 合约会判定某个 operator 的响应不是正确的响应(即,不是任务中指定的整数的平方),或者选择加入的 operator 没有在响应窗口内作出响应。如果争议得到解决,该 operator 将在 Registration 合约中被冻结,veto 委员会将决定是否否决该冻结请求。
下面是 aggregator 和 operator 流程的更详细的 UML 图:

## StakeUpdates Cronjob
AVS Registry 合约对 delegation manager 合约中的 operator shares 有一个过时的视图。为了更新其 stake 表,它们需要定期调用 [StakeRegistry.updateStakes()](https://github.com/Layr-Labs/eigenlayer-middleware/blob/f171a0812126bbb0bb6d44f53c622591a643e987/src/StakeRegistry.sol#L76) 函数。我们目前正在编写一个 cronjob 二进制文件来为你完成此操作,很快就会开源!
## 集成测试
有关更多详细信息,请参阅集成测试的 [README](tests/anvil/README.md)。
## 结构文档
此 AVS 有三个主要参与者:
- Operator:Operator 订阅 NewTasks 事件,当创建新任务时,完成该任务,计算响应,对其进行签名,并将其发送到 BLS aggregation 服务。
- Aggregator:每隔一定时间(通过链上 `TaskManager`)为 operator 创建新任务的人。它还从 BLS aggregation 服务收集聚合的响应,并将它们发送到链上 `TaskManager`,然后由其发出 TaskRespondedEvent。
- Challenger:Challenger 订阅 TaskRespondedEvents,如果 aggregator 提供的响应与 Challenger 计算的响应不同,它将提出 challenge,调用链上 `TaskManager`,由其验证 aggregator 的响应是否正确。如果不正确,那么签署该任务的 operator 将被 slashing。
现在我们将重点关注每个组件,以展示它们各自是如何运作的。
### Operator
operator 代码可以在 [`/operator` 文件夹](https://github.com/Layr-Labs/incredible-squaring-avs/tree/dev/operator)中找到。
operator 的主要逻辑集中在 [operator.go](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/operator/operator.go#L340-L363) 的这个片段:
```
for {
select {
case <-ctx.Done():
...
case err := <-metricsErrChan:
...
case err := <-sub.Err():
...
case newTaskCreatedLog := <-o.newTaskCreatedChan:
...
}
}
```
上面的三种情况处理错误情况,第四种情况是从订阅新任务创建事件的 channel 中获取数据,并处理响应逻辑:
```
o.metrics.IncNumTasksReceived()
taskResponse := o.ProcessNewTaskCreatedLog(newTaskCreatedLog)
signedTaskResponse, err := o.SignTaskResponse(taskResponse)
if err != nil {
continue
}
go o.aggregatorRpcClient.SendSignedTaskResponseToAggregator(signedTaskResponse)
```
[`ProcessNewTaskCreatedLog` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/operator/operator.go#L368-L394)生成对新任务的响应:
```
func (o *Operator) ProcessNewTaskCreatedLog(
newTaskCreatedLog *cstaskmanager.ContractIncredibleSquaringTaskManagerNewTaskCreated,
) *cstaskmanager.IIncredibleSquaringTaskManagerTaskResponse {
...
numberSquared := big.NewInt(0).Exp(newTaskCreatedLog.Task.NumberToBeSquared, big.NewInt(2), nil)
...
taskResponse := &cstaskmanager.IIncredibleSquaringTaskManagerTaskResponse{
ReferenceTaskIndex: newTaskCreatedLog.TaskIndex,
NumberSquared: numberSquared,
}
return taskResponse
}
```
这里是响应计算逻辑,如果你想计算例如数字的立方,可以在这里进行修改。请注意,`Response` 结构体包含数字的平方,因为它是 `TaskManager` 合约 bindings 的一部分,也应该进行修改。
在 ProcessNewTaskCreatedLog 函数之后,该响应会被签名(在 [SignTaskResponse](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/operator/operator.go#L396-L416) 中),并在执行 [`SendSignedTaskResponseToAggregator()` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/operator/rpc_client.go#L52)的 goroutine 中发送到 BLS aggregation 服务。该函数调用 aggregator 的 `ProcessSignedTaskResponse` 方法(通过 RPC),该方法将签名后的响应重定向到 BLS aggregation 服务。
### Aggregator
aggregator 代码可以在 [`/aggregator` 文件夹](https://github.com/Layr-Labs/incredible-squaring-avs/tree/dev/aggregator)中找到。
主要的 aggregator 逻辑可以在 [aggregator.go](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/aggregator/aggregator.go#L194-L209) 的这个循环中找到:
```
for {
select {
case <-ctx.Done():
...
case blsAggServiceResp := <-agg.blsAggregationService.GetResponseChannel():
agg.logger.Info("Received response from blsAggregationService", "blsAggServiceResp", blsAggServiceResp)
agg.sendAggregatedResponseToContract(blsAggServiceResp)
case <-ticker.C:
err := agg.sendNewTask(big.NewInt(taskNum))
taskNum++
if err != nil {
continue
}
}
}
```
第一种情况涵盖了 context-done 错误情况。第二种涵盖了从 BLS aggregation 服务接收到新的聚合响应的情况。请记住,当 operator 对任务的响应达到阈值或任务时间到期时,就会发生这种情况。在这种情况下,会调用 [`sendAggregatedResponseToContract()` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/aggregator/aggregator.go#L212-L254)。
该方法将响应封装成一个更复杂的 `TaskManager` 类型,封装该响应,并将其连同完成状态发送到链上 Task Manager 的 [`respondToTask` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/contracts/src/IncredibleSquaringTaskManager.sol#L118-L122)。
该方法对任务响应进行几项检查,存储响应元数据并发出一个 `TaskResponded` 事件,该事件将被 challenger 捕获(参见 challenger 部分继续阅读)。
主循环的第三种情况是每 10 秒为 operator 生成一个要完成的新任务,调用 aggregator 的 [`sendNewTask()` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/aggregator/aggregator.go#L258-L297)。在这里,aggregator 调用链上 `TaskManager` 合约的 [`CreateNewTask()` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/contracts/src/IncredibleSquaringTaskManager.sol#L99-L103),该方法存储新任务的哈希值并发出一个 `NewTaskCreated` 事件,该事件将被 challenger 捕获(参见 challenger 部分继续阅读)。在对 `TaskManager` 的调用之后,aggregator 将在 BLS aggregation 服务中初始化一个新任务,operator 将把它们对创建任务的签名响应发送到该服务。
### Challenger
challenger 代码可以在 [`/challenger` 文件夹](https://github.com/Layr-Labs/incredible-squaring-avs/tree/dev/challenger)中找到。
challenger 的主要行为是订阅由链上 `TaskManager` 合约发出的 `NewTaskCreated` 和 `TaskResponded` 事件,可以在 [challenger.go](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/challenger/challenger.go#L79-L117) 中找到。
```
for {
select {
case err := <-newTaskSub.Err():
...
case err := <-taskResponseSub.Err():
...
case newTaskCreatedLog := <-c.newTaskCreatedChan:
...
taskIndex := c.processNewTaskCreatedLog(newTaskCreatedLog)
if _, found := c.taskResponses[taskIndex]; found {
_ = c.callChallengeModule(taskIndex)
}
case taskResponseLog := <-c.taskResponseChan:
...
taskIndex := c.processTaskResponseLog(taskResponseLog)
if _, found := c.tasks[taskIndex]; found {
_ = c.callChallengeModule(taskIndex)
}
}
}
```
前两种情况处理订阅事件 channel 中的错误。另外两种情况用于监听事件并处理它们。在 `NewTaskCreated` 的情况下,它意味着保存创建的任务以备将来事件使用。在 TaskResponse 的情况下,它意味着生成并保存 taskResponseData,如果发生 challenge,这些数据可能会被发送到 `TaskManager`。
处理之后,newTaskCreated 情况会检查是否存在具有该索引的任务响应,而 TaskResponse 情况会检查是否存在具有该索引的初始化任务,在这两种情况下,都会调用 [callChallengeModule 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/challenger/challenger.go#L152-L169)。
```
func (c *Challenger) callChallengeModule(taskIndex uint32) error {
numberToBeSquared := c.tasks[taskIndex].NumberToBeSquared
answerInResponse := c.taskResponses[taskIndex].TaskResponse.NumberSquared
trueAnswer := numberToBeSquared.Exp(numberToBeSquared, big.NewInt(2), nil)
// Checking if the answer in the response submitted by the aggregator is correct
if trueAnswer.Cmp(answerInResponse) != 0 {
c.logger.Info("The number squared is not correct", "expectedAnswer", trueAnswer, "gotAnswer", answerInResponse)
// Raise challenge
c.raiseChallenge(taskIndex)
return nil
} else {
c.logger.Info("The number squared is correct")
return types.NoErrorInTaskResponse
}
}
```
在此方法中,challenger 会计算响应并将其与 aggregator 的响应进行比较。如果响应不相等,就会引发 challenge,这意味着调用链上 `TaskManager` 的 [`RaiseAndResolveChallenge()` 方法](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/contracts/src/IncredibleSquaringTaskManager.sol#L175-L180)。
```
function raiseAndResolveChallenge(
Task calldata task,
TaskResponse calldata taskResponse,
TaskResponseMetadata calldata taskResponseMetadata,
BN254.G1Point[] memory pubkeysOfNonSigningOperators
) external {
...
// // Logic for checking whether the challenge is valid or not
uint256 actualSquaredOutput = numberToBeSquared * numberToBeSquared;
bool isResponseCorrect = (actualSquaredOutput == taskResponse.numberSquared);
// //If the response was correct, no slashing happens so we return
if (isResponseCorrect == true) {
emit TaskChallengedUnsuccessfully(referenceTaskIndex, msg.sender);
return;
}
...
}
```
在该方法中,`TaskManager` 会计算响应并判断聚合的响应是否正确。在第一种情况下,什么都不会发生,但在第二种情况下,签署该任务的 operator 将被 slashing。
slashing 机制可以在 raiseAndResolveChallenge 方法的[第二部分](https://github.com/Layr-Labs/incredible-squaring-avs/blob/f8c379b151d8db778a12a5de1ba0266436d85366/contracts/src/IncredibleSquaringTaskManager.sol#L261-L279)中找到,但简单来说,Manager 会定义要从每个 operator 削减的 wads 数量,并调用 [`InstantSlasher.fulfillSlashingRequest()` 方法](https://github.com/Layr-Labs/eigenlayer-middleware/blob/4d63f27247587607beb67f96fdabec4b2c1321ef/src/slashers/InstantSlasher.sol#L22-L31),该方法最终会调用 [`allocationManager.slashOperator()` 方法](https://github.com/Layr-Labs/eigenlayer-contracts/blob/aa84b7a1d801510a9b893be2f2a91e8ef093faf6/src/contracts/core/AllocationManager.sol#L64-L67)。
## 故障排除
### 从 aggregator 收到错误
在 anvil 上运行时,operator 的典型日志是
```
[2024-04-09 18:25:08.647 PDT] INFO (logging/zap_logger.go:49) rpc client is nil. Dialing aggregator rpc client
[2024-04-09 18:25:08.650 PDT] INFO (logging/zap_logger.go:49) Sending signed task response header to aggregator {"signedTaskResponse":"\u0026aggregator.SignedTaskResponse{TaskResponse:contractIncredibleSquaringTaskManager.IIncredibleSquaringTaskManagerTaskResponse{ReferenceTaskIndex:0x2, NumberSquared:4}, BlsSignature:bls.Signature{G1Point:(*bls.G1Point)(0x14000282068)}, OperatorId:[32]uint8{0xc4, 0xc2, 0x10, 0x30, 0xe, 0x28, 0xab, 0x4b, 0xa7, 0xb, 0x7f, 0xbb, 0xe, 0xfa, 0x55, 0x7d, 0x2a, 0x2a, 0x5f, 0x1f, 0xbf, 0xa6, 0xf8, 0x56, 0xe4, 0xcf, 0x3e, 0x9d, 0x76, 0x6a, 0x21, 0xdc}}"}
[2024-04-09 18:25:08.651 PDT] INFO (logging/zap_logger.go:49) Received error from aggregator {"err":"task 2 not initialized or already completed"}
[2024-04-09 18:25:08.651 PDT] INFO (logging/zap_logger.go:69) Retrying in 2 seconds
[2024-04-09 18:25:10.679 PDT] INFO (logging/zap_logger.go:49) Signed task response header accepted by aggregator. {"reply":false}
```
出现 `task 2 not initialized or already completed` 错误是预期行为。这是因为 aggregator 需要先设置其数据结构,然后才能接受响应。但在本地的 anvil 设置中,operator 有时间接收新任务的 websocket 事件,对数字进行平方计算,签署响应,并将其发送给 aggregator 进程,而此时 aggregator 尚未完成其设置。因此,operator 会在 2 秒后重试发送响应,届时将被接受。
标签:AVS中间件, EigenLayer, EVTX分析, Foundry, Go, Ruby工具, 区块链, 日志审计, 智能合约, 版权保护, 示例项目, 自定义请求头, 请求拦截