@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 +23 -67
- package/package.json +1 -1
- package/src/lightning-parts/AccessControl/test/TestAdvancedAccessControl.t.sol +75 -1
- package/src/lightning-parts/AccessControl/test/TestBaseAccessControl.t.sol +245 -0
- package/src/lightning-parts/EncryptedOperations.sol +1 -5
- package/src/lightning-parts/TrivialEncryption.sol +29 -0
- package/src/lightning-parts/primitives/SignatureVerifier.sol +42 -11
- package/src/lightning-parts/primitives/test/SignatureVerifier.t.sol +454 -29
- package/src/lightning-parts/test/HandleMetadata.t.sol +335 -1
- package/src/periphery/SessionVerifier.sol +3 -1
- package/src/test/TEELifecycle/TEELifecycleMockTest.t.sol +231 -1
- package/src/test/TestEventCounter.t.sol +43 -0
- package/src/test/TestFakeInfra.t.sol +9 -2
- package/src/test/TestFeeWithdrawal.t.sol +0 -1
- package/src/test/TestIncoUtils.t.sol +50 -0
- package/src/test/TestLib.t.sol +794 -0
- package/src/test/TestUpgrade.t.sol +114 -0
- package/src/test/TestVersion.t.sol +1 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Inco
|
|
1
|
+
# Inco Lightning
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The core Inco Lightning smart contracts library for building confidential applications on EVM chains.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
## Install dependencies
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ bun install
|
|
|
12
12
|
|
|
13
13
|
## Build
|
|
14
14
|
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
41
|
+
## Directory Structure
|
|
70
42
|
|
|
71
|
-
|
|
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
|
-
##
|
|
51
|
+
## Makefile Commands
|
|
74
52
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
For comprehensive deployment and upgrade instructions, see [`lightning-deployment/`](../lightning-deployment/README.md).
|
|
88
63
|
|
|
89
|
-
|
|
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
|
@@ -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
|
-
/// @
|
|
99
|
-
/// @dev
|
|
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
|
|
128
|
+
uint256 validCount = 0; // Track number of valid (non-malformed) signatures processed
|
|
113
129
|
|
|
114
|
-
|
|
115
|
-
address currentSigner =
|
|
130
|
+
for (uint256 i = 0; i < signaturesLength && correctSignaturesCount < threshold; i++) {
|
|
131
|
+
(address currentSigner, ECDSA.RecoverError err,) = ECDSA.tryRecover(digest, signatures[i]);
|
|
116
132
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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;
|