diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index ff516576..0ea35815 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -12,7 +12,6 @@ on: env: FOUNDRY_PROFILE: ci RPC_MAINNET: ${{ secrets.RPC_MAINNET }} - RPC_HOLESKY: ${{ secrets.RPC_HOLESKY }} HOLESKY_RPC_URL: ${{ secrets.HOLESKY_RPC_URL }} CHAIN_ID: ${{ secrets.CHAIN_ID }} diff --git a/foundry.toml b/foundry.toml index 6861e452..9d48c79e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -109,7 +109,7 @@ [rpc_endpoints] mainnet = "${RPC_MAINNET}" - holesky = "${RPC_HOLESKY}" + holesky = "${HOLESKY_RPC_URL}" [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } diff --git a/src/OperatorStateRetriever.sol b/src/OperatorStateRetriever.sol index d3e06490..4e4a342d 100644 --- a/src/OperatorStateRetriever.sol +++ b/src/OperatorStateRetriever.sol @@ -112,8 +112,8 @@ contract OperatorStateRetriever { ISlashingRegistryCoordinator registryCoordinator, uint32 referenceBlockNumber, bytes calldata quorumNumbers, - bytes32[] calldata nonSignerOperatorIds - ) external view returns (CheckSignaturesIndices memory) { + bytes32[] memory nonSignerOperatorIds + ) public view returns (CheckSignaturesIndices memory) { IStakeRegistry stakeRegistry = registryCoordinator.stakeRegistry(); CheckSignaturesIndices memory checkSignaturesIndices; diff --git a/src/unaudited/BLSSigCheckOperatorStateRetriever.sol b/src/unaudited/BLSSigCheckOperatorStateRetriever.sol new file mode 100644 index 00000000..9c940753 --- /dev/null +++ b/src/unaudited/BLSSigCheckOperatorStateRetriever.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {IBLSApkRegistry} from "../interfaces/IBLSApkRegistry.sol"; +import {IBLSSignatureCheckerTypes} from "../interfaces/IBLSSignatureChecker.sol"; +import {IStakeRegistry} from "../interfaces/IStakeRegistry.sol"; +import {IIndexRegistry} from "../interfaces/IIndexRegistry.sol"; +import {ISlashingRegistryCoordinator} from "../interfaces/ISlashingRegistryCoordinator.sol"; +import {BitmapUtils} from "../libraries/BitmapUtils.sol"; +import {BN254} from "../libraries/BN254.sol"; +import {BN256G2} from "./BN256G2.sol"; +import {OperatorStateRetriever} from "../OperatorStateRetriever.sol"; +import {ECUtils} from "./ECUtils.sol"; + +/** + * @title BLSSigCheckOperatorStateRetriever with view functions that allow to retrieve the state of an AVSs registry system. + * @dev This contract inherits from OperatorStateRetriever and adds the getNonSignerStakesAndSignature function. + * @author Bread coop + */ +contract BLSSigCheckOperatorStateRetriever is OperatorStateRetriever { + using ECUtils for BN254.G1Point; + + /// @dev Thrown when the signature is not on the curve. + error InvalidSigma(); + // avoid stack too deep + + struct GetNonSignerStakesAndSignatureMemory { + BN254.G1Point[] quorumApks; + BN254.G2Point apkG2; + IIndexRegistry indexRegistry; + IBLSApkRegistry blsApkRegistry; + bytes32[] signingOperatorIds; + } + + /** + * @notice Returns the stakes and signature information for non-signing operators in specified quorums + * @param registryCoordinator The registry coordinator contract to fetch operator information from + * @param quorumNumbers Array of quorum numbers to check for non-signers + * @param sigma The aggregate BLS signature to verify + * @param operators Array of operator addresses that signed the message + * @param blockNumber Is the block number to get the indices for + * @return NonSignerStakesAndSignature Struct containing: + * - nonSignerQuorumBitmapIndices: Indices for retrieving quorum bitmaps of non-signers + * - nonSignerPubkeys: BLS public keys of operators that did not sign + * - quorumApks: Aggregate public keys for each quorum + * - apkG2: Aggregate public key of all signing operators in G2 + * - sigma: The provided signature + * - quorumApkIndices: Indices for retrieving quorum APKs + * - totalStakeIndices: Indices for retrieving total stake info + * - nonSignerStakeIndices: Indices for retrieving non-signer stake info + * @dev Computes the indices of operators that did not sign across all specified quorums + * @dev This function does not validate the signature matches the provided parameters, only that it's in a valid format + */ + function getNonSignerStakesAndSignature( + ISlashingRegistryCoordinator registryCoordinator, + bytes calldata quorumNumbers, + BN254.G1Point calldata sigma, + address[] calldata operators, + uint32 blockNumber + ) external view returns (IBLSSignatureCheckerTypes.NonSignerStakesAndSignature memory) { + GetNonSignerStakesAndSignatureMemory memory m; + m.quorumApks = new BN254.G1Point[](quorumNumbers.length); + m.indexRegistry = registryCoordinator.indexRegistry(); + m.blsApkRegistry = registryCoordinator.blsApkRegistry(); + + // Safe guard AVSs from generating NonSignerStakesAndSignature with invalid sigma + require(sigma.isOnCurve(), InvalidSigma()); + + // Compute the g2 APK of the signing operator set + m.signingOperatorIds = new bytes32[](operators.length); + for (uint256 i = 0; i < operators.length; i++) { + m.signingOperatorIds[i] = registryCoordinator.getOperatorId(operators[i]); + BN254.G2Point memory operatorG2Pk = m.blsApkRegistry.getOperatorPubkeyG2(operators[i]); + (m.apkG2.X[1], m.apkG2.X[0], m.apkG2.Y[1], m.apkG2.Y[0]) = BN256G2.ECTwistAdd( + m.apkG2.X[1], + m.apkG2.X[0], + m.apkG2.Y[1], + m.apkG2.Y[0], + operatorG2Pk.X[1], + operatorG2Pk.X[0], + operatorG2Pk.Y[1], + operatorG2Pk.Y[0] + ); + } + + // Extra scope for stack limit + { + uint32[] memory signingOperatorQuorumBitmapIndices = registryCoordinator + .getQuorumBitmapIndicesAtBlockNumber(blockNumber, m.signingOperatorIds); + // Check that all operators are registered (this is like the check in getCheckSignaturesIndices, but we check against _signing_ operators) + for (uint256 i = 0; i < operators.length; i++) { + uint192 signingOperatorQuorumBitmap = registryCoordinator + .getQuorumBitmapAtBlockNumberByIndex( + m.signingOperatorIds[i], blockNumber, signingOperatorQuorumBitmapIndices[i] + ); + require(signingOperatorQuorumBitmap != 0, OperatorNotRegistered()); + } + } + + // We use this as a dynamic array + uint256 nonSignerOperatorsCount = 0; + bytes32[] memory nonSignerOperatorIds = new bytes32[](16); + // For every quorum + for (uint256 i = 0; i < quorumNumbers.length; i++) { + bytes32[] memory operatorIdsInQuorum = + m.indexRegistry.getOperatorListAtBlockNumber(uint8(quorumNumbers[i]), blockNumber); + // Operator IDs are computed from the hash of the BLS public keys, so an operatorId's public key can't change over time + // This lets us compute the APK at the given block number + m.quorumApks[i] = _computeG1Apk(registryCoordinator, operatorIdsInQuorum); + // We check for every operator in the quorum + for (uint256 j = 0; j < operatorIdsInQuorum.length; j++) { + bool isNewNonSigner = true; + // If it is in the signing operators array + for (uint256 k = 0; k < m.signingOperatorIds.length; k++) { + if (operatorIdsInQuorum[j] == m.signingOperatorIds[k]) { + isNewNonSigner = false; + break; + } + } + // Or already in the non-signing operators array + for (uint256 l = 0; l < nonSignerOperatorsCount; l++) { + if (nonSignerOperatorIds[l] == operatorIdsInQuorum[j]) { + isNewNonSigner = false; + break; + } + } + // And if not, we add it to the non-signing operators array + if (isNewNonSigner) { + // If we are at the end of the array, we need to resize it + if (nonSignerOperatorsCount == nonSignerOperatorIds.length) { + uint256 newCapacity = nonSignerOperatorIds.length * 2; + bytes32[] memory newNonSignerOperatorIds = new bytes32[](newCapacity); + for (uint256 l = 0; l < nonSignerOperatorIds.length; l++) { + newNonSignerOperatorIds[l] = nonSignerOperatorIds[l]; + } + nonSignerOperatorIds = newNonSignerOperatorIds; + } + + nonSignerOperatorIds[nonSignerOperatorsCount] = operatorIdsInQuorum[j]; + nonSignerOperatorsCount++; + } + } + } + + // Trim the nonSignerOperatorIds array to the actual count + bytes32[] memory trimmedNonSignerOperatorIds = new bytes32[](nonSignerOperatorsCount); + for (uint256 i = 0; i < nonSignerOperatorsCount; i++) { + trimmedNonSignerOperatorIds[i] = nonSignerOperatorIds[i]; + } + + BN254.G1Point[] memory nonSignerPubkeys = new BN254.G1Point[](nonSignerOperatorsCount); + for (uint256 i = 0; i < nonSignerOperatorsCount; i++) { + address nonSignerOperator = + registryCoordinator.getOperatorFromId(trimmedNonSignerOperatorIds[i]); + (nonSignerPubkeys[i],) = m.blsApkRegistry.getRegisteredPubkey(nonSignerOperator); + } + + CheckSignaturesIndices memory checkSignaturesIndices = getCheckSignaturesIndices( + registryCoordinator, blockNumber, quorumNumbers, trimmedNonSignerOperatorIds + ); + return IBLSSignatureCheckerTypes.NonSignerStakesAndSignature({ + nonSignerQuorumBitmapIndices: checkSignaturesIndices.nonSignerQuorumBitmapIndices, + nonSignerPubkeys: nonSignerPubkeys, + quorumApks: m.quorumApks, + apkG2: m.apkG2, + sigma: sigma, + quorumApkIndices: checkSignaturesIndices.quorumApkIndices, + totalStakeIndices: checkSignaturesIndices.totalStakeIndices, + nonSignerStakeIndices: checkSignaturesIndices.nonSignerStakeIndices + }); + } + + /** + * @notice Computes the aggregate public key (APK) in G1 for a list of operators + * @dev Aggregates the individual G1 public keys of operators by adding them together + * @param registryCoordinator The registry coordinator contract to fetch operator info from + * @param operatorIds Array of operator IDs to compute the aggregate key for + * @return The aggregate public key as a G1 point, computed by summing individual operator pubkeys + */ + function _computeG1Apk( + ISlashingRegistryCoordinator registryCoordinator, + bytes32[] memory operatorIds + ) internal view returns (BN254.G1Point memory) { + BN254.G1Point memory apk = BN254.G1Point(0, 0); + IBLSApkRegistry blsApkRegistry = registryCoordinator.blsApkRegistry(); + for (uint256 i = 0; i < operatorIds.length; i++) { + address operator = registryCoordinator.getOperatorFromId(operatorIds[i]); + BN254.G1Point memory operatorPk; + (operatorPk.X, operatorPk.Y) = blsApkRegistry.operatorToPubkey(operator); + apk = BN254.plus(apk, operatorPk); + } + return apk; + } +} diff --git a/src/unaudited/BN256G2.sol b/src/unaudited/BN256G2.sol new file mode 100644 index 00000000..a1721df3 --- /dev/null +++ b/src/unaudited/BN256G2.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +/** + * @title Elliptic curve operations on twist points for alt_bn128 + * @author Mustafa Al-Bassam (mus@musalbas.com) + * @dev Homepage: https://github.com/musalbas/solidity-BN256G2 + * @dev This is a modified version of the original BN256G2 library to work with solidity 0.8.27 + */ +library BN256G2 { + uint256 internal constant FIELD_MODULUS = + 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47; + uint256 internal constant TWISTBX = + 0x2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5; + uint256 internal constant TWISTBY = + 0x9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d2; + uint256 internal constant PTXX = 0; + uint256 internal constant PTXY = 1; + uint256 internal constant PTYX = 2; + uint256 internal constant PTYY = 3; + uint256 internal constant PTZX = 4; + uint256 internal constant PTZY = 5; + + /** + * @notice Add two twist points + * @param pt1xx Coefficient 1 of x on point 1 + * @param pt1xy Coefficient 2 of x on point 1 + * @param pt1yx Coefficient 1 of y on point 1 + * @param pt1yy Coefficient 2 of y on point 1 + * @param pt2xx Coefficient 1 of x on point 2 + * @param pt2xy Coefficient 2 of x on point 2 + * @param pt2yx Coefficient 1 of y on point 2 + * @param pt2yy Coefficient 2 of y on point 2 + * @return (pt3xx, pt3xy, pt3yx, pt3yy) + */ + function ECTwistAdd( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt2xx, + uint256 pt2xy, + uint256 pt2yx, + uint256 pt2yy + ) public view returns (uint256, uint256, uint256, uint256) { + if (pt1xx == 0 && pt1xy == 0 && pt1yx == 0 && pt1yy == 0) { + if (!(pt2xx == 0 && pt2xy == 0 && pt2yx == 0 && pt2yy == 0)) { + assert(_isOnCurve(pt2xx, pt2xy, pt2yx, pt2yy)); + } + return (pt2xx, pt2xy, pt2yx, pt2yy); + } else if (pt2xx == 0 && pt2xy == 0 && pt2yx == 0 && pt2yy == 0) { + assert(_isOnCurve(pt1xx, pt1xy, pt1yx, pt1yy)); + return (pt1xx, pt1xy, pt1yx, pt1yy); + } + + assert(_isOnCurve(pt1xx, pt1xy, pt1yx, pt1yy)); + assert(_isOnCurve(pt2xx, pt2xy, pt2yx, pt2yy)); + + uint256[6] memory pt3 = + _ECTwistAddJacobian(pt1xx, pt1xy, pt1yx, pt1yy, 1, 0, pt2xx, pt2xy, pt2yx, pt2yy, 1, 0); + + return _fromJacobian(pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]); + } + + /** + * @notice Multiply a twist point by a scalar + * @param s Scalar to multiply by + * @param pt1xx Coefficient 1 of x + * @param pt1xy Coefficient 2 of x + * @param pt1yx Coefficient 1 of y + * @param pt1yy Coefficient 2 of y + * @return (pt2xx, pt2xy, pt2yx, pt2yy) + */ + function ECTwistMul( + uint256 s, + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy + ) public view returns (uint256, uint256, uint256, uint256) { + uint256 pt1zx = 1; + if (pt1xx == 0 && pt1xy == 0 && pt1yx == 0 && pt1yy == 0) { + pt1xx = 1; + pt1yx = 1; + pt1zx = 0; + } else { + assert(_isOnCurve(pt1xx, pt1xy, pt1yx, pt1yy)); + } + + uint256[6] memory pt2 = _ECTwistMulJacobian(s, pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, 0); + + return _fromJacobian(pt2[PTXX], pt2[PTXY], pt2[PTYX], pt2[PTYY], pt2[PTZX], pt2[PTZY]); + } + + /** + * @notice Get the field modulus + * @return The field modulus + */ + function GetFieldModulus() public pure returns (uint256) { + return FIELD_MODULUS; + } + + function submod(uint256 a, uint256 b, uint256 n) internal pure returns (uint256) { + return addmod(a, n - b, n); + } + + function _FQ2Mul( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (uint256, uint256) { + return ( + submod(mulmod(xx, yx, FIELD_MODULUS), mulmod(xy, yy, FIELD_MODULUS), FIELD_MODULUS), + addmod(mulmod(xx, yy, FIELD_MODULUS), mulmod(xy, yx, FIELD_MODULUS), FIELD_MODULUS) + ); + } + + function _FQ2Muc(uint256 xx, uint256 xy, uint256 c) internal pure returns (uint256, uint256) { + return (mulmod(xx, c, FIELD_MODULUS), mulmod(xy, c, FIELD_MODULUS)); + } + + function _FQ2Add( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (uint256, uint256) { + return (addmod(xx, yx, FIELD_MODULUS), addmod(xy, yy, FIELD_MODULUS)); + } + + function _FQ2Sub( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (uint256 rx, uint256 ry) { + return (submod(xx, yx, FIELD_MODULUS), submod(xy, yy, FIELD_MODULUS)); + } + + function _FQ2Div( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal view returns (uint256, uint256) { + (yx, yy) = _FQ2Inv(yx, yy); + return _FQ2Mul(xx, xy, yx, yy); + } + + function _FQ2Inv(uint256 x, uint256 y) internal view returns (uint256, uint256) { + uint256 inv = _modInv( + addmod(mulmod(y, y, FIELD_MODULUS), mulmod(x, x, FIELD_MODULUS), FIELD_MODULUS), + FIELD_MODULUS + ); + return (mulmod(x, inv, FIELD_MODULUS), FIELD_MODULUS - mulmod(y, inv, FIELD_MODULUS)); + } + + function _isOnCurve( + uint256 xx, + uint256 xy, + uint256 yx, + uint256 yy + ) internal pure returns (bool) { + uint256 yyx; + uint256 yyy; + uint256 xxxx; + uint256 xxxy; + (yyx, yyy) = _FQ2Mul(yx, yy, yx, yy); + (xxxx, xxxy) = _FQ2Mul(xx, xy, xx, xy); + (xxxx, xxxy) = _FQ2Mul(xxxx, xxxy, xx, xy); + (yyx, yyy) = _FQ2Sub(yyx, yyy, xxxx, xxxy); + (yyx, yyy) = _FQ2Sub(yyx, yyy, TWISTBX, TWISTBY); + return yyx == 0 && yyy == 0; + } + + function _modInv(uint256 a, uint256 n) internal view returns (uint256 result) { + bool success; + assembly { + let freemem := mload(0x40) + mstore(freemem, 0x20) + mstore(add(freemem, 0x20), 0x20) + mstore(add(freemem, 0x40), 0x20) + mstore(add(freemem, 0x60), a) + mstore(add(freemem, 0x80), sub(n, 2)) + mstore(add(freemem, 0xA0), n) + success := staticcall(sub(gas(), 2000), 5, freemem, 0xC0, freemem, 0x20) + result := mload(freemem) + } + require(success); + } + + function _fromJacobian( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy + ) internal view returns (uint256 pt2xx, uint256 pt2xy, uint256 pt2yx, uint256 pt2yy) { + uint256 invzx; + uint256 invzy; + (invzx, invzy) = _FQ2Inv(pt1zx, pt1zy); + (pt2xx, pt2xy) = _FQ2Mul(pt1xx, pt1xy, invzx, invzy); + (pt2yx, pt2yy) = _FQ2Mul(pt1yx, pt1yy, invzx, invzy); + } + + function _ECTwistAddJacobian( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy, + uint256 pt2xx, + uint256 pt2xy, + uint256 pt2yx, + uint256 pt2yy, + uint256 pt2zx, + uint256 pt2zy + ) internal pure returns (uint256[6] memory pt3) { + if (pt1zx == 0 && pt1zy == 0) { + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = + (pt2xx, pt2xy, pt2yx, pt2yy, pt2zx, pt2zy); + return pt3; + } else if (pt2zx == 0 && pt2zy == 0) { + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = + (pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy); + return pt3; + } + + (pt2yx, pt2yy) = _FQ2Mul(pt2yx, pt2yy, pt1zx, pt1zy); // U1 = y2 * z1 + (pt3[PTYX], pt3[PTYY]) = _FQ2Mul(pt1yx, pt1yy, pt2zx, pt2zy); // U2 = y1 * z2 + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1zx, pt1zy); // V1 = x2 * z1 + (pt3[PTZX], pt3[PTZY]) = _FQ2Mul(pt1xx, pt1xy, pt2zx, pt2zy); // V2 = x1 * z2 + + if (pt2xx == pt3[PTZX] && pt2xy == pt3[PTZY]) { + if (pt2yx == pt3[PTYX] && pt2yy == pt3[PTYY]) { + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = + _ECTwistDoubleJacobian(pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy); + return pt3; + } + (pt3[PTXX], pt3[PTXY], pt3[PTYX], pt3[PTYY], pt3[PTZX], pt3[PTZY]) = (1, 0, 1, 0, 0, 0); + return pt3; + } + + (pt2zx, pt2zy) = _FQ2Mul(pt1zx, pt1zy, pt2zx, pt2zy); // W = z1 * z2 + (pt1xx, pt1xy) = _FQ2Sub(pt2yx, pt2yy, pt3[PTYX], pt3[PTYY]); // U = U1 - U2 + (pt1yx, pt1yy) = _FQ2Sub(pt2xx, pt2xy, pt3[PTZX], pt3[PTZY]); // V = V1 - V2 + (pt1zx, pt1zy) = _FQ2Mul(pt1yx, pt1yy, pt1yx, pt1yy); // V_squared = V * V + (pt2yx, pt2yy) = _FQ2Mul(pt1zx, pt1zy, pt3[PTZX], pt3[PTZY]); // V_squared_times_V2 = V_squared * V2 + (pt1zx, pt1zy) = _FQ2Mul(pt1zx, pt1zy, pt1yx, pt1yy); // V_cubed = V * V_squared + (pt3[PTZX], pt3[PTZY]) = _FQ2Mul(pt1zx, pt1zy, pt2zx, pt2zy); // newz = V_cubed * W + (pt2xx, pt2xy) = _FQ2Mul(pt1xx, pt1xy, pt1xx, pt1xy); // U * U + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt2zx, pt2zy); // U * U * W + (pt2xx, pt2xy) = _FQ2Sub(pt2xx, pt2xy, pt1zx, pt1zy); // U * U * W - V_cubed + (pt2zx, pt2zy) = _FQ2Muc(pt2yx, pt2yy, 2); // 2 * V_squared_times_V2 + (pt2xx, pt2xy) = _FQ2Sub(pt2xx, pt2xy, pt2zx, pt2zy); // A = U * U * W - V_cubed - 2 * V_squared_times_V2 + (pt3[PTXX], pt3[PTXY]) = _FQ2Mul(pt1yx, pt1yy, pt2xx, pt2xy); // newx = V * A + (pt1yx, pt1yy) = _FQ2Sub(pt2yx, pt2yy, pt2xx, pt2xy); // V_squared_times_V2 - A + (pt1yx, pt1yy) = _FQ2Mul(pt1xx, pt1xy, pt1yx, pt1yy); // U * (V_squared_times_V2 - A) + (pt1xx, pt1xy) = _FQ2Mul(pt1zx, pt1zy, pt3[PTYX], pt3[PTYY]); // V_cubed * U2 + (pt3[PTYX], pt3[PTYY]) = _FQ2Sub(pt1yx, pt1yy, pt1xx, pt1xy); // newy = U * (V_squared_times_V2 - A) - V_cubed * U2 + } + + function _ECTwistDoubleJacobian( + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy + ) + internal + pure + returns ( + uint256 pt2xx, + uint256 pt2xy, + uint256 pt2yx, + uint256 pt2yy, + uint256 pt2zx, + uint256 pt2zy + ) + { + (pt2xx, pt2xy) = _FQ2Muc(pt1xx, pt1xy, 3); // 3 * x + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1xx, pt1xy); // W = 3 * x * x + (pt1zx, pt1zy) = _FQ2Mul(pt1yx, pt1yy, pt1zx, pt1zy); // S = y * z + (pt2yx, pt2yy) = _FQ2Mul(pt1xx, pt1xy, pt1yx, pt1yy); // x * y + (pt2yx, pt2yy) = _FQ2Mul(pt2yx, pt2yy, pt1zx, pt1zy); // B = x * y * S + (pt1xx, pt1xy) = _FQ2Mul(pt2xx, pt2xy, pt2xx, pt2xy); // W * W + (pt2zx, pt2zy) = _FQ2Muc(pt2yx, pt2yy, 8); // 8 * B + (pt1xx, pt1xy) = _FQ2Sub(pt1xx, pt1xy, pt2zx, pt2zy); // H = W * W - 8 * B + (pt2zx, pt2zy) = _FQ2Mul(pt1zx, pt1zy, pt1zx, pt1zy); // S_squared = S * S + (pt2yx, pt2yy) = _FQ2Muc(pt2yx, pt2yy, 4); // 4 * B + (pt2yx, pt2yy) = _FQ2Sub(pt2yx, pt2yy, pt1xx, pt1xy); // 4 * B - H + (pt2yx, pt2yy) = _FQ2Mul(pt2yx, pt2yy, pt2xx, pt2xy); // W * (4 * B - H) + (pt2xx, pt2xy) = _FQ2Muc(pt1yx, pt1yy, 8); // 8 * y + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1yx, pt1yy); // 8 * y * y + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt2zx, pt2zy); // 8 * y * y * S_squared + (pt2yx, pt2yy) = _FQ2Sub(pt2yx, pt2yy, pt2xx, pt2xy); // newy = W * (4 * B - H) - 8 * y * y * S_squared + (pt2xx, pt2xy) = _FQ2Muc(pt1xx, pt1xy, 2); // 2 * H + (pt2xx, pt2xy) = _FQ2Mul(pt2xx, pt2xy, pt1zx, pt1zy); // newx = 2 * H * S + (pt2zx, pt2zy) = _FQ2Mul(pt1zx, pt1zy, pt2zx, pt2zy); // S * S_squared + (pt2zx, pt2zy) = _FQ2Muc(pt2zx, pt2zy, 8); // newz = 8 * S * S_squared + } + + function _ECTwistMulJacobian( + uint256 d, + uint256 pt1xx, + uint256 pt1xy, + uint256 pt1yx, + uint256 pt1yy, + uint256 pt1zx, + uint256 pt1zy + ) internal pure returns (uint256[6] memory pt2) { + while (d != 0) { + if ((d & 1) != 0) { + pt2 = _ECTwistAddJacobian( + pt2[PTXX], + pt2[PTXY], + pt2[PTYX], + pt2[PTYY], + pt2[PTZX], + pt2[PTZY], + pt1xx, + pt1xy, + pt1yx, + pt1yy, + pt1zx, + pt1zy + ); + } + (pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy) = + _ECTwistDoubleJacobian(pt1xx, pt1xy, pt1yx, pt1yy, pt1zx, pt1zy); + + d = d / 2; + } + } +} diff --git a/src/unaudited/ECUtils.sol b/src/unaudited/ECUtils.sol new file mode 100644 index 00000000..651841f4 --- /dev/null +++ b/src/unaudited/ECUtils.sol @@ -0,0 +1,27 @@ + +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import {BN254} from "../libraries/BN254.sol"; + +/** + * @title ECUtils + * @notice Library containing utility functions for elliptic curve operations + */ +library ECUtils { + /** + * @notice Checks if a point lies on the BN254 elliptic curve + * @dev The curve equation is y^2 = x^3 + 3 (mod p) + * @param p The point to check, in G1 + * @return true if the point lies on the curve, false otherwise + */ + function isOnCurve( + BN254.G1Point memory p + ) internal pure returns (bool) { + uint256 y2 = mulmod(p.Y, p.Y, BN254.FP_MODULUS); + uint256 x2 = mulmod(p.X, p.X, BN254.FP_MODULUS); + uint256 x3 = mulmod(p.X, x2, BN254.FP_MODULUS); + uint256 rhs = addmod(x3, 3, BN254.FP_MODULUS); + return y2 == rhs; + } +} \ No newline at end of file diff --git a/test/unit/BLSSigCheckOperatorStateRetriever.t.sol b/test/unit/BLSSigCheckOperatorStateRetriever.t.sol new file mode 100644 index 00000000..6da62f28 --- /dev/null +++ b/test/unit/BLSSigCheckOperatorStateRetriever.t.sol @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "../utils/MockAVSDeployer.sol"; +import {IStakeRegistryErrors} from "../../src/interfaces/IStakeRegistry.sol"; +import {ISlashingRegistryCoordinatorTypes} from "../../src/interfaces/IRegistryCoordinator.sol"; +import {IBLSSignatureCheckerTypes} from "../../src/interfaces/IBLSSignatureChecker.sol"; +import {BN256G2} from "../../src/unaudited/BN256G2.sol"; +import {BLSSigCheckOperatorStateRetriever} from + "../../src/unaudited/BLSSigCheckOperatorStateRetriever.sol"; +import {OperatorStateRetrieverUnitTests} from "./OperatorStateRetrieverUnit.t.sol"; + +contract BLSSigCheckOperatorStateRetrieverUnitTests is + MockAVSDeployer, + OperatorStateRetrieverUnitTests +{ + using BN254 for BN254.G1Point; + + BLSSigCheckOperatorStateRetriever sigCheckOperatorStateRetriever; + + function setUp() public virtual override { + super.setUp(); + sigCheckOperatorStateRetriever = new BLSSigCheckOperatorStateRetriever(); + setOperatorStateRetriever(address(sigCheckOperatorStateRetriever)); + } + + // helper function to generate a G2 point from a scalar + function _makeG2Point( + uint256 scalar + ) internal returns (BN254.G2Point memory) { + // BN256G2.ECTwistMul returns (X0, X1, Y0, Y1) in that order + (uint256 reX, uint256 imX, uint256 reY, uint256 imY) = + BN256G2.ECTwistMul(scalar, BN254.G2x0, BN254.G2x1, BN254.G2y0, BN254.G2y1); + + // BN254.G2Point uses [im, re] ordering + return BN254.G2Point([imX, reX], [imY, reY]); + } + + // helper function to add two G2 points + function _addG2Points( + BN254.G2Point memory a, + BN254.G2Point memory b + ) internal returns (BN254.G2Point memory) { + BN254.G2Point memory sum; + // sum starts as (0,0), so we add a first: + (sum.X[1], sum.X[0], sum.Y[1], sum.Y[0]) = BN256G2.ECTwistAdd( + // sum so far + sum.X[1], + sum.X[0], + sum.Y[1], + sum.Y[0], + // a (flip to [im, re] for BN256G2) + a.X[1], + a.X[0], + a.Y[1], + a.Y[0] + ); + // then add b: + (sum.X[1], sum.X[0], sum.Y[1], sum.Y[0]) = BN256G2.ECTwistAdd( + sum.X[1], sum.X[0], sum.Y[1], sum.Y[0], b.X[1], b.X[0], b.Y[1], b.Y[0] + ); + return sum; + } + + function test_getNonSignerStakesAndSignature_returnsCorrect() public { + // setup + uint256 quorumBitmapOne = 1; + uint256 quorumBitmapThree = 3; + cheats.roll(registrationBlockNumber); + + _registerOperatorWithCoordinator(defaultOperator, quorumBitmapOne, defaultPubKey); + + address otherOperator = _incrementAddress(defaultOperator, 1); + BN254.G1Point memory otherPubKey = BN254.G1Point(1, 2); + _registerOperatorWithCoordinator( + otherOperator, quorumBitmapThree, otherPubKey, defaultStake - 1 + ); + + // Generate actual G2 pubkeys + BN254.G2Point memory op1G2 = _makeG2Point(2); + BN254.G2Point memory op2G2 = _makeG2Point(3); + + // Mock the registry calls so the contract sees those G2 points + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, otherOperator), + abi.encode(op2G2) + ); + + // Prepare inputs + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + address[] memory signingOperators = new address[](2); + signingOperators[0] = defaultOperator; + signingOperators[1] = otherOperator; + + bytes memory quorumNumbers = new bytes(2); + quorumNumbers[0] = bytes1(uint8(0)); + quorumNumbers[1] = bytes1(uint8(1)); + + // Call the function under test + IBLSSignatureCheckerTypes.NonSignerStakesAndSignature memory result = + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, quorumNumbers, dummySigma, signingOperators, uint32(block.number) + ); + + // Non-signers + assertEq(result.nonSignerQuorumBitmapIndices.length, 0, "Should have no non-signer"); + assertEq(result.nonSignerPubkeys.length, 0, "Should have no non-signer pubkeys"); + + // Quorum APKs + assertEq(result.quorumApks.length, 2, "Should have 2 quorum APKs"); + (BN254.G1Point memory expectedApk0) = + _getApkAtBlocknumber(registryCoordinator, 0, uint32(block.number)); + (BN254.G1Point memory expectedApk1) = + _getApkAtBlocknumber(registryCoordinator, 1, uint32(block.number)); + assertEq(result.quorumApks[0].X, expectedApk0.X, "First quorum APK X mismatch"); + assertEq(result.quorumApks[0].Y, expectedApk0.Y, "First quorum APK Y mismatch"); + assertEq(result.quorumApks[1].X, expectedApk1.X, "Second quorum APK X mismatch"); + assertEq(result.quorumApks[1].Y, expectedApk1.Y, "Second quorum APK Y mismatch"); + + // Aggregated G2 = op1G2 + op2G2 + BN254.G2Point memory expectedSum = _addG2Points(op1G2, op2G2); + assertEq(result.apkG2.X[0], expectedSum.X[0], "aggregated X[0] mismatch"); + assertEq(result.apkG2.X[1], expectedSum.X[1], "aggregated X[1] mismatch"); + assertEq(result.apkG2.Y[0], expectedSum.Y[0], "aggregated Y[0] mismatch"); + assertEq(result.apkG2.Y[1], expectedSum.Y[1], "aggregated Y[1] mismatch"); + + // Sigma + assertEq(result.sigma.X, dummySigma.X, "Sigma X mismatch"); + assertEq(result.sigma.Y, dummySigma.Y, "Sigma Y mismatch"); + + // Indices + assertEq(result.quorumApkIndices.length, 2, "Should have 2 quorum APK indices"); + assertEq(result.quorumApkIndices[0], 1, "First quorum APK index mismatch"); + assertEq(result.quorumApkIndices[1], 1, "Second quorum APK index mismatch"); + assertEq(result.totalStakeIndices.length, 2, "Should have 2 total stake indices"); + assertEq(result.totalStakeIndices[0], 1, "First total stake index mismatch"); + assertEq(result.totalStakeIndices[1], 1, "Second total stake index mismatch"); + + // Non-signer stake indices + assertEq( + result.nonSignerStakeIndices.length, + 2, + "Should have 2 arrays of non-signer stake indices" + ); + assertEq(result.nonSignerStakeIndices[0].length, 0, "First quorum non-signer mismatch"); + assertEq(result.nonSignerStakeIndices[1].length, 0, "Second quorum non-signer mismatch"); + } + + function test_getNonSignerStakesAndSignature_returnsCorrect_oneSigner() public { + // setup + uint256 quorumBitmapOne = 1; + uint256 quorumBitmapThree = 3; + cheats.roll(registrationBlockNumber); + + _registerOperatorWithCoordinator(defaultOperator, quorumBitmapOne, defaultPubKey); + + address otherOperator = _incrementAddress(defaultOperator, 1); + BN254.G1Point memory otherPubKey = BN254.G1Point(1, 2); + _registerOperatorWithCoordinator( + otherOperator, quorumBitmapThree, otherPubKey, defaultStake - 1 + ); + + // Generate actual G2 pubkeys + BN254.G2Point memory op1G2 = _makeG2Point(2); + BN254.G2Point memory op2G2 = _makeG2Point(3); + + // Mock them + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, otherOperator), + abi.encode(op2G2) + ); + + // Prepare input + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + + address[] memory signingOperators = new address[](1); + signingOperators[0] = defaultOperator; // only op1 + + bytes memory quorumNumbers = new bytes(2); + quorumNumbers[0] = bytes1(uint8(0)); + quorumNumbers[1] = bytes1(uint8(1)); + + // Call under test + IBLSSignatureCheckerTypes.NonSignerStakesAndSignature memory result = + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, quorumNumbers, dummySigma, signingOperators, uint32(block.number) + ); + + // Validate + // One non-signer => otherOperator + assertEq(result.nonSignerQuorumBitmapIndices.length, 1, "Should have 1 non-signer"); + assertEq( + result.nonSignerQuorumBitmapIndices[0], 0, "Unexpected non-signer quorum bitmap index" + ); + assertEq(result.nonSignerPubkeys.length, 1, "Should have 1 non-signer pubkey"); + assertEq(result.nonSignerPubkeys[0].X, otherPubKey.X, "Unexpected non-signer pubkey X"); + assertEq(result.nonSignerPubkeys[0].Y, otherPubKey.Y, "Unexpected non-signer pubkey Y"); + + // Quorum APKs + assertEq(result.quorumApks.length, 2, "Should have 2 quorum APKs"); + (BN254.G1Point memory expectedApk0) = + _getApkAtBlocknumber(registryCoordinator, 0, uint32(block.number)); + (BN254.G1Point memory expectedApk1) = + _getApkAtBlocknumber(registryCoordinator, 1, uint32(block.number)); + assertEq(result.quorumApks[0].X, expectedApk0.X, "First quorum APK X mismatch"); + assertEq(result.quorumApks[0].Y, expectedApk0.Y, "First quorum APK Y mismatch"); + assertEq(result.quorumApks[1].X, expectedApk1.X, "Second quorum APK X mismatch"); + assertEq(result.quorumApks[1].Y, expectedApk1.Y, "Second quorum APK Y mismatch"); + + // Since only defaultOperator signed, aggregator's G2 should match op1G2 + assertEq(result.apkG2.X[0], op1G2.X[0], "aggregated X[0] mismatch"); + assertEq(result.apkG2.X[1], op1G2.X[1], "aggregated X[1] mismatch"); + assertEq(result.apkG2.Y[0], op1G2.Y[0], "aggregated Y[0] mismatch"); + assertEq(result.apkG2.Y[1], op1G2.Y[1], "aggregated Y[1] mismatch"); + + // Sigma + assertEq(result.sigma.X, dummySigma.X, "Sigma X mismatch"); + assertEq(result.sigma.Y, dummySigma.Y, "Sigma Y mismatch"); + + // Indices + assertEq(result.quorumApkIndices.length, 2, "Should have 2 quorum APK indices"); + assertEq(result.quorumApkIndices[0], 1, "First quorum index mismatch"); + assertEq(result.quorumApkIndices[1], 1, "Second quorum index mismatch"); + assertEq(result.totalStakeIndices.length, 2, "Should have 2 total stake indices"); + assertEq(result.totalStakeIndices[0], 1, "First total stake index mismatch"); + assertEq(result.totalStakeIndices[1], 1, "Second total stake index mismatch"); + + // Non-signer stake indices + // Each quorum has exactly 1 non-signer (the otherOperator) + assertEq( + result.nonSignerStakeIndices.length, + 2, + "Should have 2 arrays of non-signer stake indices" + ); + assertEq( + result.nonSignerStakeIndices[0].length, + 1, + "First quorum should have 1 non-signer stake index" + ); + assertEq( + result.nonSignerStakeIndices[1].length, + 1, + "Second quorum should have 1 non-signer stake index" + ); + } + + function test_getNonSignerStakesAndSignature_changingQuorumOperatorSet() public { + // setup + uint256 quorumBitmapOne = 1; + uint256 quorumBitmapThree = 3; + cheats.roll(registrationBlockNumber); + + _registerOperatorWithCoordinator(defaultOperator, quorumBitmapOne, defaultPubKey); + + address otherOperator = _incrementAddress(defaultOperator, 1); + BN254.G1Point memory otherPubKey = BN254.G1Point(1, 2); + _registerOperatorWithCoordinator( + otherOperator, quorumBitmapThree, otherPubKey, defaultStake - 1 + ); + + // Generate actual G2 pubkeys + BN254.G2Point memory op1G2 = _makeG2Point(2); + BN254.G2Point memory op2G2 = _makeG2Point(3); + + // Mock the registry calls so the contract sees those G2 points + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, otherOperator), + abi.encode(op2G2) + ); + + // Prepare inputs + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + address[] memory signingOperators = new address[](2); + signingOperators[0] = defaultOperator; + signingOperators[1] = otherOperator; + + bytes memory quorumNumbers = new bytes(2); + quorumNumbers[0] = bytes1(uint8(0)); + quorumNumbers[1] = bytes1(uint8(1)); + + // Deregister the otherOperator + cheats.roll(registrationBlockNumber + 10); + cheats.prank(otherOperator); + registryCoordinator.deregisterOperator(BitmapUtils.bitmapToBytesArray(quorumBitmapThree)); + + // Call the function under test + IBLSSignatureCheckerTypes.NonSignerStakesAndSignature memory result = + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, + quorumNumbers, + dummySigma, + signingOperators, + registrationBlockNumber + ); + + // Non-signers + assertEq(result.nonSignerQuorumBitmapIndices.length, 0, "Should have no non-signer"); + assertEq(result.nonSignerPubkeys.length, 0, "Should have no non-signer pubkeys"); + + // Quorum APKs + assertEq(result.quorumApks.length, 2, "Should have 2 quorum APKs"); + (BN254.G1Point memory expectedApk0) = + _getApkAtBlocknumber(registryCoordinator, 0, uint32(registrationBlockNumber)); + (BN254.G1Point memory expectedApk1) = + _getApkAtBlocknumber(registryCoordinator, 1, uint32(registrationBlockNumber)); + assertEq(result.quorumApks[0].X, expectedApk0.X, "First quorum APK X mismatch"); + assertEq(result.quorumApks[0].Y, expectedApk0.Y, "First quorum APK Y mismatch"); + assertEq(result.quorumApks[1].X, expectedApk1.X, "Second quorum APK X mismatch"); + assertEq(result.quorumApks[1].Y, expectedApk1.Y, "Second quorum APK Y mismatch"); + + // Aggregated G2 = op1G2 + op2G2 + BN254.G2Point memory expectedSum = _addG2Points(op1G2, op2G2); + assertEq(result.apkG2.X[0], expectedSum.X[0], "aggregated X[0] mismatch"); + assertEq(result.apkG2.X[1], expectedSum.X[1], "aggregated X[1] mismatch"); + assertEq(result.apkG2.Y[0], expectedSum.Y[0], "aggregated Y[0] mismatch"); + assertEq(result.apkG2.Y[1], expectedSum.Y[1], "aggregated Y[1] mismatch"); + + // Sigma + assertEq(result.sigma.X, dummySigma.X, "Sigma X mismatch"); + assertEq(result.sigma.Y, dummySigma.Y, "Sigma Y mismatch"); + + // Indices + assertEq(result.quorumApkIndices.length, 2, "Should have 2 quorum APK indices"); + assertEq(result.quorumApkIndices[0], 1, "First quorum APK index mismatch"); + assertEq(result.quorumApkIndices[1], 1, "Second quorum APK index mismatch"); + assertEq(result.totalStakeIndices.length, 2, "Should have 2 total stake indices"); + assertEq(result.totalStakeIndices[0], 1, "First total stake index mismatch"); + assertEq(result.totalStakeIndices[1], 1, "Second total stake index mismatch"); + + // Non-signer stake indices + assertEq( + result.nonSignerStakeIndices.length, + 2, + "Should have 2 arrays of non-signer stake indices" + ); + assertEq(result.nonSignerStakeIndices[0].length, 0, "First quorum non-signer mismatch"); + assertEq(result.nonSignerStakeIndices[1].length, 0, "Second quorum non-signer mismatch"); + } + + function test_getNonSignerStakesAndSignature_revert_signerNeverRegistered() public { + // Setup - register only one operator + uint256 quorumBitmap = 1; // Quorum 0 only + + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + // Create G2 points for the registered operator + BN254.G2Point memory op1G2 = _makeG2Point(2); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + + // Create a dummy signature + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + + // Try to include an unregistered operator as a signer + address unregisteredOperator = _incrementAddress(defaultOperator, 1); + address[] memory signingOperators = new address[](2); + signingOperators[0] = defaultOperator; + signingOperators[1] = unregisteredOperator; // This operator was never registered + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(uint8(0)); // Quorum 0 + + // Should revert because one of the signers was never registered + cheats.expectRevert( + bytes( + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId" + ) + ); + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, quorumNumbers, dummySigma, signingOperators, uint32(block.number) + ); + } + + function test_getNonSignerStakesAndSignature_revert_signerRegisteredAfterReferenceBlock() + public + { + // Setup - register one operator + uint256 quorumBitmap = 1; // Quorum 0 only + + // Save initial block number + uint32 initialBlock = registrationBlockNumber; + + cheats.roll(initialBlock); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + // Register second operator later + cheats.roll(initialBlock + 10); + address secondOperator = _incrementAddress(defaultOperator, 1); + BN254.G1Point memory secondPubKey = BN254.G1Point(1, 2); + _registerOperatorWithCoordinator( + secondOperator, quorumBitmap, secondPubKey, defaultStake - 1 + ); + + // Create G2 points for both operators + BN254.G2Point memory op1G2 = _makeG2Point(2); + BN254.G2Point memory op2G2 = _makeG2Point(3); + + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, secondOperator), + abi.encode(op2G2) + ); + + // Create a dummy signature + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + + // Include both operators as signers + address[] memory signingOperators = new address[](2); + signingOperators[0] = defaultOperator; + signingOperators[1] = secondOperator; + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(uint8(0)); // Quorum 0 + + // Should revert when querying at a block before the second operator was registered + cheats.expectRevert( + bytes( + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId" + ) + ); + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, quorumNumbers, dummySigma, signingOperators, initialBlock + 5 + ); + } + + function test_getNonSignerStakesAndSignature_revert_signerDeregisteredAtReferenceBlock() + public + { + // Setup - register two operators + uint256 quorumBitmap = 1; // Quorum 0 only + + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + address secondOperator = _incrementAddress(defaultOperator, 1); + BN254.G1Point memory secondPubKey = BN254.G1Point(1, 2); + _registerOperatorWithCoordinator( + secondOperator, quorumBitmap, secondPubKey, defaultStake - 1 + ); + + // Create G2 points for the operators + BN254.G2Point memory op1G2 = _makeG2Point(2); + BN254.G2Point memory op2G2 = _makeG2Point(3); + + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, secondOperator), + abi.encode(op2G2) + ); + + // Deregister the second operator + cheats.roll(registrationBlockNumber + 10); + cheats.prank(secondOperator); + registryCoordinator.deregisterOperator(BitmapUtils.bitmapToBytesArray(quorumBitmap)); + + // Create a dummy signature + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + + // Include both operators as signers + address[] memory signingOperators = new address[](2); + signingOperators[0] = defaultOperator; + signingOperators[1] = secondOperator; // This operator is deregistered + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(uint8(0)); // Quorum 0 + + // Should revert because secondOperator was deregistered + cheats.expectRevert(OperatorStateRetriever.OperatorNotRegistered.selector); + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, quorumNumbers, dummySigma, signingOperators, uint32(block.number) + ); + } + + function test_getNonSignerStakesAndSignature_revert_quorumNotCreatedAtCallTime() public { + // Setup - register one operator + uint256 quorumBitmap = 1; // Quorum 0 only + + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + // Create G2 points for the operator + BN254.G2Point memory op1G2 = _makeG2Point(2); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + + // Create a dummy signature + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + + // Include the operator as a signer + address[] memory signingOperators = new address[](1); + signingOperators[0] = defaultOperator; + + // Try to query for a non-existent quorum (quorum 9) + bytes memory invalidQuorumNumbers = new bytes(1); + invalidQuorumNumbers[0] = bytes1(uint8(9)); // Invalid quorum number + + // Should revert because quorum 9 doesn't exist, but with a different error message + cheats.expectRevert( + bytes( + "IndexRegistry._operatorCountAtBlockNumber: quorum did not exist at given block number" + ) + ); + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, + invalidQuorumNumbers, + dummySigma, + signingOperators, + uint32(block.number) + ); + } + + function test_getNonSignerStakesAndSignature_revert_quorumNotCreatedAtReferenceBlock() public { + // Setup - register one operator in quorum 0 + uint256 quorumBitmap = 1; + + cheats.roll(registrationBlockNumber); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + // Save this block number + uint32 initialBlock = uint32(block.number); + + // Create a new quorum later + cheats.roll(initialBlock + 10); + + ISlashingRegistryCoordinatorTypes.OperatorSetParam memory operatorSetParams = + ISlashingRegistryCoordinatorTypes.OperatorSetParam({ + maxOperatorCount: defaultMaxOperatorCount, + kickBIPsOfOperatorStake: defaultKickBIPsOfOperatorStake, + kickBIPsOfTotalStake: defaultKickBIPsOfTotalStake + }); + uint96 minimumStake = 1; + IStakeRegistryTypes.StrategyParams[] memory strategyParams = + new IStakeRegistryTypes.StrategyParams[](1); + strategyParams[0] = IStakeRegistryTypes.StrategyParams({ + strategy: IStrategy(address(1000)), + multiplier: 1e16 + }); + + // Create quorum 8 + cheats.prank(registryCoordinator.owner()); + registryCoordinator.createTotalDelegatedStakeQuorum( + operatorSetParams, minimumStake, strategyParams + ); + + // Create G2 points for the operator + BN254.G2Point memory op1G2 = _makeG2Point(2); + vm.mockCall( + address(blsApkRegistry), + abi.encodeWithSelector(IBLSApkRegistry.getOperatorPubkeyG2.selector, defaultOperator), + abi.encode(op1G2) + ); + + // Create a dummy signature + BN254.G1Point memory dummySigma = BN254.scalar_mul_tiny(BN254.generatorG1(), 123); + + // Include the operator as a signer + address[] memory signingOperators = new address[](1); + signingOperators[0] = defaultOperator; + + // Try to query for the newly created quorum but at a historical block + bytes memory newQuorumNumbers = new bytes(1); + newQuorumNumbers[0] = bytes1(uint8(numQuorums)); + + // Should revert when querying for the newly created quorum at a block before it was created + cheats.expectRevert( + bytes( + "IndexRegistry._operatorCountAtBlockNumber: quorum did not exist at given block number" + ) + ); + sigCheckOperatorStateRetriever.getNonSignerStakesAndSignature( + registryCoordinator, newQuorumNumbers, dummySigma, signingOperators, initialBlock + ); + } + + function _getApkAtBlocknumber( + ISlashingRegistryCoordinator registryCoordinator, + uint8 quorumNumber, + uint32 blockNumber + ) internal view returns (BN254.G1Point memory) { + bytes32[] memory operatorIds = registryCoordinator.indexRegistry() + .getOperatorListAtBlockNumber(quorumNumber, blockNumber); + BN254.G1Point memory apk = BN254.G1Point(0, 0); + IBLSApkRegistry blsApkRegistry = registryCoordinator.blsApkRegistry(); + for (uint256 i = 0; i < operatorIds.length; i++) { + address operator = registryCoordinator.getOperatorFromId(operatorIds[i]); + BN254.G1Point memory operatorPk; + (operatorPk.X, operatorPk.Y) = blsApkRegistry.operatorToPubkey(operator); + apk = BN254.plus(apk, operatorPk); + } + return apk; + } +} diff --git a/test/unit/OperatorStateRetrieverUnit.t.sol b/test/unit/OperatorStateRetrieverUnit.t.sol index 4020604f..374180ee 100644 --- a/test/unit/OperatorStateRetrieverUnit.t.sol +++ b/test/unit/OperatorStateRetrieverUnit.t.sol @@ -13,6 +13,13 @@ contract OperatorStateRetrieverUnitTests is MockAVSDeployer { _deployMockEigenLayerAndAVS(numQuorums); } + /// @dev Used by inheriting contracts to test custom state retrievers + function setOperatorStateRetriever( + address operatorStateRetrieverAddress + ) internal { + operatorStateRetriever = OperatorStateRetriever(operatorStateRetrieverAddress); + } + function test_getOperatorState_revert_neverRegistered() public { cheats.expectRevert( "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId"