diff --git a/src/contracts/interfaces/IBN254CertificateVerifier.sol b/src/contracts/interfaces/IBN254CertificateVerifier.sol index 5b488d9c4f..7c7ad4eaa6 100644 --- a/src/contracts/interfaces/IBN254CertificateVerifier.sol +++ b/src/contracts/interfaces/IBN254CertificateVerifier.sol @@ -41,9 +41,18 @@ interface IBN254CertificateVerifierEvents is IBN254CertificateVerifierTypes { event TableUpdated(OperatorSet operatorSet, uint32 referenceTimestamp, BN254OperatorSetInfo operatorSetInfo); } +interface IBN254CertificateVerifierErrors { + ///@notice thrown when operator index provided in certificate is invalid + error InvalidOperatorIndex(); +} + /// @notice An interface for verifying BN254 certificates /// @notice This implements the base `IBaseCertificateVerifier` interface -interface IBN254CertificateVerifier is IBN254CertificateVerifierEvents, IBaseCertificateVerifier { +interface IBN254CertificateVerifier is + IBN254CertificateVerifierEvents, + IBaseCertificateVerifier, + IBN254CertificateVerifierErrors +{ /** * @notice updates the operator table * @param operatorSet the operatorSet to update the operator table for diff --git a/src/contracts/libraries/Merkle.sol b/src/contracts/libraries/Merkle.sol index 071dcb8d9e..e8ebcd5826 100644 --- a/src/contracts/libraries/Merkle.sol +++ b/src/contracts/libraries/Merkle.sol @@ -164,4 +164,70 @@ library Merkle { //the first node in the layer is the root return layer[0]; } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using keccak as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + */ + function merkleizeKeccak( + bytes32[] memory leaves + ) internal pure returns (bytes32) { + // TODO: very inefficient, use ZERO_HASHES + // pad to the next power of 2 + uint256 numNodesInLayer = 1; + while (numNodesInLayer < leaves.length) { + numNodesInLayer *= 2; + } + bytes32[] memory layer = new bytes32[](numNodesInLayer); + for (uint256 i = 0; i < leaves.length; i++) { + layer[i] = leaves[i]; + } + + //while we haven't computed the root + while (numNodesInLayer != 1) { + uint256 numNodesInNextLayer = numNodesInLayer / 2; + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInNextLayer; i++) { + layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer = numNodesInNextLayer; + } + //the first node in the layer is the root + return layer[0]; + } + + function getProofKeccak(bytes32[] memory leaves, uint256 index) internal pure returns (bytes memory proof) { + // TODO: very inefficient, use ZERO_HASHES + // pad to the next power of 2 + uint256 numNodesInLayer = 1; + while (numNodesInLayer < leaves.length) { + numNodesInLayer *= 2; + } + bytes32[] memory layer = new bytes32[](numNodesInLayer); + for (uint256 i = 0; i < leaves.length; i++) { + layer[i] = leaves[i]; + } + + //while we haven't computed the root + while (numNodesInLayer != 1) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < layer.length; i++) { + if (i == index) { + uint256 siblingIndex = i + 1 - 2 * (i % 2); + proof = abi.encodePacked(proof, layer[siblingIndex]); + index /= 2; + } + } + + uint256 numNodesInNextLayer = numNodesInLayer / 2; + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInNextLayer; i++) { + layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer = numNodesInNextLayer; + } + } } diff --git a/src/contracts/multichain/BN254CertificateVerifier.sol b/src/contracts/multichain/BN254CertificateVerifier.sol new file mode 100644 index 0000000000..bb2f9562e0 --- /dev/null +++ b/src/contracts/multichain/BN254CertificateVerifier.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import {BN254} from "../libraries/BN254.sol"; +import {OperatorSet} from "../libraries/OperatorSetLib.sol"; +import {IBN254TableCalculator, IBN254TableCalculatorTypes} from "../interfaces/IBN254TableCalculator.sol"; +import {IBN254CertificateVerifier, IBN254CertificateVerifierTypes} from "../interfaces/IBN254CertificateVerifier.sol"; +import {IBaseCertificateVerifier} from "../interfaces/IBaseCertificateVerifier.sol"; +import {Merkle} from "../libraries/Merkle.sol"; +import "./BN254CertificateVerifierStorage.sol"; + +/** + * @title BN254CertificateVerifier + * @notice Singleton verifier for BN254 certificates across multiple operator sets + * @dev This contract uses BN254 curves for signature verification and + * caches operator information for efficient verification + */ +contract BN254CertificateVerifier is Initializable, OwnableUpgradeable, BN254CertificateVerifierStorage { + using Merkle for bytes; + using BN254 for BN254.G1Point; + + /** + * @notice Struct to hold verification context and reduce stack depth + */ + struct VerificationContext { + bytes32 operatorSetKey; + BN254OperatorSetInfo operatorSetInfo; + uint256[] signedStakes; + BN254.G1Point nonSignerApk; + } + + /** + * @notice Restricts access to the operator table updater + */ + modifier onlyTableUpdater() { + require(msg.sender == _operatorTableUpdater, OnlyTableUpdater()); + _; + } + + /** + * @notice Constructor for the certificate verifier + * @dev Disables initializers to prevent implementation initialization + * @param __operatorTableUpdater Address authorized to update operator tables + */ + constructor( + address __operatorTableUpdater + ) BN254CertificateVerifierStorage(__operatorTableUpdater) { + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param __owner The initial owner of the contract + */ + function initialize( + address __owner + ) external initializer { + __Ownable_init(); + _transferOwnership(__owner); + } + + ///@inheritdoc IBaseCertificateVerifier + function getOperatorSetOwner( + OperatorSet memory operatorSet + ) external view returns (address) { + bytes32 operatorSetKey = operatorSet.key(); + return _operatorSetOwners[operatorSetKey]; + } + + ///@inheritdoc IBaseCertificateVerifier + function maxOperatorTableStaleness( + OperatorSet memory operatorSet + ) external view returns (uint32) { + bytes32 operatorSetKey = operatorSet.key(); + return _maxStalenessPeriods[operatorSetKey]; + } + + ///@inheritdoc IBaseCertificateVerifier + function latestReferenceTimestamp( + OperatorSet memory operatorSet + ) external view returns (uint32) { + bytes32 operatorSetKey = operatorSet.key(); + return _latestReferenceTimestamps[operatorSetKey]; + } + + ///@inheritdoc IBN254CertificateVerifier + function updateOperatorTable( + OperatorSet calldata operatorSet, + uint32 referenceTimestamp, + BN254OperatorSetInfo memory operatorSetInfo, + OperatorSetConfig calldata operatorSetConfig + ) external onlyTableUpdater { + bytes32 operatorSetKey = operatorSet.key(); + + // Validate that the new timestamp is greater than the latest reference timestamp + require(referenceTimestamp > _latestReferenceTimestamps[operatorSetKey], TableUpdateStale()); + + // Store the operator set info + _operatorSetInfos[operatorSetKey][referenceTimestamp] = operatorSetInfo; + _latestReferenceTimestamps[operatorSetKey] = referenceTimestamp; + _operatorSetOwners[operatorSetKey] = operatorSetConfig.owner; + _maxStalenessPeriods[operatorSetKey] = operatorSetConfig.maxStalenessPeriod; + + emit TableUpdated(operatorSet, referenceTimestamp, operatorSetInfo); + } + + ///@inheritdoc IBN254CertificateVerifier + function verifyCertificate( + OperatorSet memory operatorSet, + BN254Certificate memory cert + ) external returns (uint256[] memory signedStakes) { + return _verifyCertificate(operatorSet, cert); + } + + ///@inheritdoc IBN254CertificateVerifier + function verifyCertificateProportion( + OperatorSet memory operatorSet, + BN254Certificate memory cert, + uint16[] memory totalStakeProportionThresholds + ) external returns (bool) { + uint256[] memory signedStakes = _verifyCertificate(operatorSet, cert); + + bytes32 operatorSetKey = operatorSet.key(); + BN254OperatorSetInfo memory operatorSetInfo = _operatorSetInfos[operatorSetKey][cert.referenceTimestamp]; + uint256[] memory totalStakes = operatorSetInfo.totalWeights; + + require(signedStakes.length == totalStakeProportionThresholds.length, ArrayLengthMismatch()); + + for (uint256 i = 0; i < signedStakes.length; i++) { + // Calculate threshold as proportion of total stake + // totalStakeProportionThresholds is in basis points (e.g. 6600 = 66%) + uint256 threshold = (totalStakes[i] * totalStakeProportionThresholds[i]) / BPS_DENOMINATOR; + + if (signedStakes[i] < threshold) { + return false; + } + } + + return true; + } + + ///@inheritdoc IBN254CertificateVerifier + function verifyCertificateNominal( + OperatorSet memory operatorSet, + BN254Certificate memory cert, + uint256[] memory totalStakeNominalThresholds + ) external returns (bool) { + uint256[] memory signedStakes = _verifyCertificate(operatorSet, cert); + + require(signedStakes.length == totalStakeNominalThresholds.length, ArrayLengthMismatch()); + + for (uint256 i = 0; i < signedStakes.length; i++) { + if (signedStakes[i] < totalStakeNominalThresholds[i]) { + return false; + } + } + + return true; + } + + /** + * @notice Attempts signature verification with gas limit for safety + * @param msgHash The message hash that was signed + * @param aggPubkey The aggregate public key of signers + * @param apkG2 The G2 point representation of the aggregate public key + * @param signature The BLS signature to verify + * @return pairingSuccessful Whether the pairing operation completed successfully + * @return signatureValid Whether the signature is valid + */ + function trySignatureVerification( + bytes32 msgHash, + BN254.G1Point memory aggPubkey, + BN254.G2Point memory apkG2, + BN254.G1Point memory signature + ) internal view returns (bool pairingSuccessful, bool signatureValid) { + uint256 gamma = uint256( + keccak256( + abi.encodePacked( + msgHash, + aggPubkey.X, + aggPubkey.Y, + apkG2.X[0], + apkG2.X[1], + apkG2.Y[0], + apkG2.Y[1], + signature.X, + signature.Y + ) + ) + ) % BN254.FR_MODULUS; + + (pairingSuccessful, signatureValid) = BN254.safePairing( + signature.plus(aggPubkey.scalar_mul(gamma)), // sigma + apk*gamma + BN254.negGeneratorG2(), // -G2 + BN254.hashToG1(msgHash).plus(BN254.generatorG1().scalar_mul(gamma)), // H(m) + g1*gamma + apkG2, // apkG2 + PAIRING_EQUALITY_CHECK_GAS + ); + } + + /** + * @notice Internal function to verify a certificate + * @param operatorSet The operator set the certificate is for + * @param cert The certificate to verify + * @return signedStakes The amount of stake that signed the certificate for each stake type + */ + function _verifyCertificate( + OperatorSet memory operatorSet, + BN254Certificate memory cert + ) internal returns (uint256[] memory signedStakes) { + VerificationContext memory ctx; + ctx.operatorSetKey = operatorSet.key(); + + _validateCertificateTimestamp(ctx.operatorSetKey, cert.referenceTimestamp); + ctx.operatorSetInfo = _operatorSetInfos[ctx.operatorSetKey][cert.referenceTimestamp]; + + require(ctx.operatorSetInfo.operatorInfoTreeRoot != bytes32(0), ReferenceTimestampDoesNotExist()); + + // Initialize signed stakes with total stakes + ctx.signedStakes = new uint256[](ctx.operatorSetInfo.totalWeights.length); + for (uint256 i = 0; i < ctx.operatorSetInfo.totalWeights.length; i++) { + ctx.signedStakes[i] = ctx.operatorSetInfo.totalWeights[i]; + } + + ctx.nonSignerApk = _processNonSigners(ctx, cert); + + _verifySignature(ctx, cert); + + return ctx.signedStakes; + } + + /** + * @notice Validates certificate timestamp against staleness requirements + * @param operatorSetKey The operator set key + * @param referenceTimestamp The reference timestamp to validate + */ + function _validateCertificateTimestamp(bytes32 operatorSetKey, uint32 referenceTimestamp) internal view { + uint32 maxStaleness = _maxStalenessPeriods[operatorSetKey]; + require(maxStaleness == 0 || block.timestamp <= referenceTimestamp + maxStaleness, CertificateStale()); + } + + /** + * @notice Processes non-signer witnesses and returns aggregate non-signer public key + * @param ctx The verification context + * @param cert The certificate being verified + * @return nonSignerApk The aggregate public key of non-signers + */ + function _processNonSigners( + VerificationContext memory ctx, + BN254Certificate memory cert + ) internal returns (BN254.G1Point memory nonSignerApk) { + nonSignerApk = BN254.G1Point(0, 0); + + for (uint256 i = 0; i < cert.nonSignerWitnesses.length; i++) { + BN254OperatorInfoWitness memory witness = cert.nonSignerWitnesses[i]; + + require(witness.operatorIndex < ctx.operatorSetInfo.numOperators, InvalidOperatorIndex()); + + BN254OperatorInfo memory operatorInfo = + _getOrCacheOperatorInfo(ctx.operatorSetKey, cert.referenceTimestamp, witness); + + nonSignerApk = nonSignerApk.plus(operatorInfo.pubkey); + + // Subtract non-signer stakes from total signed stakes + for (uint256 j = 0; j < operatorInfo.weights.length; j++) { + if (j < ctx.signedStakes.length) { + ctx.signedStakes[j] -= operatorInfo.weights[j]; + } + } + } + } + + /** + * @notice Gets operator info from cache or verifies and caches it + * @param operatorSetKey The operator set key + * @param referenceTimestamp The reference timestamp + * @param witness The operator info witness containing proof data + * @return operatorInfo The verified operator information + */ + function _getOrCacheOperatorInfo( + bytes32 operatorSetKey, + uint32 referenceTimestamp, + BN254OperatorInfoWitness memory witness + ) internal returns (BN254OperatorInfo memory operatorInfo) { + BN254OperatorInfo storage cachedInfo = _operatorInfos[operatorSetKey][referenceTimestamp][witness.operatorIndex]; + + // Check if operator info is cached using pubkey existence (weights can be 0) + bool isInfoCached = (cachedInfo.pubkey.X != 0 || cachedInfo.pubkey.Y != 0); + + if (!isInfoCached) { + bool verified = _verifyOperatorInfoMerkleProof( + operatorSetKey, + referenceTimestamp, + witness.operatorIndex, + witness.operatorInfo, + witness.operatorInfoProof + ); + + require(verified, VerificationFailed()); + + _operatorInfos[operatorSetKey][referenceTimestamp][witness.operatorIndex] = witness.operatorInfo; + operatorInfo = witness.operatorInfo; + } else { + operatorInfo = cachedInfo; + } + } + + /** + * @notice Verifies the BLS signature + * @param ctx The verification context + * @param cert The certificate containing the signature to verify + */ + function _verifySignature(VerificationContext memory ctx, BN254Certificate memory cert) internal view { + // Calculate signer aggregate public key by subtracting non-signers from total + BN254.G1Point memory signerApk = ctx.operatorSetInfo.aggregatePubkey.plus(ctx.nonSignerApk.negate()); + + (bool pairingSuccessful, bool signatureValid) = + trySignatureVerification(cert.messageHash, signerApk, cert.apk, cert.signature); + + require(pairingSuccessful && signatureValid, VerificationFailed()); + } + + /** + * @notice Verifies a merkle proof for an operator info + * @param operatorSetKey The operator set key + * @param referenceTimestamp The reference timestamp + * @param operatorIndex The index of the operator + * @param operatorInfo The operator info + * @param proof The merkle proof as bytes + * @return verified Whether the proof is valid + */ + function _verifyOperatorInfoMerkleProof( + bytes32 operatorSetKey, + uint32 referenceTimestamp, + uint32 operatorIndex, + BN254OperatorInfo memory operatorInfo, + bytes memory proof + ) internal view returns (bool verified) { + bytes32 leaf = keccak256(abi.encode(operatorInfo)); + bytes32 root = _operatorSetInfos[operatorSetKey][referenceTimestamp].operatorInfoTreeRoot; + return proof.verifyInclusionKeccak(root, leaf, operatorIndex); + } + + /** + * @notice Get cached operator info + * @param operatorSet The operator set + * @param referenceTimestamp The reference timestamp + * @param operatorIndex The operator index + * @return The cached operator info + */ + function getOperatorInfo( + OperatorSet memory operatorSet, + uint32 referenceTimestamp, + uint256 operatorIndex + ) external view returns (BN254OperatorInfo memory) { + bytes32 operatorSetKey = operatorSet.key(); + return _operatorInfos[operatorSetKey][referenceTimestamp][operatorIndex]; + } + + /** + * @notice Get operator set info for a timestamp + * @param operatorSet The operator set + * @param referenceTimestamp The reference timestamp + * @return The operator set info + */ + function getOperatorSetInfo( + OperatorSet memory operatorSet, + uint32 referenceTimestamp + ) external view returns (BN254OperatorSetInfo memory) { + bytes32 operatorSetKey = operatorSet.key(); + return _operatorSetInfos[operatorSetKey][referenceTimestamp]; + } + + /** + * @notice Get the current operator table updater address + * @return The operator table updater address + */ + function getOperatorTableUpdater() external view returns (address) { + return _operatorTableUpdater; + } +} diff --git a/src/contracts/multichain/BN254CertificateVerifierStorage.sol b/src/contracts/multichain/BN254CertificateVerifierStorage.sol new file mode 100644 index 0000000000..baf2c45446 --- /dev/null +++ b/src/contracts/multichain/BN254CertificateVerifierStorage.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import {OperatorSet} from "../libraries/OperatorSetLib.sol"; +import {IBN254TableCalculator, IBN254TableCalculatorTypes} from "../interfaces/IBN254TableCalculator.sol"; +import {IBN254CertificateVerifier, IBN254CertificateVerifierTypes} from "../interfaces/IBN254CertificateVerifier.sol"; +import {IBaseCertificateVerifier} from "../interfaces/IBaseCertificateVerifier.sol"; + +abstract contract BN254CertificateVerifierStorage is IBN254CertificateVerifier { + // Constants + + /// @dev Gas limit for pairing operations to prevent DoS attacks + uint256 internal constant PAIRING_EQUALITY_CHECK_GAS = 400_000; + + /// @dev Basis point unit denominator for division + uint256 internal constant BPS_DENOMINATOR = 10_000; + + // Immutables - None in this case, but could be added if needed + + // Mutatables + + /// @dev The address that can update operator tables + address immutable _operatorTableUpdater; + + /// @dev Mapping from operatorSet key to owner address + mapping(bytes32 => address) internal _operatorSetOwners; + + /// @dev Mapping from operatorSet key to maximum staleness period + mapping(bytes32 => uint32) internal _maxStalenessPeriods; + + /// @dev Mapping from operatorSet key to latest reference timestamp + mapping(bytes32 => uint32) internal _latestReferenceTimestamps; + + /// @dev Mapping from operatorSet key to reference timestamp to operator set info + mapping(bytes32 => mapping(uint32 => IBN254TableCalculatorTypes.BN254OperatorSetInfo)) internal _operatorSetInfos; + + /// @dev Mapping from operatorSet key to reference timestamp to operator index to operator info + /// This is used to cache operator info that has been proven against a tree root + mapping(bytes32 => mapping(uint32 => mapping(uint256 => IBN254TableCalculatorTypes.BN254OperatorInfo))) internal + _operatorInfos; + + // Construction + + constructor( + address __operatorTableUpdater + ) { + _operatorTableUpdater = __operatorTableUpdater; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[45] private __gap; +} diff --git a/src/test/unit/BN254CertificateVerifierUnit.t.sol b/src/test/unit/BN254CertificateVerifierUnit.t.sol new file mode 100644 index 0000000000..25020beec3 --- /dev/null +++ b/src/test/unit/BN254CertificateVerifierUnit.t.sol @@ -0,0 +1,759 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import {BN254} from "../../contracts/libraries/BN254.sol"; +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {BN254CertificateVerifier} from "../../contracts/multichain/BN254CertificateVerifier.sol"; +import {IBN254CertificateVerifier, IBN254CertificateVerifierTypes} from "../../contracts/interfaces/IBN254CertificateVerifier.sol"; +import {IBN254TableCalculatorTypes} from "../../contracts/interfaces/IBN254TableCalculator.sol"; +import {ICrossChainRegistryTypes} from "../../contracts/interfaces/ICrossChainRegistry.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {OperatorSet} from "../../contracts/libraries/OperatorSetLib.sol"; +import {Merkle} from "../../contracts/libraries/Merkle.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract BN254CertificateVerifierTest is Test { + using BN254 for BN254.G1Point; + + // Contract being tested + BN254CertificateVerifier verifier; + + // Test accounts + address owner = address(0x1); + address tableUpdater = address(0x2); + address nonOwner = address(0x3); + address operatorSetOwner = address(0x4); + + // Test data + uint32 numOperators = 4; + uint32 maxStaleness = 3600; // 1 hour max staleness + + // Create an OperatorSet for testing + OperatorSet testOperatorSet; + + // BLS signature specific fields + bytes32 msgHash; + uint aggSignerPrivKey = 69; + BN254.G2Point aggSignerApkG2; // G2 public key corresponding to aggSignerPrivKey + + // Events + event TableUpdated( + OperatorSet indexed operatorSet, uint32 referenceTimestamp, IBN254TableCalculatorTypes.BN254OperatorSetInfo operatorSetInfo + ); + + function setUp() public virtual { + vm.warp(1_000_000); // Set block timestamp + + testOperatorSet.avs = address(0x5); + testOperatorSet.id = 1; + + // Deploy implementation + BN254CertificateVerifier implementation = new BN254CertificateVerifier(tableUpdater); + + // Deploy proxy and initialize + ERC1967Proxy proxy = + new ERC1967Proxy(address(implementation), abi.encodeWithSelector(BN254CertificateVerifier.initialize.selector, owner)); + + verifier = BN254CertificateVerifier(address(proxy)); + + // Set standard test message hash + msgHash = keccak256(abi.encodePacked("test message")); + + // Set up the aggregate public key in G2 + aggSignerApkG2.X[1] = 19_101_821_850_089_705_274_637_533_855_249_918_363_070_101_489_527_618_151_493_230_256_975_900_223_847; + aggSignerApkG2.X[0] = 5_334_410_886_741_819_556_325_359_147_377_682_006_012_228_123_419_628_681_352_847_439_302_316_235_957; + aggSignerApkG2.Y[1] = 354_176_189_041_917_478_648_604_979_334_478_067_325_821_134_838_555_150_300_539_079_146_482_658_331; + aggSignerApkG2.Y[0] = 4_185_483_097_059_047_421_902_184_823_581_361_466_320_657_066_600_218_863_748_375_739_772_335_928_910; + } + + // Generate signer and non-signer private keys + function generateSignerAndNonSignerPrivateKeys(uint pseudoRandomNumber, uint numSigners, uint numNonSigners) + internal + view + returns (uint[] memory, uint[] memory) + { + uint[] memory signerPrivKeys = new uint[](numSigners); + uint sum = 0; + + // Generate numSigners-1 random keys + for (uint i = 0; i < numSigners - 1; i++) { + signerPrivKeys[i] = uint(keccak256(abi.encodePacked("signerPrivateKey", pseudoRandomNumber, i))) % BN254.FR_MODULUS; + sum = addmod(sum, signerPrivKeys[i], BN254.FR_MODULUS); + } + + // Last key makes the total sum equal to aggSignerPrivKey + signerPrivKeys[numSigners - 1] = addmod(aggSignerPrivKey, BN254.FR_MODULUS - sum % BN254.FR_MODULUS, BN254.FR_MODULUS); + + // Generate non-signer keys + uint[] memory nonSignerPrivKeys = new uint[](numNonSigners); + for (uint i = 0; i < numNonSigners; i++) { + nonSignerPrivKeys[i] = uint(keccak256(abi.encodePacked("nonSignerPrivateKey", pseudoRandomNumber, i))) % BN254.FR_MODULUS; + } + + // Sort nonSignerPrivateKeys in order of ascending pubkeyHash + for (uint i = 1; i < nonSignerPrivKeys.length; i++) { + uint privateKey = nonSignerPrivKeys[i]; + bytes32 pubkeyHash = toPubkeyHash(privateKey); + uint j = i; + + // Move elements that are greater than the current key ahead + while (j > 0 && toPubkeyHash(nonSignerPrivKeys[j - 1]) > pubkeyHash) { + nonSignerPrivKeys[j] = nonSignerPrivKeys[j - 1]; + j--; + } + nonSignerPrivKeys[j] = privateKey; + } + + return (signerPrivKeys, nonSignerPrivKeys); + } + + // Helper to hash a public key + function toPubkeyHash(uint privKey) internal view returns (bytes32) { + return BN254.generatorG1().scalar_mul(privKey).hashG1Point(); + } + + // Create operators with split keys + function createOperatorsWithSplitKeys(uint pseudoRandomNumber, uint numSigners, uint numNonSigners) + internal + view + returns (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory, uint32[] memory, BN254.G1Point memory) + { + require(numSigners + numNonSigners == numOperators, "Total operators mismatch"); + + // Generate private keys + (uint[] memory signerPrivKeys, uint[] memory nonSignerPrivKeys) = + generateSignerAndNonSignerPrivateKeys(pseudoRandomNumber, numSigners, numNonSigners); + + // Create all operators + IBN254TableCalculatorTypes.BN254OperatorInfo[] memory ops = new IBN254TableCalculatorTypes.BN254OperatorInfo[](numOperators); + + // Track indices of non-signers + uint32[] memory nonSignerIndices = new uint32[](numNonSigners); + + // Create signers first + for (uint32 i = 0; i < numSigners; i++) { + ops[i].pubkey = BN254.generatorG1().scalar_mul(signerPrivKeys[i]); + ops[i].weights = new uint[](2); + ops[i].weights[0] = uint(100 + i * 10); + ops[i].weights[1] = uint(200 + i * 20); + } + + // Create non-signers + for (uint32 i = 0; i < numNonSigners; i++) { + uint32 idx = uint32(numSigners + i); + ops[idx].pubkey = BN254.generatorG1().scalar_mul(nonSignerPrivKeys[i]); + ops[idx].weights = new uint[](2); + ops[idx].weights[0] = uint(100 + idx * 10); + ops[idx].weights[1] = uint(200 + idx * 20); + nonSignerIndices[i] = idx; + } + + // Calculate aggregate signature for the signers + BN254.G1Point memory signature = BN254.hashToG1(msgHash).scalar_mul(aggSignerPrivKey); + + return (ops, nonSignerIndices, signature); + } + + // Build a complete and correct merkle tree from operator infos + function getMerkleRoot(IBN254TableCalculatorTypes.BN254OperatorInfo[] memory ops) internal returns (bytes32 root) { + bytes32[] memory leaves = new bytes32[](ops.length); + for (uint i = 0; i < ops.length; i++) { + leaves[i] = keccak256(abi.encode(ops[i])); + } + root = Merkle.merkleizeKeccak(leaves); + } + + function getMerkleProof(IBN254TableCalculatorTypes.BN254OperatorInfo[] memory ops, uint32 operatorIndex) + internal + returns (bytes memory proof) + { + bytes32[] memory leaves = new bytes32[](ops.length); + for (uint i = 0; i < ops.length; i++) { + leaves[i] = keccak256(abi.encode(ops[i])); + } + proof = Merkle.getProofKeccak(leaves, operatorIndex); + } + + // Create operator set info + function createOperatorSetInfo(IBN254TableCalculatorTypes.BN254OperatorInfo[] memory ops) + internal + returns (IBN254TableCalculatorTypes.BN254OperatorSetInfo memory) + { + uint32 _numOperators = uint32(ops.length); + bytes32 operatorInfoTreeRoot = getMerkleRoot(ops); + + // Create aggregate public key (sum of all operator pubkeys) + BN254.G1Point memory aggregatePubkey = BN254.G1Point(0, 0); + + // Create total weights (sum of all operator weights) + uint[] memory _totalWeights = new uint[](2); + + for (uint32 i = 0; i < _numOperators; i++) { + // Add pubkey to aggregate + aggregatePubkey = aggregatePubkey.plus(ops[i].pubkey); + + // Add weights to total + for (uint j = 0; j < 2; j++) { + _totalWeights[j] += ops[i].weights[j]; + } + } + + // Create the operator set info + return IBN254TableCalculatorTypes.BN254OperatorSetInfo({ + numOperators: _numOperators, + aggregatePubkey: aggregatePubkey, + totalWeights: _totalWeights, + operatorInfoTreeRoot: operatorInfoTreeRoot + }); + } + + // Helper to create a certificate with real BLS signature + function createCertificate( + uint32 referenceTimestamp, + bytes32 messageHash, + uint32[] memory nonSignerIndices, + IBN254TableCalculatorTypes.BN254OperatorInfo[] memory ops, + BN254.G1Point memory signature + ) internal returns (IBN254CertificateVerifierTypes.BN254Certificate memory) { + // Create witnesses for non-signers + IBN254CertificateVerifierTypes.BN254OperatorInfoWitness[] memory witnesses = + new IBN254CertificateVerifierTypes.BN254OperatorInfoWitness[](nonSignerIndices.length); + + for (uint i = 0; i < nonSignerIndices.length; i++) { + uint32 nonSignerIndex = nonSignerIndices[i]; + bytes memory proof = getMerkleProof(ops, nonSignerIndex); + + witnesses[i] = IBN254CertificateVerifierTypes.BN254OperatorInfoWitness({ + operatorIndex: nonSignerIndex, + operatorInfoProof: proof, + operatorInfo: ops[nonSignerIndex] + }); + } + + return IBN254CertificateVerifierTypes.BN254Certificate({ + referenceTimestamp: referenceTimestamp, + messageHash: messageHash, + signature: signature, + apk: aggSignerApkG2, + nonSignerWitnesses: witnesses + }); + } + + // Helper to create operator set config + function createOperatorSetConfig() internal view returns (ICrossChainRegistryTypes.OperatorSetConfig memory) { + return ICrossChainRegistryTypes.OperatorSetConfig({owner: operatorSetOwner, maxStalenessPeriod: maxStaleness}); + } + + // Test updating the operator table + function testUpdateOperatorTable() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators,,) = createOperatorsWithSplitKeys(123, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.startPrank(tableUpdater); + + // Update the operator table + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + vm.stopPrank(); + + // Verify storage updates + assertEq(verifier.latestReferenceTimestamp(testOperatorSet), referenceTimestamp, "Reference timestamp not updated correctly"); + assertEq(verifier.getOperatorSetOwner(testOperatorSet), operatorSetOwner, "Operator set owner not stored correctly"); + assertEq(verifier.maxOperatorTableStaleness(testOperatorSet), maxStaleness, "Max staleness not stored correctly"); + + // Verify operator set info was stored correctly + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory storedOperatorSetInfo = + verifier.getOperatorSetInfo(testOperatorSet, referenceTimestamp); + + assertEq(storedOperatorSetInfo.numOperators, operatorSetInfo.numOperators, "Num operators not stored correctly"); + assertEq(storedOperatorSetInfo.aggregatePubkey.X, operatorSetInfo.aggregatePubkey.X, "Aggregate pubkey X not stored correctly"); + assertEq(storedOperatorSetInfo.aggregatePubkey.Y, operatorSetInfo.aggregatePubkey.Y, "Aggregate pubkey Y not stored correctly"); + assertEq(storedOperatorSetInfo.operatorInfoTreeRoot, operatorSetInfo.operatorInfoTreeRoot, "Tree root not stored correctly"); + } + + // Test verifyCertificate with actual BLS signature validation and multiple signers + function testVerifyCertificate() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 123; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + uint[] memory signedStakes = verifier.verifyCertificate(testOperatorSet, cert); + + // Check that the signed stakes are correct + assertEq(signedStakes.length, 2, "Wrong number of stake types"); + + // Calculate expected signed stakes + uint[] memory expectedSignedStakes = new uint[](2); + + // Start with total stakes + expectedSignedStakes[0] = operatorSetInfo.totalWeights[0]; + expectedSignedStakes[1] = operatorSetInfo.totalWeights[1]; + + // Subtract non-signer stakes + for (uint i = 0; i < nonSignerIndices.length; i++) { + uint32 nonSignerIndex = nonSignerIndices[i]; + expectedSignedStakes[0] -= operators[nonSignerIndex].weights[0]; + expectedSignedStakes[1] -= operators[nonSignerIndex].weights[1]; + } + + assertEq(signedStakes[0], expectedSignedStakes[0], "Wrong signed stake for type 0"); + assertEq(signedStakes[1], expectedSignedStakes[1], "Wrong signed stake for type 1"); + } + + // Test verifyCertificate with a different distribution of signers/non-signers + function testVerifyCertificateDifferentSplit() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 2 signers, 2 non-signers + uint pseudoRandomNumber = 456; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 2, 2); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + // No mocking needed - verify certificate with real verification + uint[] memory signedStakes = verifier.verifyCertificate(testOperatorSet, cert); + + // Calculate expected signed stakes + uint[] memory expectedSignedStakes = new uint[](2); + + // Start with total stakes + expectedSignedStakes[0] = operatorSetInfo.totalWeights[0]; + expectedSignedStakes[1] = operatorSetInfo.totalWeights[1]; + + // Subtract non-signer stakes + for (uint i = 0; i < nonSignerIndices.length; i++) { + uint32 nonSignerIndex = nonSignerIndices[i]; + expectedSignedStakes[0] -= operators[nonSignerIndex].weights[0]; + expectedSignedStakes[1] -= operators[nonSignerIndex].weights[1]; + } + + assertEq(signedStakes[0], expectedSignedStakes[0], "Wrong signed stake for type 0"); + assertEq(signedStakes[1], expectedSignedStakes[1], "Wrong signed stake for type 1"); + } + + // Test verifyCertificateProportion + function testVerifyCertificateProportion() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 789; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + // Set thresholds at 60% of total stake for each type + uint16[] memory thresholds = new uint16[](2); + thresholds[0] = 6000; // 60% + thresholds[1] = 6000; // 60% + + // Verify certificate meets thresholds - no mocking needed + bool meetsThresholds = verifier.verifyCertificateProportion(testOperatorSet, cert, thresholds); + + // With 3 signers out of 4, should meet 60% threshold + assertTrue(meetsThresholds, "Certificate should meet thresholds"); + + // Try with higher threshold that shouldn't be met + thresholds[0] = 9000; // 90% + thresholds[1] = 9000; // 90% + + meetsThresholds = verifier.verifyCertificateProportion(testOperatorSet, cert, thresholds); + + // Calculate percentage of signed stakes to determine if it should meet threshold + uint[] memory signedStakes = verifier.verifyCertificate(testOperatorSet, cert); + uint signedPercentage0 = (signedStakes[0] * 10_000) / operatorSetInfo.totalWeights[0]; + uint signedPercentage1 = (signedStakes[1] * 10_000) / operatorSetInfo.totalWeights[1]; + + bool shouldMeetThreshold = (signedPercentage0 >= 9000) && (signedPercentage1 >= 9000); + assertEq(meetsThresholds, shouldMeetThreshold, "Certificate threshold check incorrect"); + } + + // Test with invalid signature (wrong message hash) + function testVerifyCertificateInvalidSignature() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 123; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature but WRONG message hash + bytes32 wrongHash = keccak256("wrong message"); + + IBN254CertificateVerifierTypes.BN254Certificate memory cert = createCertificate( + referenceTimestamp, + wrongHash, // Use wrong hash here + nonSignerIndices, + operators, + signature // Signature is for original msgHash, not wrongHash + ); + + // Verification should fail without mocking + vm.expectRevert(abi.encodeWithSignature("VerificationFailed()")); + verifier.verifyCertificate(testOperatorSet, cert); + } + + // Test certificate with stale timestamp (should fail) + function testVerifyCertificateStaleTimestamp() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 123; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + // Jump forward in time beyond the max staleness + vm.warp(block.timestamp + maxStaleness + 1); + + // Verification should fail due to staleness + vm.expectRevert(abi.encodeWithSignature("CertificateStale()")); + verifier.verifyCertificate(testOperatorSet, cert); + } + + // Test verifyCertificateNominal functionality + function testVerifyCertificateNominal() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 567; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + // Get the signed stakes first + uint[] memory signedStakes = verifier.verifyCertificate(testOperatorSet, cert); + + // Test with thresholds lower than signed stakes (should pass) + uint[] memory passThresholds = new uint[](2); + passThresholds[0] = signedStakes[0] - 10; + passThresholds[1] = signedStakes[1] - 10; + + bool meetsThresholds = verifier.verifyCertificateNominal(testOperatorSet, cert, passThresholds); + assertTrue(meetsThresholds, "Certificate should meet nominal thresholds"); + + // Test with thresholds higher than signed stakes (should fail) + uint[] memory failThresholds = new uint[](2); + failThresholds[0] = signedStakes[0] + 10; + failThresholds[1] = signedStakes[1] + 10; + + meetsThresholds = verifier.verifyCertificateNominal(testOperatorSet, cert, failThresholds); + assertFalse(meetsThresholds, "Certificate should not meet impossible nominal thresholds"); + } + + // Test with no non-signers (all operators sign) + function testVerifyCertificateAllSigners() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with all signers + uint pseudoRandomNumber = 999; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators,, BN254.G1Point memory signature) = + createOperatorsWithSplitKeys(pseudoRandomNumber, numOperators, 0); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with no non-signers + IBN254CertificateVerifierTypes.BN254Certificate memory cert = IBN254CertificateVerifierTypes.BN254Certificate({ + referenceTimestamp: referenceTimestamp, + messageHash: msgHash, + signature: signature, + apk: aggSignerApkG2, + nonSignerWitnesses: new IBN254CertificateVerifierTypes.BN254OperatorInfoWitness[](0) + }); + + // Verify certificate + uint[] memory signedStakes = verifier.verifyCertificate(testOperatorSet, cert); + + // All stakes should be signed + assertEq(signedStakes[0], operatorSetInfo.totalWeights[0], "All stake should be signed for type 0"); + assertEq(signedStakes[1], operatorSetInfo.totalWeights[1], "All stake should be signed for type 1"); + } + + // Test with invalid operator tree root + function testVerifyCertificateInvalidOperatorTreeRoot() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 123; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info with CORRECT tree root + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + // Modify the tree root to be invalid AFTER creating the info + operatorSetInfo.operatorInfoTreeRoot = keccak256("invalid root"); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature but proofs won't match the invalid root + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + // Verification should fail due to invalid merkle proofs + vm.expectRevert(abi.encodeWithSignature("VerificationFailed()")); + verifier.verifyCertificate(testOperatorSet, cert); + } + + // Test with invalid reference timestamp + function testInvalidReferenceTimestamp() public { + uint32 existingReferenceTimestamp = uint32(block.timestamp); + uint32 nonExistentTimestamp = existingReferenceTimestamp + 1000; + + // Create operators with split keys - 3 signers, 1 non-signer + uint pseudoRandomNumber = 123; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 3, 1); + + // Create operator set info + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + + // Create operator set config + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, existingReferenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate using a non-existent timestamp + IBN254CertificateVerifierTypes.BN254Certificate memory cert = IBN254CertificateVerifierTypes.BN254Certificate({ + referenceTimestamp: nonExistentTimestamp, + messageHash: msgHash, + signature: signature, + apk: aggSignerApkG2, + nonSignerWitnesses: new IBN254CertificateVerifierTypes.BN254OperatorInfoWitness[](0) + }); + + // Verification should fail due to timestamp not existing + vm.expectRevert(abi.encodeWithSignature("ReferenceTimestampDoesNotExist()")); + verifier.verifyCertificate(testOperatorSet, cert); + } + + // Test multiple operator sets + function testMultipleOperatorSets() public { + // Create two different operator sets + OperatorSet memory operatorSet1 = OperatorSet({avs: address(0x10), id: 1}); + OperatorSet memory operatorSet2 = OperatorSet({avs: address(0x20), id: 2}); + + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators for each set + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators1,,) = createOperatorsWithSplitKeys(111, 3, 1); + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators2,,) = createOperatorsWithSplitKeys(222, 2, 2); + + // Create operator set infos + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo1 = createOperatorSetInfo(operators1); + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo2 = createOperatorSetInfo(operators2); + + // Create operator set configs with different owners + ICrossChainRegistryTypes.OperatorSetConfig memory config1 = ICrossChainRegistryTypes.OperatorSetConfig({ + owner: address(0x100), + maxStalenessPeriod: 1800 // 30 minutes + }); + ICrossChainRegistryTypes.OperatorSetConfig memory config2 = ICrossChainRegistryTypes.OperatorSetConfig({ + owner: address(0x200), + maxStalenessPeriod: 7200 // 2 hours + }); + + vm.startPrank(tableUpdater); + + // Update both operator tables + verifier.updateOperatorTable(operatorSet1, referenceTimestamp, operatorSetInfo1, config1); + verifier.updateOperatorTable(operatorSet2, referenceTimestamp, operatorSetInfo2, config2); + + vm.stopPrank(); + + // Verify that both operator sets are stored correctly and independently + assertEq(verifier.getOperatorSetOwner(operatorSet1), address(0x100), "OperatorSet1 owner incorrect"); + assertEq(verifier.getOperatorSetOwner(operatorSet2), address(0x200), "OperatorSet2 owner incorrect"); + + assertEq(verifier.maxOperatorTableStaleness(operatorSet1), 1800, "OperatorSet1 staleness incorrect"); + assertEq(verifier.maxOperatorTableStaleness(operatorSet2), 7200, "OperatorSet2 staleness incorrect"); + + assertEq(verifier.latestReferenceTimestamp(operatorSet1), referenceTimestamp, "OperatorSet1 timestamp incorrect"); + assertEq(verifier.latestReferenceTimestamp(operatorSet2), referenceTimestamp, "OperatorSet2 timestamp incorrect"); + + // Verify operator set infos are stored independently + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory storedInfo1 = verifier.getOperatorSetInfo(operatorSet1, referenceTimestamp); + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory storedInfo2 = verifier.getOperatorSetInfo(operatorSet2, referenceTimestamp); + + assertEq(storedInfo1.numOperators, 4, "OperatorSet1 should have 4 operators"); + assertEq(storedInfo2.numOperators, 4, "OperatorSet2 should have 4 operators"); + + // Verify they have different tree roots (since operators are different) + assertTrue(storedInfo1.operatorInfoTreeRoot != storedInfo2.operatorInfoTreeRoot, "Tree roots should be different"); + } + + // Test access control for operator table updates + function testOperatorTableUpdateAccessControl() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators,,) = createOperatorsWithSplitKeys(123, 3, 1); + + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + // Try to update with non-authorized account + vm.prank(nonOwner); + vm.expectRevert(abi.encodeWithSignature("OnlyTableUpdater()")); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Should succeed with authorized account + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + } + + // Test stale table update (timestamp not increasing) + function testStaleTableUpdate() public { + uint32 firstTimestamp = uint32(block.timestamp); + uint32 staleTimestamp = firstTimestamp - 100; // Earlier timestamp + + // Create operators + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators,,) = createOperatorsWithSplitKeys(123, 3, 1); + + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.startPrank(tableUpdater); + + // First update should succeed + verifier.updateOperatorTable(testOperatorSet, firstTimestamp, operatorSetInfo, operatorSetConfig); + + // Second update with earlier timestamp should fail + vm.expectRevert(abi.encodeWithSignature("TableUpdateStale()")); + verifier.updateOperatorTable(testOperatorSet, staleTimestamp, operatorSetInfo, operatorSetConfig); + + vm.stopPrank(); + } + + // Test operator info caching + function testOperatorInfoCaching() public { + uint32 referenceTimestamp = uint32(block.timestamp); + + // Create operators with split keys - 2 signers, 2 non-signers + uint pseudoRandomNumber = 456; + (IBN254TableCalculatorTypes.BN254OperatorInfo[] memory operators, uint32[] memory nonSignerIndices, BN254.G1Point memory signature) + = createOperatorsWithSplitKeys(pseudoRandomNumber, 2, 2); + + IBN254TableCalculatorTypes.BN254OperatorSetInfo memory operatorSetInfo = createOperatorSetInfo(operators); + ICrossChainRegistryTypes.OperatorSetConfig memory operatorSetConfig = createOperatorSetConfig(); + + vm.prank(tableUpdater); + verifier.updateOperatorTable(testOperatorSet, referenceTimestamp, operatorSetInfo, operatorSetConfig); + + // Create certificate with real BLS signature + IBN254CertificateVerifierTypes.BN254Certificate memory cert = + createCertificate(referenceTimestamp, msgHash, nonSignerIndices, operators, signature); + + // First verification should cache the operator infos + verifier.verifyCertificate(testOperatorSet, cert); + + // Check that operator infos are now cached + for (uint i = 0; i < nonSignerIndices.length; i++) { + uint32 nonSignerIndex = nonSignerIndices[i]; + IBN254TableCalculatorTypes.BN254OperatorInfo memory cachedInfo = + verifier.getOperatorInfo(testOperatorSet, referenceTimestamp, nonSignerIndex); + + // Cached info should match original + assertEq(cachedInfo.pubkey.X, operators[nonSignerIndex].pubkey.X, "Cached pubkey X mismatch"); + assertEq(cachedInfo.pubkey.Y, operators[nonSignerIndex].pubkey.Y, "Cached pubkey Y mismatch"); + assertEq(cachedInfo.weights[0], operators[nonSignerIndex].weights[0], "Cached weight 0 mismatch"); + assertEq(cachedInfo.weights[1], operators[nonSignerIndex].weights[1], "Cached weight 1 mismatch"); + } + + // Second verification should use cached data (should still work) + verifier.verifyCertificate(testOperatorSet, cert); + } +}