diff --git a/.gitmodules b/.gitmodules index ef878cad..af2aceb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/eigenlayer-middleware"] path = contracts/lib/eigenlayer-middleware url = git@github.com:Layr-Labs/eigenlayer-middleware.git +[submodule "services/operatorsinfo/integration_test_deployment/lib/forge-std"] + path = services/operatorsinfo/integration_test_deployment/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/services/operatorsinfo/integration_test_deployment/.github/workflows/test.yml b/services/operatorsinfo/integration_test_deployment/.github/workflows/test.yml new file mode 100644 index 00000000..9282e829 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/services/operatorsinfo/integration_test_deployment/.gitignore b/services/operatorsinfo/integration_test_deployment/.gitignore new file mode 100644 index 00000000..85198aaa --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/services/operatorsinfo/integration_test_deployment/BLSApkRegistryMock.sol b/services/operatorsinfo/integration_test_deployment/BLSApkRegistryMock.sol new file mode 100644 index 00000000..df9795c4 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/BLSApkRegistryMock.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {BLSApkRegistryStorage} from "../../contracts/lib/eigenlayer-middleware/src/BLSApkRegistryStorage.sol"; + +import {IRegistryCoordinator} from "../../contracts/lib/eigenlayer-middleware/src/interfaces/IRegistryCoordinator.sol"; + +import {BN254} from "../../contracts/lib/eigenlayer-middleware/src/libraries/BN254.sol"; + +contract BLSApkRegistryMock is BLSApkRegistryStorage { + using BN254 for BN254.G1Point; + + /// @notice when applied to a function, only allows the RegistryCoordinator to call it + modifier onlyRegistryCoordinator() { + require( + msg.sender == address(registryCoordinator), + "BLSApkRegistry.onlyRegistryCoordinator: caller is not the registry coordinator" + ); + _; + } + + /// @notice Sets the (immutable) `registryCoordinator` address + constructor( + IRegistryCoordinator _registryCoordinator + ) BLSApkRegistryStorage(_registryCoordinator) {} + + /******************************************************************************* + EXTERNAL FUNCTIONS - REGISTRY COORDINATOR + *******************************************************************************/ + + /** + * @notice Registers the `operator`'s pubkey for the specified `quorumNumbers`. + * @param operator The address of the operator to register. + * @param quorumNumbers The quorum numbers the operator is registering for, where each byte is an 8 bit integer quorumNumber. + * @dev access restricted to the RegistryCoordinator + * @dev Preconditions (these are assumed, not validated in this contract): + * 1) `quorumNumbers` has no duplicates + * 2) `quorumNumbers.length` != 0 + * 3) `quorumNumbers` is ordered in ascending order + * 4) the operator is not already registered + */ + function registerOperator( + address operator, + bytes memory quorumNumbers + ) public virtual onlyRegistryCoordinator { + // Get the operator's pubkey. Reverts if they have not registered a key + (BN254.G1Point memory pubkey, ) = getRegisteredPubkey(operator); + + // Update each quorum's aggregate pubkey + _processQuorumApkUpdate(quorumNumbers, pubkey); + + // Return pubkeyHash, which will become the operator's unique id + emit OperatorAddedToQuorums(operator, getOperatorId(operator), quorumNumbers); + } + + /** + * @notice Deregisters the `operator`'s pubkey for the specified `quorumNumbers`. + * @param operator The address of the operator to deregister. + * @param quorumNumbers The quorum numbers the operator is deregistering from, where each byte is an 8 bit integer quorumNumber. + * @dev access restricted to the RegistryCoordinator + * @dev Preconditions (these are assumed, not validated in this contract): + * 1) `quorumNumbers` has no duplicates + * 2) `quorumNumbers.length` != 0 + * 3) `quorumNumbers` is ordered in ascending order + * 4) the operator is not already deregistered + * 5) `quorumNumbers` is a subset of the quorumNumbers that the operator is registered for + */ + function deregisterOperator( + address operator, + bytes memory quorumNumbers + ) public virtual onlyRegistryCoordinator { + // Get the operator's pubkey. Reverts if they have not registered a key + (BN254.G1Point memory pubkey, ) = getRegisteredPubkey(operator); + + // Update each quorum's aggregate pubkey + _processQuorumApkUpdate(quorumNumbers, pubkey.negate()); + emit OperatorRemovedFromQuorums(operator, getOperatorId(operator), quorumNumbers); + } + + /** + * @notice Initializes a new quorum by pushing its first apk update + * @param quorumNumber The number of the new quorum + */ + function initializeQuorum(uint8 quorumNumber) public virtual onlyRegistryCoordinator { + require(apkHistory[quorumNumber].length == 0, "BLSApkRegistry.initializeQuorum: quorum already exists"); + + apkHistory[quorumNumber].push(ApkUpdate({ + apkHash: bytes24(0), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + })); + } + + /** + * @notice Called by the RegistryCoordinator register an operator as the owner of a BLS public key. + * @param operator is the operator for whom the key is being registered + * @param params contains the G1 & G2 public keys of the operator, and a signature proving their ownership + * @param pubkeyRegistrationMessageHash is a hash that the operator must sign to prove key ownership + */ + function registerBLSPublicKey( + address operator, + PubkeyRegistrationParams calldata params, + BN254.G1Point calldata pubkeyRegistrationMessageHash + ) external returns (bytes32 operatorId) { + bytes32 pubkeyHash = BN254.hashG1Point(params.pubkeyG1); + require( + pubkeyHash != ZERO_PK_HASH, "BLSApkRegistry.registerBLSPublicKey: cannot register zero pubkey" + ); + require( + operatorToPubkeyHash[operator] == bytes32(0), + "BLSApkRegistry.registerBLSPublicKey: operator already registered pubkey" + ); + require( + pubkeyHashToOperator[pubkeyHash] == address(0), + "BLSApkRegistry.registerBLSPublicKey: public key already registered" + ); + + // gamma = h(sigma, P, P', H(m)) + uint256 gamma = uint256(keccak256(abi.encodePacked( + params.pubkeyRegistrationSignature.X, + params.pubkeyRegistrationSignature.Y, + params.pubkeyG1.X, + params.pubkeyG1.Y, + params.pubkeyG2.X, + params.pubkeyG2.Y, + pubkeyRegistrationMessageHash.X, + pubkeyRegistrationMessageHash.Y + ))) % BN254.FR_MODULUS; + + // e(sigma + P * gamma, [-1]_2) = e(H(m) + [1]_1 * gamma, P') + require(BN254.pairing( + params.pubkeyRegistrationSignature.plus(params.pubkeyG1.scalar_mul(gamma)), + BN254.negGeneratorG2(), + pubkeyRegistrationMessageHash.plus(BN254.generatorG1().scalar_mul(gamma)), + params.pubkeyG2 + ), "BLSApkRegistry.registerBLSPublicKey: either the G1 signature is wrong, or G1 and G2 private key do not match"); + + operatorToPubkey[operator] = params.pubkeyG1; + operatorToPubkeyHash[operator] = pubkeyHash; + pubkeyHashToOperator[pubkeyHash] = operator; + + emit NewPubkeyRegistration(operator, params.pubkeyG1, params.pubkeyG2); + return pubkeyHash; + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + function _processQuorumApkUpdate(bytes memory quorumNumbers, BN254.G1Point memory point) internal { + BN254.G1Point memory newApk; + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + // Validate quorum exists and get history length + uint8 quorumNumber = uint8(quorumNumbers[i]); + uint256 historyLength = apkHistory[quorumNumber].length; + require(historyLength != 0, "BLSApkRegistry._processQuorumApkUpdate: quorum does not exist"); + + // Update aggregate public key for this quorum + newApk = currentApk[quorumNumber].plus(point); + currentApk[quorumNumber] = newApk; + bytes24 newApkHash = bytes24(BN254.hashG1Point(newApk)); + + // Update apk history. If the last update was made in this block, update the entry + // Otherwise, push a new historical entry and update the prev->next pointer + ApkUpdate storage lastUpdate = apkHistory[quorumNumber][historyLength - 1]; + if (lastUpdate.updateBlockNumber == uint32(block.number)) { + lastUpdate.apkHash = newApkHash; + } else { + lastUpdate.nextUpdateBlockNumber = uint32(block.number); + apkHistory[quorumNumber].push(ApkUpdate({ + apkHash: newApkHash, + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + })); + } + } + } + + /******************************************************************************* + VIEW FUNCTIONS + *******************************************************************************/ + /** + * @notice Returns the pubkey and pubkey hash of an operator + * @dev Reverts if the operator has not registered a valid pubkey + */ + function getRegisteredPubkey(address operator) public view returns (BN254.G1Point memory, bytes32) { + BN254.G1Point memory pubkey = operatorToPubkey[operator]; + bytes32 pubkeyHash = operatorToPubkeyHash[operator]; + + require( + pubkeyHash != bytes32(0), + "BLSApkRegistry.getRegisteredPubkey: operator is not registered" + ); + + return (pubkey, pubkeyHash); + } + + /** + * @notice Returns the indices of the quorumApks index at `blockNumber` for the provided `quorumNumbers` + * @dev Returns the current indices if `blockNumber >= block.number` + */ + function getApkIndicesAtBlockNumber( + bytes calldata quorumNumbers, + uint256 blockNumber + ) external view returns (uint32[] memory) { + uint32[] memory indices = new uint32[](quorumNumbers.length); + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + + uint256 quorumApkUpdatesLength = apkHistory[quorumNumber].length; + if (quorumApkUpdatesLength == 0 || blockNumber < apkHistory[quorumNumber][0].updateBlockNumber) { + revert("BLSApkRegistry.getApkIndicesAtBlockNumber: blockNumber is before the first update"); + } + + // Loop backward through apkHistory until we find an entry that preceeds `blockNumber` + for (uint256 j = quorumApkUpdatesLength; j > 0; j--) { + if (apkHistory[quorumNumber][j - 1].updateBlockNumber <= blockNumber) { + indices[i] = uint32(j - 1); + break; + } + } + } + return indices; + } + + /// @notice Returns the current APK for the provided `quorumNumber ` + function getApk(uint8 quorumNumber) external view returns (BN254.G1Point memory) { + return currentApk[quorumNumber]; + } + + /// @notice Returns the `ApkUpdate` struct at `index` in the list of APK updates for the `quorumNumber` + function getApkUpdateAtIndex(uint8 quorumNumber, uint256 index) external view returns (ApkUpdate memory) { + return apkHistory[quorumNumber][index]; + } + + /** + * @notice get hash of the apk of `quorumNumber` at `blockNumber` using the provided `index`; + * called by checkSignatures in BLSSignatureChecker.sol. + * @param quorumNumber is the quorum whose ApkHash is being retrieved + * @param blockNumber is the number of the block for which the latest ApkHash will be retrieved + * @param index is the index of the apkUpdate being retrieved from the list of quorum apkUpdates in storage + */ + function getApkHashAtBlockNumberAndIndex( + uint8 quorumNumber, + uint32 blockNumber, + uint256 index + ) external view returns (bytes24) { + ApkUpdate memory quorumApkUpdate = apkHistory[quorumNumber][index]; + + /** + * Validate that the update is valid for the given blockNumber: + * - blockNumber should be >= the update block number + * - the next update block number should be either 0 or strictly greater than blockNumber + */ + require( + blockNumber >= quorumApkUpdate.updateBlockNumber, + "BLSApkRegistry._validateApkHashAtBlockNumber: index too recent" + ); + require( + quorumApkUpdate.nextUpdateBlockNumber == 0 || blockNumber < quorumApkUpdate.nextUpdateBlockNumber, + "BLSApkRegistry._validateApkHashAtBlockNumber: not latest apk update" + ); + + return quorumApkUpdate.apkHash; + } + + /// @notice Returns the length of ApkUpdates for the provided `quorumNumber` + function getApkHistoryLength(uint8 quorumNumber) external view returns (uint32) { + return uint32(apkHistory[quorumNumber].length); + } + + /// @notice Returns the operator address for the given `pubkeyHash` + function getOperatorFromPubkeyHash(bytes32 pubkeyHash) public view returns (address) { + return pubkeyHashToOperator[pubkeyHash]; + } + + /// @notice returns the ID used to identify the `operator` within this AVS + /// @dev Returns zero in the event that the `operator` has never registered for the AVS + function getOperatorId(address operator) public view returns (bytes32) { + return operatorToPubkeyHash[operator]; + } +} diff --git a/services/operatorsinfo/integration_test_deployment/README.md b/services/operatorsinfo/integration_test_deployment/README.md new file mode 100644 index 00000000..9265b455 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/services/operatorsinfo/integration_test_deployment/deployment_script.sol b/services/operatorsinfo/integration_test_deployment/deployment_script.sol new file mode 100644 index 00000000..ec1476aa --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/deployment_script.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.12; + +import "forge-std/Script.sol"; +import {RegistryCoordinatorMock} from "../../contracts/lib/eigenlayer-middleware/test/mocks/RegistryCoordinatorMock.sol"; +import {BLSApkRegistry} from "../../contracts/lib/eigenlayer-middleware/src/BLSApkRegistry.sol"; + +import {IBLSApkRegistry} from "../../contracts/lib/eigenlayer-middleware/src/interfaces/IBLSApkRegistry.sol"; +import {BN254} from "../../contracts/lib/eigenlayer-middleware/src/libraries/BN254.sol"; +import "forge-std/Test.sol"; + +import {BLSApkRegistryMock} from "./BLSApkRegistryMock.sol"; + + + +contract TestRegistryDeploy is Script { + using BN254 for BN254.G1Point; + Vm cheats = Vm(VM_ADDRESS); + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + RegistryCoordinatorMock registryCoordMock = new RegistryCoordinatorMock(); + BLSApkRegistryMock registry = new BLSApkRegistryMock(registryCoordMock); + // vm.stopBroadcast(); + + + + // vm.startBroadcast(address(registryCoordMock)); + + address operator = address(123); + + IBLSApkRegistry.PubkeyRegistrationParams memory pubkeyRegistrationParams; + + uint256 privKey = 69; + pubkeyRegistrationParams.pubkeyG1 = BN254.generatorG1().scalar_mul( + privKey + ); + + BN254.G1Point memory defaultPubkey = pubkeyRegistrationParams.pubkeyG1; + bytes32 defaultPubkeyHash = BN254.hashG1Point(defaultPubkey); + + //privKey*G2 + pubkeyRegistrationParams.pubkeyG2.X[ + 1 + ] = 19_101_821_850_089_705_274_637_533_855_249_918_363_070_101_489_527_618_151_493_230_256_975_900_223_847; + pubkeyRegistrationParams.pubkeyG2.X[ + 0 + ] = 5_334_410_886_741_819_556_325_359_147_377_682_006_012_228_123_419_628_681_352_847_439_302_316_235_957; + pubkeyRegistrationParams.pubkeyG2.Y[ + 1 + ] = 354_176_189_041_917_478_648_604_979_334_478_067_325_821_134_838_555_150_300_539_079_146_482_658_331; + pubkeyRegistrationParams.pubkeyG2.Y[ + 0 + ] = 4_185_483_097_059_047_421_902_184_823_581_361_466_320_657_066_600_218_863_748_375_739_772_335_928_910; + + // BN254.G1Point memory messageHash = registryCoordMock + // .pubkeyRegistrationMessageHash(operator); + // pubkeyRegistrationParams.pubkeyRegistrationSignature = _signMessage( + // operator + // ); + BN254.G1Point memory messageHash; + + + // cheats.startPrank(address(registryCoordMock)); + registry.registerBLSPublicKey( + operator, + pubkeyRegistrationParams, + messageHash + ); + // cheats.stopPrank(); + + vm.stopBroadcast(); + } + + + // function _signMessage( + // address signer + // ) internal view returns (BN254.G1Point memory) { + // BN254.G1Point memory messageHash = registryCoordinator + // .pubkeyRegistrationMessageHash(signer); + // return BN254.scalar_mul(messageHash, privKey); + // } +} \ No newline at end of file diff --git a/services/operatorsinfo/integration_test_deployment/foundry.toml b/services/operatorsinfo/integration_test_deployment/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/services/operatorsinfo/integration_test_deployment/lib/forge-std b/services/operatorsinfo/integration_test_deployment/lib/forge-std new file mode 160000 index 00000000..978ac6fa --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 diff --git a/services/operatorsinfo/integration_test_deployment/script/Counter.s.sol b/services/operatorsinfo/integration_test_deployment/script/Counter.s.sol new file mode 100644 index 00000000..df9ee8b0 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/services/operatorsinfo/integration_test_deployment/script/deployment_script.s.sol b/services/operatorsinfo/integration_test_deployment/script/deployment_script.s.sol new file mode 100644 index 00000000..3e136a00 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/script/deployment_script.s.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.12; + +import "forge-std/Script.sol"; +import {RegistryCoordinatorMock} from "../../../contracts/lib/eigenlayer-middleware/test/mocks/RegistryCoordinatorMock.sol"; +import {BLSApkRegistry} from "../../../contracts/lib/eigenlayer-middleware/src/BLSApkRegistry.sol"; + +import {IBLSApkRegistry} from "../../../contracts/lib/eigenlayer-middleware/src/interfaces/IBLSApkRegistry.sol"; +import {BN254} from "../../../contracts/lib/eigenlayer-middleware/src/libraries/BN254.sol"; +import "forge-std/Test.sol"; + +import {BLSApkRegistryMock} from "../test/BLSApkRegistryMock.sol"; + + + +contract TestRegistryDeploy is Script { + using BN254 for BN254.G1Point; + Vm cheats = Vm(VM_ADDRESS); + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + RegistryCoordinatorMock registryCoordMock = new RegistryCoordinatorMock(); + BLSApkRegistryMock registry = new BLSApkRegistryMock(registryCoordMock); + // vm.stopBroadcast(); + + + + // vm.startBroadcast(address(registryCoordMock)); + + address operator = address(123); + + IBLSApkRegistry.PubkeyRegistrationParams memory pubkeyRegistrationParams; + + uint256 privKey = 69; + pubkeyRegistrationParams.pubkeyG1 = BN254.generatorG1().scalar_mul( + privKey + ); + + BN254.G1Point memory defaultPubkey = pubkeyRegistrationParams.pubkeyG1; + bytes32 defaultPubkeyHash = BN254.hashG1Point(defaultPubkey); + + //privKey*G2 + pubkeyRegistrationParams.pubkeyG2.X[ + 1 + ] = 19_101_821_850_089_705_274_637_533_855_249_918_363_070_101_489_527_618_151_493_230_256_975_900_223_847; + pubkeyRegistrationParams.pubkeyG2.X[ + 0 + ] = 5_334_410_886_741_819_556_325_359_147_377_682_006_012_228_123_419_628_681_352_847_439_302_316_235_957; + pubkeyRegistrationParams.pubkeyG2.Y[ + 1 + ] = 354_176_189_041_917_478_648_604_979_334_478_067_325_821_134_838_555_150_300_539_079_146_482_658_331; + pubkeyRegistrationParams.pubkeyG2.Y[ + 0 + ] = 4_185_483_097_059_047_421_902_184_823_581_361_466_320_657_066_600_218_863_748_375_739_772_335_928_910; + + // BN254.G1Point memory messageHash = registryCoordMock + // .pubkeyRegistrationMessageHash(operator); + // pubkeyRegistrationParams.pubkeyRegistrationSignature = _signMessage( + // operator + // ); + BN254.G1Point memory messageHash; + + + // cheats.startPrank(address(registryCoordMock)); + registry.registerBLSPublicKey( + operator, + pubkeyRegistrationParams, + messageHash + ); + // cheats.stopPrank(); + + vm.stopBroadcast(); + } + + + // function _signMessage( + // address signer + // ) internal view returns (BN254.G1Point memory) { + // BN254.G1Point memory messageHash = registryCoordinator + // .pubkeyRegistrationMessageHash(signer); + // return BN254.scalar_mul(messageHash, privKey); + // } +} \ No newline at end of file diff --git a/services/operatorsinfo/integration_test_deployment/src/Counter.sol b/services/operatorsinfo/integration_test_deployment/src/Counter.sol new file mode 100644 index 00000000..aded7997 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/services/operatorsinfo/integration_test_deployment/test/BLSApkRegistryMock.sol b/services/operatorsinfo/integration_test_deployment/test/BLSApkRegistryMock.sol new file mode 100644 index 00000000..4ea3ea9b --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/test/BLSApkRegistryMock.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {BLSApkRegistryStorage} from "../../../contracts/lib/eigenlayer-middleware/src/BLSApkRegistryStorage.sol"; + +import {IRegistryCoordinator} from "../../../contracts/lib/eigenlayer-middleware/src/interfaces/IRegistryCoordinator.sol"; + +import {BN254} from "../../../contracts/lib/eigenlayer-middleware/src/libraries/BN254.sol"; + +contract BLSApkRegistryMock is BLSApkRegistryStorage { + using BN254 for BN254.G1Point; + + /// @notice when applied to a function, only allows the RegistryCoordinator to call it + modifier onlyRegistryCoordinator() { + require( + msg.sender == address(registryCoordinator), + "BLSApkRegistry.onlyRegistryCoordinator: caller is not the registry coordinator" + ); + _; + } + + /// @notice Sets the (immutable) `registryCoordinator` address + constructor( + IRegistryCoordinator _registryCoordinator + ) BLSApkRegistryStorage(_registryCoordinator) {} + + /******************************************************************************* + EXTERNAL FUNCTIONS - REGISTRY COORDINATOR + *******************************************************************************/ + + /** + * @notice Registers the `operator`'s pubkey for the specified `quorumNumbers`. + * @param operator The address of the operator to register. + * @param quorumNumbers The quorum numbers the operator is registering for, where each byte is an 8 bit integer quorumNumber. + * @dev access restricted to the RegistryCoordinator + * @dev Preconditions (these are assumed, not validated in this contract): + * 1) `quorumNumbers` has no duplicates + * 2) `quorumNumbers.length` != 0 + * 3) `quorumNumbers` is ordered in ascending order + * 4) the operator is not already registered + */ + function registerOperator( + address operator, + bytes memory quorumNumbers + ) public virtual onlyRegistryCoordinator { + // Get the operator's pubkey. Reverts if they have not registered a key + (BN254.G1Point memory pubkey, ) = getRegisteredPubkey(operator); + + // Update each quorum's aggregate pubkey + _processQuorumApkUpdate(quorumNumbers, pubkey); + + // Return pubkeyHash, which will become the operator's unique id + emit OperatorAddedToQuorums(operator, getOperatorId(operator), quorumNumbers); + } + + /** + * @notice Deregisters the `operator`'s pubkey for the specified `quorumNumbers`. + * @param operator The address of the operator to deregister. + * @param quorumNumbers The quorum numbers the operator is deregistering from, where each byte is an 8 bit integer quorumNumber. + * @dev access restricted to the RegistryCoordinator + * @dev Preconditions (these are assumed, not validated in this contract): + * 1) `quorumNumbers` has no duplicates + * 2) `quorumNumbers.length` != 0 + * 3) `quorumNumbers` is ordered in ascending order + * 4) the operator is not already deregistered + * 5) `quorumNumbers` is a subset of the quorumNumbers that the operator is registered for + */ + function deregisterOperator( + address operator, + bytes memory quorumNumbers + ) public virtual onlyRegistryCoordinator { + // Get the operator's pubkey. Reverts if they have not registered a key + (BN254.G1Point memory pubkey, ) = getRegisteredPubkey(operator); + + // Update each quorum's aggregate pubkey + _processQuorumApkUpdate(quorumNumbers, pubkey.negate()); + emit OperatorRemovedFromQuorums(operator, getOperatorId(operator), quorumNumbers); + } + + /** + * @notice Initializes a new quorum by pushing its first apk update + * @param quorumNumber The number of the new quorum + */ + function initializeQuorum(uint8 quorumNumber) public virtual onlyRegistryCoordinator { + require(apkHistory[quorumNumber].length == 0, "BLSApkRegistry.initializeQuorum: quorum already exists"); + + apkHistory[quorumNumber].push(ApkUpdate({ + apkHash: bytes24(0), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + })); + } + + /** + * @notice Called by the RegistryCoordinator register an operator as the owner of a BLS public key. + * @param operator is the operator for whom the key is being registered + * @param params contains the G1 & G2 public keys of the operator, and a signature proving their ownership + * @param pubkeyRegistrationMessageHash is a hash that the operator must sign to prove key ownership + */ + function registerBLSPublicKey( + address operator, + PubkeyRegistrationParams calldata params, + BN254.G1Point calldata pubkeyRegistrationMessageHash + ) external returns (bytes32 operatorId) { + bytes32 pubkeyHash = BN254.hashG1Point(params.pubkeyG1); + require( + pubkeyHash != ZERO_PK_HASH, "BLSApkRegistry.registerBLSPublicKey: cannot register zero pubkey" + ); + require( + operatorToPubkeyHash[operator] == bytes32(0), + "BLSApkRegistry.registerBLSPublicKey: operator already registered pubkey" + ); + require( + pubkeyHashToOperator[pubkeyHash] == address(0), + "BLSApkRegistry.registerBLSPublicKey: public key already registered" + ); + + // gamma = h(sigma, P, P', H(m)) + uint256 gamma = uint256(keccak256(abi.encodePacked( + params.pubkeyRegistrationSignature.X, + params.pubkeyRegistrationSignature.Y, + params.pubkeyG1.X, + params.pubkeyG1.Y, + params.pubkeyG2.X, + params.pubkeyG2.Y, + pubkeyRegistrationMessageHash.X, + pubkeyRegistrationMessageHash.Y + ))) % BN254.FR_MODULUS; + + // e(sigma + P * gamma, [-1]_2) = e(H(m) + [1]_1 * gamma, P') + require(BN254.pairing( + params.pubkeyRegistrationSignature.plus(params.pubkeyG1.scalar_mul(gamma)), + BN254.negGeneratorG2(), + pubkeyRegistrationMessageHash.plus(BN254.generatorG1().scalar_mul(gamma)), + params.pubkeyG2 + ), "BLSApkRegistry.registerBLSPublicKey: either the G1 signature is wrong, or G1 and G2 private key do not match"); + + operatorToPubkey[operator] = params.pubkeyG1; + operatorToPubkeyHash[operator] = pubkeyHash; + pubkeyHashToOperator[pubkeyHash] = operator; + + emit NewPubkeyRegistration(operator, params.pubkeyG1, params.pubkeyG2); + return pubkeyHash; + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + function _processQuorumApkUpdate(bytes memory quorumNumbers, BN254.G1Point memory point) internal { + BN254.G1Point memory newApk; + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + // Validate quorum exists and get history length + uint8 quorumNumber = uint8(quorumNumbers[i]); + uint256 historyLength = apkHistory[quorumNumber].length; + require(historyLength != 0, "BLSApkRegistry._processQuorumApkUpdate: quorum does not exist"); + + // Update aggregate public key for this quorum + newApk = currentApk[quorumNumber].plus(point); + currentApk[quorumNumber] = newApk; + bytes24 newApkHash = bytes24(BN254.hashG1Point(newApk)); + + // Update apk history. If the last update was made in this block, update the entry + // Otherwise, push a new historical entry and update the prev->next pointer + ApkUpdate storage lastUpdate = apkHistory[quorumNumber][historyLength - 1]; + if (lastUpdate.updateBlockNumber == uint32(block.number)) { + lastUpdate.apkHash = newApkHash; + } else { + lastUpdate.nextUpdateBlockNumber = uint32(block.number); + apkHistory[quorumNumber].push(ApkUpdate({ + apkHash: newApkHash, + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + })); + } + } + } + + /******************************************************************************* + VIEW FUNCTIONS + *******************************************************************************/ + /** + * @notice Returns the pubkey and pubkey hash of an operator + * @dev Reverts if the operator has not registered a valid pubkey + */ + function getRegisteredPubkey(address operator) public view returns (BN254.G1Point memory, bytes32) { + BN254.G1Point memory pubkey = operatorToPubkey[operator]; + bytes32 pubkeyHash = operatorToPubkeyHash[operator]; + + require( + pubkeyHash != bytes32(0), + "BLSApkRegistry.getRegisteredPubkey: operator is not registered" + ); + + return (pubkey, pubkeyHash); + } + + /** + * @notice Returns the indices of the quorumApks index at `blockNumber` for the provided `quorumNumbers` + * @dev Returns the current indices if `blockNumber >= block.number` + */ + function getApkIndicesAtBlockNumber( + bytes calldata quorumNumbers, + uint256 blockNumber + ) external view returns (uint32[] memory) { + uint32[] memory indices = new uint32[](quorumNumbers.length); + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + + uint256 quorumApkUpdatesLength = apkHistory[quorumNumber].length; + if (quorumApkUpdatesLength == 0 || blockNumber < apkHistory[quorumNumber][0].updateBlockNumber) { + revert("BLSApkRegistry.getApkIndicesAtBlockNumber: blockNumber is before the first update"); + } + + // Loop backward through apkHistory until we find an entry that preceeds `blockNumber` + for (uint256 j = quorumApkUpdatesLength; j > 0; j--) { + if (apkHistory[quorumNumber][j - 1].updateBlockNumber <= blockNumber) { + indices[i] = uint32(j - 1); + break; + } + } + } + return indices; + } + + /// @notice Returns the current APK for the provided `quorumNumber ` + function getApk(uint8 quorumNumber) external view returns (BN254.G1Point memory) { + return currentApk[quorumNumber]; + } + + /// @notice Returns the `ApkUpdate` struct at `index` in the list of APK updates for the `quorumNumber` + function getApkUpdateAtIndex(uint8 quorumNumber, uint256 index) external view returns (ApkUpdate memory) { + return apkHistory[quorumNumber][index]; + } + + /** + * @notice get hash of the apk of `quorumNumber` at `blockNumber` using the provided `index`; + * called by checkSignatures in BLSSignatureChecker.sol. + * @param quorumNumber is the quorum whose ApkHash is being retrieved + * @param blockNumber is the number of the block for which the latest ApkHash will be retrieved + * @param index is the index of the apkUpdate being retrieved from the list of quorum apkUpdates in storage + */ + function getApkHashAtBlockNumberAndIndex( + uint8 quorumNumber, + uint32 blockNumber, + uint256 index + ) external view returns (bytes24) { + ApkUpdate memory quorumApkUpdate = apkHistory[quorumNumber][index]; + + /** + * Validate that the update is valid for the given blockNumber: + * - blockNumber should be >= the update block number + * - the next update block number should be either 0 or strictly greater than blockNumber + */ + require( + blockNumber >= quorumApkUpdate.updateBlockNumber, + "BLSApkRegistry._validateApkHashAtBlockNumber: index too recent" + ); + require( + quorumApkUpdate.nextUpdateBlockNumber == 0 || blockNumber < quorumApkUpdate.nextUpdateBlockNumber, + "BLSApkRegistry._validateApkHashAtBlockNumber: not latest apk update" + ); + + return quorumApkUpdate.apkHash; + } + + /// @notice Returns the length of ApkUpdates for the provided `quorumNumber` + function getApkHistoryLength(uint8 quorumNumber) external view returns (uint32) { + return uint32(apkHistory[quorumNumber].length); + } + + /// @notice Returns the operator address for the given `pubkeyHash` + function getOperatorFromPubkeyHash(bytes32 pubkeyHash) public view returns (address) { + return pubkeyHashToOperator[pubkeyHash]; + } + + /// @notice returns the ID used to identify the `operator` within this AVS + /// @dev Returns zero in the event that the `operator` has never registered for the AVS + function getOperatorId(address operator) public view returns (bytes32) { + return operatorToPubkeyHash[operator]; + } +} diff --git a/services/operatorsinfo/integration_test_deployment/test/Counter.t.sol b/services/operatorsinfo/integration_test_deployment/test/Counter.t.sol new file mode 100644 index 00000000..54b724f7 --- /dev/null +++ b/services/operatorsinfo/integration_test_deployment/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/services/operatorsinfo/operatorsinfo_subgraph.go b/services/operatorsinfo/operatorsinfo_subgraph.go index d915c214..08ee8b61 100644 --- a/services/operatorsinfo/operatorsinfo_subgraph.go +++ b/services/operatorsinfo/operatorsinfo_subgraph.go @@ -15,18 +15,19 @@ import ( ) type ( - QueryOperatorByIdGql struct { - Operator IndexedOperatorInfoGql `graphql:"operator(id: $id)"` + QueryOperatorByAddressGql struct { + Operator IndexedOperatorInfoGql `graphql:"operator(address: $address)"` } OperatorsInfoServiceSubgraph struct { logger logging.Logger - client *graphql.Client + client GraphQLQuerier + name string } SocketUpdates struct { Socket graphql.String } IndexedOperatorInfoGql struct { - Id graphql.String + Address graphql.String PubkeyG1_X graphql.String `graphql:"pubkeyG1_X"` PubkeyG1_Y graphql.String `graphql:"pubkeyG1_Y"` PubkeyG2_X []graphql.String `graphql:"pubkeyG2_X"` @@ -47,6 +48,9 @@ type ( G1Point struct { *bn254.G1Affine } + GraphQLQuerier interface { + Query(ctx context.Context, q any, variables map[string]any) error + } ) var _ OperatorsInfoService = (*OperatorsInfoServiceSubgraph)(nil) @@ -58,14 +62,13 @@ var _ OperatorsInfoService = (*OperatorsInfoServiceSubgraph)(nil) // Using a separate initialize() function might lead to some users forgetting to call it and the service not behaving properly. func NewOperatorsInfoServiceSubgraph( ctx context.Context, - url string, + client GraphQLQuerier, logger logging.Logger, ) *OperatorsInfoServiceSubgraph { - client := graphql.NewClient(url, nil) - return &OperatorsInfoServiceSubgraph{ logger: logger, client: client, + name: "OperatorsInfoServiceSubgraph", } } @@ -80,12 +83,14 @@ func (ops *OperatorsInfoServiceSubgraph) GetOperatorInfo(ctx context.Context, op func (ops *OperatorsInfoServiceSubgraph) getIndexedOperatorInfoByOperatorId(ctx context.Context, operator common.Address) (*types.OperatorInfo, error) { var ( - query QueryOperatorByIdGql + query QueryOperatorByAddressGql variables = map[string]any{ "id": graphql.String(fmt.Sprintf("0x%s", hex.EncodeToString(operator[:]))), } ) - err := ops.client.Query(context.Background(), &query, variables) + fmt.Print("NAME OF SUBRAPH", ops.name) + fmt.Print("NAME OF SUBRAPH: ", ops.client) + err := ops.client.Query(ctx, &query, variables) if err != nil { ops.logger.Error("Error requesting info for operator", "err", err, "operator", hex.EncodeToString(operator[:])) return nil, err diff --git a/services/operatorsinfo/operatorsinfo_subgraph_integration_test.go b/services/operatorsinfo/operatorsinfo_subgraph_integration_test.go new file mode 100644 index 00000000..32a47d83 --- /dev/null +++ b/services/operatorsinfo/operatorsinfo_subgraph_integration_test.go @@ -0,0 +1 @@ +package operatorsinfo diff --git a/services/operatorsinfo/operatorsinfo_subgraph_test.go b/services/operatorsinfo/operatorsinfo_subgraph_test.go new file mode 100644 index 00000000..b64a3fc6 --- /dev/null +++ b/services/operatorsinfo/operatorsinfo_subgraph_test.go @@ -0,0 +1,60 @@ +package operatorsinfo + +import ( + "context" + "testing" + + "github.com/Layr-Labs/eigensdk-go/logging" + "github.com/Layr-Labs/eigensdk-go/types" + "github.com/ethereum/go-ethereum/common" + "github.com/shurcooL/graphql" + "github.com/stretchr/testify/assert" +) + +type mockGraphQLQuerier struct { + QueryFn func(ctx context.Context, q any, variables map[string]any) error +} + +func (m mockGraphQLQuerier) Query(ctx context.Context, q any, variables map[string]any) error { + return m.QueryFn(ctx, q, variables) +} + +func TestIndexedChainState_GetIndexedOperatorState(t *testing.T) { + logger := logging.NewNoopLogger() + + operatorAddress := common.Address{0x1} + + operatorsQueryCalled := false + querier := &mockGraphQLQuerier{} + querier.QueryFn = func(ctx context.Context, q any, variables map[string]any) error { + switch res := q.(type) { + case *QueryOperatorByAddressGql: + if operatorsQueryCalled { + return nil + } + res.Operator = IndexedOperatorInfoGql{ + Address: graphql.String(operatorAddress.String()), + PubkeyG1_X: "3336192159512049190945679273141887248666932624338963482128432381981287252980", + PubkeyG1_Y: "15195175002875833468883745675063986308012687914999552116603423331534089122704", + PubkeyG2_X: []graphql.String{ + "21597023645215426396093421944506635812143308313031252511177204078669540440732", + "11405255666568400552575831267661419473985517916677491029848981743882451844775", + }, + PubkeyG2_Y: []graphql.String{ + "9416989242565286095121881312760798075882411191579108217086927390793923664442", + "13612061731370453436662267863740141021994163834412349567410746669651828926551", + }, + SocketUpdates: []SocketUpdates{{Socket: "localhost:32006;32007"}}, + } + operatorsQueryCalled = true + return nil + default: + return nil + } + } + + cs := NewOperatorsInfoServiceSubgraph(context.Background(), querier, logger) + operatorPubkeys, err := cs.GetOperatorInfo(context.Background(), operatorAddress) + assert.True(t, err) + assert.Equal(t, operatorPubkeys.Socket, types.Socket("localhost:32006;32007")) +}