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"