From d9fafcf85cd0bd5a40dbc9e01fad6fe3f12c75e5 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:09:41 +0700 Subject: [PATCH 1/9] fix: update template following latest blueprint sdk --- Cargo.toml | 13 +- Dockerfile | 2 +- build.rs | 4 +- contracts/src/ITangleTaskManager.sol | 30 ++- contracts/src/TangleServiceManager.sol | 17 +- contracts/src/TangleTaskManager.sol | 143 ++++++++----- foundry.toml | 5 +- remappings.txt | 5 +- rust-toolchain.toml | 2 +- settings.env | 20 +- soldeer.lock | 14 +- src/contexts/aggregator.rs | 274 +++++++++++++++++++++++++ src/contexts/client.rs | 84 ++++++++ src/contexts/combined.rs | 31 +++ src/contexts/eigen_task.rs | 137 +++++++++++++ src/contexts/example_context.rs | 11 + src/contexts/mod.rs | 6 + src/error.rs | 15 ++ src/jobs/example_task.rs | 111 ++++++++++ src/jobs/initialize_task.rs | 46 +++++ src/jobs/mod.rs | 2 + src/lib.rs | 76 ++----- src/main.rs | 123 ++++++----- 23 files changed, 965 insertions(+), 206 deletions(-) create mode 100644 src/contexts/aggregator.rs create mode 100644 src/contexts/client.rs create mode 100644 src/contexts/combined.rs create mode 100644 src/contexts/eigen_task.rs create mode 100644 src/contexts/example_context.rs create mode 100644 src/contexts/mod.rs create mode 100644 src/error.rs create mode 100644 src/jobs/example_task.rs create mode 100644 src/jobs/initialize_task.rs create mode 100644 src/jobs/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 17d2f77..c97b934 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,16 +10,19 @@ repository = "https://github.com/{{gh-username}}/{{project-name}}" readme = "README.md" categories = ["cryptography", "cryptography::cryptocurrencies"] keywords = ["tangle", "blueprint", "avs"] -rust-version = "1.81" +rust-version = "1.88" [dependencies] -# Gadget -blueprint-sdk = { git = "https://github.com/tangle-network/gadget.git", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build"] } -serde = { version = "1.0.208", features = ["derive"] } +blueprint-sdk = { version = "0.1.0-alpha.19", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build"], git = "https://github.com/tangle-network/blueprint.git" } +jsonrpc-core = { version = "18.0.0", default-features = false } +jsonrpc-http-server = { version = "18.0.0", default-features = false } +thiserror = "1.0" +reqwest = "0.12" +color-eyre = { version = "0.6", default-features = false } [build-dependencies] -blueprint-sdk = { git = "https://github.com/tangle-network/gadget.git", default-features = false, features = ["std", "build"] } +blueprint-sdk = { version = "0.1.0-alpha.19", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build"], git = "https://github.com/tangle-network/blueprint.git" } [features] default = ["std"] diff --git a/Dockerfile b/Dockerfile index 8d711e9..2fd5104 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ LABEL org.opencontainers.image.description="{{project-description}}" LABEL org.opencontainers.image.source="https://github.com/{{gh-username}}/{{project-name}}" LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0" -ENV RUST_LOG="gadget=info" +ENV RUST_LOG="blueprint_sdk=info" ENV BIND_ADDR="0.0.0.0" ENV BIND_PORT=9632 ENV BLUEPRINT_ID=0 diff --git a/build.rs b/build.rs index 2460465..d20e17d 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,5 @@ fn main() { let contract_dirs: Vec<&str> = vec!["./contracts"]; - blueprint_sdk::build::utils::soldeer_update(); - blueprint_sdk::build::utils::build_contracts(contract_dirs); + blueprint_sdk::build::soldeer_update(); + blueprint_sdk::build::build_contracts(contract_dirs); } diff --git a/contracts/src/ITangleTaskManager.sol b/contracts/src/ITangleTaskManager.sol index 90440ab..681f259 100644 --- a/contracts/src/ITangleTaskManager.sol +++ b/contracts/src/ITangleTaskManager.sol @@ -1,32 +1,24 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.13; -import "eigenlayer-middleware/src/libraries/BN254.sol"; +import "@eigenlayer-middleware/src/libraries/BN254.sol"; interface ITangleTaskManager { // EVENTS event NewTaskCreated(uint32 indexed taskIndex, Task task); - event TaskResponded( - TaskResponse taskResponse, - TaskResponseMetadata taskResponseMetadata - ); + event TaskResponded(TaskResponse taskResponse, TaskResponseMetadata taskResponseMetadata); event TaskCompleted(uint32 indexed taskIndex); - event TaskChallengedSuccessfully( - uint32 indexed taskIndex, - address indexed challenger - ); + event TaskChallengedSuccessfully(uint32 indexed taskIndex, address indexed challenger); - event TaskChallengedUnsuccessfully( - uint32 indexed taskIndex, - address indexed challenger - ); + event TaskChallengedUnsuccessfully(uint32 indexed taskIndex, address indexed challenger); // STRUCTS struct Task { - uint256 numberToBeSquared; + // TODO: Replace your task params + bytes message; uint32 taskCreatedBlock; // task submitter decides on the criteria for a task to be completed // note that this does not mean the task was "correctly" answered (i.e. the number was squared correctly) @@ -44,21 +36,23 @@ interface ITangleTaskManager { // Can be obtained by the operator from the event NewTaskCreated. uint32 referenceTaskIndex; // This is just the response that the operator has to compute by itself. - uint256 numberSquared; + // TODO: Replace with your expected param following Task + bytes message; } // Extra information related to taskResponse, which is filled inside the contract. // It thus cannot be signed by operators, so we keep it in a separate struct than TaskResponse // This metadata is needed by the challenger, so we emit it in the TaskResponded event struct TaskResponseMetadata { - uint32 taskResponsedBlock; + uint32 taskRespondedBlock; bytes32 hashOfNonSigners; } // FUNCTIONS // NOTE: this function creates new task. function createNewTask( - uint256 numberToBeSquared, + // TODO: Replace param following Task + bytes calldata message, uint32 quorumThresholdPercentage, bytes calldata quorumNumbers ) external; @@ -66,7 +60,7 @@ interface ITangleTaskManager { /// @notice Returns the current 'taskNumber' for the middleware function taskNumber() external view returns (uint32); - // // NOTE: this function raises challenge to existing tasks. + // NOTE: this function raises challenge to existing tasks. function raiseAndResolveChallenge( Task calldata task, TaskResponse calldata taskResponse, diff --git a/contracts/src/TangleServiceManager.sol b/contracts/src/TangleServiceManager.sol index f9a0bad..53dc464 100644 --- a/contracts/src/TangleServiceManager.sol +++ b/contracts/src/TangleServiceManager.sol @@ -1,17 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.13; -import "eigenlayer-contracts/src/contracts/libraries/BytesLib.sol"; import "contracts/src/ITangleTaskManager.sol"; -import "eigenlayer-middleware/src/ServiceManagerBase.sol"; +import "@eigenlayer-middleware/src/ServiceManagerBase.sol"; +import {IAllocationManager} from "@eigenlayer/contracts/interfaces/IAllocationManager.sol"; +import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; +import {ISlashingRegistryCoordinator} from +"@eigenlayer-middleware/src/interfaces/ISlashingRegistryCoordinator.sol"; /** * @title Primary entrypoint for procuring services from Hello. * @author Layr Labs, Inc. */ contract TangleServiceManager is ServiceManagerBase { - using BytesLib for bytes; - ITangleTaskManager public immutable TangleTaskManager; @@ -27,15 +28,19 @@ contract TangleServiceManager is ServiceManagerBase { constructor( IAVSDirectory _avsDirectory, IRewardsCoordinator _rewardsCoordinator, - IRegistryCoordinator _registryCoordinator, + ISlashingRegistryCoordinator _registryCoordinator, IStakeRegistry _stakeRegistry, + IPermissionController _permissionController, + IAllocationManager _allocationManager, ITangleTaskManager _TangleTaskManager ) ServiceManagerBase( _avsDirectory, _rewardsCoordinator, _registryCoordinator, - _stakeRegistry + _stakeRegistry, + _permissionController, + _allocationManager ) { TangleTaskManager = _TangleTaskManager; diff --git a/contracts/src/TangleTaskManager.sol b/contracts/src/TangleTaskManager.sol index bb6e649..73fc142 100644 --- a/contracts/src/TangleTaskManager.sol +++ b/contracts/src/TangleTaskManager.sol @@ -1,15 +1,21 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.13; + import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; -import "eigenlayer-contracts/src/contracts/permissions/Pausable.sol"; -import "eigenlayer-middleware/src/interfaces/IServiceManager.sol"; -import {BLSApkRegistry} from "eigenlayer-middleware/src/BLSApkRegistry.sol"; -import {RegistryCoordinator} from "eigenlayer-middleware/src/RegistryCoordinator.sol"; -import {BLSSignatureChecker, IRegistryCoordinator} from "eigenlayer-middleware/src/BLSSignatureChecker.sol"; -import {OperatorStateRetriever} from "eigenlayer-middleware/src/OperatorStateRetriever.sol"; -import "eigenlayer-middleware/src/libraries/BN254.sol"; +import "@eigenlayer/contracts/permissions/Pausable.sol"; +import "@eigenlayer-middleware/src/interfaces/IServiceManager.sol"; +import {BLSApkRegistry} from "@eigenlayer-middleware/src/BLSApkRegistry.sol"; +import {ISlashingRegistryCoordinator} from + "@eigenlayer-middleware/src/interfaces/ISlashingRegistryCoordinator.sol"; +import {BLSSignatureChecker} from "@eigenlayer-middleware/src/BLSSignatureChecker.sol"; +import {OperatorStateRetriever} from "@eigenlayer-middleware/src/OperatorStateRetriever.sol"; +import {InstantSlasher} from "@eigenlayer-middleware/src/slashers/InstantSlasher.sol"; +import "@eigenlayer-middleware/src/libraries/BN254.sol"; +import {IStrategy} from "@eigenlayer/contracts/interfaces/IStrategy.sol"; +import {IAllocationManagerTypes} from "@eigenlayer/contracts/interfaces/IAllocationManager.sol"; +import {OperatorSet} from "@eigenlayer/contracts/libraries/OperatorSetLib.sol"; import "contracts/src/ITangleTaskManager.sol"; contract TangleTaskManager is @@ -18,7 +24,7 @@ contract TangleTaskManager is Pausable, BLSSignatureChecker, OperatorStateRetriever, -ITangleTaskManager + ITangleTaskManager { using BN254 for BN254.G1Point; @@ -27,6 +33,7 @@ ITangleTaskManager uint32 public immutable TASK_RESPONSE_WINDOW_BLOCK; uint32 public constant TASK_CHALLENGE_WINDOW_BLOCK = 100; uint256 internal constant _THRESHOLD_DENOMINATOR = 100; + uint256 public constant WADS_TO_SLASH = 100_000_000_000_000_000; // 10% /* STORAGE */ // The latest task index @@ -45,6 +52,10 @@ ITangleTaskManager address public aggregator; address public generator; + address public instantSlasher; + address public allocationManager; + address public serviceManager; + /* MODIFIERS */ modifier onlyAggregator() { @@ -60,34 +71,40 @@ ITangleTaskManager } constructor( - IRegistryCoordinator _registryCoordinator, + ISlashingRegistryCoordinator _registryCoordinator, + IPauserRegistry _pauserRegistry, uint32 _taskResponseWindowBlock - ) BLSSignatureChecker(_registryCoordinator) { + ) BLSSignatureChecker(_registryCoordinator) Pausable(_pauserRegistry) { TASK_RESPONSE_WINDOW_BLOCK = _taskResponseWindowBlock; } function initialize( - IPauserRegistry _pauserRegistry, address initialOwner, address _aggregator, - address _generator + address _generator, + address _allocationManager, + address _slasher, + address _serviceManager ) public initializer { - _initializePauser(_pauserRegistry, UNPAUSE_ALL); _transferOwnership(initialOwner); aggregator = _aggregator; generator = _generator; + allocationManager = _allocationManager; + instantSlasher = _slasher; + serviceManager = _serviceManager; } /* FUNCTIONS */ // NOTE: this function creates new task, assigns it a taskId function createNewTask( - uint256 numberToBeSquared, + // TODO: replace with your Task params + bytes calldata message, uint32 quorumThresholdPercentage, bytes calldata quorumNumbers ) external onlyTaskGenerator { // create a new task struct Task memory newTask; - newTask.numberToBeSquared = numberToBeSquared; + newTask.message = message; newTask.taskCreatedBlock = uint32(block.number); newTask.quorumThresholdPercentage = quorumThresholdPercentage; newTask.quorumNumbers = quorumNumbers; @@ -170,9 +187,6 @@ ITangleTaskManager return latestTaskNum; } - // NOTE: this function enables a challenger to raise and resolve a challenge. - // TODO: require challenger to pay a bond for raising a challenge - // TODO(samlaf): should we check that quorumNumbers is same as the one recorded in the task? function raiseAndResolveChallenge( Task calldata task, TaskResponse calldata taskResponse, @@ -180,15 +194,13 @@ ITangleTaskManager BN254.G1Point[] memory pubkeysOfNonSigningOperators ) external { uint32 referenceTaskIndex = taskResponse.referenceTaskIndex; - uint256 numberToBeSquared = task.numberToBeSquared; // some logical checks require( - allTaskResponses[referenceTaskIndex] != bytes32(0), - "Task hasn't been responded to yet" + allTaskResponses[referenceTaskIndex] != bytes32(0), "Task hasn't been responded to yet" ); require( - allTaskResponses[referenceTaskIndex] == - keccak256(abi.encode(taskResponse, taskResponseMetadata)), + allTaskResponses[referenceTaskIndex] + == keccak256(abi.encode(taskResponse, taskResponseMetadata)), "Task response does not match the one recorded in the contract" ); require( @@ -197,17 +209,15 @@ ITangleTaskManager ); require( - uint32(block.number) <= - taskResponseMetadata.taskResponsedBlock + - TASK_CHALLENGE_WINDOW_BLOCK, + uint32(block.number) + <= taskResponseMetadata.taskRespondedBlock + TASK_CHALLENGE_WINDOW_BLOCK, "The challenge period for this task has already expired." ); + // TODO: Replace to your use cases // logic for checking whether challenge is valid or not - uint256 actualSquaredOutput = numberToBeSquared * numberToBeSquared; - bool isResponseCorrect = (actualSquaredOutput == - taskResponse.numberSquared); - + // check message length contains more characters + bool isResponseCorrect = task.message.length < taskResponse.message.length; // if response was correct, no slashing happens so we return if (isResponseCorrect == true) { emit TaskChallengedUnsuccessfully(referenceTaskIndex, msg.sender); @@ -215,13 +225,10 @@ ITangleTaskManager } // get the list of hash of pubkeys of operators who weren't part of the task response submitted by the aggregator - bytes32[] memory hashesOfPubkeysOfNonSigningOperators = new bytes32[]( - pubkeysOfNonSigningOperators.length - ); - for (uint i = 0; i < pubkeysOfNonSigningOperators.length; i++) { - hashesOfPubkeysOfNonSigningOperators[ - i - ] = pubkeysOfNonSigningOperators[i].hashG1Point(); + bytes32[] memory hashesOfPubkeysOfNonSigningOperators = + new bytes32[](pubkeysOfNonSigningOperators.length); + for (uint256 i = 0; i < pubkeysOfNonSigningOperators.length; i++) { + hashesOfPubkeysOfNonSigningOperators[i] = pubkeysOfNonSigningOperators[i].hashG1Point(); } // verify whether the pubkeys of "claimed" non-signers supplied by challenger are actually non-signers as recorded before @@ -229,25 +236,63 @@ ITangleTaskManager // currently inlined, as the MiddlewareUtils.computeSignatoryRecordHash function was removed from BLSSignatureChecker // in this PR: https://github.com/Layr-Labs/eigenlayer-contracts/commit/c836178bf57adaedff37262dff1def18310f3dce#diff-8ab29af002b60fc80e3d6564e37419017c804ae4e788f4c5ff468ce2249b4386L155-L158 // TODO(samlaf): contracts team will add this function back in the BLSSignatureChecker, which we should use to prevent potential bugs from code duplication - bytes32 signatoryRecordHash = keccak256( - abi.encodePacked( - task.taskCreatedBlock, - hashesOfPubkeysOfNonSigningOperators - ) - ); + bytes32 signatoryRecordHash = + keccak256(abi.encodePacked(task.taskCreatedBlock, hashesOfPubkeysOfNonSigningOperators)); require( signatoryRecordHash == taskResponseMetadata.hashOfNonSigners, "The pubkeys of non-signing operators supplied by the challenger are not correct." ); // get the address of operators who didn't sign - address[] memory addresssOfNonSigningOperators = new address[]( - pubkeysOfNonSigningOperators.length + address[] memory addressOfNonSigningOperators = + new address[](pubkeysOfNonSigningOperators.length); + for (uint256 i = 0; i < pubkeysOfNonSigningOperators.length; i++) { + addressOfNonSigningOperators[i] = BLSApkRegistry(address(blsApkRegistry)) + .pubkeyHashToOperator(hashesOfPubkeysOfNonSigningOperators[i]); + } + + // get the list of all operators who were active when the task was initialized + Operator[][] memory allOperatorInfo = getOperatorState( + ISlashingRegistryCoordinator(address(registryCoordinator)), + task.quorumNumbers, + task.taskCreatedBlock ); - for (uint i = 0; i < pubkeysOfNonSigningOperators.length; i++) { - addresssOfNonSigningOperators[i] = BLSApkRegistry( - address(blsApkRegistry) - ).pubkeyHashToOperator(hashesOfPubkeysOfNonSigningOperators[i]); + // first for loop iterate over quorums + for (uint256 i = 0; i < allOperatorInfo.length; i++) { + // second for loop iterate over operators active in the quorum when the task was initialized + for (uint256 j = 0; j < allOperatorInfo[i].length; j++) { + // get the operator address + bytes32 operatorID = allOperatorInfo[i][j].operatorId; + address operatorAddress = blsApkRegistry.getOperatorFromPubkeyHash(operatorID); + // check whether the operator was a signer for the task + bool wasSigningOperator = true; + for (uint256 k = 0; k < addressOfNonSigningOperators.length; k++) { + if (operatorAddress == addressOfNonSigningOperators[k]) { + // if the operator was a non-signer, then we set the flag to false + wasSigningOperator = false; + break; + } + } + if (wasSigningOperator == true) { + OperatorSet memory operatorset = + OperatorSet({avs: serviceManager, id: uint8(task.quorumNumbers[i])}); + IStrategy[] memory istrategy = IAllocationManager(allocationManager) + .getStrategiesInOperatorSet(operatorset); + uint256[] memory wadsToSlash = new uint256[](istrategy.length); + for (uint256 z = 0; z < wadsToSlash.length; z++) { + wadsToSlash[z] = WADS_TO_SLASH; + } + IAllocationManagerTypes.SlashingParams memory slashingparams = + IAllocationManagerTypes.SlashingParams({ + operator: operatorAddress, + operatorSetId: uint8(task.quorumNumbers[i]), + strategies: istrategy, + wadsToSlash: wadsToSlash, + description: "slash_the_operator" + }); + InstantSlasher(instantSlasher).fulfillSlashingRequest(slashingparams); + } + } } // the task response has been challenged successfully diff --git a/foundry.toml b/foundry.toml index c1e1690..94a69d7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ cache_path = "contracts/cache" broadcast = "contracts/broadcast" libs = ["dependencies"] auto_detect_remappings = true +via-ir = true [soldeer] recursive_deps = true @@ -14,7 +15,7 @@ remappings_location = "txt" remappings_version = false [dependencies] -eigenlayer-middleware = { version = "0.5.4", git = "https://github.com/Layr-Labs/eigenlayer-middleware.git", tag = "v0.5.4-mainnet-rewards-v2" } -forge-std = { version = "1.9.4" } +eigenlayer-middleware = { version = "1.3.1", git = "https://github.com/Layr-Labs/eigenlayer-middleware", rev = "fd26169c7f988a53ad9ca1e745f40754afed4ed4" } +forge-std = { version = "1.9.6" } # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/remappings.txt b/remappings.txt index 6ac4867..88ec8a6 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ -eigenlayer-middleware/=dependencies/eigenlayer-middleware-0.5.4/ -forge-std/=dependencies/forge-std-1.9.4 +@eigenlayer-middleware/=dependencies/eigenlayer-middleware-1.3.1/ +@eigenlayer/contracts/=dependencies/eigenlayer-middleware-1.3.1/lib/eigenlayer-contracts/src/contracts +forge-std/=dependencies/forge-std-1.9.6 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d358d90..07ccef3 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -2,7 +2,7 @@ # Currently we are using this specific nightly version since we rely on the # rustdoc API which is not yet stabilized. We will keep updating this version # as we go along. -channel = "nightly-2025-01-30" +channel = "nightly-2025-06-26" components = ["rustfmt", "clippy", "rust-src"] targets = ["wasm32-unknown-unknown"] profile = "minimal" diff --git a/settings.env b/settings.env index 84b47d7..d57312e 100644 --- a/settings.env +++ b/settings.env @@ -1,8 +1,12 @@ -REGISTRY_COORDINATOR_ADDRESS=c3e53f4d16ae77db1c982e75a937b9f60fe63690 -OPERATOR_STATE_RETRIEVER_ADDRESS=1613beb3b2c4f22ee086b2b38c1476a3ce7f78e8 -DELEGATION_MANAGER_ADDRESS=dc64a140aa3e981100a9beca4e685f962f0cf6c9 -STRATEGY_MANAGER_ADDRESS=5fc8d32690cc91d4c39d9d3abcbd16989f875707 -SERVICE_MANAGER_ADDRESS=67d269191c92caf3cd7723f116c85e6e9bf55933 -STAKE_REGISTRY_ADDRESS=5fc8d32690cc91d4c39d9d3abcbd16989f875707 -AVS_DIRECTORY_ADDRESS=0000000000000000000000000000000000000000 -REWARDS_COORDINATOR_ADDRESS=0000000000000000000000000000000000000000 \ No newline at end of file +REGISTRY_COORDINATOR_ADDRESS=fd471836031dc5108809d173a067e8486b9047a3 +OPERATOR_STATE_RETRIEVER_ADDRESS=922d6956c99e12dfeb3224dea977d0939758a1fe +DELEGATION_MANAGER_ADDRESS=cf7ed3acca5a467e9e704c703e8d87f634fb0fc9 +STRATEGY_MANAGER_ADDRESS=a513e6e4b8f2a923d98304ec87f64353c4d5c853 +STRATEGY_ADDRESS=1613beb3b2c4f22ee086b2b38c1476a3ce7f78e8 +SERVICE_MANAGER_ADDRESS=2bdcc0de6be1f7d2ee689a0342d76f52e8efaba3 +STAKE_REGISTRY_ADDRESS=7bc06c482dead17c0e297afbc32f6e63d3846650 +AVS_DIRECTORY_ADDRESS=b7f8bc63bbcad18155201308c8f3540b07f84f5e +REWARDS_COORDINATOR_ADDRESS=0dcd1bf9a1b36ce34237eeafef220932846bcd82 +PERMISSION_CONTROLLER_ADDRESS=322813fd9a801c5507c9de605d63cea4f2ce6c44 +PAUSE_REGISTRY_ADDRESS=9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae +ALLOCATION_MANAGER_ADDRESS=8a791620dd6260079bf849dc5567adc3f2fdc318 \ No newline at end of file diff --git a/soldeer.lock b/soldeer.lock index 9111e9a..aa07cb5 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -1,12 +1,12 @@ [[dependencies]] name = "eigenlayer-middleware" -version = "0.5.4" -git = "https://github.com/Layr-Labs/eigenlayer-middleware.git" -rev = "2475ab8b8c7698e69bb18f3a19e0c518381f24df" +version = "1.3.1" +git = "https://github.com/Layr-Labs/eigenlayer-middleware" +rev = "fd26169c7f988a53ad9ca1e745f40754afed4ed4" [[dependencies]] name = "forge-std" -version = "1.9.4" -url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_4_25-10-2024_14:36:59_forge-std-1.9.zip" -checksum = "b5be24beb5e4dab5e42221b2ad1288b64c826bee5ee71b6159ba93ffe86f14d4" -integrity = "3874463846ab995a6a9a88412913cacec6144f7605daa1af57c2d8bf3f210b13" +version = "1.9.6" +url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_6_01-02-2025_20:49:10_forge-std-1.9.zip" +checksum = "55f341818321b3f925161a72fd0dcd62e4a0a4b66785a7a932bf2bfaf96fb9d1" +integrity = "e9ecdc364d152157431e5df5aa041ffddbe9bb1c1ad81634b1e72df9e23814e8" diff --git a/src/contexts/aggregator.rs b/src/contexts/aggregator.rs new file mode 100644 index 0000000..80d664d --- /dev/null +++ b/src/contexts/aggregator.rs @@ -0,0 +1,274 @@ +use crate::ITangleTaskManager::{Task, TaskResponse}; +use crate::error::TaskError as Error; +use crate::{ + contexts::client::SignedTaskResponse, + contexts::eigen_task::{IndexedTask, SquaringTaskResponseSender}, +}; +use blueprint_sdk::alloy::network::EthereumWallet; +use blueprint_sdk::alloy::primitives::Address; +use blueprint_sdk::contexts::eigenlayer::EigenlayerContext; +use blueprint_sdk::eigenlayer::generic_task_aggregation::{ + AggregatorConfig, SignedTaskResponse as GenericSignedTaskResponse, TaskAggregator, +}; +use blueprint_sdk::macros::context::{EigenlayerContext, KeystoreContext}; +use blueprint_sdk::runner::{BackgroundService, config::BlueprintEnvironment, error::RunnerError}; +use blueprint_sdk::{debug, error, info}; +use blueprint_sdk::eigensdk::types::avs::TaskIndex; +use jsonrpc_core::{IoHandler, Params, Value}; +use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; +use reqwest::Url; +use std::{collections::VecDeque, net::SocketAddr, sync::Arc, time::Duration}; +use blueprint_sdk::tokio::sync::{Mutex, Notify, oneshot}; +use blueprint_sdk::tokio::task::JoinHandle; + +#[derive(Clone, EigenlayerContext, KeystoreContext)] +pub struct AggregatorContext { + pub port_address: String, + pub task_manager_address: Address, + pub http_rpc_url: Url, + pub wallet: EthereumWallet, + pub response_cache: Arc>>, + #[config] + pub env: BlueprintEnvironment, + shutdown: Arc<(Notify, Mutex)>, + pub task_aggregator: + Option>>, +} + +impl AggregatorContext { + pub async fn new( + port_address: String, + task_manager_address: Address, + wallet: EthereumWallet, + env: BlueprintEnvironment, + ) -> Result { + let mut aggregator_context = AggregatorContext { + port_address, + task_manager_address, + http_rpc_url: env.http_rpc_endpoint.clone(), + wallet, + response_cache: Arc::new(Mutex::new(VecDeque::new())), + env: env.clone(), + shutdown: Arc::new((Notify::new(), Mutex::new(false))), + task_aggregator: None, + }; + + // Initialize the bls registry service + let bls_service = aggregator_context + .eigenlayer_client() + .await + .map_err(|e| Error::Context(e.to_string()))? + .bls_aggregation_service_in_memory() + .await + .map_err(|e| Error::Context(e.to_string()))?; + + // Create the response sender + let response_sender = SquaringTaskResponseSender { + task_manager_address, + http_rpc_url: env.http_rpc_endpoint.clone(), + }; + + // Create the task aggregator with default config + let task_aggregator = + TaskAggregator::new(bls_service, response_sender, AggregatorConfig::default()); + + aggregator_context.task_aggregator = Some(Arc::new(task_aggregator)); + + Ok(aggregator_context) + } + + pub async fn start(self) -> JoinHandle<()> { + let aggregator = Arc::new(Mutex::new(self)); + + blueprint_sdk::tokio::spawn(async move { + info!("Starting aggregator RPC server"); + + // Start the task aggregator + if let Some(task_agg) = &aggregator.lock().await.task_aggregator { + info!("Starting task aggregator"); + task_agg.start().await; + } + + let server_handle = blueprint_sdk::tokio::spawn(Self::start_server(Arc::clone(&aggregator))); + + info!("Aggregator server started and running in the background"); + // Wait for server task to complete + if let Err(e) = server_handle.await { + error!("Server task failed: {}", e); + } + + info!("Aggregator shutdown complete"); + }) + } + + pub async fn shutdown(&self) { + info!("Initiating aggregator shutdown"); + + if let Some(task_agg) = &self.task_aggregator { + match blueprint_sdk::tokio::time::timeout(Duration::from_secs(10), task_agg.stop()).await { + Ok(Ok(_)) => info!("Task aggregator stopped successfully"), + Ok(Err(e)) => error!("Error stopping task aggregator: {}", e), + Err(_) => error!("Timeout while stopping task aggregator"), + } + } else { + info!("No task aggregator to stop"); + } + + // Set internal shutdown flag + let (notify, is_shutdown) = &*self.shutdown; + *is_shutdown.lock().await = true; + notify.notify_waiters(); + + debug!("Aggregator shutdown flag set"); + } + + async fn start_server(aggregator: Arc>) -> Result<(), Error> { + let mut io = IoHandler::new(); + io.add_method("process_signed_task_response", { + let aggregator = Arc::clone(&aggregator); + move |params: Params| { + let aggregator = Arc::clone(&aggregator); + async move { + // Parse the outer structure first + let outer_params: Value = params.parse()?; + + // Extract the inner "params" object + let inner_params = outer_params.get("params").ok_or_else(|| { + jsonrpc_core::Error::invalid_params("Missing 'params' field") + })?; + + // Now parse the inner params as SignedTaskResponse + let signed_task_response: SignedTaskResponse = + jsonrpc_core::serde_json::from_value(inner_params.clone()).map_err(|e| { + jsonrpc_core::Error::invalid_params(format!( + "Invalid SignedTaskResponse: {e}", + )) + })?; + + aggregator + .lock() + .await + .process_signed_task_response(signed_task_response) + .await + .map(|_| Value::Bool(true)) + .map_err(|e| jsonrpc_core::Error::invalid_params(e.to_string())) + } + } + }); + + let socket: SocketAddr = aggregator + .lock() + .await + .port_address + .parse() + .map_err(Error::Parse)?; + let server = ServerBuilder::new(io) + .cors(DomainsValidation::AllowOnly(vec![ + AccessControlAllowOrigin::Any, + ])) + .start_http(&socket) + .map_err(|e| Error::Context(e.to_string()))?; + + info!("Server running at {}", socket); + + // Create a close handle before we move the server + let close_handle = server.close_handle(); + + // Get shutdown components + let shutdown = { + let agg = aggregator.lock().await; + agg.shutdown.clone() + }; + + // Create a channel to coordinate shutdown + let (server_tx, server_rx) = oneshot::channel(); + + // Spawn the server in a blocking task + let server_handle = blueprint_sdk::tokio::task::spawn_blocking(move || { + server.wait(); + let _ = server_tx.send(()); + }); + + // Use tokio::select! to wait for either the server to finish or the shutdown signal + blueprint_sdk::tokio::select! { + result = server_handle => { + info!("Server has stopped naturally"); + result.map_err(|e| { + error!("Server task failed: {}", e); + Error::Runtime(e.to_string()) + })?; + } + _ = server_rx => { + info!("Server has been shut down via close handle"); + } + _ = async { + let (notify, is_shutdown) = &*shutdown; + loop { + notify.notified().await; + if *is_shutdown.lock().await { + break; + } + } + } => { + info!("Shutdown signal received, stopping server"); + close_handle.close(); + } + } + + Ok(()) + } + + pub async fn process_signed_task_response( + &mut self, + resp: SignedTaskResponse, + ) -> Result<(), Error> { + // Convert the SignedTaskResponse to GenericSignedTaskResponse + let generic_signed_response = GenericSignedTaskResponse { + response: resp.task_response, + signature: resp.signature, + operator_id: resp.operator_id, + }; + + // Process the signed response using the generic task aggregator + if let Some(task_agg) = &self.task_aggregator { + task_agg + .process_signed_response(generic_signed_response) + .await; + Ok(()) + } else { + Err(Error::Context( + "Task aggregator not initialized".to_string(), + )) + } + } + + // Register a task with the aggregator + pub async fn register_task(&self, task_index: TaskIndex, task: Task) -> Result<(), Error> { + if let Some(task_agg) = &self.task_aggregator { + // Create an indexed task with the task index + let indexed_task = IndexedTask::new(task, task_index); + + // Register the task with the generic task aggregator + task_agg + .register_task(indexed_task) + .await + .map_err(|e| Error::Context(e.to_string())) + } else { + Err(Error::Context( + "Task aggregator not initialized".to_string(), + )) + } + } +} + +impl BackgroundService for AggregatorContext { + async fn start(&self) -> Result>, RunnerError> { + let (tx, rx) = oneshot::channel(); + let ctx = self.clone(); + blueprint_sdk::tokio::spawn(async move { + ctx.start().await; + let _ = tx.send(Ok(())); + }); + Ok(rx) + } +} \ No newline at end of file diff --git a/src/contexts/client.rs b/src/contexts/client.rs new file mode 100644 index 0000000..9ce9942 --- /dev/null +++ b/src/contexts/client.rs @@ -0,0 +1,84 @@ +use blueprint_sdk::alloy::rpc::client::ReqwestClient; +use blueprint_sdk::{debug, info}; +use color_eyre::Result; +use blueprint_sdk::eigensdk::crypto_bls::{OperatorId, Signature}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use jsonrpc_core::serde_json::json; +use blueprint_sdk::tokio::time::{Duration, sleep}; + +use crate::ITangleTaskManager::TaskResponse; + +const MAX_RETRIES: u32 = 5; +const INITIAL_RETRY_DELAY: Duration = Duration::from_secs(1); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedTaskResponse { + pub task_response: TaskResponse, + pub signature: Signature, + pub operator_id: OperatorId, +} + +/// Client for interacting with the Aggregator RPC server +#[derive(Debug, Clone)] +pub struct AggregatorClient { + client: ReqwestClient, +} + +impl AggregatorClient { + /// Creates a new AggregatorClient + pub fn new(aggregator_address: &str) -> Result { + let url = Url::parse(&format!("http://{aggregator_address}"))?; + let client = ReqwestClient::new_http(url); + Ok(Self { client }) + } + + /// Sends a signed task response to the aggregator + pub async fn send_signed_task_response(&self, response: SignedTaskResponse) -> Result<()> { + let params = json!({ + "params": response, + "id": 1, + "jsonrpc": "2.0" + }); + + for attempt in 1..=MAX_RETRIES { + match self + .client + .request::<_, bool>("process_signed_task_response", ¶ms) + .await + { + Ok(true) => { + info!("Task response accepted by aggregator"); + // MARK: Uncomment when metrics are implemented + // incredible_metrics::inc_num_tasks_accepted_by_aggregator(); + return Ok(()); + } + Ok(false) => debug!("Task response not accepted, retrying..."), + Err(e) => debug!("Error sending task response: {}", e), + } + + if attempt < MAX_RETRIES { + let delay = INITIAL_RETRY_DELAY * 2u32.pow(attempt - 1); + info!("Retrying in {} seconds...", delay.as_secs()); + sleep(delay).await; + } + } + + debug!( + "Failed to send signed task response after {} attempts", + MAX_RETRIES + ); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_client() { + let client = AggregatorClient::new("127.0.0.1:8545"); + assert!(client.is_ok()); + } +} \ No newline at end of file diff --git a/src/contexts/combined.rs b/src/contexts/combined.rs new file mode 100644 index 0000000..15560e2 --- /dev/null +++ b/src/contexts/combined.rs @@ -0,0 +1,31 @@ +use crate::contexts::aggregator::AggregatorContext; +// TODO: Replace with your context name +use crate::contexts::example_context::ExampleContext; +use blueprint_sdk::macros::context::KeystoreContext; +use blueprint_sdk::runner::config::BlueprintEnvironment; + +/// Combined context that includes both the ExampleContext and AggregatorContext +/// This allows both jobs to share the same context in the router +#[derive(Clone, KeystoreContext)] +pub struct CombinedContext { + // TODO: Replace with your context name + pub example_context: ExampleContext, + pub aggregator_context: Option, + #[config] + pub std_config: BlueprintEnvironment, +} + +impl CombinedContext { + pub fn new( + // TODO: Replace with your context name + example_context: ExampleContext, + aggregator_context: Option, + std_config: BlueprintEnvironment, + ) -> Self { + Self { + example_context, + aggregator_context, + std_config, + } + } +} \ No newline at end of file diff --git a/src/contexts/eigen_task.rs b/src/contexts/eigen_task.rs new file mode 100644 index 0000000..d79f910 --- /dev/null +++ b/src/contexts/eigen_task.rs @@ -0,0 +1,137 @@ +use crate::BN254::{G1Point, G2Point}; +use crate::IBLSSignatureCheckerTypes::NonSignerStakesAndSignature; +use crate::TangleTaskManager as ExampleTask; +use crate::ITangleTaskManager::{Task, TaskResponse}; +use blueprint_sdk::alloy::primitives::address; +use blueprint_sdk::alloy::core::sol_types::SolType; +use blueprint_sdk::eigenlayer::generic_task_aggregation::{ + EigenTask, ResponseSender, Result as AggResult, TaskResponse as GenericTaskResponse, +}; +use blueprint_sdk::evm::util::get_provider_from_signer; +use blueprint_sdk::eigensdk::crypto_bls::{BlsG1Point, BlsG2Point, convert_to_g1_point, convert_to_g2_point}; +use blueprint_sdk::eigensdk::services_blsaggregation::bls_aggregation_service_response::BlsAggregationServiceResponse; +use blueprint_sdk::eigensdk::types::avs::TaskIndex; +use reqwest::Url; +use std::future::Future; +use std::pin::Pin; + +// Wrapper for Task that includes the task index +#[derive(Clone)] +pub struct IndexedTask { + pub task: Task, + pub task_index: TaskIndex, +} + +impl IndexedTask { + pub fn new(task: Task, task_index: TaskIndex) -> Self { + Self { task, task_index } + } +} + +// Implement EigenTask for the IndexedTask type +impl EigenTask for IndexedTask { + fn task_index(&self) -> TaskIndex { + self.task_index + } + + fn created_block(&self) -> u32 { + self.task.taskCreatedBlock + } + + fn quorum_numbers(&self) -> Vec { + self.task.quorumNumbers.to_vec() + } + + fn quorum_threshold_percentage(&self) -> u8 { + self.task.quorumThresholdPercentage as u8 + } + + fn encode(&self) -> Vec { + ::abi_encode(&self.task).to_vec() + } +} + +// Implement TaskResponse for the existing TaskResponse type +impl GenericTaskResponse for TaskResponse { + fn reference_task_index(&self) -> TaskIndex { + self.referenceTaskIndex + } + + fn encode(&self) -> Vec { + ::abi_encode(self).to_vec() + } +} + +// Implement ResponseSender for sending aggregated responses to the contract +#[derive(Clone)] +pub struct SquaringTaskResponseSender { + pub task_manager_address: blueprint_sdk::alloy::primitives::Address, + pub http_rpc_url: Url, +} + +impl ResponseSender for SquaringTaskResponseSender { + type Future = Pin> + Send + 'static>>; + + fn send_aggregated_response( + &self, + indexed_task: &IndexedTask, + response: &TaskResponse, + aggregation_result: BlsAggregationServiceResponse, + ) -> Self::Future { + let task_clone = indexed_task.task.clone(); + let response_clone = response.clone(); + let task_manager_address = self.task_manager_address; + let http_rpc_url = self.http_rpc_url.clone(); + + Box::pin(async move { + let key = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6"; // Private key from our Aggregator Anvil account + let provider = get_provider_from_signer(key, http_rpc_url); + + let contract = + ExampleTask::new(task_manager_address, provider.clone()); + + // Convert the aggregation result to the NonSignerStakesAndSignature format + let non_signer_stakes_and_signature = NonSignerStakesAndSignature { + nonSignerPubkeys: aggregation_result + .non_signers_pub_keys_g1 + .into_iter() + .map(to_g1_point) + .collect(), + nonSignerQuorumBitmapIndices: aggregation_result.non_signer_quorum_bitmap_indices, + quorumApks: aggregation_result + .quorum_apks_g1 + .into_iter() + .map(to_g1_point) + .collect(), + apkG2: to_g2_point(aggregation_result.signers_apk_g2), + sigma: to_g1_point(aggregation_result.signers_agg_sig_g1.g1_point()), + quorumApkIndices: aggregation_result.quorum_apk_indices, + totalStakeIndices: aggregation_result.total_stake_indices, + nonSignerStakeIndices: aggregation_result.non_signer_stake_indices, + }; + + // Send the response to the contract + contract + .respondToTask(task_clone, response_clone, non_signer_stakes_and_signature) + .from(address!("a0Ee7A142d267C1f36714E4a8F75612F20a79720")) // Aggregator Anvil account address + .send() + .await + .map_err(|e| blueprint_sdk::eigenlayer::generic_task_aggregation::AggregationError::ContractError(e.to_string()))? + .get_receipt() + .await + .map_err(|e| blueprint_sdk::eigenlayer::generic_task_aggregation::AggregationError::ContractError(e.to_string()))?; + + Ok(()) + }) + } +} + +fn to_g1_point(pk: BlsG1Point) -> G1Point { + let pt = convert_to_g1_point(pk.g1()).expect("Invalid G1 point"); + G1Point { X: pt.X, Y: pt.Y } +} + +fn to_g2_point(pk: BlsG2Point) -> G2Point { + let pt = convert_to_g2_point(pk.g2()).expect("Invalid G2 point"); + G2Point { X: pt.X, Y: pt.Y } +} \ No newline at end of file diff --git a/src/contexts/example_context.rs b/src/contexts/example_context.rs new file mode 100644 index 0000000..61172cd --- /dev/null +++ b/src/contexts/example_context.rs @@ -0,0 +1,11 @@ +use crate::contexts::client::AggregatorClient; +use blueprint_sdk::macros::context::KeystoreContext; +use blueprint_sdk::runner::config::BlueprintEnvironment; + +// TODO: Replace with your context name +#[derive(Clone, KeystoreContext)] +pub struct ExampleContext { + pub client: AggregatorClient, + #[config] + pub std_config: BlueprintEnvironment, +} \ No newline at end of file diff --git a/src/contexts/mod.rs b/src/contexts/mod.rs new file mode 100644 index 0000000..4d46571 --- /dev/null +++ b/src/contexts/mod.rs @@ -0,0 +1,6 @@ +pub mod aggregator; +pub mod client; +pub mod combined; +pub mod eigen_task; +// TODO: Replace with your context name +pub mod example_context; \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c7b5438 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,15 @@ +use std::net::AddrParseError; + +use blueprint_sdk::eigensdk::{ + services_blsaggregation::bls_aggregation_service_error::BlsAggregationServiceError, + types::operator::OperatorTypesError, +}; + +#[expect(clippy::large_enum_variant, reason = "SDK error is large currently")] +#[derive(Debug, thiserror::Error)] +pub enum TaskError { + #[error("Aggregation: {0}")] + Aggregation(String), + #[error("Task: {0}")] + Task(String), +} \ No newline at end of file diff --git a/src/jobs/example_task.rs b/src/jobs/example_task.rs new file mode 100644 index 0000000..20570cb --- /dev/null +++ b/src/jobs/example_task.rs @@ -0,0 +1,111 @@ +#![allow(dead_code)] +use crate::ITangleTaskManager::TaskResponse; +use crate::TangleTaskManager::NewTaskCreated; +use crate::contexts::client::SignedTaskResponse; +use crate::contexts::combined::CombinedContext; +use crate::error::TaskError; +use blueprint_sdk::alloy::primitives::{keccak256}; +use blueprint_sdk::alloy::core::sol_types::{SolEvent, SolType, SolValue}; +use blueprint_sdk::contexts::keystore::KeystoreContext; +use blueprint_sdk::crypto::bn254::ArkBlsBn254; +use blueprint_sdk::evm::extract::BlockEvents; +use blueprint_sdk::extract::Context; +use blueprint_sdk::keystore::backends::bn254::Bn254Backend; +use blueprint_sdk::keystore::backends::Backend; +use blueprint_sdk::{error, info}; +use blueprint_sdk::eigensdk::crypto_bls::BlsKeyPair; +use blueprint_sdk::eigensdk::types::operator::operator_id_from_g1_pub_key; + +// TODO: Replace with your job id identifier +pub const EXAMPLE_JOB_ID: u32 = 0; + +/// TODO: Replace with your job logic +/// Sends a signed task response to the BLS Aggregator. +/// This job is triggered by the `NewTaskCreated` event emitted by the `TangleTaskManager`. +/// The job say hello and sends the signed task response to the BLS Aggregator. +#[blueprint_sdk::macros::debug_job] +pub async fn example_task( + Context(ctx): Context, + BlockEvents(events): BlockEvents, +) -> Result<(), TaskError> { + let client = ctx.example_context.client.clone(); + + let task_created_events = events.iter().filter_map(|log| { + NewTaskCreated::decode_log(&log.inner) + .map(|event| event.data) + .ok() + }); + + // TODO: Replace with your use cases + for task_created in task_created_events { + let task = task_created.task; + let task_index = task_created.taskIndex; + + let message_bytes = &task.message; + let greeting = std::str::from_utf8(message_bytes) + .unwrap_or("") + .to_string(); + info!("Greeting: {}", greeting); + + // Calculate the square + let greeting_result = format!("Hello, {}!", greeting); + info!("Greeting result: {}", greeting_result); + + // Properly encode the result as a uint256 instead of a string + let message = SolValue::abi_encode(&greeting_result.as_bytes()); + info!("Result message: {:?}", message); + + // Calculate our response to job + let task_response = TaskResponse { + referenceTaskIndex: task_index, + message: message.into(), + }; + + let bn254_public = ctx.keystore().first_local::().unwrap(); + let bn254_secret = match ctx.keystore().expose_bls_bn254_secret(&bn254_public) { + Ok(s) => match s { + Some(s) => s, + None => { + return Err(TaskError::Task( + "Failed to send signed task response".to_string(), + )); + } + }, + Err(e) => { + return Err(TaskError::Task(format!( + "Failed to send signed task response: {e:?}", + ))); + } + }; + let bls_key_pair = match BlsKeyPair::new(bn254_secret.0.to_string()) { + Ok(pair) => pair, + Err(e) => { + return Err(TaskError::Task(format!( + "Failed to send signed task response: {e:?}", + ))); + } + }; + let operator_id = operator_id_from_g1_pub_key(bls_key_pair.public_key())?; + + // Sign the Hashed Message and send it to the BLS Aggregator + let msg_hash = keccak256(::abi_encode(&task_response)); + let signed_response = SignedTaskResponse { + task_response, + signature: bls_key_pair.sign_message(msg_hash.as_ref()), + operator_id, + }; + + info!( + "Sending signed task response to BLS Aggregator: {:#?}", + signed_response + ); + if let Err(e) = client.send_signed_task_response(signed_response).await { + error!("Failed to send signed task response: {e:?}"); + return Err(TaskError::Task(format!( + "Failed to send signed task response: {e:?}", + ))); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/jobs/initialize_task.rs b/src/jobs/initialize_task.rs new file mode 100644 index 0000000..c7c9d33 --- /dev/null +++ b/src/jobs/initialize_task.rs @@ -0,0 +1,46 @@ +use crate::TangleTaskManager::NewTaskCreated; +use crate::contexts::combined::CombinedContext; +use crate::error::TaskError; +use blueprint_sdk::alloy::core::sol_types::SolEvent; +use blueprint_sdk::evm::extract::BlockEvents; +use blueprint_sdk::extract::Context; +use blueprint_sdk::{info, warn}; + +const TASK_CHALLENGE_WINDOW_BLOCK: u32 = 100; +const BLOCK_TIME_SECONDS: u32 = 12; +pub const INITIALIZE_TASK_JOB_ID: u32 = 1; + +/// Initializes the task for the aggregator server +#[blueprint_sdk::macros::debug_job] +pub async fn initialize_bls_task( + Context(ctx): Context, + BlockEvents(events): BlockEvents, +) -> Result<(), TaskError> { + let task_created_events = events.iter().filter_map(|log| { + NewTaskCreated::decode_log(&log.inner) + .map(|event| event.data) + .ok() + }); + + for task_created in task_created_events { + let task = task_created.task; + let task_index = task_created.taskIndex; + + info!("Initializing task {} for BLS aggregation", task_index); + + if let Some(aggregator_ctx) = &ctx.aggregator_context { + aggregator_ctx + .register_task(task_index, task.clone()) + .await + .map_err(|e| TaskError::Aggregation(e.to_string()))?; + + info!( + "Successfully registered task {} with the task aggregator", + task_index + ); + } else { + warn!("Aggregator context not available, skipping task initialization"); + } + } + Ok(()) +} \ No newline at end of file diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs new file mode 100644 index 0000000..b937e88 --- /dev/null +++ b/src/jobs/mod.rs @@ -0,0 +1,2 @@ +pub mod initialize_task; +pub mod example_task; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index fd856bf..96e7bef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,13 @@ +pub mod contexts; +pub mod jobs; +pub mod error; + use blueprint_sdk::alloy::primitives::{address, Address}; -use blueprint_sdk::alloy::rpc::types::Log; use blueprint_sdk::alloy::sol; -use blueprint_sdk::config::GadgetConfiguration; -use blueprint_sdk::event_listeners::evm::EvmContractEventListener; -use blueprint_sdk::job; -use blueprint_sdk::logging::info; -use blueprint_sdk::macros::load_abi; -use blueprint_sdk::std::convert::Infallible; -use blueprint_sdk::std::sync::LazyLock; +use std::env; +use std::sync::LazyLock; use serde::{Deserialize, Serialize}; -type ProcessorError = - blueprint_sdk::event_listeners::core::Error; - sol!( #[allow(missing_docs)] #[sol(rpc)] @@ -21,56 +16,19 @@ sol!( "contracts/out/TangleTaskManager.sol/TangleTaskManager.json" ); -load_abi!( - TANGLE_TASK_MANAGER_ABI_STRING, - "contracts/out/TangleTaskManager.sol/TangleTaskManager.json" -); - pub static TASK_MANAGER_ADDRESS: LazyLock
= LazyLock::new(|| { - std::env::var("TASK_MANAGER_ADDRESS") + env::var("TASK_MANAGER_ADDRESS") .map(|addr| addr.parse().expect("Invalid TASK_MANAGER_ADDRESS")) .unwrap_or_else(|_| address!("0000000000000000000000000000000000000000")) }); -#[derive(Clone)] -pub struct ExampleContext { - pub config: GadgetConfiguration, -} - -/// Returns "Hello, {who}!" -#[job( - id = 0, - params(who), - event_listener( - listener = EvmContractEventListener, - instance = TangleTaskManager, - abi = TANGLE_TASK_MANAGER_ABI_STRING, - pre_processor = example_pre_processor, - ), -)] -pub fn say_hello(context: ExampleContext, who: String) -> Result { - blueprint_sdk::logging::trace!("Successfully ran job function!"); - info!("Successfully ran job function!"); - Ok(format!("Hello, {who}!")) -} - -/// Example pre-processor for handling inbound events -async fn example_pre_processor( - (_event, log): (TangleTaskManager::NewTaskCreated, Log), -) -> Result, ProcessorError> { - let who = log.address(); - Ok(Some((who.to_string(),))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let config = GadgetConfiguration::default(); - let context = ExampleContext { config }; - let result = say_hello(context, "Alice".into()).unwrap(); - assert_eq!(result, "Hello, Alice!"); - } -} +pub static PRIVATE_KEY: LazyLock = LazyLock::new(|| { + env::var("PRIVATE_KEY").unwrap_or_else(|_| { + "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string() + }) +}); +pub static AGGREGATOR_PRIVATE_KEY: LazyLock = LazyLock::new(|| { + env::var("PRIVATE_KEY").unwrap_or_else(|_| { + "2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6".to_string() + }) +}); diff --git a/src/main.rs b/src/main.rs index 7a68fdb..29aafb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,56 +1,87 @@ -use {{project-name | snake_case}} as blueprint; -use blueprint::{TangleTaskManager, TASK_MANAGER_ADDRESS}; -use blueprint_sdk::alloy::primitives::{address, Address, U256}; -use blueprint_sdk::logging::{info, warn}; -use blueprint_sdk::macros::main; -use blueprint_sdk::runners::core::runner::BlueprintRunner; -use blueprint_sdk::runners::eigenlayer::bls::EigenlayerBLSConfig; -use blueprint_sdk::utils::evm::get_provider_http; - -#[main(env)] -async fn main() { - // Create your service context - // Here you can pass any configuration or context that your service needs. - let context = blueprint::ExampleContext { - config: env.clone(), +use test_eigen_bls_blueprint as blueprint; + +use blueprint::{AGGREGATOR_PRIVATE_KEY, TASK_MANAGER_ADDRESS}; +use std::sync::Arc; +use std::time::Duration; +use blueprint_sdk::alloy::network::EthereumWallet; +use blueprint_sdk::alloy::primitives::Address; +use blueprint_sdk::alloy::signers::local::PrivateKeySigner; +use blueprint_sdk::evm::producer::{PollingConfig, PollingProducer}; +use blueprint_sdk::evm::util::get_wallet_provider_http; +use blueprint_sdk::runner::BlueprintRunner; +use blueprint_sdk::runner::config::BlueprintEnvironment; +use blueprint_sdk::runner::eigenlayer::bls::EigenlayerBLSConfig; +use blueprint_sdk::{Router, info, tokio}; + +use blueprint::contexts::aggregator::AggregatorContext; +use blueprint::contexts::client::AggregatorClient; +use blueprint::contexts::combined::CombinedContext; +use blueprint::jobs::initialize_task::{initialize_bls_task, INITIALIZE_TASK_JOB_ID}; +// TODO: Replace with your context name +use blueprint::contexts::example_context::ExampleContext; +use blueprint::jobs::example_task::{example_task, EXAMPLE_JOB_ID}; + +#[tokio::main] +async fn main() -> Result<(), blueprint_sdk::Error> { + let env = BlueprintEnvironment::load()?; + + let signer: PrivateKeySigner = AGGREGATOR_PRIVATE_KEY + .parse() + .expect("failed to generate wallet "); + let wallet = EthereumWallet::from(signer); + let provider = get_wallet_provider_http(env.http_rpc_endpoint.clone(), wallet.clone()); + let server_address = format!("{}:{}", "127.0.0.1", 8081); + + // TODO: Replace with your context name + let context = ExampleContext { + client: AggregatorClient::new(&server_address) + .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?, + std_config: env.clone(), }; - // Get the provider - let rpc_endpoint = env.http_rpc_endpoint.clone(); - let provider = get_provider_http(&rpc_endpoint); + // Create the aggregator context + let aggregator_context = AggregatorContext::new( + server_address, + *TASK_MANAGER_ADDRESS, + wallet.clone(), + env.clone(), + ) + .await + .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?; + - // Create an instance of your task manager - let contract = TangleTaskManager::new(*TASK_MANAGER_ADDRESS, provider); + // Create the combined context for both tasks + let combined_context = CombinedContext::new( + context, + Some(aggregator_context.clone()), + env.clone(), + ); + let client = Arc::new(provider); - // Create the event handler from the job - let say_hello_job = blueprint::SayHelloEventHandler::new(contract, context.clone()); + // Create producer for task events + let task_producer = PollingProducer::new( + client.clone(), + PollingConfig::default().poll_interval(Duration::from_secs(1)), + ) + .await + .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?; - // Spawn a task to create a task - this is just for testing/example purposes info!("Spawning a task to create a task on the contract..."); - blueprint_sdk::tokio::spawn(async move { - let provider = get_provider_http(&rpc_endpoint); - let contract = TangleTaskManager::new(*TASK_MANAGER_ADDRESS, provider); - loop { - blueprint_sdk::tokio::time::sleep(std::time::Duration::from_secs(5)).await; - // We use the Anvil Account #4 as the Task generator address - let task = contract - .createNewTask(U256::from(5), 100u32, vec![0].into()) - .from(address!("15d34AAf54267DB7D7c367839AAf71A00a2C6A65")); - let receipt = task.send().await.unwrap().get_receipt().await.unwrap(); - if receipt.status() { - info!("Task created successfully"); - } else { - warn!("Task creation failed"); - } - } - }); - - info!("Starting the event watcher ..."); let eigen_config = EigenlayerBLSConfig::new(Address::default(), Address::default()); - BlueprintRunner::new(eigen_config, env) - .job(say_hello_job) - .run() - .await?; + BlueprintRunner::builder(eigen_config, BlueprintEnvironment::default()) + .router( + Router::new() + .route(EXAMPLE_JOB_ID, example_task) + .route(INITIALIZE_TASK_JOB_ID, initialize_bls_task) + .with_context(combined_context), + ) + .producer(task_producer) + .background_service(aggregator_context) + .with_shutdown_handler(async { + blueprint_sdk::info!("Shutting down task manager service"); + }) + .run() + .await?; info!("Exiting..."); Ok(()) From ae27aa38bed93293bd1c41059eda0f1bd1406603 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:46:40 +0700 Subject: [PATCH 2/9] fix: revert crate name as template --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 29aafb6..4acea43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use test_eigen_bls_blueprint as blueprint; +use {{project-name | snake_case}} as blueprint; use blueprint::{AGGREGATOR_PRIVATE_KEY, TASK_MANAGER_ADDRESS}; use std::sync::Arc; From 579bf93ac570e4bb26848000742db5f2390ea73f Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:25:50 +0700 Subject: [PATCH 3/9] fix: simplify template --- src/contexts/aggregator.rs | 274 -------------------------------- src/contexts/client.rs | 84 ---------- src/contexts/combined.rs | 31 ---- src/contexts/eigen_task.rs | 137 ---------------- src/contexts/example_context.rs | 11 -- src/contexts/mod.rs | 6 - src/error.rs | 11 +- src/jobs/example_task.rs | 111 ------------- src/jobs/initialize_task.rs | 46 ------ src/jobs/mod.rs | 2 - src/lib.rs | 64 +++++++- src/main.rs | 107 +++++-------- 12 files changed, 101 insertions(+), 783 deletions(-) delete mode 100644 src/contexts/aggregator.rs delete mode 100644 src/contexts/client.rs delete mode 100644 src/contexts/combined.rs delete mode 100644 src/contexts/eigen_task.rs delete mode 100644 src/contexts/example_context.rs delete mode 100644 src/contexts/mod.rs delete mode 100644 src/jobs/example_task.rs delete mode 100644 src/jobs/initialize_task.rs delete mode 100644 src/jobs/mod.rs diff --git a/src/contexts/aggregator.rs b/src/contexts/aggregator.rs deleted file mode 100644 index 80d664d..0000000 --- a/src/contexts/aggregator.rs +++ /dev/null @@ -1,274 +0,0 @@ -use crate::ITangleTaskManager::{Task, TaskResponse}; -use crate::error::TaskError as Error; -use crate::{ - contexts::client::SignedTaskResponse, - contexts::eigen_task::{IndexedTask, SquaringTaskResponseSender}, -}; -use blueprint_sdk::alloy::network::EthereumWallet; -use blueprint_sdk::alloy::primitives::Address; -use blueprint_sdk::contexts::eigenlayer::EigenlayerContext; -use blueprint_sdk::eigenlayer::generic_task_aggregation::{ - AggregatorConfig, SignedTaskResponse as GenericSignedTaskResponse, TaskAggregator, -}; -use blueprint_sdk::macros::context::{EigenlayerContext, KeystoreContext}; -use blueprint_sdk::runner::{BackgroundService, config::BlueprintEnvironment, error::RunnerError}; -use blueprint_sdk::{debug, error, info}; -use blueprint_sdk::eigensdk::types::avs::TaskIndex; -use jsonrpc_core::{IoHandler, Params, Value}; -use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; -use reqwest::Url; -use std::{collections::VecDeque, net::SocketAddr, sync::Arc, time::Duration}; -use blueprint_sdk::tokio::sync::{Mutex, Notify, oneshot}; -use blueprint_sdk::tokio::task::JoinHandle; - -#[derive(Clone, EigenlayerContext, KeystoreContext)] -pub struct AggregatorContext { - pub port_address: String, - pub task_manager_address: Address, - pub http_rpc_url: Url, - pub wallet: EthereumWallet, - pub response_cache: Arc>>, - #[config] - pub env: BlueprintEnvironment, - shutdown: Arc<(Notify, Mutex)>, - pub task_aggregator: - Option>>, -} - -impl AggregatorContext { - pub async fn new( - port_address: String, - task_manager_address: Address, - wallet: EthereumWallet, - env: BlueprintEnvironment, - ) -> Result { - let mut aggregator_context = AggregatorContext { - port_address, - task_manager_address, - http_rpc_url: env.http_rpc_endpoint.clone(), - wallet, - response_cache: Arc::new(Mutex::new(VecDeque::new())), - env: env.clone(), - shutdown: Arc::new((Notify::new(), Mutex::new(false))), - task_aggregator: None, - }; - - // Initialize the bls registry service - let bls_service = aggregator_context - .eigenlayer_client() - .await - .map_err(|e| Error::Context(e.to_string()))? - .bls_aggregation_service_in_memory() - .await - .map_err(|e| Error::Context(e.to_string()))?; - - // Create the response sender - let response_sender = SquaringTaskResponseSender { - task_manager_address, - http_rpc_url: env.http_rpc_endpoint.clone(), - }; - - // Create the task aggregator with default config - let task_aggregator = - TaskAggregator::new(bls_service, response_sender, AggregatorConfig::default()); - - aggregator_context.task_aggregator = Some(Arc::new(task_aggregator)); - - Ok(aggregator_context) - } - - pub async fn start(self) -> JoinHandle<()> { - let aggregator = Arc::new(Mutex::new(self)); - - blueprint_sdk::tokio::spawn(async move { - info!("Starting aggregator RPC server"); - - // Start the task aggregator - if let Some(task_agg) = &aggregator.lock().await.task_aggregator { - info!("Starting task aggregator"); - task_agg.start().await; - } - - let server_handle = blueprint_sdk::tokio::spawn(Self::start_server(Arc::clone(&aggregator))); - - info!("Aggregator server started and running in the background"); - // Wait for server task to complete - if let Err(e) = server_handle.await { - error!("Server task failed: {}", e); - } - - info!("Aggregator shutdown complete"); - }) - } - - pub async fn shutdown(&self) { - info!("Initiating aggregator shutdown"); - - if let Some(task_agg) = &self.task_aggregator { - match blueprint_sdk::tokio::time::timeout(Duration::from_secs(10), task_agg.stop()).await { - Ok(Ok(_)) => info!("Task aggregator stopped successfully"), - Ok(Err(e)) => error!("Error stopping task aggregator: {}", e), - Err(_) => error!("Timeout while stopping task aggregator"), - } - } else { - info!("No task aggregator to stop"); - } - - // Set internal shutdown flag - let (notify, is_shutdown) = &*self.shutdown; - *is_shutdown.lock().await = true; - notify.notify_waiters(); - - debug!("Aggregator shutdown flag set"); - } - - async fn start_server(aggregator: Arc>) -> Result<(), Error> { - let mut io = IoHandler::new(); - io.add_method("process_signed_task_response", { - let aggregator = Arc::clone(&aggregator); - move |params: Params| { - let aggregator = Arc::clone(&aggregator); - async move { - // Parse the outer structure first - let outer_params: Value = params.parse()?; - - // Extract the inner "params" object - let inner_params = outer_params.get("params").ok_or_else(|| { - jsonrpc_core::Error::invalid_params("Missing 'params' field") - })?; - - // Now parse the inner params as SignedTaskResponse - let signed_task_response: SignedTaskResponse = - jsonrpc_core::serde_json::from_value(inner_params.clone()).map_err(|e| { - jsonrpc_core::Error::invalid_params(format!( - "Invalid SignedTaskResponse: {e}", - )) - })?; - - aggregator - .lock() - .await - .process_signed_task_response(signed_task_response) - .await - .map(|_| Value::Bool(true)) - .map_err(|e| jsonrpc_core::Error::invalid_params(e.to_string())) - } - } - }); - - let socket: SocketAddr = aggregator - .lock() - .await - .port_address - .parse() - .map_err(Error::Parse)?; - let server = ServerBuilder::new(io) - .cors(DomainsValidation::AllowOnly(vec![ - AccessControlAllowOrigin::Any, - ])) - .start_http(&socket) - .map_err(|e| Error::Context(e.to_string()))?; - - info!("Server running at {}", socket); - - // Create a close handle before we move the server - let close_handle = server.close_handle(); - - // Get shutdown components - let shutdown = { - let agg = aggregator.lock().await; - agg.shutdown.clone() - }; - - // Create a channel to coordinate shutdown - let (server_tx, server_rx) = oneshot::channel(); - - // Spawn the server in a blocking task - let server_handle = blueprint_sdk::tokio::task::spawn_blocking(move || { - server.wait(); - let _ = server_tx.send(()); - }); - - // Use tokio::select! to wait for either the server to finish or the shutdown signal - blueprint_sdk::tokio::select! { - result = server_handle => { - info!("Server has stopped naturally"); - result.map_err(|e| { - error!("Server task failed: {}", e); - Error::Runtime(e.to_string()) - })?; - } - _ = server_rx => { - info!("Server has been shut down via close handle"); - } - _ = async { - let (notify, is_shutdown) = &*shutdown; - loop { - notify.notified().await; - if *is_shutdown.lock().await { - break; - } - } - } => { - info!("Shutdown signal received, stopping server"); - close_handle.close(); - } - } - - Ok(()) - } - - pub async fn process_signed_task_response( - &mut self, - resp: SignedTaskResponse, - ) -> Result<(), Error> { - // Convert the SignedTaskResponse to GenericSignedTaskResponse - let generic_signed_response = GenericSignedTaskResponse { - response: resp.task_response, - signature: resp.signature, - operator_id: resp.operator_id, - }; - - // Process the signed response using the generic task aggregator - if let Some(task_agg) = &self.task_aggregator { - task_agg - .process_signed_response(generic_signed_response) - .await; - Ok(()) - } else { - Err(Error::Context( - "Task aggregator not initialized".to_string(), - )) - } - } - - // Register a task with the aggregator - pub async fn register_task(&self, task_index: TaskIndex, task: Task) -> Result<(), Error> { - if let Some(task_agg) = &self.task_aggregator { - // Create an indexed task with the task index - let indexed_task = IndexedTask::new(task, task_index); - - // Register the task with the generic task aggregator - task_agg - .register_task(indexed_task) - .await - .map_err(|e| Error::Context(e.to_string())) - } else { - Err(Error::Context( - "Task aggregator not initialized".to_string(), - )) - } - } -} - -impl BackgroundService for AggregatorContext { - async fn start(&self) -> Result>, RunnerError> { - let (tx, rx) = oneshot::channel(); - let ctx = self.clone(); - blueprint_sdk::tokio::spawn(async move { - ctx.start().await; - let _ = tx.send(Ok(())); - }); - Ok(rx) - } -} \ No newline at end of file diff --git a/src/contexts/client.rs b/src/contexts/client.rs deleted file mode 100644 index 9ce9942..0000000 --- a/src/contexts/client.rs +++ /dev/null @@ -1,84 +0,0 @@ -use blueprint_sdk::alloy::rpc::client::ReqwestClient; -use blueprint_sdk::{debug, info}; -use color_eyre::Result; -use blueprint_sdk::eigensdk::crypto_bls::{OperatorId, Signature}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use jsonrpc_core::serde_json::json; -use blueprint_sdk::tokio::time::{Duration, sleep}; - -use crate::ITangleTaskManager::TaskResponse; - -const MAX_RETRIES: u32 = 5; -const INITIAL_RETRY_DELAY: Duration = Duration::from_secs(1); - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignedTaskResponse { - pub task_response: TaskResponse, - pub signature: Signature, - pub operator_id: OperatorId, -} - -/// Client for interacting with the Aggregator RPC server -#[derive(Debug, Clone)] -pub struct AggregatorClient { - client: ReqwestClient, -} - -impl AggregatorClient { - /// Creates a new AggregatorClient - pub fn new(aggregator_address: &str) -> Result { - let url = Url::parse(&format!("http://{aggregator_address}"))?; - let client = ReqwestClient::new_http(url); - Ok(Self { client }) - } - - /// Sends a signed task response to the aggregator - pub async fn send_signed_task_response(&self, response: SignedTaskResponse) -> Result<()> { - let params = json!({ - "params": response, - "id": 1, - "jsonrpc": "2.0" - }); - - for attempt in 1..=MAX_RETRIES { - match self - .client - .request::<_, bool>("process_signed_task_response", ¶ms) - .await - { - Ok(true) => { - info!("Task response accepted by aggregator"); - // MARK: Uncomment when metrics are implemented - // incredible_metrics::inc_num_tasks_accepted_by_aggregator(); - return Ok(()); - } - Ok(false) => debug!("Task response not accepted, retrying..."), - Err(e) => debug!("Error sending task response: {}", e), - } - - if attempt < MAX_RETRIES { - let delay = INITIAL_RETRY_DELAY * 2u32.pow(attempt - 1); - info!("Retrying in {} seconds...", delay.as_secs()); - sleep(delay).await; - } - } - - debug!( - "Failed to send signed task response after {} attempts", - MAX_RETRIES - ); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new_client() { - let client = AggregatorClient::new("127.0.0.1:8545"); - assert!(client.is_ok()); - } -} \ No newline at end of file diff --git a/src/contexts/combined.rs b/src/contexts/combined.rs deleted file mode 100644 index 15560e2..0000000 --- a/src/contexts/combined.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::contexts::aggregator::AggregatorContext; -// TODO: Replace with your context name -use crate::contexts::example_context::ExampleContext; -use blueprint_sdk::macros::context::KeystoreContext; -use blueprint_sdk::runner::config::BlueprintEnvironment; - -/// Combined context that includes both the ExampleContext and AggregatorContext -/// This allows both jobs to share the same context in the router -#[derive(Clone, KeystoreContext)] -pub struct CombinedContext { - // TODO: Replace with your context name - pub example_context: ExampleContext, - pub aggregator_context: Option, - #[config] - pub std_config: BlueprintEnvironment, -} - -impl CombinedContext { - pub fn new( - // TODO: Replace with your context name - example_context: ExampleContext, - aggregator_context: Option, - std_config: BlueprintEnvironment, - ) -> Self { - Self { - example_context, - aggregator_context, - std_config, - } - } -} \ No newline at end of file diff --git a/src/contexts/eigen_task.rs b/src/contexts/eigen_task.rs deleted file mode 100644 index d79f910..0000000 --- a/src/contexts/eigen_task.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::BN254::{G1Point, G2Point}; -use crate::IBLSSignatureCheckerTypes::NonSignerStakesAndSignature; -use crate::TangleTaskManager as ExampleTask; -use crate::ITangleTaskManager::{Task, TaskResponse}; -use blueprint_sdk::alloy::primitives::address; -use blueprint_sdk::alloy::core::sol_types::SolType; -use blueprint_sdk::eigenlayer::generic_task_aggregation::{ - EigenTask, ResponseSender, Result as AggResult, TaskResponse as GenericTaskResponse, -}; -use blueprint_sdk::evm::util::get_provider_from_signer; -use blueprint_sdk::eigensdk::crypto_bls::{BlsG1Point, BlsG2Point, convert_to_g1_point, convert_to_g2_point}; -use blueprint_sdk::eigensdk::services_blsaggregation::bls_aggregation_service_response::BlsAggregationServiceResponse; -use blueprint_sdk::eigensdk::types::avs::TaskIndex; -use reqwest::Url; -use std::future::Future; -use std::pin::Pin; - -// Wrapper for Task that includes the task index -#[derive(Clone)] -pub struct IndexedTask { - pub task: Task, - pub task_index: TaskIndex, -} - -impl IndexedTask { - pub fn new(task: Task, task_index: TaskIndex) -> Self { - Self { task, task_index } - } -} - -// Implement EigenTask for the IndexedTask type -impl EigenTask for IndexedTask { - fn task_index(&self) -> TaskIndex { - self.task_index - } - - fn created_block(&self) -> u32 { - self.task.taskCreatedBlock - } - - fn quorum_numbers(&self) -> Vec { - self.task.quorumNumbers.to_vec() - } - - fn quorum_threshold_percentage(&self) -> u8 { - self.task.quorumThresholdPercentage as u8 - } - - fn encode(&self) -> Vec { - ::abi_encode(&self.task).to_vec() - } -} - -// Implement TaskResponse for the existing TaskResponse type -impl GenericTaskResponse for TaskResponse { - fn reference_task_index(&self) -> TaskIndex { - self.referenceTaskIndex - } - - fn encode(&self) -> Vec { - ::abi_encode(self).to_vec() - } -} - -// Implement ResponseSender for sending aggregated responses to the contract -#[derive(Clone)] -pub struct SquaringTaskResponseSender { - pub task_manager_address: blueprint_sdk::alloy::primitives::Address, - pub http_rpc_url: Url, -} - -impl ResponseSender for SquaringTaskResponseSender { - type Future = Pin> + Send + 'static>>; - - fn send_aggregated_response( - &self, - indexed_task: &IndexedTask, - response: &TaskResponse, - aggregation_result: BlsAggregationServiceResponse, - ) -> Self::Future { - let task_clone = indexed_task.task.clone(); - let response_clone = response.clone(); - let task_manager_address = self.task_manager_address; - let http_rpc_url = self.http_rpc_url.clone(); - - Box::pin(async move { - let key = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6"; // Private key from our Aggregator Anvil account - let provider = get_provider_from_signer(key, http_rpc_url); - - let contract = - ExampleTask::new(task_manager_address, provider.clone()); - - // Convert the aggregation result to the NonSignerStakesAndSignature format - let non_signer_stakes_and_signature = NonSignerStakesAndSignature { - nonSignerPubkeys: aggregation_result - .non_signers_pub_keys_g1 - .into_iter() - .map(to_g1_point) - .collect(), - nonSignerQuorumBitmapIndices: aggregation_result.non_signer_quorum_bitmap_indices, - quorumApks: aggregation_result - .quorum_apks_g1 - .into_iter() - .map(to_g1_point) - .collect(), - apkG2: to_g2_point(aggregation_result.signers_apk_g2), - sigma: to_g1_point(aggregation_result.signers_agg_sig_g1.g1_point()), - quorumApkIndices: aggregation_result.quorum_apk_indices, - totalStakeIndices: aggregation_result.total_stake_indices, - nonSignerStakeIndices: aggregation_result.non_signer_stake_indices, - }; - - // Send the response to the contract - contract - .respondToTask(task_clone, response_clone, non_signer_stakes_and_signature) - .from(address!("a0Ee7A142d267C1f36714E4a8F75612F20a79720")) // Aggregator Anvil account address - .send() - .await - .map_err(|e| blueprint_sdk::eigenlayer::generic_task_aggregation::AggregationError::ContractError(e.to_string()))? - .get_receipt() - .await - .map_err(|e| blueprint_sdk::eigenlayer::generic_task_aggregation::AggregationError::ContractError(e.to_string()))?; - - Ok(()) - }) - } -} - -fn to_g1_point(pk: BlsG1Point) -> G1Point { - let pt = convert_to_g1_point(pk.g1()).expect("Invalid G1 point"); - G1Point { X: pt.X, Y: pt.Y } -} - -fn to_g2_point(pk: BlsG2Point) -> G2Point { - let pt = convert_to_g2_point(pk.g2()).expect("Invalid G2 point"); - G2Point { X: pt.X, Y: pt.Y } -} \ No newline at end of file diff --git a/src/contexts/example_context.rs b/src/contexts/example_context.rs deleted file mode 100644 index 61172cd..0000000 --- a/src/contexts/example_context.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::contexts::client::AggregatorClient; -use blueprint_sdk::macros::context::KeystoreContext; -use blueprint_sdk::runner::config::BlueprintEnvironment; - -// TODO: Replace with your context name -#[derive(Clone, KeystoreContext)] -pub struct ExampleContext { - pub client: AggregatorClient, - #[config] - pub std_config: BlueprintEnvironment, -} \ No newline at end of file diff --git a/src/contexts/mod.rs b/src/contexts/mod.rs deleted file mode 100644 index 4d46571..0000000 --- a/src/contexts/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod aggregator; -pub mod client; -pub mod combined; -pub mod eigen_task; -// TODO: Replace with your context name -pub mod example_context; \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index c7b5438..9239b8e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,6 @@ -use std::net::AddrParseError; - -use blueprint_sdk::eigensdk::{ - services_blsaggregation::bls_aggregation_service_error::BlsAggregationServiceError, - types::operator::OperatorTypesError, -}; - #[expect(clippy::large_enum_variant, reason = "SDK error is large currently")] #[derive(Debug, thiserror::Error)] pub enum TaskError { - #[error("Aggregation: {0}")] - Aggregation(String), #[error("Task: {0}")] Task(String), -} \ No newline at end of file +} diff --git a/src/jobs/example_task.rs b/src/jobs/example_task.rs deleted file mode 100644 index 20570cb..0000000 --- a/src/jobs/example_task.rs +++ /dev/null @@ -1,111 +0,0 @@ -#![allow(dead_code)] -use crate::ITangleTaskManager::TaskResponse; -use crate::TangleTaskManager::NewTaskCreated; -use crate::contexts::client::SignedTaskResponse; -use crate::contexts::combined::CombinedContext; -use crate::error::TaskError; -use blueprint_sdk::alloy::primitives::{keccak256}; -use blueprint_sdk::alloy::core::sol_types::{SolEvent, SolType, SolValue}; -use blueprint_sdk::contexts::keystore::KeystoreContext; -use blueprint_sdk::crypto::bn254::ArkBlsBn254; -use blueprint_sdk::evm::extract::BlockEvents; -use blueprint_sdk::extract::Context; -use blueprint_sdk::keystore::backends::bn254::Bn254Backend; -use blueprint_sdk::keystore::backends::Backend; -use blueprint_sdk::{error, info}; -use blueprint_sdk::eigensdk::crypto_bls::BlsKeyPair; -use blueprint_sdk::eigensdk::types::operator::operator_id_from_g1_pub_key; - -// TODO: Replace with your job id identifier -pub const EXAMPLE_JOB_ID: u32 = 0; - -/// TODO: Replace with your job logic -/// Sends a signed task response to the BLS Aggregator. -/// This job is triggered by the `NewTaskCreated` event emitted by the `TangleTaskManager`. -/// The job say hello and sends the signed task response to the BLS Aggregator. -#[blueprint_sdk::macros::debug_job] -pub async fn example_task( - Context(ctx): Context, - BlockEvents(events): BlockEvents, -) -> Result<(), TaskError> { - let client = ctx.example_context.client.clone(); - - let task_created_events = events.iter().filter_map(|log| { - NewTaskCreated::decode_log(&log.inner) - .map(|event| event.data) - .ok() - }); - - // TODO: Replace with your use cases - for task_created in task_created_events { - let task = task_created.task; - let task_index = task_created.taskIndex; - - let message_bytes = &task.message; - let greeting = std::str::from_utf8(message_bytes) - .unwrap_or("") - .to_string(); - info!("Greeting: {}", greeting); - - // Calculate the square - let greeting_result = format!("Hello, {}!", greeting); - info!("Greeting result: {}", greeting_result); - - // Properly encode the result as a uint256 instead of a string - let message = SolValue::abi_encode(&greeting_result.as_bytes()); - info!("Result message: {:?}", message); - - // Calculate our response to job - let task_response = TaskResponse { - referenceTaskIndex: task_index, - message: message.into(), - }; - - let bn254_public = ctx.keystore().first_local::().unwrap(); - let bn254_secret = match ctx.keystore().expose_bls_bn254_secret(&bn254_public) { - Ok(s) => match s { - Some(s) => s, - None => { - return Err(TaskError::Task( - "Failed to send signed task response".to_string(), - )); - } - }, - Err(e) => { - return Err(TaskError::Task(format!( - "Failed to send signed task response: {e:?}", - ))); - } - }; - let bls_key_pair = match BlsKeyPair::new(bn254_secret.0.to_string()) { - Ok(pair) => pair, - Err(e) => { - return Err(TaskError::Task(format!( - "Failed to send signed task response: {e:?}", - ))); - } - }; - let operator_id = operator_id_from_g1_pub_key(bls_key_pair.public_key())?; - - // Sign the Hashed Message and send it to the BLS Aggregator - let msg_hash = keccak256(::abi_encode(&task_response)); - let signed_response = SignedTaskResponse { - task_response, - signature: bls_key_pair.sign_message(msg_hash.as_ref()), - operator_id, - }; - - info!( - "Sending signed task response to BLS Aggregator: {:#?}", - signed_response - ); - if let Err(e) = client.send_signed_task_response(signed_response).await { - error!("Failed to send signed task response: {e:?}"); - return Err(TaskError::Task(format!( - "Failed to send signed task response: {e:?}", - ))); - } - } - - Ok(()) -} \ No newline at end of file diff --git a/src/jobs/initialize_task.rs b/src/jobs/initialize_task.rs deleted file mode 100644 index c7c9d33..0000000 --- a/src/jobs/initialize_task.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::TangleTaskManager::NewTaskCreated; -use crate::contexts::combined::CombinedContext; -use crate::error::TaskError; -use blueprint_sdk::alloy::core::sol_types::SolEvent; -use blueprint_sdk::evm::extract::BlockEvents; -use blueprint_sdk::extract::Context; -use blueprint_sdk::{info, warn}; - -const TASK_CHALLENGE_WINDOW_BLOCK: u32 = 100; -const BLOCK_TIME_SECONDS: u32 = 12; -pub const INITIALIZE_TASK_JOB_ID: u32 = 1; - -/// Initializes the task for the aggregator server -#[blueprint_sdk::macros::debug_job] -pub async fn initialize_bls_task( - Context(ctx): Context, - BlockEvents(events): BlockEvents, -) -> Result<(), TaskError> { - let task_created_events = events.iter().filter_map(|log| { - NewTaskCreated::decode_log(&log.inner) - .map(|event| event.data) - .ok() - }); - - for task_created in task_created_events { - let task = task_created.task; - let task_index = task_created.taskIndex; - - info!("Initializing task {} for BLS aggregation", task_index); - - if let Some(aggregator_ctx) = &ctx.aggregator_context { - aggregator_ctx - .register_task(task_index, task.clone()) - .await - .map_err(|e| TaskError::Aggregation(e.to_string()))?; - - info!( - "Successfully registered task {} with the task aggregator", - task_index - ); - } else { - warn!("Aggregator context not available, skipping task initialization"); - } - } - Ok(()) -} \ No newline at end of file diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs deleted file mode 100644 index b937e88..0000000 --- a/src/jobs/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod initialize_task; -pub mod example_task; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 96e7bef..14f42ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,17 @@ -pub mod contexts; -pub mod jobs; pub mod error; +use crate::error::TaskError; +use crate::TangleTaskManager::NewTaskCreated; use blueprint_sdk::alloy::primitives::{address, Address}; +use blueprint_sdk::alloy::sol_types::{SolEvent, SolValue}; use blueprint_sdk::alloy::sol; +use blueprint_sdk::info; +use blueprint_sdk::runner::config::BlueprintEnvironment; +use blueprint_sdk::extract::Context; +use blueprint_sdk::evm::extract::BlockEvents; +use serde::{Deserialize, Serialize}; use std::env; use std::sync::LazyLock; -use serde::{Deserialize, Serialize}; sol!( #[allow(missing_docs)] @@ -27,8 +32,51 @@ pub static PRIVATE_KEY: LazyLock = LazyLock::new(|| { "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string() }) }); -pub static AGGREGATOR_PRIVATE_KEY: LazyLock = LazyLock::new(|| { - env::var("PRIVATE_KEY").unwrap_or_else(|_| { - "2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6".to_string() - }) -}); + +// TODO: Replace with your context name +#[derive(Clone)] +pub struct ExampleContext { + pub std_config: BlueprintEnvironment, +} + +pub const EXAMPLE_JOB_ID: u32 = 0; + +/// Example task that responds to a task created event +/// This function is triggered by the NewTaskCreated event emitted by the TangleTaskManager contract +/// This function response to greeting `Task.message` +#[blueprint_sdk::macros::debug_job] +pub async fn example_task( + Context(_ctx): Context, + BlockEvents(events): BlockEvents, +) -> Result<(), TaskError> { + info!("Successfully ran job function!"); + + let task_created_events = events.iter().filter_map(|log| { + NewTaskCreated::decode_log(&log.inner) + .map(|event| event.data) + .ok() + }); + + for task_created in task_created_events { + let task = task_created.task; + let task_index = task_created.taskIndex; + + info!("Task created: {}", task_index); + + let message_bytes = &task.message; + let greeting = std::str::from_utf8(message_bytes) + .unwrap_or("") + .to_string(); + info!("Greeting: {}", greeting); + + // Calculate the square + let greeting_result = format!("Hello, {}!", greeting); + info!("Greeting result: {}", greeting_result); + + // Properly encode the result as a uint256 instead of a string + let message = SolValue::abi_encode(&greeting_result.as_bytes()); + info!("Result message: {:?}", message); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 4acea43..836a9ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,87 +1,68 @@ use {{project-name | snake_case}} as blueprint; -use blueprint::{AGGREGATOR_PRIVATE_KEY, TASK_MANAGER_ADDRESS}; -use std::sync::Arc; -use std::time::Duration; + use blueprint_sdk::alloy::network::EthereumWallet; -use blueprint_sdk::alloy::primitives::Address; +use blueprint_sdk::alloy::primitives::{address, Address, Bytes}; use blueprint_sdk::alloy::signers::local::PrivateKeySigner; -use blueprint_sdk::evm::producer::{PollingConfig, PollingProducer}; use blueprint_sdk::evm::util::get_wallet_provider_http; -use blueprint_sdk::runner::BlueprintRunner; use blueprint_sdk::runner::config::BlueprintEnvironment; use blueprint_sdk::runner::eigenlayer::bls::EigenlayerBLSConfig; -use blueprint_sdk::{Router, info, tokio}; - -use blueprint::contexts::aggregator::AggregatorContext; -use blueprint::contexts::client::AggregatorClient; -use blueprint::contexts::combined::CombinedContext; -use blueprint::jobs::initialize_task::{initialize_bls_task, INITIALIZE_TASK_JOB_ID}; +use blueprint_sdk::runner::BlueprintRunner; +use blueprint_sdk::{info, warn, tokio, Router}; +use std::time::Duration; +use blueprint::TangleTaskManager; +use blueprint::{PRIVATE_KEY, TASK_MANAGER_ADDRESS}; // TODO: Replace with your context name -use blueprint::contexts::example_context::ExampleContext; -use blueprint::jobs::example_task::{example_task, EXAMPLE_JOB_ID}; +use blueprint::ExampleContext; +use blueprint::{EXAMPLE_JOB_ID, example_task}; #[tokio::main] async fn main() -> Result<(), blueprint_sdk::Error> { let env = BlueprintEnvironment::load()?; - let signer: PrivateKeySigner = AGGREGATOR_PRIVATE_KEY - .parse() - .expect("failed to generate wallet "); - let wallet = EthereumWallet::from(signer); - let provider = get_wallet_provider_http(env.http_rpc_endpoint.clone(), wallet.clone()); - let server_address = format!("{}:{}", "127.0.0.1", 8081); - // TODO: Replace with your context name let context = ExampleContext { - client: AggregatorClient::new(&server_address) - .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?, std_config: env.clone(), }; - // Create the aggregator context - let aggregator_context = AggregatorContext::new( - server_address, - *TASK_MANAGER_ADDRESS, - wallet.clone(), - env.clone(), - ) - .await - .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?; - - - // Create the combined context for both tasks - let combined_context = CombinedContext::new( - context, - Some(aggregator_context.clone()), - env.clone(), - ); - let client = Arc::new(provider); - - // Create producer for task events - let task_producer = PollingProducer::new( - client.clone(), - PollingConfig::default().poll_interval(Duration::from_secs(1)), - ) - .await - .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?; + let signer: PrivateKeySigner = PRIVATE_KEY.parse().expect("failed to generate wallet "); + let wallet = EthereumWallet::from(signer); + let provider = get_wallet_provider_http(env.http_rpc_endpoint.clone(), wallet.clone()); + // Create an instance of your task manager + let contract = TangleTaskManager::new(*TASK_MANAGER_ADDRESS, provider); + // Spawn a task to create a task - this is just for testing/example purposes info!("Spawning a task to create a task on the contract..."); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + // We use the Anvil Account #4 as the Task generator address + let task = contract + .createNewTask(Bytes::from_static(b"World"), 100u32, vec![0].into()) + .from(address!("15d34AAf54267DB7D7c367839AAf71A00a2C6A65")); + let receipt = task.send().await.unwrap().get_receipt().await.unwrap(); + if receipt.status() { + info!("Task created successfully"); + } else { + warn!("Task creation failed"); + } + } + }); + + info!("Starting the event watcher ..."); let eigen_config = EigenlayerBLSConfig::new(Address::default(), Address::default()); - BlueprintRunner::builder(eigen_config, BlueprintEnvironment::default()) - .router( - Router::new() - .route(EXAMPLE_JOB_ID, example_task) - .route(INITIALIZE_TASK_JOB_ID, initialize_bls_task) - .with_context(combined_context), - ) - .producer(task_producer) - .background_service(aggregator_context) - .with_shutdown_handler(async { - blueprint_sdk::info!("Shutting down task manager service"); - }) - .run() - .await?; + BlueprintRunner::builder(eigen_config, env) + .router( + // TODO: Update your task + Router::new() + .route(EXAMPLE_JOB_ID, example_task) + .with_context(context), + ) + .with_shutdown_handler(async { + info!("Shutting down task manager service"); + }) + .run() + .await?; info!("Exiting..."); Ok(()) From 2dd14014639dcf0f24109d021d3889fa468aa458 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:01:14 +0700 Subject: [PATCH 4/9] chore: bump dependency --- contracts/src/ITangleTaskManager.sol | 14 --- contracts/src/TangleTaskManager.sol | 127 +-------------------------- src/lib.rs | 2 - src/main.rs | 24 +++-- 4 files changed, 20 insertions(+), 147 deletions(-) diff --git a/contracts/src/ITangleTaskManager.sol b/contracts/src/ITangleTaskManager.sol index 681f259..fa8a770 100644 --- a/contracts/src/ITangleTaskManager.sol +++ b/contracts/src/ITangleTaskManager.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.13; -import "@eigenlayer-middleware/src/libraries/BN254.sol"; - interface ITangleTaskManager { // EVENTS event NewTaskCreated(uint32 indexed taskIndex, Task task); @@ -11,10 +9,6 @@ interface ITangleTaskManager { event TaskCompleted(uint32 indexed taskIndex); - event TaskChallengedSuccessfully(uint32 indexed taskIndex, address indexed challenger); - - event TaskChallengedUnsuccessfully(uint32 indexed taskIndex, address indexed challenger); - // STRUCTS struct Task { // TODO: Replace your task params @@ -60,14 +54,6 @@ interface ITangleTaskManager { /// @notice Returns the current 'taskNumber' for the middleware function taskNumber() external view returns (uint32); - // NOTE: this function raises challenge to existing tasks. - function raiseAndResolveChallenge( - Task calldata task, - TaskResponse calldata taskResponse, - TaskResponseMetadata calldata taskResponseMetadata, - BN254.G1Point[] memory pubkeysOfNonSigningOperators - ) external; - /// @notice Returns the TASK_RESPONSE_WINDOW_BLOCK function getTaskResponseWindowBlock() external view returns (uint32); } diff --git a/contracts/src/TangleTaskManager.sol b/contracts/src/TangleTaskManager.sol index 73fc142..f4fcef1 100644 --- a/contracts/src/TangleTaskManager.sol +++ b/contracts/src/TangleTaskManager.sol @@ -48,13 +48,8 @@ contract TangleTaskManager is // mapping of task indices to hash of abi.encode(taskResponse, taskResponseMetadata) mapping(uint32 => bytes32) public allTaskResponses; - mapping(uint32 => bool) public taskSuccesfullyChallenged; - address public aggregator; address public generator; - address public instantSlasher; - address public allocationManager; - address public serviceManager; /* MODIFIERS */ @@ -79,19 +74,13 @@ contract TangleTaskManager is } function initialize( - address initialOwner, address _aggregator, address _generator, - address _allocationManager, - address _slasher, - address _serviceManager + address initialOwner ) public initializer { _transferOwnership(initialOwner); aggregator = _aggregator; generator = _generator; - allocationManager = _allocationManager; - instantSlasher = _slasher; - serviceManager = _serviceManager; } /* FUNCTIONS */ @@ -187,120 +176,6 @@ contract TangleTaskManager is return latestTaskNum; } - function raiseAndResolveChallenge( - Task calldata task, - TaskResponse calldata taskResponse, - TaskResponseMetadata calldata taskResponseMetadata, - BN254.G1Point[] memory pubkeysOfNonSigningOperators - ) external { - uint32 referenceTaskIndex = taskResponse.referenceTaskIndex; - // some logical checks - require( - allTaskResponses[referenceTaskIndex] != bytes32(0), "Task hasn't been responded to yet" - ); - require( - allTaskResponses[referenceTaskIndex] - == keccak256(abi.encode(taskResponse, taskResponseMetadata)), - "Task response does not match the one recorded in the contract" - ); - require( - taskSuccesfullyChallenged[referenceTaskIndex] == false, - "The response to this task has already been challenged successfully." - ); - - require( - uint32(block.number) - <= taskResponseMetadata.taskRespondedBlock + TASK_CHALLENGE_WINDOW_BLOCK, - "The challenge period for this task has already expired." - ); - - // TODO: Replace to your use cases - // logic for checking whether challenge is valid or not - // check message length contains more characters - bool isResponseCorrect = task.message.length < taskResponse.message.length; - // if response was correct, no slashing happens so we return - if (isResponseCorrect == true) { - emit TaskChallengedUnsuccessfully(referenceTaskIndex, msg.sender); - return; - } - - // get the list of hash of pubkeys of operators who weren't part of the task response submitted by the aggregator - bytes32[] memory hashesOfPubkeysOfNonSigningOperators = - new bytes32[](pubkeysOfNonSigningOperators.length); - for (uint256 i = 0; i < pubkeysOfNonSigningOperators.length; i++) { - hashesOfPubkeysOfNonSigningOperators[i] = pubkeysOfNonSigningOperators[i].hashG1Point(); - } - - // verify whether the pubkeys of "claimed" non-signers supplied by challenger are actually non-signers as recorded before - // when the aggregator responded to the task - // currently inlined, as the MiddlewareUtils.computeSignatoryRecordHash function was removed from BLSSignatureChecker - // in this PR: https://github.com/Layr-Labs/eigenlayer-contracts/commit/c836178bf57adaedff37262dff1def18310f3dce#diff-8ab29af002b60fc80e3d6564e37419017c804ae4e788f4c5ff468ce2249b4386L155-L158 - // TODO(samlaf): contracts team will add this function back in the BLSSignatureChecker, which we should use to prevent potential bugs from code duplication - bytes32 signatoryRecordHash = - keccak256(abi.encodePacked(task.taskCreatedBlock, hashesOfPubkeysOfNonSigningOperators)); - require( - signatoryRecordHash == taskResponseMetadata.hashOfNonSigners, - "The pubkeys of non-signing operators supplied by the challenger are not correct." - ); - - // get the address of operators who didn't sign - address[] memory addressOfNonSigningOperators = - new address[](pubkeysOfNonSigningOperators.length); - for (uint256 i = 0; i < pubkeysOfNonSigningOperators.length; i++) { - addressOfNonSigningOperators[i] = BLSApkRegistry(address(blsApkRegistry)) - .pubkeyHashToOperator(hashesOfPubkeysOfNonSigningOperators[i]); - } - - // get the list of all operators who were active when the task was initialized - Operator[][] memory allOperatorInfo = getOperatorState( - ISlashingRegistryCoordinator(address(registryCoordinator)), - task.quorumNumbers, - task.taskCreatedBlock - ); - // first for loop iterate over quorums - for (uint256 i = 0; i < allOperatorInfo.length; i++) { - // second for loop iterate over operators active in the quorum when the task was initialized - for (uint256 j = 0; j < allOperatorInfo[i].length; j++) { - // get the operator address - bytes32 operatorID = allOperatorInfo[i][j].operatorId; - address operatorAddress = blsApkRegistry.getOperatorFromPubkeyHash(operatorID); - // check whether the operator was a signer for the task - bool wasSigningOperator = true; - for (uint256 k = 0; k < addressOfNonSigningOperators.length; k++) { - if (operatorAddress == addressOfNonSigningOperators[k]) { - // if the operator was a non-signer, then we set the flag to false - wasSigningOperator = false; - break; - } - } - if (wasSigningOperator == true) { - OperatorSet memory operatorset = - OperatorSet({avs: serviceManager, id: uint8(task.quorumNumbers[i])}); - IStrategy[] memory istrategy = IAllocationManager(allocationManager) - .getStrategiesInOperatorSet(operatorset); - uint256[] memory wadsToSlash = new uint256[](istrategy.length); - for (uint256 z = 0; z < wadsToSlash.length; z++) { - wadsToSlash[z] = WADS_TO_SLASH; - } - IAllocationManagerTypes.SlashingParams memory slashingparams = - IAllocationManagerTypes.SlashingParams({ - operator: operatorAddress, - operatorSetId: uint8(task.quorumNumbers[i]), - strategies: istrategy, - wadsToSlash: wadsToSlash, - description: "slash_the_operator" - }); - InstantSlasher(instantSlasher).fulfillSlashingRequest(slashingparams); - } - } - } - - // the task response has been challenged successfully - taskSuccesfullyChallenged[referenceTaskIndex] = true; - - emit TaskChallengedSuccessfully(referenceTaskIndex, msg.sender); - } - function getTaskResponseWindowBlock() external view returns (uint32) { return TASK_RESPONSE_WINDOW_BLOCK; } diff --git a/src/lib.rs b/src/lib.rs index 14f42ff..bc9a7cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,8 +39,6 @@ pub struct ExampleContext { pub std_config: BlueprintEnvironment, } -pub const EXAMPLE_JOB_ID: u32 = 0; - /// Example task that responds to a task created event /// This function is triggered by the NewTaskCreated event emitted by the TangleTaskManager contract /// This function response to greeting `Task.message` diff --git a/src/main.rs b/src/main.rs index 836a9ce..a5be712 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,26 @@ use {{project-name | snake_case}} as blueprint; - use blueprint_sdk::alloy::network::EthereumWallet; use blueprint_sdk::alloy::primitives::{address, Address, Bytes}; use blueprint_sdk::alloy::signers::local::PrivateKeySigner; +use blueprint_sdk::evm::producer::{PollingConfig, PollingProducer}; use blueprint_sdk::evm::util::get_wallet_provider_http; use blueprint_sdk::runner::config::BlueprintEnvironment; use blueprint_sdk::runner::eigenlayer::bls::EigenlayerBLSConfig; use blueprint_sdk::runner::BlueprintRunner; +use blueprint_sdk::testing::utils::setup_log; use blueprint_sdk::{info, warn, tokio, Router}; +use std::sync::Arc; use std::time::Duration; use blueprint::TangleTaskManager; use blueprint::{PRIVATE_KEY, TASK_MANAGER_ADDRESS}; // TODO: Replace with your context name use blueprint::ExampleContext; -use blueprint::{EXAMPLE_JOB_ID, example_task}; +use blueprint::example_task; #[tokio::main] async fn main() -> Result<(), blueprint_sdk::Error> { + setup_log(); let env = BlueprintEnvironment::load()?; // TODO: Replace with your context name @@ -29,7 +32,16 @@ async fn main() -> Result<(), blueprint_sdk::Error> { let wallet = EthereumWallet::from(signer); let provider = get_wallet_provider_http(env.http_rpc_endpoint.clone(), wallet.clone()); // Create an instance of your task manager - let contract = TangleTaskManager::new(*TASK_MANAGER_ADDRESS, provider); + let contract = TangleTaskManager::new(*TASK_MANAGER_ADDRESS, provider.clone()); + + // Create producer for task events + let task_producer = PollingProducer::new( + Arc::new(provider), + // PollingConfig::default().poll_interval(Duration::from_secs(1)), + PollingConfig::from_current().step(1).confirmations(1u64).poll_interval(Duration::from_secs(1)), + ) + .await + .map_err(|e| blueprint_sdk::Error::Other(e.to_string()))?; // Spawn a task to create a task - this is just for testing/example purposes info!("Spawning a task to create a task on the contract..."); @@ -50,14 +62,16 @@ async fn main() -> Result<(), blueprint_sdk::Error> { }); info!("Starting the event watcher ..."); - let eigen_config = EigenlayerBLSConfig::new(Address::default(), Address::default()); + let eigen_config = EigenlayerBLSConfig::new(Address::default(), Address::default()) + .with_exit_after_register(false); BlueprintRunner::builder(eigen_config, env) .router( // TODO: Update your task Router::new() - .route(EXAMPLE_JOB_ID, example_task) + .always(example_task) .with_context(context), ) + .producer(task_producer) .with_shutdown_handler(async { info!("Shutting down task manager service"); }) From 4aa9a04e6bf9ecad7c70580f3d3be89571812e74 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:17:06 +0700 Subject: [PATCH 5/9] chore: update ci --- .github/workflows/verify-template.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/verify-template.yml b/.github/workflows/verify-template.yml index 35d0478..c39e5d0 100644 --- a/.github/workflows/verify-template.yml +++ b/.github/workflows/verify-template.yml @@ -36,6 +36,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: stable + - name: Install protobuf compiler + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - run: | cp -r $PROJECT_NAME ${{ runner.temp }}/ cd ${{ runner.temp }}/$PROJECT_NAME From 19f749321b7c960968cf8293de238b7642669c0b Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:39:51 +0700 Subject: [PATCH 6/9] chore: update cargo generate temp --- hooks/pre.rhai | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hooks/pre.rhai b/hooks/pre.rhai index dc16e6b..490cf3e 100644 --- a/hooks/pre.rhai +++ b/hooks/pre.rhai @@ -33,12 +33,12 @@ if !check_bool("flakes") { file::delete("./.envrc"); } -if !variable::is_set("docker") { - let docker = variable::prompt("Do you want to generate a Dockerfile for your gadget?", true); - variable::set("docker", docker); +if !variable::is_set("container") { + let container = variable::prompt("Do you want to generate a Dockerfile for your gadget?", true); + variable::set("container", container); } -if check_bool("docker") { +if check_bool("container") { if !variable::is_set("base-image") { let base_image = variable::prompt("What base image should be used?", "rustlang/rust:nightly"); variable::set("base-image", base_image); From 56f6c5fcd3b162925112c0a5f969f915a1901343 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:55:31 +0700 Subject: [PATCH 7/9] chore: bump dependency --- Cargo.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c97b934..a82d14a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,17 @@ keywords = ["tangle", "blueprint", "avs"] rust-version = "1.88" [dependencies] -blueprint-sdk = { version = "0.1.0-alpha.19", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build"], git = "https://github.com/tangle-network/blueprint.git" } +blueprint-sdk = { version = "0.1.0-alpha.19", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build", "testing"], git = "https://github.com/tangle-network/blueprint.git" } +serde = { version = "1.0.208", features = ["derive"] } jsonrpc-core = { version = "18.0.0", default-features = false } jsonrpc-http-server = { version = "18.0.0", default-features = false } -thiserror = "1.0" -reqwest = "0.12" +thiserror = { version = "2.0.17", default-features = false } +reqwest = { version = "0.12.22", default-features = false } color-eyre = { version = "0.6", default-features = false } [build-dependencies] -blueprint-sdk = { version = "0.1.0-alpha.19", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build"], git = "https://github.com/tangle-network/blueprint.git" } +blueprint-sdk = { version = "0.1.0-alpha.19", default-features = false, features = ["std", "eigenlayer", "evm", "macros", "build", "testing"], git = "https://github.com/tangle-network/blueprint.git" } [features] default = ["std"] From c8956119e453d39831016fc7ad55358ba0d428a9 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:35:42 +0700 Subject: [PATCH 8/9] chore: update ci following template --- .github/workflows/verify-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-template.yml b/.github/workflows/verify-template.yml index c39e5d0..d6dc6d5 100644 --- a/.github/workflows/verify-template.yml +++ b/.github/workflows/verify-template.yml @@ -22,7 +22,7 @@ jobs: --define project-description="An example blueprint" --define project-homepage="https://tangle.tools" --define flakes=true - --define docker=true + --define container=true --define base-image="rustlang/rust:nightly" --define ci=true --define rust-ci=true From 4d2f2a33998853d853c5b8a2b8f9e4724dbb5cfc Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:55:32 +0700 Subject: [PATCH 9/9] chore(ci): updating rust version --- .github/workflows/ci.yml.liquid | 17 ++++++++++------- .github/workflows/verify-template.yml | 5 +++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml.liquid b/.github/workflows/ci.yml.liquid index a577c5c..5640d48 100644 --- a/.github/workflows/ci.yml.liquid +++ b/.github/workflows/ci.yml.liquid @@ -19,13 +19,14 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2024-10-13 + toolchain: nightly-2025-10-20 components: rustfmt - name: Check Formatting - run: cargo fmt -- --check + run: cargo +nightly-2025-10-20 fmt -- --check lint: timeout-minutes: 120 @@ -46,9 +47,10 @@ jobs: - name: Install Solidity Dependencies run: forge soldeer update -d - - uses: dtolnay/rust-toolchain@nightly + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2024-10-13 + toolchain: nightly-2025-10-20 components: clippy - uses: swatinem/rust-cache@v2 @@ -82,9 +84,10 @@ jobs: - name: Install Solidity Dependencies run: forge soldeer update -d - - uses: dtolnay/rust-toolchain@nightly + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2024-10-13 + toolchain: nightly-2025-10-20 components: clippy - uses: swatinem/rust-cache@v2 diff --git a/.github/workflows/verify-template.yml b/.github/workflows/verify-template.yml index d6dc6d5..f69a514 100644 --- a/.github/workflows/verify-template.yml +++ b/.github/workflows/verify-template.yml @@ -33,9 +33,10 @@ jobs: version: nightly - name: Show forge version run: forge --version - - uses: dtolnay/rust-toolchain@stable + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly-2025-10-20 - name: Install protobuf compiler run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - run: |