Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: BLS key rotation #249

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
135 changes: 120 additions & 15 deletions src/BLSApkRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.12;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import {BLSApkRegistryStorage} from "./BLSApkRegistryStorage.sol";

import {IRegistryCoordinator} from "./interfaces/IRegistryCoordinator.sol";

import {BN254} from "./libraries/BN254.sol";

contract BLSApkRegistry is BLSApkRegistryStorage {
contract BLSApkRegistry is BLSApkRegistryStorage, EIP712 {
ChaoticWalrus marked this conversation as resolved.
Show resolved Hide resolved
using BN254 for BN254.G1Point;

/// @notice when applied to a function, only allows the RegistryCoordinator to call it
Expand All @@ -22,7 +21,10 @@ contract BLSApkRegistry is BLSApkRegistryStorage {
/// @notice Sets the (immutable) `registryCoordinator` address
constructor(
IRegistryCoordinator _registryCoordinator
) BLSApkRegistryStorage(_registryCoordinator) {}
)
BLSApkRegistryStorage(_registryCoordinator)
EIP712("BLSApkRegistry", "v0.0.1")
{}

/*******************************************************************************
EXTERNAL FUNCTIONS - REGISTRY COORDINATOR
Expand Down Expand Up @@ -95,26 +97,24 @@ contract BLSApkRegistry is BLSApkRegistryStorage {
* @notice Called by the RegistryCoordinator register an operator as the owner of a BLS public key.
* @param operator is the operator for whom the key is being registered
* @param params contains the G1 & G2 public keys of the operator, and a signature proving their ownership
* @param pubkeyRegistrationMessageHash is a hash that the operator must sign to prove key ownership
*/
function registerBLSPublicKey(
address operator,
PubkeyRegistrationParams calldata params,
BN254.G1Point calldata pubkeyRegistrationMessageHash
PubkeyRegistrationParams calldata params
) external onlyRegistryCoordinator returns (bytes32 operatorId) {
bytes32 pubkeyHash = BN254.hashG1Point(params.pubkeyG1);
if(operatorToPubkeyHash[operator] == pubkeyHash) {
return pubkeyHash;
}
require(
pubkeyHash != ZERO_PK_HASH, "BLSApkRegistry.registerBLSPublicKey: cannot register zero pubkey"
);
require(
operatorToPubkeyHash[operator] == bytes32(0),
"BLSApkRegistry.registerBLSPublicKey: operator already registered pubkey"
);
require(
pubkeyHashToOperator[pubkeyHash] == address(0),
"BLSApkRegistry.registerBLSPublicKey: public key already registered"
);

BN254.G1Point memory _pubkeyRegistrationMessageHash = pubkeyRegistrationMessageHash(operator);
// gamma = h(sigma, P, P', H(m))
uint256 gamma = uint256(keccak256(abi.encodePacked(
params.pubkeyRegistrationSignature.X,
Expand All @@ -123,18 +123,27 @@ contract BLSApkRegistry is BLSApkRegistryStorage {
params.pubkeyG1.Y,
params.pubkeyG2.X,
params.pubkeyG2.Y,
pubkeyRegistrationMessageHash.X,
pubkeyRegistrationMessageHash.Y
_pubkeyRegistrationMessageHash.X,
_pubkeyRegistrationMessageHash.Y
))) % BN254.FR_MODULUS;

// e(sigma + P * gamma, [-1]_2) = e(H(m) + [1]_1 * gamma, P')
require(BN254.pairing(
params.pubkeyRegistrationSignature.plus(params.pubkeyG1.scalar_mul(gamma)),
BN254.negGeneratorG2(),
pubkeyRegistrationMessageHash.plus(BN254.generatorG1().scalar_mul(gamma)),
_pubkeyRegistrationMessageHash.plus(BN254.generatorG1().scalar_mul(gamma)),
params.pubkeyG2
), "BLSApkRegistry.registerBLSPublicKey: either the G1 signature is wrong, or G1 and G2 private key do not match");

//if keys are already registered for the operator then checkpoint the previous pubkey and pubkeyHash of the operator
if(operatorToPubkeyHash[operator] != bytes32(0)) {
operatorPubkeyHistory[operator].push(PubkeyCheckpoint({
previousPubkeyG1: operatorToPubkey[operator],
previousPubkeyHash: operatorToPubkeyHash[operator],
blockNumber: uint32(block.number)
}));
}

operatorToPubkey[operator] = params.pubkeyG1;
operatorToPubkeyHash[operator] = pubkeyHash;
pubkeyHashToOperator[pubkeyHash] = operator;
Expand Down Expand Up @@ -281,4 +290,100 @@ contract BLSApkRegistry is BLSApkRegistryStorage {
function getOperatorId(address operator) public view returns (bytes32) {
return operatorToPubkeyHash[operator];
}
}

/// @notice Returns the length of the pubkey checkpoint history for the given `operator`
function getOperatorPubkeyCheckpointHistoryLength(address operator) external view returns (uint256) {
return operatorPubkeyHistory[operator].length;
}

/// @notice Returns the `index`th entry in the pubkey checkpoint history for the given `operator`
function getOperatorPubkeyCheckpointByIndex(address operator, uint256 index) external view returns (PubkeyCheckpoint memory) {
require(index < operatorPubkeyHistory[operator].length, "BLSApkRegistry.getPubkeyCheckpointByIndex: index out of bounds");
return operatorPubkeyHistory[operator][index];
}

/**
* @notice Returns the pubkey for the provided `operator` at the given `blockNumber`
* @dev This function is designed to find proper inputs to the `getPubkeyHashAtBlockNumberByIndex` function
* @dev Will revert if an operator has not ever registered a pubkey
* @dev Will revert if `blockNumber` is greater than the current block number
* @dev Will return the first pubkey for the provided `operator` if `blockNumber` is before the first pubkey checkpoint
* and additional logic is required to check if the operator was active at the given `blockNumber`
*/
function getOperatorPubkeyHashAtBlockNumber(
uint32 blockNumber,
address operator
) external view returns (bytes32 pubkeyHash) {
require(blockNumber <= uint32(block.number), "BLSApkRegistry.getOperatorPubkeyHashAtBlockNumber: blockNumber is after current block");
pubkeyHash = getOperatorId(operator);

uint256 historyLength = operatorPubkeyHistory[operator].length;
for (uint256 i = 0; i < historyLength; i++) {
if (operatorPubkeyHistory[operator][i].blockNumber <= blockNumber) {
pubkeyHash = operatorPubkeyHistory[operator][i].previousPubkeyHash;
}
}

if (pubkeyHash == bytes32(0)) {
revert("BLSApkRegistry.getOperatorPubkeyHashAtBlockNumber: operator has not registered a pubkey");
}
}

/**
* @notice Returns the pubkey for the given `operator` at the given `blockNumber` via the `index`,
* reverting if `index` is incorrect
* @dev This function is meant to be used in concert with `getOperatorPubkeyHashAtBlockNumber`, which
* helps off-chain processes to fetch the correct `index` input
* @dev Will revert if an operator has not ever registered a pubkey
* @dev Will revert if `blockNumber` is greater than the current block number
* @dev Will return the first pubkey for the provided `operator` if `blockNumber` is before the first pubkey checkpoint
* and additional logic is required to check if the operator was active at the given `blockNumber`
* @dev If an operator has no pubkey checkpoints, then the operator's current pubkey is returned for any `blockNumber` and `index`
* @dev If the blockNumber is after the last pubkey checkpoint, then the operator's current pubkey is returned for any `index`
*/
function getOperatorPubkeyHashAtBlockNumberByIndex(
address operator,
uint32 blockNumber,
uint256 index
) external view returns (bytes32 pubkeyHash) {
require(blockNumber <= uint32(block.number), "BLSApkRegistry.getPubkeyHashAtBlockNumberByIndex: blockNumber is after current block");
uint256 historyLength = operatorPubkeyHistory[operator].length;
if (historyLength == 0) {
pubkeyHash = getOperatorId(operator);
} else if (operatorPubkeyHistory[operator][historyLength - 1].blockNumber < blockNumber) {
pubkeyHash = getOperatorId(operator);
} else {
PubkeyCheckpoint memory pubkeyCheckpoint = operatorPubkeyHistory[operator][index];

require(
blockNumber <= pubkeyCheckpoint.blockNumber,
"BLSApkRegistry.getPubkeyHashAtBlockNumberByIndex: pubkeyHash is from after blockNumber"
);

if(index > 0) {
PubkeyCheckpoint memory previousPubkeyCheckpoint = operatorPubkeyHistory[operator][index - 1];
require(
blockNumber > previousPubkeyCheckpoint.blockNumber,
"BLSApkRegistry.getPubkeyHashAtBlockNumberByIndex: pubkeyHash is from before blockNumber"
);
}

pubkeyHash = pubkeyCheckpoint.previousPubkeyHash;
}
if (pubkeyHash == bytes32(0)) {
revert("BLSApkRegistry.getPubkeyHashAtBlockNumberByIndex: operator has not registered a pubkey");
}
}

/**
* @notice Returns the message hash that an operator must sign to register their BLS public key.
* @param operator is the address of the operator registering their BLS public key
*/
function pubkeyRegistrationMessageHash(address operator) public view returns (BN254.G1Point memory) {
return BN254.hashToG1(
_hashTypedDataV4(
keccak256(abi.encode(PUBKEY_REGISTRATION_TYPEHASH, operator))
)
);
}
}
9 changes: 7 additions & 2 deletions src/BLSApkRegistryStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {BN254} from "./libraries/BN254.sol";
abstract contract BLSApkRegistryStorage is Initializable, IBLSApkRegistry {
/// @notice the hash of the zero pubkey aka BN254.G1Point(0,0)
bytes32 internal constant ZERO_PK_HASH = hex"ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5";
/// @notice The EIP-712 typehash used for registering BLS public keys
bytes32 public constant PUBKEY_REGISTRATION_TYPEHASH = keccak256("BN254PubkeyRegistration(address operator)");

/// @notice the registry coordinator contract
address public immutable registryCoordinator;
Expand All @@ -29,12 +31,15 @@ abstract contract BLSApkRegistryStorage is Initializable, IBLSApkRegistry {
/// @notice maps quorumNumber => current aggregate pubkey of quorum
mapping(uint8 => BN254.G1Point) public currentApk;

/// @notice maps operator address to pubkey history
mapping(address => PubkeyCheckpoint[]) public operatorPubkeyHistory;
ChaoticWalrus marked this conversation as resolved.
Show resolved Hide resolved

constructor(IRegistryCoordinator _registryCoordinator) {
registryCoordinator = address(_registryCoordinator);
// disable initializers so that the implementation contract cannot be initialized
_disableInitializers();
}

// storage gap for upgradeability
uint256[45] private __GAP;
}
uint256[44] private __GAP;
}
32 changes: 24 additions & 8 deletions src/RegistryCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,16 @@ contract RegistryCoordinator is
ejectionCooldown = _ejectionCooldown;
}

/**
* @notice Sets the deregistration cooldown, which is the time an operator must wait in
* seconds after fully deregistering before registering for any quorum
* @param _deregistrationCooldown the new deregistration cooldown in seconds
* @dev only callable by the owner
*/
function setDeregistrationCooldown(uint256 _deregistrationCooldown) external onlyOwner {
deregistrationCooldown = _deregistrationCooldown;
}
Comment on lines +455 to +457
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs an event.
might want this added to the initializer as well.

I'm considering if any input sanitization makes sense here too -- should this have a min or max? is 0 an OK input? I feel like it's probably OK to not have this, given the owner-only nature of the function... I don't see any super "unsafe" values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't add to init because of bytecode size but I will see if it can be squeezed in. I could see really long values being not great for the delay but will probably not add sanitization for the same bytecode size issue

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed in-person, no sanitization seems OK, not in initializer is also acceptable to me (although not my ideal), no event is something I can live with but probably the most desirable of these


/*******************************************************************************
INTERNAL FUNCTIONS
*******************************************************************************/
Expand Down Expand Up @@ -480,8 +490,12 @@ contract RegistryCoordinator is
require(quorumsToAdd.noBitsInCommon(currentBitmap), "RegistryCoordinator._registerOperator: operator already registered for some quorums being registered for");
uint192 newBitmap = uint192(currentBitmap.plus(quorumsToAdd));

// Check that the operator can reregister if ejected
require(lastEjectionTimestamp[operator] + ejectionCooldown < block.timestamp, "RegistryCoordinator._registerOperator: operator cannot reregister yet");
// Check that the operator can reregister
require(
lastEjectionTimestamp[operator] + ejectionCooldown < block.timestamp &&
lastDeregistrationTimestamp[operator] + deregistrationCooldown < block.timestamp,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a deregistrationCooldown if we keep the entire history of keys onchain?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is 0 a reasonable value for deregistrationCooldown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gpsanant has suggested this should be a few days. I think something like an hour is more reasonable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this...limits the rate at which operators can edit their keys. I don't think it's serving a serious purpose beyond maybe addressing some theoretical worry where an operator keeps changing their key? I guess if you need to search past keys it's potentially problematic if the growth rate of the historical storage isn't limited?

"RegistryCoordinator._registerOperator: operator cannot reregister yet"
);

/**
* Update operator's bitmap, socket, and status. Only update operatorInfo if needed:
Expand Down Expand Up @@ -531,7 +545,12 @@ contract RegistryCoordinator is
) internal returns (bytes32 operatorId) {
operatorId = blsApkRegistry.getOperatorId(operator);
if (operatorId == 0) {
operatorId = blsApkRegistry.registerBLSPublicKey(operator, params, pubkeyRegistrationMessageHash(operator));
operatorId = blsApkRegistry.registerBLSPublicKey(operator, params);
} else if (_operatorInfo[operator].status == OperatorStatus.DEREGISTERED &&
params.pubkeyRegistrationSignature.X != 0 &&
params.pubkeyRegistrationSignature.Y != 0
) {
operatorId = blsApkRegistry.registerBLSPublicKey(operator, params);
}
return operatorId;
}
Expand Down Expand Up @@ -616,6 +635,7 @@ contract RegistryCoordinator is
// them from the AVS via the EigenLayer core contracts
if (newBitmap.isEmpty()) {
operatorInfo.status = OperatorStatus.DEREGISTERED;
lastDeregistrationTimestamp[operator] = block.timestamp;
serviceManager.deregisterOperatorFromAVS(operator);
emit OperatorDeregistered(operator, operatorId);
}
Expand Down Expand Up @@ -926,11 +946,7 @@ contract RegistryCoordinator is
* @param operator is the address of the operator registering their BLS public key
*/
function pubkeyRegistrationMessageHash(address operator) public view returns (BN254.G1Point memory) {
return BN254.hashToG1(
_hashTypedDataV4(
keccak256(abi.encode(PUBKEY_REGISTRATION_TYPEHASH, operator))
)
);
return blsApkRegistry.pubkeyRegistrationMessageHash(operator);
}

/// @dev need to override function here since its defined in both these contracts
Expand Down
9 changes: 6 additions & 3 deletions src/RegistryCoordinatorStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ abstract contract RegistryCoordinatorStorage is IRegistryCoordinator {
/// @notice The EIP-712 typehash for the `DelegationApproval` struct used by the contract
bytes32 public constant OPERATOR_CHURN_APPROVAL_TYPEHASH =
keccak256("OperatorChurnApproval(address registeringOperator,bytes32 registeringOperatorId,OperatorKickParam[] operatorKickParams,bytes32 salt,uint256 expiry)OperatorKickParam(uint8 quorumNumber,address operator)");
/// @notice The EIP-712 typehash used for registering BLS public keys
bytes32 public constant PUBKEY_REGISTRATION_TYPEHASH = keccak256("BN254PubkeyRegistration(address operator)");
/// @notice The maximum value of a quorum bitmap
uint256 internal constant MAX_QUORUM_BITMAP = type(uint192).max;
/// @notice The basis point denominator
Expand Down Expand Up @@ -69,6 +67,11 @@ abstract contract RegistryCoordinatorStorage is IRegistryCoordinator {
/// @notice the delay in seconds before an operator can reregister after being ejected
uint256 public ejectionCooldown;

/// @notice the last timestamp an operator fully deregistered
mapping(address => uint256) public lastDeregistrationTimestamp;
/// @notice the delay in seconds before an operator can reregister after fully deregistering
uint256 public deregistrationCooldown;

constructor(
IServiceManager _serviceManager,
IStakeRegistry _stakeRegistry,
Expand All @@ -83,5 +86,5 @@ abstract contract RegistryCoordinatorStorage is IRegistryCoordinator {

// storage gap for upgradeability
// slither-disable-next-line shadowing-state
uint256[39] private __GAP;
uint256[37] private __GAP;
}
24 changes: 20 additions & 4 deletions src/interfaces/IBLSApkRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ interface IBLSApkRegistry is IRegistry {
BN254.G2Point pubkeyG2;
}

/**
* @notice Struct used to checkpoint the previous pubkeys and their hashes for a given operator
* @param previousPubkeyG1 is the G1 public key of the operator
* @param previoudPubkeyHash is the hash of the public key of the operator
* @param blockNumber is the block number at which the pubkey was updated
*/
struct PubkeyCheckpoint {
BN254.G1Point previousPubkeyG1;
bytes32 previousPubkeyHash;
uint32 blockNumber;
ChaoticWalrus marked this conversation as resolved.
Show resolved Hide resolved
}

// EVENTS
/// @notice Emitted when `operator` registers with the public keys `pubkeyG1` and `pubkeyG2`.
event NewPubkeyRegistration(address indexed operator, BN254.G1Point pubkeyG1, BN254.G2Point pubkeyG2);
Expand Down Expand Up @@ -101,12 +113,10 @@ interface IBLSApkRegistry is IRegistry {
* @notice Called by the RegistryCoordinator register an operator as the owner of a BLS public key.
* @param operator is the operator for whom the key is being registered
* @param params contains the G1 & G2 public keys of the operator, and a signature proving their ownership
* @param pubkeyRegistrationMessageHash is a hash that the operator must sign to prove key ownership
*/
function registerBLSPublicKey(
address operator,
PubkeyRegistrationParams calldata params,
BN254.G1Point calldata pubkeyRegistrationMessageHash
PubkeyRegistrationParams calldata params
) external returns (bytes32 operatorId);

/**
Expand Down Expand Up @@ -139,4 +149,10 @@ interface IBLSApkRegistry is IRegistry {
/// @notice returns the ID used to identify the `operator` within this AVS.
/// @dev Returns zero in the event that the `operator` has never registered for the AVS
function getOperatorId(address operator) external view returns (bytes32);
}

/**
* @notice Returns the message hash that an operator must sign to register their BLS public key.
* @param operator is the address of the operator registering their BLS public key
*/
function pubkeyRegistrationMessageHash(address operator) external view returns (BN254.G1Point memory);
}
2 changes: 1 addition & 1 deletion test/ffi/BLSPubKeyCompendiumFFI.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ contract BLSApkRegistryFFITests is G2Operations {
pubkeyRegistrationParams.pubkeyRegistrationSignature = _signMessage(alice);

vm.prank(address(registryCoordinator));
blsApkRegistry.registerBLSPublicKey(alice, pubkeyRegistrationParams, registryCoordinator.pubkeyRegistrationMessageHash(alice));
blsApkRegistry.registerBLSPublicKey(alice, pubkeyRegistrationParams);

assertEq(blsApkRegistry.operatorToPubkeyHash(alice), BN254.hashG1Point(pubkeyRegistrationParams.pubkeyG1),
"pubkey hash not stored correctly");
Expand Down
Loading
Loading