@inco/lightning 0.8.0-devnet-2 → 0.8.0-devnet-4
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/EncryptedInput.sol +32 -15
- package/src/lightning-parts/EncryptedOperations.sol +1 -5
- package/src/lightning-parts/TrivialEncryption.sol +29 -0
- package/src/lightning-parts/interfaces/IEncryptedInput.sol +3 -3
- package/src/lightning-parts/primitives/HandleGeneration.sol +11 -7
- 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 +345 -3
- package/src/periphery/SessionVerifier.sol +3 -1
- package/src/test/FakeIncoInfra/FakeIncoInfraBase.sol +10 -2
- package/src/test/FakeIncoInfra/MockOpHandler.sol +1 -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/src/libs/incoLightning_devnet_v1_887305889.sol +0 -453
- package/src/libs/incoLightning_testnet_v1_938327937.sol +0 -453
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
|
}
|
|
@@ -17,52 +17,61 @@ error ExternalHandleDoesNotMatchComputedHandle(
|
|
|
17
17
|
uint256 chainId,
|
|
18
18
|
address aclAddress,
|
|
19
19
|
address userAddress,
|
|
20
|
-
address contractAddress
|
|
20
|
+
address contractAddress,
|
|
21
|
+
int32 version
|
|
21
22
|
);
|
|
22
23
|
|
|
23
24
|
abstract contract EncryptedInput is IEncryptedInput, BaseAccessControlList, HandleGeneration, Fee {
|
|
24
25
|
|
|
25
26
|
event NewInput(
|
|
26
|
-
bytes32 indexed result,
|
|
27
|
+
bytes32 indexed result,
|
|
28
|
+
address indexed contractAddress,
|
|
29
|
+
address indexed user,
|
|
30
|
+
int32 version,
|
|
31
|
+
bytes ciphertext,
|
|
32
|
+
uint256 eventId
|
|
27
33
|
);
|
|
28
34
|
|
|
29
|
-
function newEuint256(bytes
|
|
35
|
+
function newEuint256(bytes calldata input, address user) external payable returns (euint256 newValue) {
|
|
30
36
|
return euint256.wrap(newInput(input, user, ETypes.Uint256));
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
function newEbool(bytes
|
|
39
|
+
function newEbool(bytes calldata input, address user) external payable returns (ebool newValue) {
|
|
34
40
|
return ebool.wrap(newInput(input, user, ETypes.Bool));
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
function newEaddress(bytes
|
|
43
|
+
function newEaddress(bytes calldata input, address user) external payable returns (eaddress newValue) {
|
|
38
44
|
return eaddress.wrap(newInput(input, user, ETypes.AddressOrUint160OrBytes20));
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
function newInput(bytes
|
|
47
|
+
function newInput(bytes calldata input, address user, ETypes inputType)
|
|
42
48
|
internal
|
|
43
49
|
paying
|
|
44
50
|
returns (bytes32 newHandle)
|
|
45
51
|
{
|
|
46
|
-
newHandle = _newInput(
|
|
52
|
+
newHandle = _newInput(input, user, inputType);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
function newInputNotPaying(bytes
|
|
55
|
+
function newInputNotPaying(bytes calldata input, address user, ETypes inputType)
|
|
50
56
|
internal
|
|
51
57
|
returns (bytes32 newHandle)
|
|
52
58
|
{
|
|
53
|
-
newHandle = _newInput(
|
|
59
|
+
newHandle = _newInput(input, user, inputType);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/// @notice Creates a new input with a prepended handle as a checksum.
|
|
57
63
|
/// @param input The input that contains the handle prepended to the ciphertext.
|
|
58
64
|
/// @param user The user address associated with the input.
|
|
59
|
-
function _newInput(bytes
|
|
65
|
+
function _newInput(bytes calldata input, address user, ETypes inputType) private returns (bytes32 handle) {
|
|
60
66
|
// Since there is no sensible way to handle abi.decode errors (https://github.com/argotorg/solidity/issues/10381)
|
|
61
67
|
// at least fail early on a conservative minimum length
|
|
62
|
-
require(input.length >=
|
|
68
|
+
require(input.length >= 68, "Input too short, should be at least 68 bytes");
|
|
69
|
+
// Parse the version from the first 4 bytes.
|
|
70
|
+
bytes4 prefix = bytes4(input[:4]);
|
|
71
|
+
int32 version = int32(uint32(prefix));
|
|
63
72
|
// Remove external handle prepended to input as a checksum
|
|
64
|
-
(bytes32 externalHandle, bytes memory ciphertext) = abi.decode(input, (bytes32, bytes));
|
|
65
|
-
handle = getInputHandle(ciphertext, user, msg.sender, inputType);
|
|
73
|
+
(bytes32 externalHandle, bytes memory ciphertext) = abi.decode(input[4:], (bytes32, bytes));
|
|
74
|
+
handle = getInputHandle(ciphertext, user, msg.sender, version, inputType);
|
|
66
75
|
require(
|
|
67
76
|
handle == externalHandle,
|
|
68
77
|
ExternalHandleDoesNotMatchComputedHandle({
|
|
@@ -71,14 +80,22 @@ abstract contract EncryptedInput is IEncryptedInput, BaseAccessControlList, Hand
|
|
|
71
80
|
chainId: block.chainid,
|
|
72
81
|
aclAddress: address(this),
|
|
73
82
|
userAddress: user,
|
|
74
|
-
contractAddress: msg.sender
|
|
83
|
+
contractAddress: msg.sender,
|
|
84
|
+
version: version
|
|
75
85
|
})
|
|
76
86
|
);
|
|
77
87
|
// We assume that providing the same handle (which via HADU implies same plaintext, same context, and same
|
|
78
88
|
// instance of encryption)
|
|
79
89
|
require(!isAllowed(handle, user), HandleAlreadyExists(handle));
|
|
80
90
|
uint256 id = getNextEventId();
|
|
81
|
-
emit NewInput({
|
|
91
|
+
emit NewInput({
|
|
92
|
+
result: handle,
|
|
93
|
+
contractAddress: msg.sender,
|
|
94
|
+
user: user,
|
|
95
|
+
version: version,
|
|
96
|
+
ciphertext: ciphertext,
|
|
97
|
+
eventId: id
|
|
98
|
+
});
|
|
82
99
|
setDigest(abi.encodePacked(handle, id));
|
|
83
100
|
// We allow to user since this is harmless and it is convenient to use the allow mapping to track inputs.
|
|
84
101
|
// NOTE: the allow must come after emitting the new input event, since allow emits its own event.
|
|
@@ -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);
|
|
@@ -5,8 +5,8 @@ import {euint256, ebool, eaddress} from "../../Types.sol";
|
|
|
5
5
|
|
|
6
6
|
interface IEncryptedInput {
|
|
7
7
|
|
|
8
|
-
function newEuint256(bytes
|
|
9
|
-
function newEbool(bytes
|
|
10
|
-
function newEaddress(bytes
|
|
8
|
+
function newEuint256(bytes calldata ciphertext, address user) external payable returns (euint256 newValue);
|
|
9
|
+
function newEbool(bytes calldata ciphertext, address user) external payable returns (ebool newValue);
|
|
10
|
+
function newEaddress(bytes calldata ciphertext, address user) external payable returns (eaddress newValue);
|
|
11
11
|
|
|
12
12
|
}
|