diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 08bc12a0..16200188 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -248,12 +248,12 @@ "sourceCodeHash": "0xb9786dc79b4b494d81905235f7bda044c5b1a98ad82416829f5f6948be1175cc" }, "src/multiproof/tee/SystemConfigGlobal.sol:SystemConfigGlobal": { - "initCodeHash": "0x76da4f2a736d7a39a01720e5d900a85fcaa60ba0430fcacbb8ab367f55ba5411", - "sourceCodeHash": "0xa6261402efe0105e2a4f9369818bafb4e65515e51850b44d47504151e1c39d01" + "initCodeHash": "0xee219c003a6af440b447e214e43d520e802001ae3d557262a7921ca3d57ebddf", + "sourceCodeHash": "0xe350108585e0855f10bac20d0e8894b3de9afe3be413908b4c59a1036e7a9842" }, "src/multiproof/tee/SystemConfigGlobal.sol:SystemConfigGlobal:dispute": { - "initCodeHash": "0xfae3a71157f3c64a7bda037ec116bf6e7265099397d9bcad22c01cc1f029ed7d", - "sourceCodeHash": "0xa6261402efe0105e2a4f9369818bafb4e65515e51850b44d47504151e1c39d01" + "initCodeHash": "0x8ae045f0121d2c63ab6f0a830be842aaf0445096bfabe29d85cfd9bd38b40565", + "sourceCodeHash": "0xe350108585e0855f10bac20d0e8894b3de9afe3be413908b4c59a1036e7a9842" }, "src/multiproof/tee/TEEVerifier.sol:TEEVerifier": { "initCodeHash": "0x78317d9088a26523d938ce0d88ed0134abc60292c0cdb3f8a6a9530638fd9e9a", diff --git a/src/multiproof/mocks/MockDevSystemConfigGlobal.sol b/src/multiproof/mocks/MockDevSystemConfigGlobal.sol index dfd61fa5..4dc6f715 100644 --- a/src/multiproof/mocks/MockDevSystemConfigGlobal.sol +++ b/src/multiproof/mocks/MockDevSystemConfigGlobal.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; import { INitroEnclaveVerifier } from "lib/aws-nitro-enclave-attestation/contracts/src/interfaces/INitroEnclaveVerifier.sol"; +import { EnumerableSetLib } from "@solady-v0.0.245/utils/EnumerableSetLib.sol"; import { SystemConfigGlobal } from "src/multiproof/tee/SystemConfigGlobal.sol"; @@ -12,6 +13,8 @@ import { SystemConfigGlobal } from "src/multiproof/tee/SystemConfigGlobal.sol"; /// @dev This contract adds addDevSigner() which bypasses AWS Nitro attestation verification. /// DO NOT deploy this contract to production networks. contract DevSystemConfigGlobal is SystemConfigGlobal { + using EnumerableSetLib for EnumerableSetLib.AddressSet; + constructor(INitroEnclaveVerifier nitroVerifier) SystemConfigGlobal(nitroVerifier) { } /// @notice Registers a signer for testing (bypasses attestation verification). @@ -20,6 +23,7 @@ contract DevSystemConfigGlobal is SystemConfigGlobal { /// @param pcr0Hash The PCR0 hash to associate with this signer. function addDevSigner(address signer, bytes32 pcr0Hash) external onlyOwner { signerPCR0[signer] = pcr0Hash; + _registeredSigners.add(signer); emit SignerRegistered(signer, pcr0Hash); } } diff --git a/src/multiproof/tee/SystemConfigGlobal.sol b/src/multiproof/tee/SystemConfigGlobal.sol index 3cce9895..c15b876a 100644 --- a/src/multiproof/tee/SystemConfigGlobal.sol +++ b/src/multiproof/tee/SystemConfigGlobal.sol @@ -11,6 +11,7 @@ import { } from "lib/aws-nitro-enclave-attestation/contracts/src/interfaces/INitroEnclaveVerifier.sol"; import { OwnableManagedUpgradeable } from "lib/op-enclave/contracts/src/OwnableManagedUpgradeable.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { EnumerableSetLib } from "@solady-v0.0.245/utils/EnumerableSetLib.sol"; /// @title SystemConfigGlobal /// @notice Manages TEE signer registration via ZK-verified AWS Nitro attestation. @@ -19,6 +20,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// Each signer is associated with the PCR0 (enclave image hash) from their attestation, /// which allows TEEVerifier to validate that a signer was registered with a specific image. contract SystemConfigGlobal is OwnableManagedUpgradeable, ISemver { + using EnumerableSetLib for EnumerableSetLib.AddressSet; /// @notice Maximum age of an attestation document (60 minutes), in seconds. uint256 public constant MAX_AGE = 60 minutes; @@ -41,6 +43,11 @@ contract SystemConfigGlobal is OwnableManagedUpgradeable, ISemver { /// @notice Mapping of whether an address is a valid proposer. mapping(address => bool) public isValidProposer; + /// @notice Enumerable set of all currently registered signer addresses. + /// @dev Kept in sync with `signerPCR0`: add on register, remove on deregister. + /// Enables O(1) on-chain enumeration via `getRegisteredSigners()`. + EnumerableSetLib.AddressSet internal _registeredSigners; + /// @notice Emitted when a signer is registered. event SignerRegistered(address indexed signer, bytes32 indexed pcr0); @@ -128,6 +135,7 @@ contract SystemConfigGlobal is OwnableManagedUpgradeable, ISemver { address enclaveAddress = address(uint160(uint256(publicKeyHash))); signerPCR0[enclaveAddress] = pcr0Hash; + _registeredSigners.add(enclaveAddress); emit SignerRegistered(enclaveAddress, pcr0Hash); } @@ -135,6 +143,7 @@ contract SystemConfigGlobal is OwnableManagedUpgradeable, ISemver { /// @param signer The address of the signer to deregister. function deregisterSigner(address signer) external onlyOwnerOrManager { delete signerPCR0[signer]; + _registeredSigners.remove(signer); emit SignerDeregistered(signer); } @@ -145,6 +154,14 @@ contract SystemConfigGlobal is OwnableManagedUpgradeable, ISemver { return signerPCR0[signer] != bytes32(0); } + /// @notice Returns all currently registered signer addresses. + /// @dev Reads directly from the on-chain enumerable set — no event scanning required. + /// The order of addresses in the returned array is not guaranteed. + /// @return An array of all registered signer addresses. + function getRegisteredSigners() external view returns (address[] memory) { + return _registeredSigners.values(); + } + /// @notice Initializes the contract with owner and manager. /// @param initialOwner The initial owner address. /// @param initialManager The initial manager address. @@ -155,9 +172,9 @@ contract SystemConfigGlobal is OwnableManagedUpgradeable, ISemver { } /// @notice Semantic version. - /// @custom:semver 0.1.0 + /// @custom:semver 0.2.0 function version() public pure virtual returns (string memory) { - return "0.1.0"; + return "0.2.0"; } /// @dev Finds PCR0 (index 0) in the PCR array and returns its keccak256 hash. diff --git a/test/multiproof/SystemConfigGlobal.t.sol b/test/multiproof/SystemConfigGlobal.t.sol index 10636cbf..c57409d4 100644 --- a/test/multiproof/SystemConfigGlobal.t.sol +++ b/test/multiproof/SystemConfigGlobal.t.sol @@ -67,7 +67,7 @@ contract SystemConfigGlobalTest is Test { function testInitialization() public view { assertEq(systemConfigGlobal.owner(), owner); assertEq(systemConfigGlobal.manager(), manager); - assertEq(systemConfigGlobal.version(), "0.1.0"); + assertEq(systemConfigGlobal.version(), "0.2.0"); } // ============ PCR0 Registration Tests ============ @@ -394,4 +394,120 @@ contract SystemConfigGlobalTest is Test { assertEq(systemConfigGlobal.signerPCR0(signer2), pcr0Hash2); assertEq(systemConfigGlobal.signerPCR0(signer3), pcr0Hash3); } + + // ============ getRegisteredSigners Tests ============ + + function testGetRegisteredSignersEmpty() public view { + address[] memory signers = systemConfigGlobal.getRegisteredSigners(); + assertEq(signers.length, 0); + } + + function testGetRegisteredSignersAfterRegister() public { + address signer = makeAddr("signer"); + bytes32 signerPcr0 = keccak256("pcr0"); + + vm.prank(owner); + systemConfigGlobal.addDevSigner(signer, signerPcr0); + + address[] memory signers = systemConfigGlobal.getRegisteredSigners(); + assertEq(signers.length, 1); + assertEq(signers[0], signer); + } + + function testGetRegisteredSignersAfterDeregister() public { + address signer = makeAddr("signer"); + bytes32 signerPcr0 = keccak256("pcr0"); + + vm.prank(owner); + systemConfigGlobal.addDevSigner(signer, signerPcr0); + + assertEq(systemConfigGlobal.getRegisteredSigners().length, 1); + + vm.prank(owner); + systemConfigGlobal.deregisterSigner(signer); + + address[] memory signers = systemConfigGlobal.getRegisteredSigners(); + assertEq(signers.length, 0); + } + + function testGetRegisteredSignersMultiple() public { + address signer1 = makeAddr("signer-1"); + address signer2 = makeAddr("signer-2"); + address signer3 = makeAddr("signer-3"); + + bytes32 sharedPcr0 = keccak256("pcr0"); + + vm.startPrank(owner); + systemConfigGlobal.addDevSigner(signer1, sharedPcr0); + systemConfigGlobal.addDevSigner(signer2, sharedPcr0); + systemConfigGlobal.addDevSigner(signer3, sharedPcr0); + vm.stopPrank(); + + address[] memory signers = systemConfigGlobal.getRegisteredSigners(); + assertEq(signers.length, 3); + + // Verify all three are present (order not guaranteed) + bool foundSigner1; + bool foundSigner2; + bool foundSigner3; + for (uint256 i = 0; i < signers.length; i++) { + if (signers[i] == signer1) foundSigner1 = true; + if (signers[i] == signer2) foundSigner2 = true; + if (signers[i] == signer3) foundSigner3 = true; + } + assertTrue(foundSigner1); + assertTrue(foundSigner2); + assertTrue(foundSigner3); + } + + function testGetRegisteredSignersConsistencyAfterMixedOperations() public { + address signer1 = makeAddr("signer-1"); + address signer2 = makeAddr("signer-2"); + address signer3 = makeAddr("signer-3"); + + bytes32 sharedPcr0 = keccak256("pcr0"); + + // Register three signers + vm.startPrank(owner); + systemConfigGlobal.addDevSigner(signer1, sharedPcr0); + systemConfigGlobal.addDevSigner(signer2, sharedPcr0); + systemConfigGlobal.addDevSigner(signer3, sharedPcr0); + vm.stopPrank(); + + assertEq(systemConfigGlobal.getRegisteredSigners().length, 3); + + // Deregister the middle one + vm.prank(manager); + systemConfigGlobal.deregisterSigner(signer2); + + address[] memory signers = systemConfigGlobal.getRegisteredSigners(); + assertEq(signers.length, 2); + + // Mapping and set stay consistent + for (uint256 i = 0; i < signers.length; i++) { + assertTrue(systemConfigGlobal.isValidSigner(signers[i])); + assertNotEq(signers[i], signer2); + } + + // Deregistered signer not in mapping either + assertFalse(systemConfigGlobal.isValidSigner(signer2)); + assertEq(systemConfigGlobal.signerPCR0(signer2), bytes32(0)); + } + + function testGetRegisteredSignersDeregisterIdempotent() public { + address signer = makeAddr("signer"); + bytes32 signerPcr0 = keccak256("pcr0"); + + vm.prank(owner); + systemConfigGlobal.addDevSigner(signer, signerPcr0); + + vm.prank(owner); + systemConfigGlobal.deregisterSigner(signer); + + // Deregistering again should not revert and set should still be empty + vm.prank(owner); + systemConfigGlobal.deregisterSigner(signer); + + assertEq(systemConfigGlobal.getRegisteredSigners().length, 0); + } }