@inco/lightning 0.8.0-devnet-2 → 0.8.0-devnet-3

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Inco lite
1
+ # Inco Lightning
2
2
 
3
- ![coverage](./coverage.svg)
3
+ The core Inco Lightning smart contracts library for building confidential applications on EVM chains.
4
4
 
5
- <!-- todo #1035 upgrade deployment and upgrade documentation now outdated @silasdavis -->
5
+ ![coverage](./coverage.svg)
6
6
 
7
7
  ## Install dependencies
8
8
 
@@ -12,7 +12,7 @@ bun install
12
12
 
13
13
  ## Build
14
14
 
15
- Use [forge](https://book.getfoundry.sh/getting-started/installation) version 1.0 or higher
15
+ See [Foundry version requirements](../README.md#foundry-version-requirements) for version guidance.
16
16
 
17
17
  ```sh
18
18
  forge build
@@ -36,58 +36,32 @@ make coverage
36
36
 
37
37
  > This generates an lcov report, filters out `node_modules`, autogenerated contracts and other contracts that we don't want to include in the report (`Lib.XXXnet.sol`, etc.). Eventually, it `genhtml` the filtered lcov in `coverage` and a `lcov-badge` visible in this readme. You can check the latest coverage stats [here](./coverage/index.html).
38
38
 
39
- > Note: You need `lcov` for the `make coverage` to work
40
- > For MacOS - `brew install lcov`
41
-
42
- ## Deploy
43
-
44
- ### One-time key import
45
-
46
- 1. Import key into Foundry cast wallet using [`make import_sepolia_deployer`](./Makefile)
47
-
48
- ### Deploy new IncoLite contracts
49
-
50
- Targest for deploying the contracts are in the [Makefile](./Makefile). There are separate targtes for each chain and a target that deploys to all of them.
51
-
52
- The version of the contracts that will be deployed is defined by the current working directory and it will be deployed to an address that is deterministically calculated based on:
53
-
54
- - The contract name
55
- - The contract version
56
- - The deployer address
57
- - An optional deploy-time 'pepper'
58
-
59
- The contract name and version are constants in [IncoLightningConfig.sol](./src/version/IncoLightningConfig.sol) and the deployer address is the address of the wallet that is used to deploy the contracts. The optional deploy-time 'pepper' is a deployment-time string that can be chosen to alter the contract address for each deployment in the case where you what to differentiate a deployment from others.
60
-
61
- The point of the deterministic address is that holding the values above constant we can obtain a consistent canonical deployment address across all chains.
62
-
63
- #### Deployment process
64
-
65
- 1. Bump IncoLite version in [IncoLightningConfig.sol](./src/version/IncoLightningConfig.sol)
66
- 2. Run [`make deploy_multichain`](./Makefile) - this will update the manifest with the new release
67
- 3. Run [`make deploy_test_contracts_multichain`](./Makefile)
39
+ > **Note:** Coverage requires `lcov` (`brew install lcov` on macOS) and Foundry v1.3.6. See [Foundry version requirements](../README.md#foundry-version-requirements).
68
40
 
69
- ##### Providing a pepper
41
+ ## Directory Structure
70
42
 
71
- If you want to provide a pepper then use `make deploy_multichain INCO_LITE_PEPPER=<pepper>` where `pepper` is the string you want to use. This will be used to calculate the contract address and will be included in the manifest.
43
+ | Directory | Description |
44
+ | -------------- | -------------------------------------------------- |
45
+ | `src/` | Main contract source code |
46
+ | `src/libs/` | Release libraries that link to versioned executors |
47
+ | `src/version/` | Version configuration (`IncoLightningConfig.sol`) |
48
+ | `test/` | Unit tests |
49
+ | `script/` | Utility scripts |
72
50
 
73
- ## Defining a release without explicit deployment
51
+ ## Makefile Commands
74
52
 
75
- The deployment machinery is currently in a halfway house whereby we capture releases at the same time as performing smart contract deployments locally. In the future we expect the contract deployment (and later upgrade) processes to be run by CI or a by a covalidator initialisation container. At that point rather than the manifest being an artefact of record it will be a declaration of intent. At the moment it serves both purposes, but will be refactored to remove the deployment log capture element.
53
+ | Command | Description |
54
+ | --------------- | ------------------------------------ |
55
+ | `make build` | Build contracts with Foundry |
56
+ | `make test` | Run unit tests |
57
+ | `make coverage` | Generate coverage report (uses lcov) |
58
+ | `make lint` | Lint Solidity files |
76
59
 
77
- With a nod to the future we provide the `make release` target which performs a simulated deploy, updates the manifest with a dummy deployment, and declares a release.
78
-
79
- ## Deploy NPM modules
80
-
81
- 1. Bump contracts NPM version in [package.json](./package.json)
82
- 2. Bump JS NPM version in [package.json](../js/package.json)
83
- 3. Publish contracts `npm publish:github`
84
- 4. Build JS [`make build`](../js/Makefile) (depends on contract deployment being done first)
85
- 5. Publish js `npm publish:github`
60
+ ## Deploy
86
61
 
87
- ## Build covalidator docker images
62
+ For comprehensive deployment and upgrade instructions, see [`lightning-deployment/`](../lightning-deployment/README.md).
88
63
 
89
- 1. PR to main
90
- 2. Manually grab hash from dockerhub which is also `git describe --tags --always --dirty`
64
+ Contract version is defined in [`IncoLightningConfig.sol`](./src/version/IncoLightningConfig.sol).
91
65
 
92
66
  ## Manifest file
93
67
 
@@ -102,21 +76,3 @@ incoLightning_<major>_<minor>_<patch>__<salt>
102
76
  ```
103
77
 
104
78
  The salt is derived from the version and additional value called the `pepper`. The salt determines the on-chain address of the release, whence the pepper can be used to for releases of the same contract versions with separate state and addresses (e.g. for testing purposes).
105
-
106
- ## Deployment dumps
107
-
108
- In [`dumps`](./dumps) you can find a sequence of pairs of files named with the following format:
109
-
110
- ```
111
- <release_name>.dump.json
112
- ```
113
-
114
- Contains an [Anvil](https://book.getfoundry.sh/anvil/) state dump that can be loaded with `anvil --load-state <state-dump-file>`, which you see used in our local node [`docker-compose`](../../docker-compose.yaml) setup.
115
-
116
- ```
117
- <release_name>.env
118
- ```
119
-
120
- Is a dump parameters file, it contains a set of private and public keys, configuration for a covalidator, utility addresss, and the executor address as it exists in the particular state dump.
121
-
122
- Everything require to run a local covalidator (in docker or otherwise), to encrypt values and communicate with the executor contract of the dump, is included within this parameters file. The secrets are generated for each dump.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inco/lightning",
3
- "version": "0.8.0-devnet-2",
3
+ "version": "0.8.0-devnet-3",
4
4
  "repository": "https://github.com/Inco-fhevm/inco-monorepo",
5
5
  "files": [
6
6
  "src/",
@@ -4,11 +4,12 @@ pragma solidity ^0.8;
4
4
  import {IncoTest} from "../../../test/IncoTest.sol";
5
5
  import {SessionVerifier, Session} from "../../../periphery/SessionVerifier.sol";
6
6
  import {AllowanceVoucher, AllowanceProof} from "../AdvancedAccessControl.sol";
7
- import {euint256} from "../../../Types.sol";
7
+ import {euint256, SharerNotAllowedForHandle} from "../../../Types.sol";
8
8
  import {e, inco} from "../../../Lib.sol";
9
9
  import {AdvancedAccessControl} from "../AdvancedAccessControl.sol";
10
10
  import {ALLOWANCE_GRANTED_MAGIC_VALUE} from "../../../Types.sol";
11
11
  import {IIncoVerifier} from "../../../interfaces/IIncoVerifier.sol";
12
+ import {BaseAccessControlList} from "../BaseAccessControlList.sol";
12
13
 
13
14
  contract SomeContractWithConfidentialData {
14
15
 
@@ -192,4 +193,77 @@ contract TestAdvancedAccessControl is IncoTest {
192
193
  return getSignatureForDigest(incoVerifier.allowanceVoucherDigest(voucher), alicePrivKey);
193
194
  }
194
195
 
196
+ /// @notice Test SharerNotAllowedForHandle error when sharer is not allowed
197
+ function testIsAllowedWithProofSharerNotAllowed() public {
198
+ DoesNotVerifyAnything verifier = new DoesNotVerifyAnything();
199
+ AllowanceVoucher memory voucher = AllowanceVoucher({
200
+ sessionNonce: bytes32(0),
201
+ verifyingContract: address(verifier),
202
+ callFunction: verifier.someCheck.selector,
203
+ sharerArgData: ""
204
+ });
205
+ // Use bob as sharer, but bob is NOT allowed on the secret (only alice is)
206
+ AllowanceProof memory proof = AllowanceProof({
207
+ sharer: bob,
208
+ voucher: voucher,
209
+ voucherSignature: getSignatureForDigest(incoVerifier.allowanceVoucherDigest(voucher), bobPrivKey),
210
+ requesterArgData: ""
211
+ });
212
+ vm.expectRevert(abi.encodeWithSelector(SharerNotAllowedForHandle.selector, secretHandle, bob));
213
+ incoVerifier.isAllowedWithProof(secretHandle, carol, proof);
214
+ }
215
+
216
+ /// @notice Test InvalidVoucherSignature error when signature is invalid
217
+ function testIsAllowedWithProofInvalidSignature() public {
218
+ DoesNotVerifyAnything verifier = new DoesNotVerifyAnything();
219
+ AllowanceVoucher memory voucher = AllowanceVoucher({
220
+ sessionNonce: bytes32(0),
221
+ verifyingContract: address(verifier),
222
+ callFunction: verifier.someCheck.selector,
223
+ sharerArgData: ""
224
+ });
225
+ // Alice is the sharer (and is allowed), but we sign with Bob's key
226
+ bytes memory wrongSignature = getSignatureForDigest(incoVerifier.allowanceVoucherDigest(voucher), bobPrivKey);
227
+ AllowanceProof memory proof =
228
+ AllowanceProof({sharer: alice, voucher: voucher, voucherSignature: wrongSignature, requesterArgData: ""});
229
+ bytes32 voucherDigest = incoVerifier.allowanceVoucherDigest(voucher);
230
+ vm.expectRevert(
231
+ abi.encodeWithSelector(
232
+ AdvancedAccessControl.InvalidVoucherSignature.selector, alice, voucherDigest, wrongSignature
233
+ )
234
+ );
235
+ incoVerifier.isAllowedWithProof(secretHandle, bob, proof);
236
+ }
237
+
238
+ /// @notice Test claimHandle fails when proof verification fails (line 107)
239
+ function testClaimHandleProofVerificationFailed() public {
240
+ SomeVerifier verifier = new SomeVerifier();
241
+ // Create a voucher that will be valid signature-wise but the verifier will return false
242
+ // because we pass wrong requesterArgData (not 0xbeef)
243
+ AllowanceVoucher memory voucher = AllowanceVoucher({
244
+ sessionNonce: bytes32(0),
245
+ verifyingContract: address(verifier),
246
+ callFunction: verifier.someCheck.selector,
247
+ sharerArgData: abi.encode(SomeVerifier.SharerArg({handleShared: secretHandle, allowedAccount: bob}))
248
+ });
249
+ AllowanceProof memory proof = AllowanceProof({
250
+ sharer: alice, // alice IS allowed on the secret
251
+ voucher: voucher,
252
+ voucherSignature: getAliceSig(voucher),
253
+ requesterArgData: abi.encode(SomeVerifier.RequesterArg({mustBeBeef: bytes2(0x1234)})) // WRONG! Not 0xbeef
254
+ });
255
+
256
+ // isAllowedWithProof should return false (not revert), then claimHandle should revert
257
+ vm.prank(bob);
258
+ vm.expectRevert(
259
+ abi.encodeWithSelector(
260
+ BaseAccessControlList.ProofVerificationFailed.selector,
261
+ address(verifier),
262
+ verifier.someCheck.selector,
263
+ abi.encode(SomeVerifier.SharerArg({handleShared: secretHandle, allowedAccount: bob}))
264
+ )
265
+ );
266
+ inco.claimHandle(secretHandle, proof);
267
+ }
268
+
195
269
  }
@@ -4,6 +4,7 @@ pragma solidity ^0.8;
4
4
  import {BaseAccessControlList} from "../BaseAccessControlList.sol";
5
5
  import {VerifierAddressGetter} from "../../primitives/VerifierAddressGetter.sol";
6
6
  import {euint256, inco} from "../../../Lib.sol";
7
+ import {SenderNotAllowedForHandle} from "../../../Types.sol";
7
8
  import {IncoTest} from "../../../test/IncoTest.sol";
8
9
 
9
10
  contract TestBaseAccessControl is BaseAccessControlList, IncoTest {
@@ -25,4 +26,248 @@ contract TestBaseAccessControl is BaseAccessControlList, IncoTest {
25
26
  assert(inco.isAllowed(euint256.unwrap(secret), alice));
26
27
  }
27
28
 
29
+ // ============ allowTransient Tests ============
30
+
31
+ function testAllowTransient() public {
32
+ euint256 secret = inco.asEuint256(42);
33
+ bytes32 handle = euint256.unwrap(secret);
34
+
35
+ // Initially bob is not allowed
36
+ assertFalse(inco.isAllowed(handle, bob));
37
+
38
+ // Allow transient access from this contract (which is allowed)
39
+ inco.allowTransient(handle, bob);
40
+
41
+ // Bob should now have transient access
42
+ assertTrue(inco.allowedTransient(handle, bob));
43
+ assertTrue(inco.isAllowed(handle, bob));
44
+ }
45
+
46
+ function testAllowTransient_RevertsWhenSenderNotAllowed() public {
47
+ euint256 secret = inco.asEuint256(42);
48
+ bytes32 handle = euint256.unwrap(secret);
49
+
50
+ // Try to allow transient from an account that doesn't have access
51
+ vm.prank(bob);
52
+ vm.expectRevert(abi.encodeWithSelector(SenderNotAllowedForHandle.selector, handle, bob));
53
+ inco.allowTransient(handle, carol);
54
+ }
55
+
56
+ // ============ cleanTransientStorage Tests ============
57
+
58
+ function testCleanTransientStorage() public {
59
+ euint256 secret1 = inco.asEuint256(100);
60
+ euint256 secret2 = inco.asEuint256(200);
61
+ bytes32 handle1 = euint256.unwrap(secret1);
62
+ bytes32 handle2 = euint256.unwrap(secret2);
63
+
64
+ // Grant transient access
65
+ inco.allowTransient(handle1, bob);
66
+ inco.allowTransient(handle2, carol);
67
+
68
+ // Verify transient access is granted
69
+ assertTrue(inco.allowedTransient(handle1, bob));
70
+ assertTrue(inco.allowedTransient(handle2, carol));
71
+
72
+ // Clean transient storage
73
+ inco.cleanTransientStorage();
74
+
75
+ // Transient access should be revoked
76
+ assertFalse(inco.allowedTransient(handle1, bob));
77
+ assertFalse(inco.allowedTransient(handle2, carol));
78
+ }
79
+
80
+ // ============ allowedTransient Direct Call Tests ============
81
+
82
+ function testAllowedTransient_DirectCall() public {
83
+ euint256 secret = inco.asEuint256(42);
84
+ bytes32 handle = euint256.unwrap(secret);
85
+
86
+ // Direct call should return false initially
87
+ assertFalse(inco.allowedTransient(handle, bob));
88
+
89
+ // After allowing, direct call should return true
90
+ inco.allowTransient(handle, bob);
91
+ assertTrue(inco.allowedTransient(handle, bob));
92
+ }
93
+
94
+ // ============ isRevealed Direct Call Tests ============
95
+
96
+ function testIsRevealed_DirectCall() public {
97
+ euint256 secret = inco.asEuint256(42);
98
+ bytes32 handle = euint256.unwrap(secret);
99
+
100
+ // Direct call should return false initially
101
+ assertFalse(inco.isRevealed(handle));
102
+
103
+ // After reveal, direct call should return true
104
+ inco.reveal(handle);
105
+ assertTrue(inco.isRevealed(handle));
106
+ }
107
+
108
+ // ============ allow Revert Tests ============
109
+
110
+ function testAllow_RevertsWhenSenderNotAllowed() public {
111
+ euint256 secret = inco.asEuint256(42);
112
+ bytes32 handle = euint256.unwrap(secret);
113
+
114
+ // Try to allow from an account that doesn't have access
115
+ vm.prank(bob);
116
+ vm.expectRevert(abi.encodeWithSelector(SenderNotAllowedForHandle.selector, handle, bob));
117
+ inco.allow(handle, carol);
118
+ }
119
+
120
+ // ============ reveal Revert Tests ============
121
+
122
+ function testReveal_RevertsWhenSenderNotAllowed() public {
123
+ euint256 secret = inco.asEuint256(42);
124
+ bytes32 handle = euint256.unwrap(secret);
125
+
126
+ // Try to reveal from an account that doesn't have access
127
+ vm.prank(bob);
128
+ vm.expectRevert(abi.encodeWithSelector(SenderNotAllowedForHandle.selector, handle, bob));
129
+ inco.reveal(handle);
130
+ }
131
+
132
+ // ============ Fuzz Tests for isAllowed ============
133
+
134
+ /// @dev Fuzz test that isAllowed returns true when handle is revealed (regardless of account)
135
+ function testFuzzIsAllowedWhenRevealed(bytes32 randomSeed, address randomAccount) public {
136
+ vm.assume(randomAccount != address(0));
137
+
138
+ // Create a unique handle using the random seed
139
+ euint256 secret = inco.asEuint256(uint256(randomSeed));
140
+ bytes32 handle = euint256.unwrap(secret);
141
+
142
+ // Initially, random account should not have access (unless it's this contract)
143
+ if (randomAccount != address(this)) {
144
+ assertFalse(inco.isAllowed(handle, randomAccount));
145
+ }
146
+
147
+ // Reveal the handle
148
+ inco.reveal(handle);
149
+
150
+ // Now any account should have access
151
+ assertTrue(inco.isAllowed(handle, randomAccount));
152
+ assertTrue(inco.isRevealed(handle));
153
+ }
154
+
155
+ /// @dev Fuzz test that isAllowed returns true when persistAllowed is set
156
+ function testFuzzIsAllowedWhenPersisted(bytes32 randomSeed, address allowedAccount) public {
157
+ vm.assume(allowedAccount != address(0));
158
+ vm.assume(allowedAccount != address(this));
159
+
160
+ // Create a unique handle
161
+ euint256 secret = inco.asEuint256(uint256(randomSeed));
162
+ bytes32 handle = euint256.unwrap(secret);
163
+
164
+ // Initially not allowed
165
+ assertFalse(inco.isAllowed(handle, allowedAccount));
166
+
167
+ // Allow the account (from this contract which has access)
168
+ inco.allow(handle, allowedAccount);
169
+
170
+ // Now should be allowed
171
+ assertTrue(inco.isAllowed(handle, allowedAccount));
172
+ assertTrue(inco.persistAllowed(handle, allowedAccount));
173
+ }
174
+
175
+ /// @dev Fuzz test that isAllowed returns true when transient access is granted
176
+ function testFuzzIsAllowedWhenTransient(bytes32 randomSeed, address allowedAccount) public {
177
+ vm.assume(allowedAccount != address(0));
178
+ vm.assume(allowedAccount != address(this));
179
+
180
+ // Create a unique handle
181
+ euint256 secret = inco.asEuint256(uint256(randomSeed));
182
+ bytes32 handle = euint256.unwrap(secret);
183
+
184
+ // Initially not allowed
185
+ assertFalse(inco.isAllowed(handle, allowedAccount));
186
+
187
+ // Allow transient access
188
+ inco.allowTransient(handle, allowedAccount);
189
+
190
+ // Should be allowed via transient
191
+ assertTrue(inco.isAllowed(handle, allowedAccount));
192
+ assertTrue(inco.allowedTransient(handle, allowedAccount));
193
+ // But not persisted
194
+ assertFalse(inco.persistAllowed(handle, allowedAccount));
195
+ }
196
+
197
+ /// @dev Fuzz test the OR logic: isAllowed = transient OR persisted OR revealed
198
+ function testFuzzIsAllowedOrLogic(bytes32 randomSeed, uint8 accessMode) public {
199
+ // Create a unique handle
200
+ euint256 secret = inco.asEuint256(uint256(randomSeed));
201
+ bytes32 handle = euint256.unwrap(secret);
202
+
203
+ // accessMode determines which access type to grant:
204
+ // 0 = none, 1 = transient, 2 = persisted, 3 = revealed, 4+ = combinations
205
+ uint8 mode = accessMode % 8;
206
+
207
+ bool grantTransient = (mode & 1) != 0;
208
+ bool grantPersisted = (mode & 2) != 0;
209
+ bool grantRevealed = (mode & 4) != 0;
210
+
211
+ // Initially bob has no access
212
+ assertFalse(inco.isAllowed(handle, bob));
213
+
214
+ // Grant access based on mode
215
+ if (grantTransient) {
216
+ inco.allowTransient(handle, bob);
217
+ }
218
+ if (grantPersisted) {
219
+ inco.allow(handle, bob);
220
+ }
221
+ if (grantRevealed) {
222
+ inco.reveal(handle);
223
+ }
224
+
225
+ // isAllowed should be true if ANY access was granted
226
+ bool expectedAllowed = grantTransient || grantPersisted || grantRevealed;
227
+ assertEq(inco.isAllowed(handle, bob), expectedAllowed);
228
+ }
229
+
230
+ /// @dev Fuzz test that cleaning transient storage removes transient access
231
+ function testFuzzCleanTransientStorageRemovesAccess(bytes32 randomSeed) public {
232
+ // Create a unique handle
233
+ euint256 secret = inco.asEuint256(uint256(randomSeed));
234
+ bytes32 handle = euint256.unwrap(secret);
235
+
236
+ // Grant transient access
237
+ inco.allowTransient(handle, bob);
238
+ assertTrue(inco.allowedTransient(handle, bob));
239
+ assertTrue(inco.isAllowed(handle, bob));
240
+
241
+ // Clean transient storage
242
+ inco.cleanTransientStorage();
243
+
244
+ // Transient access should be revoked
245
+ assertFalse(inco.allowedTransient(handle, bob));
246
+ assertFalse(inco.isAllowed(handle, bob));
247
+ }
248
+
249
+ /// @dev Fuzz test multiple accounts with different access levels
250
+ function testFuzzMultipleAccountsAccessLevels(bytes32 randomSeed) public {
251
+ euint256 secret = inco.asEuint256(uint256(randomSeed));
252
+ bytes32 handle = euint256.unwrap(secret);
253
+
254
+ // Grant different access types to different accounts
255
+ inco.allowTransient(handle, bob);
256
+ inco.allow(handle, carol);
257
+ // dave gets no access
258
+
259
+ // Verify access levels
260
+ assertTrue(inco.isAllowed(handle, bob));
261
+ assertTrue(inco.allowedTransient(handle, bob));
262
+ assertFalse(inco.persistAllowed(handle, bob));
263
+
264
+ assertTrue(inco.isAllowed(handle, carol));
265
+ assertFalse(inco.allowedTransient(handle, carol));
266
+ assertTrue(inco.persistAllowed(handle, carol));
267
+
268
+ assertFalse(inco.isAllowed(handle, dave));
269
+ assertFalse(inco.allowedTransient(handle, dave));
270
+ assertFalse(inco.persistAllowed(handle, dave));
271
+ }
272
+
28
273
  }
@@ -61,11 +61,6 @@ abstract contract EncryptedOperations is IEncryptedOperations, BaseAccessControl
61
61
  _;
62
62
  }
63
63
 
64
- modifier checkedHandle(euint256 handle) {
65
- checkInput(euint256.unwrap(handle), typeToBitMask(ETypes.Uint256));
66
- _;
67
- }
68
-
69
64
  function checkInput(bytes32 input, bytes32 requiredTypes) internal view {
70
65
  require(isAllowed(input, msg.sender), SenderNotAllowedForHandle(input, msg.sender));
71
66
  require(requiredTypes & typeToBitMask(typeOf(input)) != 0, UnexpectedType(typeOf(input), requiredTypes));
@@ -279,6 +274,7 @@ abstract contract EncryptedOperations is IEncryptedOperations, BaseAccessControl
279
274
  }
280
275
 
281
276
  function eCast(bytes32 ct, ETypes toType) external returns (bytes32 result) {
277
+ checkInput(ct, SUPPORTED_TYPES_MASK);
282
278
  require(isTypeSupported(toType), UnsupportedType(toType));
283
279
  result = createResultHandle(EOps.Cast, toType, abi.encodePacked(ct));
284
280
  allowTransientInternal(result, msg.sender);
@@ -7,24 +7,53 @@ import {BaseAccessControlList} from "./AccessControl/BaseAccessControlList.sol";
7
7
  import {HandleGeneration} from "./primitives/HandleGeneration.sol";
8
8
  import {ITrivialEncryption} from "./interfaces/ITrivialEncryption.sol";
9
9
 
10
+ /// @title TrivialEncryption
11
+ /// @notice Provides functions to create encrypted handles from plaintext values.
12
+ /// @dev Trivial encryption wraps plaintext values as encrypted handles. The plaintext is visible
13
+ /// on-chain (emitted in events), so this should only be used for values that are already public.
14
+ /// Common use cases include initializing encrypted state with known values or creating encrypted
15
+ /// constants. The resulting handles are granted transient access to the caller.
10
16
  abstract contract TrivialEncryption is ITrivialEncryption, EventCounter, BaseAccessControlList, HandleGeneration {
11
17
 
18
+ /// @notice Emitted when a trivial encryption is performed.
19
+ /// @param result The handle representing the encrypted value.
20
+ /// @param plainTextBytes The plaintext value that was encrypted (visible on-chain).
21
+ /// @param handleType The type of the encrypted value.
22
+ /// @param eventId The unique event ID for ordering and verification.
12
23
  event TrivialEncrypt(bytes32 indexed result, bytes32 plainTextBytes, ETypes handleType, uint256 eventId);
13
24
 
25
+ /// @notice Creates an encrypted uint256 handle from a plaintext value.
26
+ /// @dev The plaintext value is visible on-chain. Use this for public values only.
27
+ /// @param value The plaintext uint256 value to encrypt.
28
+ /// @return newEuint256 The encrypted handle representing the value.
14
29
  function asEuint256(uint256 value) external returns (euint256 newEuint256) {
15
30
  return euint256.wrap(newTrivialEncrypt(bytes32(value), ETypes.Uint256));
16
31
  }
17
32
 
33
+ /// @notice Creates an encrypted boolean handle from a plaintext value.
34
+ /// @dev The plaintext value is visible on-chain. Use this for public values only.
35
+ /// @param value The plaintext boolean value to encrypt.
36
+ /// @return newEbool The encrypted handle representing the value.
18
37
  function asEbool(bool value) external returns (ebool newEbool) {
19
38
  bytes32 castedValue = bytes32(uint256(value ? 1 : 0));
20
39
  return ebool.wrap(newTrivialEncrypt(castedValue, ETypes.Bool));
21
40
  }
22
41
 
42
+ /// @notice Creates an encrypted address handle from a plaintext value.
43
+ /// @dev The plaintext value is visible on-chain. Use this for public values only.
44
+ /// @param value The plaintext address value to encrypt.
45
+ /// @return newEaddress The encrypted handle representing the value.
23
46
  function asEaddress(address value) external returns (eaddress newEaddress) {
24
47
  bytes32 castedValue = bytes32(uint256(uint160(value)));
25
48
  return eaddress.wrap(newTrivialEncrypt(castedValue, ETypes.AddressOrUint160OrBytes20));
26
49
  }
27
50
 
51
+ /// @notice Internal function that performs the trivial encryption.
52
+ /// @dev Generates a deterministic handle, grants transient access to the caller,
53
+ /// emits the TrivialEncrypt event, and updates the digest for verification.
54
+ /// @param plainTextBytes The plaintext value as bytes32.
55
+ /// @param handleType The type of encrypted value being created.
56
+ /// @return newHandle The generated handle for the encrypted value.
28
57
  function newTrivialEncrypt(bytes32 plainTextBytes, ETypes handleType) internal returns (bytes32 newHandle) {
29
58
  newHandle = getTrivialEncryptHandle(plainTextBytes, handleType);
30
59
  allowTransientInternal(newHandle, msg.sender);
@@ -34,7 +34,6 @@ abstract contract SignatureVerifier is ISignatureVerifier, OwnableUpgradeable, S
34
34
  error SignerNotFound(address signerAddress);
35
35
  error SignerAlreadyAdded(address signerAddress);
36
36
  error InvalidThreshold(uint256 threshold, uint256 nbOfSigners);
37
- error SignersNotInAscendingOrder(address currentSigner, address lastSigner);
38
37
 
39
38
  event AddedSignatureVerifier(address signerAddress);
40
39
  event RemovedSignatureVerifier(address signerAddress);
@@ -95,8 +94,23 @@ abstract contract SignatureVerifier is ISignatureVerifier, OwnableUpgradeable, S
95
94
  return getSigVerifierStorage().signers.length;
96
95
  }
97
96
 
98
- /// @dev signature signers MUST be in ascending order, reverts with SignersNotInAscendingOrder if not
99
- /// @dev signatures beyond the threshold are ignored
97
+ /// @notice Verifies that a digest has been signed by at least `threshold` authorized signers
98
+ /// @dev Duplicate detection is optimized when signatures are sorted by signer address (ascending)
99
+ /// @dev Behavior notes:
100
+ /// - Returns false if threshold is 0 (not yet configured)
101
+ /// - Returns false if fewer signatures provided than threshold
102
+ /// - Invalid/malformed signatures are skipped (not counted, don't cause failure)
103
+ /// - If all signatures are invalid/malformed, returns false (no valid signatures can reach the threshold)
104
+ /// - Duplicate signers cause immediate rejection (returns false)
105
+ /// - Valid signatures from non-authorized addresses are skipped
106
+ /// - Processing stops early once threshold is reached (remaining signatures ignored)
107
+ /// @dev Gas considerations:
108
+ /// - O(n) when signatures are sorted by signer address (ascending) - recommended
109
+ /// - O(n²) worst case when signatures are in reverse order (fallback duplicate detection)
110
+ /// - Callers should provide signatures in ascending signer address order for optimal gas usage
111
+ /// @param digest The message digest (hash) that was signed
112
+ /// @param signatures Array of ECDSA signatures
113
+ /// @return bool True if at least `threshold` unique authorized signers signed the digest
100
114
  function isValidSignature(bytes32 digest, bytes[] memory signatures) public view returns (bool) {
101
115
  StorageForSigVerifier storage $ = getSigVerifierStorage();
102
116
  uint256 threshold = $.threshold;
@@ -107,22 +121,39 @@ abstract contract SignatureVerifier is ISignatureVerifier, OwnableUpgradeable, S
107
121
  return false;
108
122
  }
109
123
 
124
+ // Track recovered signers for duplicate detection (only need signaturesLength slots max)
125
+ address[] memory recoveredSigners = new address[](signaturesLength);
110
126
  address lastSigner = address(0);
111
127
  uint256 correctSignaturesCount = 0;
112
- uint256 i = 0;
128
+ uint256 validCount = 0; // Track number of valid (non-malformed) signatures processed
113
129
 
114
- while (correctSignaturesCount < threshold && i < signaturesLength) {
115
- address currentSigner = digest.recover(signatures[i]);
130
+ for (uint256 i = 0; i < signaturesLength && correctSignaturesCount < threshold; i++) {
131
+ (address currentSigner, ECDSA.RecoverError err,) = ECDSA.tryRecover(digest, signatures[i]);
116
132
 
117
- // Ensure signers are in ascending order to prevent duplicates
118
- // Revert is used instead of return false to signal the user/developer an error (call malformed) is on his side
119
- require(currentSigner > lastSigner, SignersNotInAscendingOrder(currentSigner, lastSigner));
120
- lastSigner = currentSigner;
133
+ // Skip invalid signatures (malformed, wrong length, etc.)
134
+ if (err != ECDSA.RecoverError.NoError) {
135
+ continue;
136
+ }
137
+
138
+ // Optimistic duplicate detection (OZ pattern)
139
+ if (currentSigner > lastSigner) {
140
+ // Fast path: signer is in ascending order
141
+ lastSigner = currentSigner;
142
+ } else {
143
+ // Fallback: check all previous valid signers for duplicates
144
+ for (uint256 j = 0; j < validCount; ++j) {
145
+ if (currentSigner == recoveredSigners[j]) {
146
+ return false; // Duplicate signer found
147
+ }
148
+ }
149
+ }
150
+
151
+ recoveredSigners[validCount] = currentSigner;
152
+ validCount++;
121
153
 
122
154
  if (isSigner(currentSigner)) {
123
155
  correctSignaturesCount++;
124
156
  }
125
- i++;
126
157
  }
127
158
 
128
159
  return correctSignaturesCount >= threshold;