Layr-Labs/incredible-squaring-avs

GitHub: Layr-Labs/incredible-squaring-avs

一个基于 EigenLayer 的 AVS 中间件演示项目,通过简单的数字平方任务完整展示 AVS 的任务分发、计算、签名聚合、争议削减及奖励分配流程。

Stars: 186 | Forks: 136

# 令人惊叹的平方 AVS [![Go Report Card](https://goreportcard.com/badge/github.com/Layr-Labs/incredible-squaring-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 ![](https://static.pigsec.cn/wp-content/uploads/insight/cas/image/22/22daf81d1d2b19faddf950fe5aa8abd7bd63bea2bac53ed814c99163ee256df7.png) 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 图: ![](https://static.pigsec.cn/wp-content/uploads/repos/cas/c6/c63b0110f7b393dd8be4c67870a91fe2ba0a88fa2fdc1622c2d76aabf76f1f2b.png) ## 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工具, 区块链, 日志审计, 智能合约, 版权保护, 示例项目, 自定义请求头, 请求拦截