Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/contracts/multichain/BN254CertificateVerifier.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "../libraries/BN254.sol";
import "../libraries/BN254SignatureVerifier.sol";
import "../libraries/Merkle.sol";
import "../libraries/OperatorSetLib.sol";
import "../mixins/SemVerMixin.sol";
import "../mixins/LeafCalculatorMixin.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,
BN254CertificateVerifierStorage,
SemVerMixin,
LeafCalculatorMixin
{
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[] totalSignedStakeWeights;
BN254.G1Point nonSignerApk;
}
/**
* @notice Restricts access to the operator table updater
*/
modifier onlyTableUpdater() {
require(msg.sender == address(operatorTableUpdater), OnlyTableUpdater());
_;
}
/**
* @notice Constructor for the certificate verifier
* @dev Disables initializers to prevent implementation initialization
* @param _operatorTableUpdater Address authorized to update operator tables
* @param _version The semantic version of the contract
*/
constructor(
IOperatorTableUpdater _operatorTableUpdater,
string memory _version
) BN254CertificateVerifierStorage(_operatorTableUpdater) SemVerMixin(_version) {
_disableInitializers();
}
/**
*
* EXTERNAL FUNCTIONS
*
*/
///@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;
_referenceTimestampsSet[operatorSetKey][referenceTimestamp] = true;
emit TableUpdated(operatorSet, referenceTimestamp, operatorSetInfo);
}
///@inheritdoc IBN254CertificateVerifier
function verifyCertificate(
OperatorSet memory operatorSet,
BN254Certificate memory cert
) external returns (uint256[] memory totalSignedStakeWeights) {
return _verifyCertificate(operatorSet, cert);
}
///@inheritdoc IBN254CertificateVerifier
function verifyCertificateProportion(
OperatorSet memory operatorSet,
BN254Certificate memory cert,
uint16[] memory totalStakeProportionThresholds
) external returns (bool) {
uint256[] memory totalSignedStakeWeights = _verifyCertificate(operatorSet, cert);
bytes32 operatorSetKey = operatorSet.key();
BN254OperatorSetInfo memory operatorSetInfo = _operatorSetInfos[operatorSetKey][cert.referenceTimestamp];
uint256[] memory totalStakes = operatorSetInfo.totalWeights;
require(totalSignedStakeWeights.length == totalStakeProportionThresholds.length, ArrayLengthMismatch());
for (uint256 i = 0; i < totalSignedStakeWeights.length; i++) {
// Calculate threshold as proportion of total signed stake weight
// totalStakeProportionThresholds is in basis points (e.g. 6600 = 66%)
uint256 threshold = (totalStakes[i] * totalStakeProportionThresholds[i]) / BPS_DENOMINATOR;
if (totalSignedStakeWeights[i] < threshold) {
return false;
}
}
return true;
}
///@inheritdoc IBN254CertificateVerifier
function verifyCertificateNominal(
OperatorSet memory operatorSet,
BN254Certificate memory cert,
uint256[] memory totalStakeNominalThresholds
) external returns (bool) {
uint256[] memory totalSignedStakeWeights = _verifyCertificate(operatorSet, cert);
require(totalSignedStakeWeights.length == totalStakeNominalThresholds.length, ArrayLengthMismatch());
for (uint256 i = 0; i < totalSignedStakeWeights.length; i++) {
if (totalSignedStakeWeights[i] < totalStakeNominalThresholds[i]) {
return false;
}
}
return true;
}
/**
*
* INTERNAL FUNCTIONS
*
*/
/**
* @notice Internal function to verify a certificate
* @param operatorSet The operator set the certificate is for
* @param cert The certificate to verify
* @return totalSignedStakeWeights The amount of stake that signed the certificate for each stake type
*/
function _verifyCertificate(
OperatorSet memory operatorSet,
BN254Certificate memory cert
) internal returns (uint256[] memory totalSignedStakeWeights) {
VerificationContext memory ctx;
ctx.operatorSetKey = operatorSet.key();
_validateCertificateTimestamp(ctx.operatorSetKey, cert.referenceTimestamp);
ctx.operatorSetInfo = _operatorSetInfos[ctx.operatorSetKey][cert.referenceTimestamp];
// Initialize signed stakes with total stake weights
ctx.totalSignedStakeWeights = new uint256[](ctx.operatorSetInfo.totalWeights.length);
for (uint256 i = 0; i < ctx.operatorSetInfo.totalWeights.length; i++) {
ctx.totalSignedStakeWeights[i] = ctx.operatorSetInfo.totalWeights[i];
}
ctx.nonSignerApk = _processNonSigners(ctx, cert);
_verifySignature(ctx, cert);
return ctx.totalSignedStakeWeights;
}
/**
* @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 {
// Assert that the certificate is not stale
uint32 maxStaleness = _maxStalenessPeriods[operatorSetKey];
require(maxStaleness == 0 || block.timestamp <= referenceTimestamp + maxStaleness, CertificateStale());
// Assert that the reference timestamp has been set
require(_referenceTimestampsSet[operatorSetKey][referenceTimestamp], ReferenceTimestampDoesNotExist());
// Assert that the root that corresponds to the reference timestamp is not disabled
require(operatorTableUpdater.isRootValidByTimestamp(referenceTimestamp), RootDisabled());
}
/**
* @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);
uint32 previousOperatorIndex = 0;
for (uint256 i = 0; i < cert.nonSignerWitnesses.length; i++) {
BN254OperatorInfoWitness memory witness = cert.nonSignerWitnesses[i];
if (i > 0) {
// Enforce strictly increasing order of non-signer operator indices
require(witness.operatorIndex > previousOperatorIndex, NonSignerIndicesNotSorted());
}
require(witness.operatorIndex < ctx.operatorSetInfo.numOperators, InvalidOperatorIndex());
BN254OperatorInfo memory operatorInfo =
_getOrCacheNonsignerOperatorInfo(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.totalSignedStakeWeights.length) {
ctx.totalSignedStakeWeights[j] -= operatorInfo.weights[j];
}
}
previousOperatorIndex = witness.operatorIndex;
}
}
/**
* @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 _getOrCacheNonsignerOperatorInfo(
bytes32 operatorSetKey,
uint32 referenceTimestamp,
BN254OperatorInfoWitness memory witness
) internal returns (BN254OperatorInfo memory operatorInfo) {
BN254OperatorInfo memory 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());
// Compute EIP-712 digest that binds referenceTimestamp and messageHash
bytes32 signableDigest = calculateCertificateDigest(cert.referenceTimestamp, cert.messageHash);
(bool pairingSuccessful, bool signatureValid) =
trySignatureVerification(signableDigest, 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 = calculateOperatorInfoLeaf(operatorInfo);
bytes32 root = _operatorSetInfos[operatorSetKey][referenceTimestamp].operatorInfoTreeRoot;
return proof.verifyInclusionKeccak(root, leaf, operatorIndex);
}
/**
*
* VIEW FUNCTIONS
*
*/
///@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 IBaseCertificateVerifier
function isReferenceTimestampSet(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (bool) {
bytes32 operatorSetKey = operatorSet.key();
return _referenceTimestampsSet[operatorSetKey][referenceTimestamp];
}
///@inheritdoc IBaseCertificateVerifier
function getTotalStakeWeights(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (uint256[] memory) {
bytes32 operatorSetKey = operatorSet.key();
return _operatorSetInfos[operatorSetKey][referenceTimestamp].totalWeights;
}
/// @inheritdoc IBaseCertificateVerifier
function getOperatorCount(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (uint256) {
bytes32 operatorSetKey = operatorSet.key();
return _operatorSetInfos[operatorSetKey][referenceTimestamp].numOperators;
}
///@inheritdoc IBN254CertificateVerifier
function trySignatureVerification(
bytes32 msgHash,
BN254.G1Point memory aggPubkey,
BN254.G2Point memory apkG2,
BN254.G1Point memory signature
) public view returns (bool pairingSuccessful, bool signatureValid) {
return BN254SignatureVerifier.verifySignature(
msgHash,
signature,
aggPubkey,
apkG2,
true, // use gas limit
PAIRING_EQUALITY_CHECK_GAS
);
}
///@inheritdoc IBN254CertificateVerifier
function getNonsignerOperatorInfo(
OperatorSet memory operatorSet,
uint32 referenceTimestamp,
uint256 operatorIndex
) external view returns (BN254OperatorInfo memory) {
bytes32 operatorSetKey = operatorSet.key();
return _operatorInfos[operatorSetKey][referenceTimestamp][operatorIndex];
}
///@inheritdoc IBN254CertificateVerifier
function isNonsignerCached(
OperatorSet memory operatorSet,
uint32 referenceTimestamp,
uint256 operatorIndex
) external view returns (bool) {
bytes32 operatorSetKey = operatorSet.key();
BN254OperatorInfo memory operatorInfo = _operatorInfos[operatorSetKey][referenceTimestamp][operatorIndex];
// Check if operator info is cached using pubkey existence (weights can be 0)
return operatorInfo.pubkey.X != 0 && operatorInfo.pubkey.Y != 0;
}
///@inheritdoc IBN254CertificateVerifier
function getOperatorSetInfo(
OperatorSet memory operatorSet,
uint32 referenceTimestamp
) external view returns (BN254OperatorSetInfo memory) {
bytes32 operatorSetKey = operatorSet.key();
return _operatorSetInfos[operatorSetKey][referenceTimestamp];
}
/// @inheritdoc IBN254CertificateVerifier
function calculateCertificateDigest(uint32 referenceTimestamp, bytes32 messageHash) public pure returns (bytes32) {
return keccak256(abi.encode(BN254_CERTIFICATE_TYPEHASH, referenceTimestamp, messageHash));
}
}
"
},
"lib/openzeppelin-contracts-upgradeable-v4.9.0/contracts/proxy/utils/Initializable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.2;
import "../../utils/AddressUpgradeable.sol";
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
* reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
* case an upgrade adds a module that needs to be initialized.
*
* For example:
*
* [.hljs-theme-light.nopadding]
* ```solidity
* contract MyToken is ERC20Upgradeable {
* function initialize() initializer public {
* __ERC20_init("MyToken", "MTK");
* }
* }
*
* contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
* function initializeV2() reinitializer(2) public {
* __ERC20Permit_init("MyToken");
* }
* }
* ```
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*
* [CAUTION]
* ====
* Avoid leaving a contract uninitialized.
*
* An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
* contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
* the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
*
* [.hljs-theme-light.nopadding]
* ```
* /// @custom:oz-upgrades-unsafe-allow constructor
* constructor() {
* _disableInitializers();
* }
* ```
* ====
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
* @custom:oz-retyped-from bool
*/
uint8 private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint8 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
* constructor.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
bool isTopLevelCall = !_initializing;
require(
(isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1),
"Initializable: contract is already initialized"
);
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
emit Initialized(1);
}
}
/**
* @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
* contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
* used to initialize parent contracts.
*
* A reinitializer may be used after the original initialization step. This is essential to configure modules that
* are added through upgrades and that require initialization.
*
* When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
* cannot be nested. If one is invoked in the context of another, execution will revert.
*
* Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
* a contract, executing them in the right order is up to the developer or operator.
*
* WARNING: setting the version to 255 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint8 version) {
require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
_initialized = version;
_initializing = true;
_;
_initializing = false;
emit Initialized(version);
}
/**
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
* {initializer} and {reinitializer} modifiers, directly or indirectly.
*/
modifier onlyInitializing() {
require(_initializing, "Initializable: contract is not initializing");
_;
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
require(!_initializing, "Initializable: contract is initializing");
if (_initialized != type(uint8).max) {
_initialized = type(uint8).max;
emit Initialized(type(uint8).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint8) {
return _initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _initializing;
}
}
"
},
"src/contracts/libraries/BN254.sol": {
"content": "// SPDX-License-Identifier: MIT
// several functions are taken or adapted from https://github.com/HarryR/solcrypto/blob/master/contracts/altbn128.sol (MIT license):
// Copyright 2017 Christian Reitwiessner
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// The remainder of the code in this library is written by LayrLabs Inc. and is also under an MIT license
pragma solidity ^0.8.27;
/**
* @title Library for operations on the BN254 elliptic curve.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Contains BN254 parameters, common operations (addition, scalar mul, pairing), and BLS signature functionality.
*/
library BN254 {
// modulus for the underlying field F_p of the elliptic curve
uint256 internal constant FP_MODULUS =
21_888_242_871_839_275_222_246_405_745_257_275_088_696_311_157_297_823_662_689_037_894_645_226_208_583;
// modulus for the underlying field F_r of the elliptic curve
uint256 internal constant FR_MODULUS =
21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617;
struct G1Point {
uint256 X;
uint256 Y;
}
// Encoding of field elements is: X[1] * i + X[0]
struct G2Point {
uint256[2] X;
uint256[2] Y;
}
/// @dev Thrown when the sum of two points of G1 fails
error ECAddFailed();
/// @dev Thrown when the scalar multiplication of a point of G1 fails
error ECMulFailed();
/// @dev Thrown when the scalar is too large.
error ScalarTooLarge();
/// @dev Thrown when the pairing check fails
error ECPairingFailed();
/// @dev Thrown when the exponentiation mod fails
error ExpModFailed();
function generatorG1() internal pure returns (G1Point memory) {
return G1Point(1, 2);
}
// generator of group G2
/// @dev Generator point in F_q2 is of the form: (x0 + ix1, y0 + it1).
uint256 internal constant G2x1 =
11_559_732_032_986_387_107_991_004_021_392_285_783_925_812_861_821_192_530_917_403_151_452_391_805_634;
uint256 internal constant G2x0 =
10_857_046_999_023_057_135_944_570_762_232_829_481_370_756_359_578_518_086_990_519_993_285_655_852_781;
uint256 internal constant G2y1 =
4_082_367_875_863_433_681_332_203_403_145_435_568_316_851_327_593_401_208_105_741_076_214_120_093_531;
uint256 internal constant G2y0 =
8_495_653_923_123_431_417_604_973_247_489_272_438_418_190_587_263_600_148_770_280_649_306_958_101_930;
/// @notice returns the G2 generator
/// @dev mind the ordering of the 1s and 0s!
/// this is because of the (unknown to us) convention used in the bn254 pairing precompile contract
/// "Elements a * i + b of F_p^2 are encoded as two elements of F_p, (a, b)."
/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-197.md#encoding
function generatorG2() internal pure returns (G2Point memory) {
return G2Point([G2x1, G2x0], [G2y1, G2y0]);
}
// negation of the generator of group G2
/// @dev Generator point in F_q2 is of the form: (x0 + ix1, y0 + it1).
uint256 internal constant nG2x1 =
11_559_732_032_986_387_107_991_004_021_392_285_783_925_812_861_821_192_530_917_403_151_452_391_805_634;
uint256 internal constant nG2x0 =
10_857_046_999_023_057_135_944_570_762_232_829_481_370_756_359_578_518_086_990_519_993_285_655_852_781;
uint256 internal constant nG2y1 =
17_805_874_995_975_841_540_914_202_342_111_839_520_379_459_829_704_422_454_583_296_818_431_106_115_052;
uint256 internal constant nG2y0 =
13_392_588_948_715_843_804_641_432_497_768_002_650_278_120_570_034_223_513_918_757_245_338_268_106_653;
function negGeneratorG2() internal pure returns (G2Point memory) {
return G2Point([nG2x1, nG2x0], [nG2y1, nG2y0]);
}
bytes32 internal constant powersOfTauMerkleRoot = 0x22c998e49752bbb1918ba87d6d59dd0e83620a311ba91dd4b2cc84990b31b56f;
/**
* @param p Some point in G1.
* @return The negation of `p`, i.e. p.plus(p.negate()) should be zero.
*/
function negate(
G1Point memory p
) internal pure returns (G1Point memory) {
// The prime q in the base field F_q for G1
if (p.X == 0 && p.Y == 0) {
return G1Point(0, 0);
} else {
return G1Point(p.X, FP_MODULUS - (p.Y % FP_MODULUS));
}
}
/**
* @return r the sum of two points of G1
*/
function plus(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) {
uint256[4] memory input;
input[0] = p1.X;
input[1] = p1.Y;
input[2] = p2.X;
input[3] = p2.Y;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 6, input, 0x80, r, 0x40)
// Use "invalid" to make gas estimation work
switch success
case 0 { invalid() }
}
require(success, ECAddFailed());
}
/**
* @notice an optimized ecMul implementation that takes O(log_2(s)) ecAdds
* @param p the point to multiply
* @param s the scalar to multiply by
* @dev this function is only safe to use if the scalar is 9 bits or less
*/
function scalar_mul_tiny(BN254.G1Point memory p, uint16 s) internal view returns (BN254.G1Point memory) {
require(s < 2 ** 9, ScalarTooLarge());
// if s is 1 return p
if (s == 1) {
return p;
}
// the accumulated product to return
BN254.G1Point memory acc = BN254.G1Point(0, 0);
// the 2^n*p to add to the accumulated product in each iteration
BN254.G1Point memory p2n = p;
// value of most significant bit
uint16 m = 1;
// index of most significant bit
uint8 i = 0;
//loop until we reach the most significant bit
while (s >= m) {
unchecked {
// if the current bit is 1, add the 2^n*p to the accumulated product
if ((s >> i) & 1 == 1) {
acc = plus(acc, p2n);
}
// double the 2^n*p for the next iteration
p2n = plus(p2n, p2n);
// increment the index and double the value of the most significant bit
m <<= 1;
++i;
}
}
// return the accumulated product
return acc;
}
/**
* @return r the product of a point on G1 and a scalar, i.e.
* p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
* points p.
*/
function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) {
uint256[3] memory input;
input[0] = p.X;
input[1] = p.Y;
input[2] = s;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 7, input, 0x60, r, 0x40)
// Use "invalid" to make gas estimation work
switch success
case 0 { invalid() }
}
require(success, ECMulFailed());
}
/**
* @return The result of computing the pairing check
* e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1
* For example,
* pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
*/
function pairing(
G1Point memory a1,
G2Point memory a2,
G1Point memory b1,
G2Point memory b2
) internal view returns (bool) {
G1Point[2] memory p1 = [a1, b1];
G2Point[2] memory p2 = [a2, b2];
uint256[12] memory input;
for (uint256 i = 0; i < 2; i++) {
uint256 j = i * 6;
input[j + 0] = p1[i].X;
input[j + 1] = p1[i].Y;
input[j + 2] = p2[i].X[0];
input[j + 3] = p2[i].X[1];
input[j + 4] = p2[i].Y[0];
input[j + 5] = p2[i].Y[1];
}
uint256[1] memory out;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(sub(gas(), 2000), 8, input, mul(12, 0x20), out, 0x20)
// Use "invalid" to make gas estimation work
switch success
case 0 { invalid() }
}
require(success, ECPairingFailed());
return out[0] != 0;
}
/**
* @notice This function is functionally the same as pairing(), however it specifies a gas limit
* the user can set, as a precompile may use the entire gas budget if it reverts.
*/
function safePairing(
G1Point memory a1,
G2Point memory a2,
G1Point memory b1,
G2Point memory b2,
uint256 pairingGas
) internal view returns (bool, bool) {
G1Point[2] memory p1 = [a1, b1];
G2Point[2] memory p2 = [a2, b2];
uint256[12] memory input;
for (uint256 i = 0; i < 2; i++) {
uint256 j = i * 6;
input[j + 0] = p1[i].X;
input[j + 1] = p1[i].Y;
input[j + 2] = p2[i].X[0];
input[j + 3] = p2[i].X[1];
input[j + 4] = p2[i].Y[0];
input[j + 5] = p2[i].Y[1];
}
uint256[1] memory out;
bool success;
// solium-disable-next-line security/no-inline-assembly
assembly {
success := staticcall(pairingGas, 8, input, mul(12, 0x20), out, 0x20)
}
//Out is the output of the pairing precompile, either 0 or 1 based on whether the two pairings are equal.
//Success is true if the precompile actually goes through (aka all inputs are valid)
return (success, out[0] != 0);
}
/// @return hashedG1 the keccak256 hash of the G1 Point
/// @dev used for BLS signatures
function hashG1Point(
BN254.G1Point memory pk
) internal pure returns (bytes32 hashedG1) {
assembly {
mstore(0, mload(pk))
mstore(0x20, mload(add(0x20, pk)))
hashedG1 := keccak256(0, 0x40)
}
}
/// @return the keccak256 hash of the G2 Point
/// @dev used for BLS signatures
function hashG2Point(
BN254.G2Point memory pk
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(pk.X[0], pk.X[1], pk.Y[0], pk.Y[1]));
}
/**
* @notice adapted from https://github.com/HarryR/solcrypto/blob/master/contracts/altbn128.sol
*/
function hashToG1(
bytes32 _x
) internal view returns (G1Point memory) {
uint256 beta = 0;
uint256 y = 0;
uint256 x = uint256(_x) % FP_MODULUS;
while (true) {
(beta, y) = findYFromX(x);
// y^2 == beta
if (beta == mulmod(y, y, FP_MODULUS)) {
return G1Point(x, y);
}
x = addmod(x, 1, FP_MODULUS);
}
return G1Point(0, 0);
}
/**
* Given X, find Y
*
* where y = sqrt(x^3 + b)
*
* Returns: (x^3 + b), y
*/
function findYFromX(
uint256 x
) internal view returns (uint256, uint256) {
// beta = (x^3 + b) % p
uint256 beta = addmod(mulmod(mulmod(x, x, FP_MODULUS), x, FP_MODULUS), 3, FP_MODULUS);
// y^2 = x^3 + b
// this acts like: y = sqrt(beta) = beta^((p+1) / 4)
uint256 y = expMod(beta, 0xc19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52, FP_MODULUS);
return (beta, y);
}
function expMod(uint256 _base, uint256 _exponent, uint256 _modulus) internal view returns (uint256 retval) {
bool success;
uint256[1] memory output;
uint256[6] memory input;
input[0] = 0x20; // baseLen = new(big.Int).SetBytes(getData(input, 0, 32))
input[1] = 0x20; // expLen = new(big.Int).SetBytes(getData(input, 32, 32))
input[2] = 0x20; // modLen = new(big.Int).SetBytes(getData(input, 64, 32))
input[3] = _base;
input[4] = _exponent;
input[5] = _modulus;
assembly {
success := staticcall(sub(gas(), 2000), 5, input, 0xc0, output, 0x20)
// Use "invalid" to make gas estimation work
switch success
case 0 { invalid() }
}
require(success, ExpModFailed());
return output[0];
}
}
"
},
"src/contracts/libraries/BN254SignatureVerifier.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {BN254} from "./BN254.sol";
/**
* @title BN254SignatureVerifier
* @notice Library for BN254 signature verification
* @dev Provides unified signature verification with consistent gamma calculation and hash-to-G1 conversion
*/
library BN254SignatureVerifier {
using BN254 for BN254.G1Point;
/**
* @notice Core BN254 signature verification function with optional gas limiting
* @param msgHash The message hash that was signed
* @param signature The BLS signature to verify (G1 point)
* @param pubkeyG1 The G1 component of the public key
* @param pubkeyG2 The G2 component of the public key
* @param useGasLimit Whether to use gas-limited safe pairing
* @param pairingGas Gas limit for pairing (ignored if useGasLimit is false)
* @return success True if verification succeeded (always true if useGasLimit=false due to revert)
* @return pairingSuccessful True if pairing operation completed (only relevant when useGasLimit=true)
*/
function verifySignature(
bytes32 msgHash,
BN254.G1Point memory signature,
BN254.G1Point memory pubkeyG1,
BN254.G2Point memory pubkeyG2,
bool useGasLimit,
uint256 pairingGas
) internal view returns (bool success, bool pairingSuccessful) {
BN254.G1Point memory messagePoint = BN254.hashToG1(msgHash);
uint256 gamma = _calculateGamma(msgHash, pubkeyG1, pubkeyG2, signature);
// Calculate pairing inputs
BN254.G1Point memory leftG1 = signature.plus(pubkeyG1.scalar_mul(gamma));
BN254.G1Point memory rightG1 = messagePoint.plus(BN254.generatorG1().scalar_mul(gamma));
if (useGasLimit) {
// Use safe pairing with gas limit
(pairingSuccessful, success) =
BN254.safePairing(leftG1, BN254.negGeneratorG2(), rightG1, pubkeyG2, pairingGas);
} else {
success = BN254.pairing(leftG1, BN254.negGeneratorG2(), rightG1, pubkeyG2);
if (success) {
pairingSuccessful = true;
}
}
}
/**
* @notice Internal function to calculate gamma value for signature verification
* @param msgHash The message hash
* @param pubkeyG1 The G1 component of the public key
* @param pubkeyG2 The G2 component of the public key
* @param signature The signature point
* @return gamma The calculated gamma value
*/
function _calculateGamma(
bytes32 msgHash,
BN254.G1Point memory pubkeyG1,
BN254.G2Point memory pubkeyG2,
BN254.G1Point memory signature
) internal pure returns (uint256 gamma) {
gamma = uint256(
keccak256(
abi.encodePacked(
msgHash,
pubkeyG1.X,
pubkeyG1.Y,
pubkeyG2.X[0],
pubkeyG2.X[1],
pubkeyG2.Y[0],
pubkeyG2.Y[1],
signature.X,
signature.Y
)
)
) % BN254.FR_MODULUS;
}
}
"
},
"src/contracts/libraries/Merkle.sol": {
"content": "// SPDX-License-Identifier: MIT
// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol)
pragma solidity ^0.8.0;
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, salt the leaves, or hash the leaves with a hash function other than
* what is used for the Merkle tree's internal nodes. This is because the
* concatenation of a sorted pair of internal nodes in the Merkle tree could
* be reinterpreted as a leaf value.
*/
library Merkle {
/// @notice Thrown when the provided proof was not a multiple of 32, or was empty for SHA256.
/// @dev Error code: 0x4dc5f6a4
error InvalidProofLength();
/// @notice Thrown when the provided index was outside the max index for the tree.
/// @dev Error code: 0x63df8171
error InvalidIndex();
/// @notice Thrown when the provided leaves' length was not a power of two.
/// @dev Error code: 0xf6558f51
error LeavesNotPowerOfTwo();
/// @notice Thrown when the provided leaves' length was 0.
/// @dev Error code: 0xbaec3d9a
error NoLeaves();
/// @notice Thrown when the provided leaves' length was insufficient.
/// @dev Error code: 0xf8ef0367
/// @dev This is used for the SHA256 Merkle tree, where the tree must have more than 1 leaf.
error NotEnoughLeaves();
/// @notice Thrown when the root is empty.
/// @dev Error code: 0x53ce4ece
/// @dev Empty roots should never be valid. We prevent them to avoid issues like the Nomad bridge attack: <https://medium.com/nomad-xyz-blog/nomad-bridge-hack-root-cause-analysis-875ad2e5aacd>
error EmptyRoot();
/**
* @notice Verifies that a given leaf is included in a Merkle tree
* @param proof The proof of inclusion for the leaf
* @param root The root of the Merkle tree
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return True if the leaf is included in the Merkle tree, false otherwise
* @dev A `proof` is valid if and only if the rebuilt hash matches the root of the tree.
* @dev Reverts for:
* - InvalidProofLength: proof.length is not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
*/
function verifyInclusionKeccak(
bytes memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) internal pure returns (bool) {
require(root != bytes32(0), EmptyRoot());
return processInclusionProofKeccak(proof, leaf, index) == root;
}
/**
* @notice Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`.
* @param proof The proof of inclusion for the leaf
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return The rebuilt hash
* @dev Reverts for:
* - InvalidProofLength: proof.length is not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
* @dev The tree is built assuming `leaf` is the 0 indexed `index`'th leaf from the bottom left of the tree.
*/
function processInclusionProofKeccak(
bytes memory proof,
bytes32 leaf,
uint256 index
) internal pure returns (bytes32) {
if (proof.length == 0) {
return leaf;
}
require(proof.length % 32 == 0, InvalidProofLength());
bytes32 computedHash = leaf;
for (uint256 i = 32; i <= proof.length; i += 32) {
if (index % 2 == 0) {
// if index is even, then computedHash is a left sibling
assembly {
mstore(0x00, computedHash)
mstore(0x20, mload(add(proof, i)))
computedHash := keccak256(0x00, 0x40)
index := div(index, 2)
}
} else {
// if index is odd, then computedHash is a right sibling
assembly {
mstore(0x00, mload(add(proof, i)))
mstore(0x20, computedHash)
computedHash := keccak256(0x00, 0x40)
index := div(index, 2)
}
}
}
// Confirm proof was fully consumed by end of computation
require(index == 0, InvalidIndex());
return computedHash;
}
/**
* @notice Verifies that a given leaf is included in a Merkle tree
* @param proof The proof of inclusion for the leaf
* @param root The root of the Merkle tree
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return True if the leaf is included in the Merkle tree, false otherwise
* @dev A `proof` is valid if and only if the rebuilt hash matches the root of the tree.
* @dev Reverts for:
* - InvalidProofLength: proof.length is 0 or not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
*/
function verifyInclusionSha256(
bytes memory proof,
bytes32 root,
bytes32 leaf,
uint256 index
) internal view returns (bool) {
require(root != bytes32(0), EmptyRoot());
return processInclusionProofSha256(proof, leaf, index) == root;
}
/**
* @notice Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`.
* @param proof The proof of inclusion for the leaf
* @param leaf The leaf to verify
* @param index The index of the leaf in the Merkle tree
* @return The rebuilt hash
* @dev Reverts for:
* - InvalidProofLength: proof.length is 0 or not a multiple of 32.
* - InvalidIndex: index is not 0 at conclusion of computation (implying outside the max index for the tree).
* @dev The tree is built assuming `leaf` is the 0 indexed `index`'th leaf from the bottom left of the tree.
*/
function processInclusionProofSha256(
bytes memory proof,
bytes32 leaf,
uint256 index
) internal view returns (bytes32) {
require(proof.length != 0 && proof.length % 32 == 0, InvalidProofLength());
bytes32[1] memory computedHash = [leaf];
for (uint256 i = 32; i <= proof.length; i += 32) {
if (index % 2 == 0) {
// if index is even, then computedHash is a left sibling
assembly {
mstore(0x00, mload(computedHash))
mstore(0x20, mload(add(proof, i)))
if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) }
index := div(index, 2)
}
} else {
// if index is odd, then computedHash is a right sibling
assembly {
mstore(0x00, mload(add(proof, i)))
mstore(0x20, mload(computedHash))
if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) }
index := div(index, 2)
}
}
}
// Confirm proof was fully consumed by end of computation
require(index == 0, InvalidIndex());
return computedHash[0];
}
/**
* @notice Returns the Merkle root of a tree created from a set of leaves using SHA-256 as its hash function
* @param leaves the leaves of the Merkle tree
* @return The computed Merkle root of the tree.
* @dev Reverts for:
* - NotEnoughLeaves: leaves.length is less than 2.
* - LeavesNotPowerOfTwo: leaves.length is not a power of two.
* @dev Unlike the Keccak version, this function does not allow a single-leaf tree.
*/
function merkleizeSha256(
bytes32[] memory leaves
) internal pure returns (bytes32) {
require(leaves.length > 1, NotEnoughLeaves());
require(isPowerOfTwo(leaves.length), LeavesNotPowerOfTwo());
// There are half as many nodes in the layer above the leaves
uint256 numNodesInLayer = leaves.length / 2;
// Create a layer to store the internal nodes
bytes32[] memory layer = new bytes32[](numNodesInLayer);
// Fill the layer with the pairwise hashes of the leaves
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1]));
}
// While we haven't computed the root
while (numNodesInLayer != 1) {
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
// The first node in the layer is the root
return layer[0];
}
/**
* @notice 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.
* @dev Reverts for:
* - NoLeaves: leaves.length is 0.
*/
function merkleizeKeccak(
bytes32[] memory leaves
) internal pure returns (bytes32) {
require(leaves.length > 0, NoLeaves());
uint256 numNodesInLayer;
if (!isPowerOfTwo(leaves.length)) {
// Pad to the next power of 2
numNodesInLayer = 1;
while (numNodesInLayer < leaves.length) {
numNodesInLayer *= 2;
}
} else {
numNodesInLayer = leaves.length;
}
// Create a layer to store the internal nodes
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) {
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
// The first node in the layer is the root
return layer[0];
}
/**
* @notice Returns the Merkle proof for a given index in a tree created from a set of leaves using Keccak as its hash function
* @param leaves the leaves of the Merkle tree
* @param index the index of the leaf to get the proof for
* @return proof The computed Merkle proof for the leaf at index.
* @dev Reverts for:
* - InvalidIndex: index is outside the max index for the tree.
*/
function getProofKeccak(bytes32[] memory leaves, uint256 index) internal pure returns (bytes memory proof) {
require(leaves.length > 0, NoLeaves());
// 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];
}
if (index >= layer.length) revert InvalidIndex();
// While we haven't computed the root
while (numNodesInLayer != 1) {
// Flip the least significant bit of index to get the sibling index
uint256 siblingIndex = index ^ 1;
// Add the sibling to the proof
proof = abi.encodePacked(proof, layer[siblingIndex]);
index /= 2;
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
}
/**
* @notice Returns the Merkle proof for a given index in a tree created from a set of leaves using SHA-256 as its hash function
* @param leaves the leaves of the Merkle tree
* @param index the index of the leaf to get the proof for
* @return proof The computed Merkle proof for the leaf at index.
* @dev Reverts for:
* - NotEnoughLeaves: leaves.length is less than 2.
* @dev Unlike the Keccak version, this function does not allow a single-leaf proof.
*/
function getProofSha256(bytes32[] memory leaves, uint256 index) internal pure returns (bytes memory proof) {
require(leaves.length > 1, NotEnoughLeaves());
// 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];
}
if (index >= layer.length) revert InvalidIndex();
// While we haven't computed the root
while (numNodesInLayer != 1) {
// Flip the least significant bit of index to get the sibling index
uint256 siblingIndex = index ^ 1;
// Add the sibling to the proof
proof = abi.encodePacked(proof, layer[siblingIndex]);
index /= 2;
// The next layer above has half as many nodes
numNodesInLayer /= 2;
// Overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children
for (uint256 i = 0; i < numNodesInLayer; i++) {
layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1]));
}
}
}
/**
* @notice Returns whether the input is a power of two
* @param value the value to check
* @return True if the input is a power of two, false otherwise
*/
function isPowerOfTwo(
uint256 value
) internal pure returns (bool) {
return value != 0 && (value & (value - 1)) == 0;
}
}
"
},
"src/contracts/libraries/OperatorSetLib.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
using OperatorSetLib for OperatorSet global;
/**
* @notice An operator set identified by the AVS address and an identifier
* @param avs The address of the AVS this operator set belongs to
* @param id The unique identifier for the operator set
*/
struct OperatorSet {
address avs;
uint32 id;
}
library OperatorSetLib {
function key(
OperatorSet memory os
) internal pure returns (bytes32) {
return bytes32(abi.encodePacked(os.avs, uint96(os.id)));
}
function decode(
bytes32 _key
) internal pure returns (OperatorSet memory) {
/// forgefmt: disable-next-item
return OperatorSet({
avs: address(uint160(uint256(_key) >> 96)),
id: uint32(uint256(_key) & type(uint96).max)
});
}
}
"
},
"src/contracts/mixins/SemVerMixin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../interfaces/ISemVerMixin.sol";
import "@openzeppelin-upgrades/contracts/utils/ShortStringsUpgradeable.sol";
/// @title SemVerMixin
/// @notice A mixin contract that provides semantic versioning functionality.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/).
abstract contract SemVerMixin is ISemVerMixin {
using ShortStringsUpgradeable for *;
/// @notice The semantic version string for this contract, stored as a ShortString for gas efficiency.
/// @dev Follows SemVer 2.0.0 specification (https://semver.org/).
ShortString internal immutable _VERSION;
/// @notice Initializes the contract with a semantic version string.
/// @param _version The SemVer-formatted version string (e.g., "1.2.3")
/// @dev Version should follow SemVer 2.0.0 format: MAJOR.MINOR.PATCH
constructor(
string memory _version
) {
_VERSION = _version.toShortString();
}
/// @inheritdoc ISemVerMixin
function version() public view virtual returns (string memory) {
return _VERSION.toString();
}
/// @notice Returns the major version of the contract.
/// @dev Supports single digit major versions (e.g., "1" for version "1.2.3")
/// @return The major version string (e.g., "1" for version "1.2.3")
function _majorVersion() internal view returns (string memory) {
bytes memory v = bytes(_VERSION.toString());
return string(abi.encodePacked(v[0]));
}
}
"
},
"src/contracts/mixins/LeafCalculatorMixin.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import {IOperatorTableCalculatorTypes} from "../interfaces/IOperatorTableCalculator.sol";
/**
* @title LeafCalculatorMixin
* @notice Reusable mixin for calculating operator info and operator table leaf hashes
* @dev Provides standardized leaf calculation functions for use across multiple contracts and repositories.
* This mixin centralizes the leaf hashing logic to ensure consistency across the EigenLayer ecosystem
* and maintains proper cryptographic security through salt-based domain separation.
*/
abstract contract LeafCalculatorMixin {
/// @dev Salt for operator info leaf hash calculation
/// @dev The salt is used to prevent against second preimage attacks: attacks where an
/// attacker can create a partial proof using an internal node rather than a leaf to
/// validate a proof. The salt ensures that leaves cannot be concatenated together to
/// form a valid proof, as well as reducing the likelihood of an internal node matching
/// the salt prefix.
/// @dev Value derived from keccak256("OPERATOR_INFO_LEAF_SALT") = 0x75...
/// This ensures collision resistance and semantic meaning.
uint8 public constant OPERATOR_INFO_LEAF_SALT = 0x75;
/// @dev Salt for operator table leaf hash calculation
/// @dev The salt is used to prevent against second preimage attacks: attacks where an
/// attacker can create a partial proof using an internal node rather than a leaf to
/// validate a proof. The salt ensures that leaves cannot be concatenated together to
/// form a valid proof, as well as reducing the likelihood of an internal node matching
/// the salt prefix.
/// @dev Value derived from keccak256("OPERATOR_TABLE_LEAF_SALT") = 0x8e...
/// This ensures collision resistance and semantic meaning.
uint8 public constant OPERATOR_TABLE_LEAF_SALT = 0x8e;
/**
* @notice Calculate the leaf hash for an operator info
* @param operatorInfo The BN254 operator info struct containing the operator's public key and stake weights
* @return The leaf hash (keccak256 of salt and encoded operator info)
* @dev The salt is used to prevent against second preimage attacks: attacks where an
* attacker can create a partial proof using an internal node rather than a leaf to
* validate a proof. The salt ensures that leaves cannot be concatenated together to
* form a valid proof, as well as reducing the likelihood of an internal node matching
* the salt prefix.
*
* This is a standard "domain separation" technique in Merkle tree implementations
* to ensure leaf nodes and internal nodes can never be confused with each other.
* See Section 2.1 of <https://www.rfc-editor.org/rfc/rfc9162#name-merkle-trees> for more.
*
* Uses abi.encodePacked for the salt and abi.encode for the struct to handle complex types
* (structs with dynamic arrays) while maintaining gas efficiency where possible.
*/
function calculateOperatorInfoLeaf(
IOperatorTableCalculatorTypes.BN254OperatorInfo memory operatorInfo
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(OPERATOR_INFO_LEAF_SALT, abi.encode(operatorInfo)));
}
/**
* @notice Calculate the leaf hash for an operator table
* @param operatorTableBytes The encoded operator table as bytes containing operator set data
* @return The leaf hash (keccak256 of salt and operator table bytes)
* @dev The salt is used to prevent against second preimage attacks: attacks where an
* attacker can create a partial proof using an internal node rather than a leaf to
* validate a proof. The salt ensures that leaves cannot be concatenated together to
* form a valid proof, as well as reducing the likelihood of an internal node matching
* the salt prefix.
*
* This is a standard "domain separation" technique in Merkle tree implementations
* to ensure leaf nodes and internal nodes can never be confused with each other.
* See Section 2.1 of <https://www.rfc-editor.org/rfc/rfc9162#name-merkle-trees> for more.
*
* Uses abi.encodePacked for both salt and bytes for optimal gas efficiency since both
* are simple byte arrays without complex nested structures.
*/
function calculateOperatorTableLeaf(
bytes calldata operatorTableBytes
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(OPERATOR_TABLE_LEAF_SALT, operatorTableBytes));
}
}
"
},
"src/contracts/multichain/BN254CertificateVerifierStorage.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IOperatorTableUpdater.sol";
import "../interfaces/IBN254CertificateVerifier.sol";
import "../interfaces/IBaseCertificateVerifier.sol";
abstract contract BN254CertificateVerifierStorage is IBN254CertificateVerifier {
// Constants
/// @dev EIP-712 type hash for certificate verification
bytes32 internal constant BN254_CERTIFICATE_TYPEHASH =
keccak256("BN254Certificate(uint32 referenceTimestamp,bytes32 messageHash)");
/// @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;
// OPERATOR_INFO_LEAF_SALT is now inherited from LeafCalculatorMixin
// Immutables
/// @dev The address that can update operator tables
IOperatorTableUpdater public immutable operatorTableUpdater;
// Mutatables
/// @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 => 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
Submitted on: 2025-09-25 10:05:33
Comments
Log in to comment.
No comments yet.