@inco/lightning 0.8.0-devnet-7 → 0.8.0-devnet-8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inco/lightning",
3
- "version": "0.8.0-devnet-7",
3
+ "version": "0.8.0-devnet-8",
4
4
  "repository": "https://github.com/Inco-fhevm/inco-monorepo",
5
5
  "files": [
6
6
  "src/",
@@ -54,6 +54,7 @@ contract IncoLightning is
54
54
  /// @param owner The address that will own this contract and can authorize upgrades
55
55
  function initialize(address owner) public initializer {
56
56
  __Ownable_init(owner);
57
+ _setAcceptedVersion(2, true);
57
58
  }
58
59
 
59
60
  /// @notice Withdraws accumulated protocol fees to the owner
@@ -63,6 +64,18 @@ contract IncoLightning is
63
64
  _withdrawFeesTo(owner());
64
65
  }
65
66
 
67
+ /// @notice Adds a version to the accepted input versions whitelist.
68
+ /// @param version The version number to accept (must be non-negative).
69
+ function addAcceptedVersion(uint16 version) external onlyOwner {
70
+ _setAcceptedVersion(version, true);
71
+ }
72
+
73
+ /// @notice Removes a version from the accepted input versions whitelist.
74
+ /// @param version The version number to remove.
75
+ function removeAcceptedVersion(uint16 version) external onlyOwner {
76
+ _setAcceptedVersion(version, false);
77
+ }
78
+
66
79
  /// @notice Required for CreateX deterministic deployment
67
80
  /// @dev Empty fallback allows the contract to be deployed via CreateX's create2 mechanism
68
81
  fallback() external {}
@@ -21,4 +21,8 @@ interface IIncoLightning is
21
21
 
22
22
  function withdrawFees() external;
23
23
 
24
+ function addAcceptedVersion(uint16 version) external;
25
+
26
+ function removeAcceptedVersion(uint16 version) external;
27
+
24
28
  }
@@ -24,15 +24,67 @@ error ExternalHandleDoesNotMatchComputedHandle(
24
24
  address aclAddress,
25
25
  address userAddress,
26
26
  address contractAddress,
27
- int32 version
27
+ uint16 version
28
28
  );
29
29
 
30
+ /// @title EncryptedInputStorage
31
+ /// @notice Storage for EncryptedInput contract using ERC-7201 namespaced storage pattern
32
+ /// @dev Stores a whitelist of accepted versions for encrypted inputs.
33
+ abstract contract EncryptedInputStorage {
34
+
35
+ /// @notice Storage for version whitelist, using ERC-7201 namespaced storage.
36
+ struct StorageForEncryptedInput {
37
+ /// @notice Mapping of accepted version numbers for encrypted inputs.
38
+ /// @dev Only versions in this mapping are accepted for new inputs.
39
+ mapping(uint16 => bool) acceptedVersions;
40
+ }
41
+
42
+ /// @notice Storage slot location using keccak256 of a unique namespace string
43
+ bytes32 private constant ENCRYPTED_INPUT_STORAGE_LOCATION = keccak256("inco.storage.EncryptedInput");
44
+
45
+ /// @notice Retrieves the storage struct from its dedicated slot
46
+ /// @dev Uses assembly to directly access the storage slot for gas efficiency
47
+ /// @return $ Reference to the StorageForEncryptedInput struct
48
+ function _getEncryptedInputStorage() internal pure returns (StorageForEncryptedInput storage $) {
49
+ bytes32 loc = ENCRYPTED_INPUT_STORAGE_LOCATION;
50
+ assembly {
51
+ $.slot := loc
52
+ }
53
+ }
54
+
55
+ }
56
+
30
57
  /// @title EncryptedInput
31
58
  /// @notice Handles the submission of client-encrypted values to create on-chain encrypted handles.
32
59
  /// @dev Users encrypt values client-side and submit them with a prepended handle as a checksum.
33
60
  /// The contract verifies the handle matches the on-chain computation to ensure context consistency.
34
61
  /// This is a paid operation - users must send ETH to cover processing costs.
35
- abstract contract EncryptedInput is IEncryptedInput, BaseAccessControlList, HandleGeneration, Fee {
62
+ abstract contract EncryptedInput is
63
+ IEncryptedInput,
64
+ EncryptedInputStorage,
65
+ BaseAccessControlList,
66
+ HandleGeneration,
67
+ Fee
68
+ {
69
+
70
+ /// @notice Sets whether a version is accepted.
71
+ /// @param version The version number (must be non-negative).
72
+ /// @param accepted Whether the version should be accepted.
73
+ function _setAcceptedVersion(uint16 version, bool accepted) internal {
74
+ _getEncryptedInputStorage().acceptedVersions[version] = accepted;
75
+ if (accepted) {
76
+ emit VersionAccepted(version);
77
+ } else {
78
+ emit VersionRemoved(version);
79
+ }
80
+ }
81
+
82
+ /// @notice Checks whether a version is accepted.
83
+ /// @param version The version number to check.
84
+ /// @return True if the version is in the whitelist.
85
+ function isAcceptedVersion(uint16 version) public view returns (bool) {
86
+ return _getEncryptedInputStorage().acceptedVersions[version];
87
+ }
36
88
 
37
89
  /// @notice Emitted when a new encrypted input is submitted.
38
90
  /// @param result The generated handle for the encrypted value.
@@ -44,7 +96,7 @@ abstract contract EncryptedInput is IEncryptedInput, BaseAccessControlList, Hand
44
96
  bytes32 indexed result,
45
97
  address indexed contractAddress,
46
98
  address indexed user,
47
- int32 version,
99
+ uint16 version,
48
100
  bytes ciphertext,
49
101
  uint256 eventId
50
102
  );
@@ -111,10 +163,13 @@ abstract contract EncryptedInput is IEncryptedInput, BaseAccessControlList, Hand
111
163
  function _newInput(bytes calldata input, address user, ETypes inputType) private returns (bytes32 handle) {
112
164
  // Since there is no sensible way to handle abi.decode errors (https://github.com/argotorg/solidity/issues/10381)
113
165
  // at least fail early on a conservative minimum length
114
- require(input.length >= 68, "Input too short, should be at least 68 bytes");
166
+ require(input.length >= 68, InputLengthTooShort(input.length));
115
167
  // Parse the version from the first 4 bytes.
116
168
  bytes4 prefix = bytes4(input[:4]);
117
- int32 version = int32(uint32(prefix));
169
+ uint16 version = uint16(uint32(prefix));
170
+
171
+ // Reject versions not in the whitelist
172
+ require(isAcceptedVersion(version), InvalidInputVersion(version));
118
173
  // Remove external handle prepended to input as a checksum
119
174
  (bytes32 externalHandle, bytes memory ciphertext) = abi.decode(input[4:], (bytes32, bytes));
120
175
  handle = getInputHandle(ciphertext, user, msg.sender, version, inputType);
@@ -23,8 +23,6 @@ contract TEELifecycleStorage {
23
23
 
24
24
  struct StorageForTeeLifecycle {
25
25
  IQuoteVerifier quoteVerifier;
26
- BootstrapResult verifiedBootstrapResult;
27
- bool bootstrapComplete;
28
26
  // @notice The list of approved TEE versions, each item is the MR_AGGREGATED 32 bytes
29
27
  // @dev The index of the TEE version is the version number
30
28
  bytes32[] approvedTeeVersions;
@@ -70,8 +68,6 @@ abstract contract TEELifecycle is
70
68
  // Events
71
69
  // @notice A new MR_AGGREGATED has been approved
72
70
  event NewTEEVersionApproved(uint256 indexed version, bytes32 indexed mrAggregated);
73
- // @notice A previously approved TEE version has been removed
74
- event TEEVersionRemoved(bytes32 indexed mrAggregated);
75
71
  event NewCovalidatorAdded(address covalidatorAddress, bytes quote);
76
72
  event BootstrapStageComplete(address indexed newEoaSigner, BootstrapResult bootstrapResult);
77
73
  // @notice Emitted to prove that an EOA has upgraded their TDX to a new
@@ -213,28 +209,6 @@ abstract contract TEELifecycle is
213
209
  emit NewTEEVersionApproved($.approvedTeeVersions.length - 1, newMrAggregated);
214
210
  }
215
211
 
216
- /**
217
- * @notice Removes a previously approved TEE version from the contract state
218
- * @param mrAggregated - The MR_AGGREGATED bytes of the TEE version to remove
219
- */
220
- function removeApprovedTeeVersion(bytes32 mrAggregated) public onlyOwner {
221
- StorageForTeeLifecycle storage $ = getTeeLifecycleStorage();
222
- bool found = false;
223
- for (uint256 i = 0; i < $.approvedTeeVersions.length; i++) {
224
- if ($.approvedTeeVersions[i] == mrAggregated) {
225
- // Shift all elements after index i to the left to preserve insertion order
226
- for (uint256 j = i; j < $.approvedTeeVersions.length - 1; j++) {
227
- $.approvedTeeVersions[j] = $.approvedTeeVersions[j + 1];
228
- }
229
- $.approvedTeeVersions.pop();
230
- found = true;
231
- break;
232
- }
233
- }
234
- require(found, TEEVersionNotFound());
235
- emit TEEVersionRemoved(mrAggregated);
236
- }
237
-
238
212
  function _isApprovedTeeVersion(bytes32 newMrAggregated) internal view returns (bool) {
239
213
  StorageForTeeLifecycle storage $ = getTeeLifecycleStorage();
240
214
  for (uint256 i = 0; i < $.approvedTeeVersions.length; i++) {
@@ -283,6 +257,40 @@ abstract contract TEELifecycle is
283
257
  return getTeeLifecycleStorage().networkPubkey.length > 0;
284
258
  }
285
259
 
260
+ /**
261
+ * @notice Resets the contract state to the initial state.
262
+ * @dev This function performs a destructive operation and should be used carefully. In particular:
263
+ * 1) This function will remove all signers and reset the network pubkey and approved TEE versions, requiring a full bootstrap to restore normal operation.
264
+ * 2) The contract effectively enters a maintenance mode where any operations that depend on signers or the network key will fail until re-bootstrap is completed.
265
+ * 3) This function is intended to be called only as part of a coordinated key rotation or recovery process, with off-chain components updated accordingly.
266
+ * @dev This function is only callable by the owner.
267
+ */
268
+ function reset() public onlyOwner {
269
+ getTeeLifecycleStorage().networkPubkey = bytes("");
270
+ getTeeLifecycleStorage().approvedTeeVersions = new bytes32[](0);
271
+
272
+ // Directly access SignatureVerifier storage to reset signers and threshold
273
+ // We bypass the normal functions because:
274
+ // 1. removeSigner is external and has threshold validation
275
+ // 2. setThreshold requires newThreshold > 0
276
+ // We emit RemovedSignatureVerifier and ThresholdChanged so off-chain
277
+ // systems tracking signer changes via events stay in sync.
278
+ StorageForSigVerifier storage sigStorage = getSigVerifierStorage();
279
+ uint256 oldThreshold = sigStorage.threshold;
280
+ if (oldThreshold > 0) {
281
+ emit ThresholdChanged(oldThreshold, 0);
282
+ }
283
+ sigStorage.threshold = 0;
284
+ // In theory, this could revert if there are many signers. But in
285
+ // practice, this should not be a problem.
286
+ while (sigStorage.signers.length > 0) {
287
+ address signer = sigStorage.signers[sigStorage.signers.length - 1];
288
+ sigStorage.isSigner[signer] = false;
289
+ sigStorage.signers.pop();
290
+ emit RemovedSignatureVerifier(signer);
291
+ }
292
+ }
293
+
286
294
  /**
287
295
  * @notice From https://github.com/automata-network/automata-dcap-attestation/blob/evm-v1.0.0/evm/contracts/AttestationEntrypointBase.sol#L103
288
296
  * @notice full on-chain verification for an attestation
@@ -381,7 +389,7 @@ abstract contract TEELifecycle is
381
389
 
382
390
  /**
383
391
  * @notice Computes the MR_AGGREGATED from the TD10 report body. It is defined as:
384
- * KECCAK256(mrTd ++ rtMr0 ++ rtMr1 ++ rtMr2 ++ rtMr3)
392
+ * as KECCAK256(abi.encode(MRTD, RTMR0, RTMR1, RTMR2))
385
393
  * @param mrTd - The MR_TD bytes
386
394
  * @param rtMr0 - The RTMR0 bytes
387
395
  * @param rtMr1 - The RTMR1 bytes
@@ -393,7 +401,7 @@ abstract contract TEELifecycle is
393
401
  pure
394
402
  returns (bytes32)
395
403
  {
396
- bytes memory message = abi.encodePacked(mrTd, rtMr0, rtMr1, rtMr2);
404
+ bytes memory message = abi.encode(mrTd, rtMr0, rtMr1, rtMr2);
397
405
  return keccak256(message);
398
406
  }
399
407
 
@@ -5,6 +5,12 @@ import {euint256, ebool, eaddress} from "../../Types.sol";
5
5
 
6
6
  interface IEncryptedInput {
7
7
 
8
+ error InvalidInputVersion(uint16 version);
9
+ error InputLengthTooShort(uint256 length);
10
+
11
+ event VersionAccepted(uint16 version);
12
+ event VersionRemoved(uint16 version);
13
+
8
14
  function newEuint256(bytes calldata ciphertext, address user) external payable returns (euint256 newValue);
9
15
  function newEbool(bytes calldata ciphertext, address user) external payable returns (ebool newValue);
10
16
  function newEaddress(bytes calldata ciphertext, address user) external payable returns (eaddress newValue);
@@ -41,7 +41,7 @@ contract HandleGeneration is IHandleGeneration, HandleMetadata {
41
41
  bytes memory ciphertext,
42
42
  address user,
43
43
  address contractAddress,
44
- int32 version,
44
+ uint16 version,
45
45
  ETypes inputType
46
46
  ) internal view returns (bytes32 generatedHandle) {
47
47
  return getInputHandle(ciphertext, address(this), user, contractAddress, version, inputType);
@@ -61,7 +61,7 @@ contract HandleGeneration is IHandleGeneration, HandleMetadata {
61
61
  address executorAddress,
62
62
  address user,
63
63
  address contractAddress,
64
- int32 version,
64
+ uint16 version,
65
65
  ETypes inputType
66
66
  ) internal view returns (bytes32 generatedHandle) {
67
67
  // Here we ensure that our hashing scheme is binary-compatible between IncoLightning and IncoFhevm, this helps
@@ -17,7 +17,7 @@ contract TestSignatureVerifier is TestUtils, SignatureVerifier {
17
17
  // Helper function to create sorted signatures
18
18
  // Takes a digest and an array of private keys, generates signatures,
19
19
  // and returns them sorted by signer address in ascending order
20
- function getSortedSignatures(bytes32 digest, uint256[] memory privKeys) internal returns (bytes[] memory) {
20
+ function getSortedSignatures(bytes32 digest, uint256[] memory privKeys) internal pure returns (bytes[] memory) {
21
21
  bytes[] memory signatures = new bytes[](privKeys.length);
22
22
  address[] memory signers = new address[](privKeys.length);
23
23
 
@@ -22,6 +22,7 @@ import {VerifierAddressGetter} from "../primitives/VerifierAddressGetter.sol";
22
22
  import {FEE} from "../Fee.sol";
23
23
  import {HandleAlreadyExists} from "../../Errors.sol";
24
24
  import {ExternalHandleDoesNotMatchComputedHandle} from "../EncryptedInput.sol";
25
+ import {IEncryptedInput} from "../interfaces/IEncryptedInput.sol";
25
26
 
26
27
  contract TestHandleMetadata is
27
28
  EIP712,
@@ -32,7 +33,9 @@ contract TestHandleMetadata is
32
33
  EncryptedInput
33
34
  {
34
35
 
35
- constructor() EIP712("", "") VerifierAddressGetter(address(0)) {}
36
+ constructor() EIP712("", "") VerifierAddressGetter(address(0)) {
37
+ _setAcceptedVersion(2, true);
38
+ }
36
39
 
37
40
  function testTypeAssignment() public pure {
38
41
  bytes32 someHandle = bytes32(keccak256("someHandle"));
@@ -105,16 +108,16 @@ contract TestHandleMetadata is
105
108
  {
106
109
  // We need a single word here to get correct encoding
107
110
  bytes memory ciphertext = abi.encode(word);
111
+ uint16 version = 2; // version - X-Wing
108
112
  bytes32 handle = getInputHandle(
109
113
  ciphertext,
110
114
  address(this),
111
115
  user,
112
116
  contractAddress,
113
- 0,
114
- /* unspecified */
117
+ version, // version - X-Wing
115
118
  inputType
116
119
  );
117
- input = abi.encodePacked(int32(0), abi.encode(handle, ciphertext));
120
+ input = abi.encodePacked(uint32(version), abi.encode(handle, ciphertext));
118
121
  }
119
122
 
120
123
  // ============ Tests for HandleGeneration functions ============
@@ -155,6 +158,53 @@ contract TestHandleMetadata is
155
158
  return newInputNotPaying(input, user, inputType);
156
159
  }
157
160
 
161
+ function testNewInputUnwhitelistedVersion() public {
162
+ address self = address(this);
163
+ bytes32 ciphertextData = keccak256(abi.encodePacked("unwhitelisted_version"));
164
+ bytes memory ciphertext = abi.encode(ciphertextData);
165
+ uint16 unwhitelistedVersion = 0;
166
+ bytes32 handle =
167
+ getInputHandle(ciphertext, address(this), self, address(this), unwhitelistedVersion, ETypes.Uint256);
168
+ bytes memory badInput = abi.encodePacked(uint32(unwhitelistedVersion), abi.encode(handle, ciphertext));
169
+
170
+ vm.expectRevert(abi.encodeWithSelector(IEncryptedInput.InvalidInputVersion.selector, unwhitelistedVersion));
171
+ this.exposedNewInputNotPaying(badInput, self, ETypes.Uint256);
172
+ }
173
+
174
+ function testNewInputVersionAddedThenRemoved() public {
175
+ address self = address(this);
176
+ bytes32 ciphertextData = keccak256(abi.encodePacked("version_toggle"));
177
+ bytes memory ciphertext = abi.encode(ciphertextData);
178
+ uint16 version = 3;
179
+
180
+ bytes32 handle = getInputHandle(ciphertext, address(this), self, address(this), version, ETypes.Uint256);
181
+ bytes memory input = abi.encodePacked(uint32(version), abi.encode(handle, ciphertext));
182
+
183
+ // Version 3 is not accepted yet
184
+ vm.expectRevert(abi.encodeWithSelector(IEncryptedInput.InvalidInputVersion.selector, version));
185
+ this.exposedNewInputNotPaying(input, self, ETypes.Uint256);
186
+
187
+ // Whitelist version 3
188
+ _setAcceptedVersion(3, true);
189
+
190
+ // Now it should succeed
191
+ bytes32 resultHandle = this.exposedNewInputNotPaying(input, self, ETypes.Uint256);
192
+ assert(typeOf(resultHandle) == ETypes.Uint256);
193
+
194
+ // Remove version 3
195
+ _setAcceptedVersion(3, false);
196
+
197
+ // Use different ciphertext to avoid HandleAlreadyExists
198
+ bytes32 ciphertextData2 = keccak256(abi.encodePacked("version_toggle_2"));
199
+ bytes memory ciphertext2 = abi.encode(ciphertextData2);
200
+ bytes32 handle2 = getInputHandle(ciphertext2, address(this), self, address(this), version, ETypes.Uint256);
201
+ bytes memory input2 = abi.encodePacked(uint32(version), abi.encode(handle2, ciphertext2));
202
+
203
+ // Should revert again
204
+ vm.expectRevert(abi.encodeWithSelector(IEncryptedInput.InvalidInputVersion.selector, version));
205
+ this.exposedNewInputNotPaying(input2, self, ETypes.Uint256);
206
+ }
207
+
158
208
  function testNewInputNotPaying() public {
159
209
  address self = address(this);
160
210
  bytes32 ciphertextData = keccak256(abi.encodePacked("notpaying_ciphertext"));
@@ -172,10 +222,10 @@ contract TestHandleMetadata is
172
222
  returns (bytes memory input)
173
223
  {
174
224
  bytes memory ciphertext = abi.encode(word);
175
- int32 version = 0; // unspecified
225
+ uint16 version = 2; // version - X-Wing
176
226
  // For external calls via this., msg.sender is address(this)
177
227
  bytes32 handle = getInputHandle(ciphertext, address(this), user, address(this), version, inputType);
178
- input = abi.encodePacked(version, abi.encode(handle, ciphertext));
228
+ input = abi.encodePacked(uint32(version), abi.encode(handle, ciphertext));
179
229
  }
180
230
 
181
231
  // ============ Tests for EncryptedInput error branches ============
@@ -184,7 +234,7 @@ contract TestHandleMetadata is
184
234
  address self = address(this);
185
235
  // Input less than 64 bytes should revert
186
236
  bytes memory shortInput = hex"deadbeef";
187
- vm.expectRevert("Input too short, should be at least 68 bytes");
237
+ vm.expectRevert(abi.encodeWithSelector(IEncryptedInput.InputLengthTooShort.selector, shortInput.length));
188
238
  this.exposedNewInputNotPaying(shortInput, self, ETypes.Uint256);
189
239
  }
190
240
 
@@ -194,8 +244,8 @@ contract TestHandleMetadata is
194
244
  bytes memory ciphertext = abi.encode(ciphertextData);
195
245
  // Create input with wrong handle (just a random bytes32)
196
246
  bytes32 wrongHandle = bytes32(uint256(12345));
197
- int32 version = 0; // unspecified
198
- bytes memory badInput = abi.encodePacked(version, abi.encode(wrongHandle, ciphertext));
247
+ uint16 version = 2; // version - X-Wing
248
+ bytes memory badInput = abi.encodePacked(uint32(version), abi.encode(wrongHandle, ciphertext));
199
249
 
200
250
  // Compute the expected handle for the error message
201
251
  bytes32 expectedHandle = getInputHandle(ciphertext, address(this), self, address(this), version, ETypes.Uint256);
@@ -19,16 +19,16 @@ contract FakeIncoInfraBase is TestUtils, KVStore, HandleGeneration {
19
19
  {
20
20
  // We need a single word here to get correct encoding
21
21
  bytes memory ciphertext = abi.encode(word);
22
+ uint16 version = 2; // version - X-Wing
22
23
  bytes32 handle = getInputHandle(
23
24
  ciphertext,
24
25
  address(inco),
25
26
  user,
26
27
  contractAddress,
27
- 0,
28
- /* unspecified */
28
+ version, // version - X-Wing
29
29
  inputType
30
30
  );
31
- input = abi.encodePacked(int32(0), abi.encode(handle, ciphertext));
31
+ input = abi.encodePacked(uint32(version), abi.encode(handle, ciphertext));
32
32
  }
33
33
 
34
34
  function fakePrepareEuint256Ciphertext(uint256 value, address userAddress, address contractAddress)
@@ -60,7 +60,8 @@ contract MockRemoteAttestation is TestUtils {
60
60
  hex"010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101";
61
61
  // See DEFAULT_MR_AGGREGATED in attestation/src/remote_attestation.rs to
62
62
  // see the calculation of the default value.
63
- mrAggregated = hex"c3a67bac251d4946d7b17481d39631676042fe3afab06e70c22105ad8383c19f";
63
+ // Note: This uses abi.encode (not encodePacked) to avoid hash collision vulnerabilities.
64
+ mrAggregated = hex"3d48a1faa8620d86ae037f4fd6746987733d085314b3cd5d5d074ade8bab6ebd";
64
65
  bootstrapResult = BootstrapResult({networkPubkey: networkPubkey});
65
66
 
66
67
  quote = createQuote(mrtd, signer);
@@ -24,7 +24,8 @@ contract TEELifecycleMockTest is MockRemoteAttestation, TEELifecycle {
24
24
  hex"010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101";
25
25
  // See DEFAULT_MR_AGGREGATED in attestation/src/remote_attestation.rs to
26
26
  // see the calculation of the default value.
27
- bytes32 testMrAggregated = hex"c3a67bac251d4946d7b17481d39631676042fe3afab06e70c22105ad8383c19f";
27
+ // Note: This uses abi.encode (not encodePacked) to avoid hash collision vulnerabilities.
28
+ bytes32 testMrAggregated = hex"3d48a1faa8620d86ae037f4fd6746987733d085314b3cd5d5d074ade8bab6ebd";
28
29
 
29
30
  function setUp() public {
30
31
  getTeeLifecycleStorage().quoteVerifier = new FakeQuoteVerifier();
@@ -133,61 +134,6 @@ contract TEELifecycleMockTest is MockRemoteAttestation, TEELifecycle {
133
134
  vm.stopPrank();
134
135
  }
135
136
 
136
- function testRemoveApprovedTeeVersionPreservesOrder() public {
137
- bytes32 mrAggregated1 = hex"1111111111111111111111111111111111111111111111111111111111111111";
138
- bytes32 mrAggregated2 = hex"2222222222222222222222222222222222222222222222222222222222222222";
139
- bytes32 mrAggregated3 = hex"3333333333333333333333333333333333333333333333333333333333333333";
140
-
141
- vm.startPrank(this.owner());
142
-
143
- // Add three versions
144
- this.approveNewTeeVersion(mrAggregated1);
145
- this.approveNewTeeVersion(mrAggregated2);
146
- this.approveNewTeeVersion(mrAggregated3);
147
-
148
- // Verify all exist in order
149
- assertEq(this.approvedTeeVersions(0), mrAggregated1);
150
- assertEq(this.approvedTeeVersions(1), mrAggregated2);
151
- assertEq(this.approvedTeeVersions(2), mrAggregated3);
152
-
153
- // Remove the middle one (mrAggregated2)
154
- this.removeApprovedTeeVersion(mrAggregated2);
155
-
156
- // Verify insertion order is preserved: mrAggregated1 stays at 0, mrAggregated3 shifts to 1
157
- assertEq(this.approvedTeeVersions(0), mrAggregated1);
158
- assertEq(this.approvedTeeVersions(1), mrAggregated3);
159
-
160
- // Verify index 2 is now out of bounds
161
- vm.expectRevert(TEELifecycle.IndexOutOfBounds.selector);
162
- this.approvedTeeVersions(2);
163
-
164
- vm.stopPrank();
165
- }
166
-
167
- function testRemoveApprovedTeeVersionNotFound() public {
168
- bytes32 nonExistentMrAggregated = hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
169
-
170
- vm.startPrank(this.owner());
171
- vm.expectRevert(TEELifecycle.TEEVersionNotFound.selector);
172
- this.removeApprovedTeeVersion(nonExistentMrAggregated);
173
- vm.stopPrank();
174
- }
175
-
176
- function testRemoveApprovedTeeVersionOnlyOwner() public {
177
- bytes32 mrAggregated = hex"1111111111111111111111111111111111111111111111111111111111111111";
178
-
179
- vm.startPrank(this.owner());
180
- this.approveNewTeeVersion(mrAggregated);
181
- vm.stopPrank();
182
-
183
- // Try to remove as non-owner
184
- address nonOwner = address(0x1234);
185
- vm.startPrank(nonOwner);
186
- vm.expectRevert();
187
- this.removeApprovedTeeVersion(mrAggregated);
188
- vm.stopPrank();
189
- }
190
-
191
137
  // Helper function to create a successful bootstrap result
192
138
  function successfulBootstrapResult()
193
139
  internal
@@ -449,4 +395,83 @@ contract TEELifecycleMockTest is MockRemoteAttestation, TEELifecycle {
449
395
  );
450
396
  }
451
397
 
398
+ // ============ Tests for reset() ============
399
+
400
+ function testReset_OnlyOwner() public {
401
+ address nonOwner = address(0x1234);
402
+ vm.startPrank(nonOwner);
403
+ vm.expectRevert();
404
+ this.reset();
405
+ vm.stopPrank();
406
+ }
407
+
408
+ function testReset_WithMultipleSigners() public {
409
+ // Complete bootstrap with first signer
410
+ (
411
+ BootstrapResult memory bootstrapResult,,
412
+ address bootstrapPartyAddress,
413
+ bytes memory quote,
414
+ bytes memory signature,
415
+ bytes32 mrAggregated
416
+ ) = successfulBootstrapResult();
417
+
418
+ vm.startPrank(this.owner());
419
+ this.approveNewTeeVersion(mrAggregated);
420
+ this.verifyBootstrapResult(bootstrapResult, quote, signature);
421
+
422
+ // Add a second node
423
+ (uint256 newNodePrivkey, address newNodeAddress) = getLabeledKeyPair("newNode");
424
+ AddNodeResult memory addNodeResult = AddNodeResult({networkPubkey: testNetworkPubkey});
425
+ bytes memory addNodeSignature = signAddNodeResult(addNodeResult, newNodePrivkey);
426
+ bytes memory addNodeQuote = createQuote(testMrtd, newNodeAddress);
427
+ this.verifyAddNodeResult(mrAggregated, addNodeResult, addNodeQuote, addNodeSignature);
428
+
429
+ // Verify state before reset
430
+ assertTrue(this.isBootstrapComplete(), "Bootstrap should be complete before reset");
431
+ assertEq(this.networkPubkey(), testNetworkPubkey, "Network pubkey should be set before reset");
432
+ assertEq(this.approvedTeeVersions(0), mrAggregated, "Approved TEE version should exist before reset");
433
+ assertEq(this.getSignersCount(), 2, "Should have 2 signers before reset");
434
+ assertTrue(this.isSigner(bootstrapPartyAddress), "First signer should exist");
435
+ assertTrue(this.isSigner(newNodeAddress), "Second signer should exist");
436
+
437
+ // Call reset
438
+ this.reset();
439
+
440
+ // Verify all state has been cleared
441
+ assertFalse(this.isBootstrapComplete(), "Bootstrap should not be complete after reset");
442
+ assertEq(this.networkPubkey().length, 0, "Network pubkey should be empty after reset");
443
+ assertEq(this.getSignersCount(), 0, "Should have 0 signers after reset");
444
+ assertFalse(this.isSigner(bootstrapPartyAddress), "First signer should be removed");
445
+ assertFalse(this.isSigner(newNodeAddress), "Second signer should be removed");
446
+ assertEq(this.getThreshold(), 0, "Threshold should be 0 after reset");
447
+
448
+ // Verify approved TEE versions array is empty
449
+ vm.expectRevert(TEELifecycle.IndexOutOfBounds.selector);
450
+ this.approvedTeeVersions(0);
451
+
452
+ vm.stopPrank();
453
+ }
454
+
455
+ function testReset_AllowsNewBootstrap() public {
456
+ // Complete bootstrap
457
+ (BootstrapResult memory bootstrapResult,,, bytes memory quote, bytes memory signature, bytes32 mrAggregated) =
458
+ successfulBootstrapResult();
459
+
460
+ vm.startPrank(this.owner());
461
+ this.approveNewTeeVersion(mrAggregated);
462
+ this.verifyBootstrapResult(bootstrapResult, quote, signature);
463
+ assertTrue(this.isBootstrapComplete(), "Bootstrap should be complete");
464
+
465
+ // Reset the contract
466
+ this.reset();
467
+ assertFalse(this.isBootstrapComplete(), "Bootstrap should not be complete after reset");
468
+
469
+ // Should be able to bootstrap again
470
+ this.approveNewTeeVersion(mrAggregated);
471
+ this.verifyBootstrapResult(bootstrapResult, quote, signature);
472
+ assertTrue(this.isBootstrapComplete(), "Should be able to bootstrap again after reset");
473
+
474
+ vm.stopPrank();
475
+ }
476
+
452
477
  }
@@ -5,6 +5,7 @@ import {Test} from "forge-std/Test.sol";
5
5
  import {TrivialEncryption} from "../lightning-parts/TrivialEncryption.sol";
6
6
  import {ETypes} from "../Types.sol";
7
7
  import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
8
+ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
8
9
  import {inco} from "../Lib.sol";
9
10
  import {IncoTest} from "./IncoTest.sol";
10
11
 
@@ -26,6 +27,7 @@ contract TestDeploy is Test, IncoTest {
26
27
  vm.expectEmit(false, false, true, false, address(inco));
27
28
  emit TrivialEncryption.TrivialEncrypt(bytes32(uint256(1)), bytes32(uint256(1)), ETypes.Bool, 0);
28
29
  inco.asEbool(true);
30
+ assertTrue(inco.isAcceptedVersion(2));
29
31
  }
30
32
 
31
33
  function testUpgrade() public {
@@ -35,4 +37,30 @@ contract TestDeploy is Test, IncoTest {
35
37
  assertEq(ReturnTwo(address(inco)).getTwo(), 2);
36
38
  }
37
39
 
40
+ function testAddAcceptedVersion() public {
41
+ assertFalse(inco.isAcceptedVersion(42));
42
+ vm.prank(owner);
43
+ inco.addAcceptedVersion(42);
44
+ assertTrue(inco.isAcceptedVersion(42));
45
+ }
46
+
47
+ function testAddAcceptedVersionNotOwner() public {
48
+ vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice));
49
+ vm.prank(alice);
50
+ inco.addAcceptedVersion(42);
51
+ }
52
+
53
+ function testRemoveAcceptedVersion() public {
54
+ assertFalse(inco.isAcceptedVersion(42));
55
+ vm.prank(owner);
56
+ inco.removeAcceptedVersion(42); // removing a non-existent version should be no-op
57
+ assertFalse(inco.isAcceptedVersion(42));
58
+ }
59
+
60
+ function testRemoveAcceptedVersionNotOwner() public {
61
+ vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice));
62
+ vm.prank(alice);
63
+ inco.removeAcceptedVersion(42);
64
+ }
65
+
38
66
  }