Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/bindings/BN254CertificateVerifier/binding.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/bindings/ECDSACertificateVerifier/binding.go

Large diffs are not rendered by default.

112 changes: 87 additions & 25 deletions pkg/bindings/IOperatorTableUpdater/binding.go

Large diffs are not rendered by default.

114 changes: 88 additions & 26 deletions pkg/bindings/OperatorTableUpdater/binding.go

Large diffs are not rendered by default.

112 changes: 87 additions & 25 deletions pkg/bindings/OperatorTableUpdaterStorage/binding.go

Large diffs are not rendered by default.

32 changes: 30 additions & 2 deletions src/contracts/interfaces/IOperatorTableUpdater.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ interface IOperatorTableUpdater is
* @param globalTableRootCert certificate of the root
* @param globalTableRoot merkle root of all operatorSet tables
* @param referenceTimestamp timestamp of the root
* @param referenceBlockNumber block number of the root
* @dev Any entity can submit with a valid certificate signed off by the `globalRootConfirmerSet`
* @dev The `msgHash` in the `globalOperatorTableRootCert` is the hash of the `globalOperatorTableRoot`
*/
function confirmGlobalTableRoot(
BN254Certificate calldata globalTableRootCert,
bytes32 globalTableRoot,
uint32 referenceTimestamp
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) external;

/**
Expand Down Expand Up @@ -145,15 +147,41 @@ interface IOperatorTableUpdater is
*/
function getLatestReferenceTimestamp() external view returns (uint32);

/**
* @notice Get the latest reference block number
* @return The latest reference block number
*/
function getLatestReferenceBlockNumber() external view returns (uint32);

/**
* @notice Get the reference block number for a given reference timestamp
* @param referenceTimestamp the reference timestamp
* @return The reference block number for the given reference timestamp
*/
function getReferenceBlockNumberByTimestamp(
uint32 referenceTimestamp
) external view returns (uint32);

/**
* @notice Get the reference timestamp for a given reference block number
* @param referenceBlockNumber the reference block number
* @return The reference timestamp for the given reference block number
*/
function getReferenceTimestampByBlockNumber(
uint32 referenceBlockNumber
) external view returns (uint32);

/**
* @notice Get the message hash for the certificate of a global table root update
* @param globalTableRoot the global table root
* @param referenceTimestamp the reference timestamp
* @param referenceBlockNumber the reference block number
* @return The message hash for a global table root
*/
function getGlobalTableUpdateMessageHash(
bytes32 globalTableRoot,
uint32 referenceTimestamp
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) external view returns (bytes32);

/**
Expand Down
36 changes: 31 additions & 5 deletions src/contracts/multichain/OperatorTableUpdater.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
function confirmGlobalTableRoot(
BN254Certificate calldata globalTableRootCert,
bytes32 globalTableRoot,
uint32 referenceTimestamp
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) external {
// Table roots can only be updated for current or past timestamps and after the latest reference timestamp
require(referenceTimestamp <= block.timestamp, GlobalTableRootInFuture());
require(referenceTimestamp > _latestReferenceTimestamp, GlobalTableRootStale());
require(
globalTableRootCert.messageHash == getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp),
globalTableRootCert.messageHash
== getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp, referenceBlockNumber),
InvalidMessageHash()
);

Expand All @@ -82,8 +84,10 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl

require(isValid, CertificateInvalid());

// Update the global table root
// Update the global table root & reference timestamps
_latestReferenceTimestamp = referenceTimestamp;
_referenceBlockNumbers[referenceTimestamp] = referenceBlockNumber;
_referenceTimestamps[referenceBlockNumber] = referenceTimestamp;
_globalTableRoots[referenceTimestamp] = globalTableRoot;

emit NewGlobalTableRoot(referenceTimestamp, globalTableRoot);
Expand Down Expand Up @@ -195,12 +199,34 @@ contract OperatorTableUpdater is Initializable, OwnableUpgradeable, OperatorTabl
return _latestReferenceTimestamp;
}

/// @inheritdoc IOperatorTableUpdater
function getLatestReferenceBlockNumber() external view returns (uint32) {
return _referenceBlockNumbers[_latestReferenceTimestamp];
}

/// @inheritdoc IOperatorTableUpdater
function getReferenceBlockNumberByTimestamp(
uint32 referenceTimestamp
) external view returns (uint32) {
return _referenceBlockNumbers[referenceTimestamp];
}

/// @inheritdoc IOperatorTableUpdater
function getReferenceTimestampByBlockNumber(
uint32 referenceBlockNumber
) external view returns (uint32) {
return _referenceTimestamps[referenceBlockNumber];
}

/// @inheritdoc IOperatorTableUpdater
function getGlobalTableUpdateMessageHash(
bytes32 globalTableRoot,
uint32 referenceTimestamp
uint32 referenceTimestamp,
uint32 referenceBlockNumber
) public pure returns (bytes32) {
return keccak256(abi.encode(GLOBAL_TABLE_ROOT_CERT_TYPEHASH, globalTableRoot, referenceTimestamp));
return keccak256(
abi.encode(GLOBAL_TABLE_ROOT_CERT_TYPEHASH, globalTableRoot, referenceTimestamp, referenceBlockNumber)
);
}

/// @inheritdoc IOperatorTableUpdater
Expand Down
8 changes: 7 additions & 1 deletion src/contracts/multichain/OperatorTableUpdaterStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ abstract contract OperatorTableUpdaterStorage is IOperatorTableUpdater {
/// @notice The global table roots by timestamp
mapping(uint32 timestamp => bytes32 globalTableRoot) internal _globalTableRoots;

/// @notice Mapping from latest reference timestamp to reference block number
mapping(uint32 referenceTimestamp => uint32 referenceBlockNumber) internal _referenceBlockNumbers;

/// @notice Mapping from reference block number to reference timestamp
mapping(uint32 referenceBlockNumber => uint32 referenceTimestamp) internal _referenceTimestamps;

// Constructor
constructor(
IBN254CertificateVerifier _bn254CertificateVerifier,
Expand All @@ -50,5 +56,5 @@ abstract contract OperatorTableUpdaterStorage is IOperatorTableUpdater {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[45] private __gap;
uint256[44] private __gap;
}
68 changes: 68 additions & 0 deletions src/test/tree/OperatorTableUpdaterUnit.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.
└── OperatorTableUpdater (**** denotes that integration tests are needed to fully validate path)
├── when initialize is called
│ ├── given that the contract is already initialized
│ │ └── it should revert
│ └── given that the contract is not initialized
│ └── it should set the owner, global root confirmer set, threshold, and update operator table & emit events
├── when confirmGlobalTableRoot is called
│ ├── given that the reference timestamp is in the future
│ │ └── it should revert with GlobalTableRootInFuture
│ ├── given that the reference timestamp is not greater than latest
│ │ └── it should revert with GlobalTableRootStale
│ ├── given that the message hash is invalid
│ │ └── it should revert with InvalidMessageHash
│ ├── given that the certificate is invalid
│ │ └── it should revert with CertificateInvalid
│ └── given that all parameters are valid
│ └── it should update global table root, reference timestamp, block number mappings (both directions) & emit NewGlobalTableRoot event
├── when updateOperatorTable is called
│ ├── given that the reference timestamp is not greater than operator set's latest
│ │ └── it should revert with TableUpdateForPastTimestamp
│ ├── given that the global table root does not match the reference timestamp
│ │ └── it should revert with InvalidGlobalTableRoot
│ ├── given that the merkle proof is invalid
│ │ └── it should revert with InvalidOperatorSetProof
│ ├── given that the curve type is invalid
│ │ └── it should revert with InvalidCurveType
│ ├── given that the curve type is BN254
│ │ └── it should call bn254CertificateVerifier.updateOperatorTable with decoded info
│ └── given that the curve type is ECDSA
│ └── it should call ecdsaCertificateVerifier.updateOperatorTable with decoded info
├── when setGlobalRootConfirmerSet is called
│ ├── given that the caller is not the owner
│ │ └── it should revert
│ └── given that the caller is the owner
│ └── it should update the global root confirmer set & emit GlobalRootConfirmerSetUpdated event
├── when setGlobalRootConfirmationThreshold is called
│ ├── given that the caller is not the owner
│ │ └── it should revert
│ ├── given that the threshold exceeds MAX_BPS
│ │ └── it should revert with InvalidConfirmationThreshold
│ └── given that the caller is owner and threshold is valid
│ └── it should update the threshold & emit GlobalRootConfirmationThresholdUpdated event
├── when getGlobalTableRootByTimestamp is called
│ └── it should return the global table root for the given timestamp
├── when getCurrentGlobalTableRoot is called
│ └── it should return the global table root for the latest reference timestamp
├── when getGlobalRootConfirmerSet is called
│ └── it should return the current global root confirmer set
├── when getCertificateVerifier is called
│ ├── given that the curve type is BN254
│ │ └── it should return the bn254CertificateVerifier address
│ ├── given that the curve type is ECDSA
│ │ └── it should return the ecdsaCertificateVerifier address
│ └── given that the curve type is invalid
│ └── it should revert with InvalidCurveType
├── when getLatestReferenceTimestamp is called
│ └── it should return the latest reference timestamp
├── when getLatestReferenceBlockNumber is called
│ └── it should return the reference block number for the latest reference timestamp
├── when getReferenceBlockNumberByTimestamp is called
│ └── it should return the reference block number for the given timestamp
├── when getReferenceTimestampByBlockNumber is called
│ └── it should return the reference timestamp for the given block number
├── when getGlobalTableUpdateMessageHash is called
│ └── it should return the keccak256 hash of encoded typehash, root, timestamp, and block number
└── when getGlobalConfirmerSetReferenceTimestamp is called
└── it should return the latest reference timestamp for the global root confirmer set
35 changes: 24 additions & 11 deletions src/test/unit/OperatorTableUpdaterUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,11 @@ contract OperatorTableUpdaterUnitTests is
/// @dev Updates the global table root in the BN254 certificate verifier
function _updateGlobalTableRoot(bytes32 globalTableRoot) internal {
BN254Certificate memory mockCertificate;
mockCertificate.messageHash = operatorTableUpdater.getGlobalTableUpdateMessageHash(globalTableRoot, uint32(block.timestamp));
uint32 referenceBlockNumber = uint32(block.number);
mockCertificate.messageHash =
operatorTableUpdater.getGlobalTableUpdateMessageHash(globalTableRoot, uint32(block.timestamp), referenceBlockNumber);
_setIsValidCertificate(mockCertificate, true);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, globalTableRoot, uint32(block.timestamp));
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, globalTableRoot, uint32(block.timestamp), referenceBlockNumber);
}
}

Expand Down Expand Up @@ -204,52 +206,63 @@ contract OperatorTableUpdaterUnitTests_confirmGlobalTableRoot is OperatorTableUp

function testFuzz_revert_futureTableRoot(Randomness r) public rand(r) {
uint32 referenceTimestamp = r.Uint32(uint32(block.timestamp + 1), type(uint32).max);
uint32 referenceBlockNumber = r.Uint32();

cheats.expectRevert(GlobalTableRootInFuture.selector);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), referenceTimestamp + 1);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), referenceTimestamp + 1, referenceBlockNumber);
}

function testFuzz_revert_staleCertificate(Randomness r) public rand(r) {
mockCertificate.messageHash = operatorTableUpdater.getGlobalTableUpdateMessageHash(bytes32(0), uint32(block.timestamp));
uint32 referenceBlockNumber = uint32(block.number);
mockCertificate.messageHash =
operatorTableUpdater.getGlobalTableUpdateMessageHash(bytes32(0), uint32(block.timestamp), referenceBlockNumber);
_setIsValidCertificate(mockCertificate, true);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), uint32(block.timestamp));
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), uint32(block.timestamp), referenceBlockNumber);

uint32 referenceTimestamp = r.Uint32(0, operatorTableUpdater.getLatestReferenceTimestamp() - 1);
cheats.expectRevert(GlobalTableRootStale.selector);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), referenceTimestamp);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), referenceTimestamp, referenceBlockNumber);
}

function testFuzz_revert_InvalidMessageHash(Randomness r) public rand(r) {
bytes32 invalidTableRoot = bytes32(r.Uint256(1, type(uint).max));
mockCertificate.messageHash = invalidTableRoot;
uint32 referenceBlockNumber = r.Uint32();

cheats.expectRevert(InvalidMessageHash.selector);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), uint32(block.timestamp));
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), uint32(block.timestamp), referenceBlockNumber);
}

function test_revert_invalidCertificate() public {
mockCertificate.messageHash = operatorTableUpdater.getGlobalTableUpdateMessageHash(bytes32(0), uint32(block.timestamp));
uint32 referenceBlockNumber = uint32(block.number);
mockCertificate.messageHash =
operatorTableUpdater.getGlobalTableUpdateMessageHash(bytes32(0), uint32(block.timestamp), referenceBlockNumber);
_setIsValidCertificate(mockCertificate, false);
cheats.expectRevert(CertificateInvalid.selector);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), uint32(block.timestamp));
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, bytes32(0), uint32(block.timestamp), referenceBlockNumber);
}

function testFuzz_correctness(Randomness r) public rand(r) {
uint32 referenceTimestamp = r.Uint32(operatorTableUpdater.getLatestReferenceTimestamp() + 1, type(uint32).max);
uint32 referenceBlockNumber = r.Uint32();
cheats.warp(uint(referenceTimestamp));
bytes32 globalTableRoot = bytes32(r.Uint256(1, type(uint).max));
mockCertificate.messageHash = operatorTableUpdater.getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp);
mockCertificate.messageHash =
operatorTableUpdater.getGlobalTableUpdateMessageHash(globalTableRoot, referenceTimestamp, referenceBlockNumber);
_setIsValidCertificate(mockCertificate, true);

// Expect the global table root to be updated
cheats.expectEmit(true, true, true, true);
emit NewGlobalTableRoot(referenceTimestamp, globalTableRoot);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, globalTableRoot, referenceTimestamp);
operatorTableUpdater.confirmGlobalTableRoot(mockCertificate, globalTableRoot, referenceTimestamp, referenceBlockNumber);

// Expect the global table root to be updated
assertEq(operatorTableUpdater.getGlobalTableRootByTimestamp(referenceTimestamp), globalTableRoot);
assertEq(operatorTableUpdater.getCurrentGlobalTableRoot(), globalTableRoot);
assertEq(operatorTableUpdater.getLatestReferenceTimestamp(), referenceTimestamp);
assertEq(operatorTableUpdater.getLatestReferenceBlockNumber(), referenceBlockNumber);
assertEq(operatorTableUpdater.getReferenceBlockNumberByTimestamp(referenceTimestamp), referenceBlockNumber);
assertEq(operatorTableUpdater.getReferenceTimestampByBlockNumber(referenceBlockNumber), referenceTimestamp);
}
}

Expand Down