-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathLightClient.sol
More file actions
295 lines (247 loc) · 11.5 KB
/
Copy pathLightClient.sol
File metadata and controls
295 lines (247 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
pragma solidity 0.8.16;
import {SSZ} from "src/libraries/SimpleSerialize.sol";
import {ILightClient} from "src/lightclient/interfaces/ILightClient.sol";
import {StepVerifier} from "src/lightclient/StepVerifier.sol";
import {RotateVerifier} from "src/lightclient/RotateVerifier.sol";
import {OptimizedRotateVerifier} from "src/lightclient/OptimizedRotateVerifier.sol";
struct Groth16Proof {
uint256[2] a;
uint256[2][2] b;
uint256[2] c;
}
struct LightClientStep {
uint256 attestedSlot;
uint256 finalizedSlot;
uint256 participation;
bytes32 finalizedHeaderRoot;
bytes32 executionStateRoot;
Groth16Proof proof;
}
struct LightClientRotate {
LightClientStep step;
bytes32 syncCommitteeSSZ;
bytes32 syncCommitteePoseidon;
Groth16Proof proof;
}
struct LightClientOptimizedRotate {
LightClientStep step;
bytes32 syncCommitteeSSZ;
bytes32 syncCommitteePoseidon;
Groth16Proof proof;
}
/// @title Light Client
/// @author Succinct Labs
/// @notice Uses Ethereum 2's Sync Committee Protocol to keep up-to-date with block headers from a
/// Beacon Chain. This is done in a gas-efficient manner using zero-knowledge proofs.
contract LightClient is ILightClient, StepVerifier, RotateVerifier, OptimizedRotateVerifier {
bytes32 public immutable GENESIS_VALIDATORS_ROOT;
uint256 public immutable GENESIS_TIME;
uint256 public immutable SECONDS_PER_SLOT;
uint256 public immutable SLOTS_PER_PERIOD;
uint32 public immutable SOURCE_CHAIN_ID;
uint16 public immutable FINALITY_THRESHOLD;
uint256 internal constant MIN_SYNC_COMMITTEE_PARTICIPANTS = 10;
uint256 internal constant SYNC_COMMITTEE_SIZE = 512;
uint256 internal constant FINALIZED_ROOT_INDEX = 105;
uint256 internal constant NEXT_SYNC_COMMITTEE_INDEX = 55;
uint256 internal constant EXECUTION_STATE_ROOT_INDEX = 402;
/// @notice Whether the light client has had conflicting variables for the same slot.
bool public consistent = true;
/// @notice The latest slot the light client has a finalized header for.
uint256 public head = 0;
/// @notice Maps from a slot to a beacon block header root.
mapping(uint256 => bytes32) public headers;
/// @notice Maps from a slot to the timestamp of when the headers mapping was updated with slot as a key
mapping(uint256 => uint256) public timestamps;
/// @notice Maps from a slot to the current finalized ethereum1 execution state root.
mapping(uint256 => bytes32) public executionStateRoots;
/// @notice Maps from a period to the poseidon commitment for the sync committee.
mapping(uint256 => bytes32) public syncCommitteePoseidons;
event HeadUpdate(uint256 indexed slot, bytes32 indexed root);
event SyncCommitteeUpdate(uint256 indexed period, bytes32 indexed root);
constructor(
bytes32 genesisValidatorsRoot,
uint256 genesisTime,
uint256 secondsPerSlot,
uint256 slotsPerPeriod,
uint256 syncCommitteePeriod,
bytes32 syncCommitteePoseidon,
uint32 sourceChainId,
uint16 finalityThreshold
) {
GENESIS_VALIDATORS_ROOT = genesisValidatorsRoot;
GENESIS_TIME = genesisTime;
SECONDS_PER_SLOT = secondsPerSlot;
SLOTS_PER_PERIOD = slotsPerPeriod;
SOURCE_CHAIN_ID = sourceChainId;
FINALITY_THRESHOLD = finalityThreshold;
setSyncCommitteePoseidon(syncCommitteePeriod, syncCommitteePoseidon);
}
/// @notice Updates the head of the light client to the provided slot.
/// @dev The conditions for updating the head of the light client involve checking:
/// 1) Enough signatures from the current sync committee for n=512
/// 2) A valid finality proof
/// 3) A valid execution state root proof
function step(LightClientStep memory update) external {
bool finalized = processStep(update);
if (getCurrentSlot() < update.attestedSlot) {
revert("Update slot is too far in the future");
}
if (update.finalizedSlot < head) {
revert("Update slot less than current head");
}
if (finalized) {
setSlotRoots(
update.finalizedSlot, update.finalizedHeaderRoot, update.executionStateRoot
);
} else {
revert("Not enough participants");
}
}
/// @notice Sets the sync committee for the next sync committeee period.
/// @dev A commitment to the the next sync committeee is signed by the current sync committee.
function rotate(LightClientRotate memory update) external {
LightClientStep memory stepUpdate = update.step;
bool finalized = processStep(update.step);
uint256 currentPeriod = getSyncCommitteePeriod(stepUpdate.finalizedSlot);
uint256 nextPeriod = currentPeriod + 1;
zkLightClientRotate(update);
if (finalized) {
setSyncCommitteePoseidon(nextPeriod, update.syncCommitteePoseidon);
}
}
/// @notice Calls step using the update in rotate. Sets the sync committee for the next sync committeee period.
/// @dev A commitment to the the next sync committeee is signed by the current sync committee.
function optimizedRotate(LightClientOptimizedRotate memory update) external {
LightClientStep memory stepUpdate = update.step;
bool finalized = processStep(stepUpdate);
if (getCurrentSlot() < stepUpdate.attestedSlot) {
revert("Update slot is too far in the future");
}
if (stepUpdate.finalizedSlot < head) {
revert("Update slot less than current head");
}
if (finalized) {
setSlotRoots(
stepUpdate.finalizedSlot,
stepUpdate.finalizedHeaderRoot,
stepUpdate.executionStateRoot
);
} else {
revert("Not enough participants");
}
uint256 currentPeriod = getSyncCommitteePeriod(stepUpdate.finalizedSlot);
uint256 nextPeriod = currentPeriod + 1;
zkLightClientOptimizedRotate(update);
// Always finalized, as otherwise we would have reverted in step.
setSyncCommitteePoseidon(nextPeriod, update.syncCommitteePoseidon);
}
/// @notice Verifies that the header has enough signatures for finality.
function processStep(LightClientStep memory update) internal view returns (bool) {
uint256 currentPeriod = getSyncCommitteePeriod(update.attestedSlot);
if (syncCommitteePoseidons[currentPeriod] == 0) {
revert("Sync committee for current period is not initialized.");
} else if (update.participation < MIN_SYNC_COMMITTEE_PARTICIPANTS) {
revert("Less than MIN_SYNC_COMMITTEE_PARTICIPANTS signed.");
}
zkLightClientStep(update);
return update.participation > FINALITY_THRESHOLD;
}
/// @notice Serializes the public inputs into a compressed form and verifies the step proof.
function zkLightClientStep(LightClientStep memory update) internal view {
bytes32 attestedSlotLE = SSZ.toLittleEndian(update.attestedSlot);
bytes32 finalizedSlotLE = SSZ.toLittleEndian(update.finalizedSlot);
bytes32 participationLE = SSZ.toLittleEndian(update.participation);
uint256 currentPeriod = getSyncCommitteePeriod(update.attestedSlot);
bytes32 syncCommitteePoseidon = syncCommitteePoseidons[currentPeriod];
bytes32 h;
h = sha256(bytes.concat(attestedSlotLE, finalizedSlotLE));
h = sha256(bytes.concat(h, update.finalizedHeaderRoot));
h = sha256(bytes.concat(h, participationLE));
h = sha256(bytes.concat(h, update.executionStateRoot));
h = sha256(bytes.concat(h, syncCommitteePoseidon));
uint256 t = uint256(SSZ.toLittleEndian(uint256(h)));
t = t & ((uint256(1) << 253) - 1);
Groth16Proof memory proof = update.proof;
uint256[1] memory inputs = [uint256(t)];
require(verifyProofStep(proof.a, proof.b, proof.c, inputs));
}
/// @notice Serializes the public inputs and verifies the rotate proof.
function zkLightClientRotate(LightClientRotate memory update) internal view {
Groth16Proof memory proof = update.proof;
uint256[65] memory inputs;
uint256 syncCommitteeSSZNumeric = uint256(update.syncCommitteeSSZ);
for (uint256 i = 0; i < 32; i++) {
inputs[32 - 1 - i] = syncCommitteeSSZNumeric % 2 ** 8;
syncCommitteeSSZNumeric = syncCommitteeSSZNumeric / 2 ** 8;
}
uint256 finalizedHeaderRootNumeric = uint256(update.step.finalizedHeaderRoot);
for (uint256 i = 0; i < 32; i++) {
inputs[64 - i] = finalizedHeaderRootNumeric % 2 ** 8;
finalizedHeaderRootNumeric = finalizedHeaderRootNumeric / 2 ** 8;
}
inputs[32] = uint256(SSZ.toLittleEndian(uint256(update.syncCommitteePoseidon)));
require(verifyProofRotate(proof.a, proof.b, proof.c, inputs));
}
/// @notice Serializes the public inputs into a compressed form and verifies the rotate proof.
function zkLightClientOptimizedRotate(LightClientOptimizedRotate memory update) internal view {
bytes32 h;
h = sha256(
bytes.concat(
update.step.finalizedHeaderRoot,
update.syncCommitteeSSZ,
update.syncCommitteePoseidon
)
);
uint256 t = uint256(SSZ.toLittleEndian(uint256(h)));
t = t & ((uint256(1) << 253) - 1);
Groth16Proof memory proof = update.proof;
uint256[1] memory inputs = [uint256(t)];
require(verifyProofOptimizedRotate(proof.a, proof.b, proof.c, inputs));
}
/// @notice Gets the sync committee period from a slot.
function getSyncCommitteePeriod(uint256 slot) internal view returns (uint256) {
return slot / SLOTS_PER_PERIOD;
}
/// @notice Gets the current slot for the chain the light client is reflecting.
function getCurrentSlot() internal view returns (uint256) {
return (block.timestamp - GENESIS_TIME) / SECONDS_PER_SLOT;
}
/// @notice Sets the current slot for the chain the light client is reflecting.
/// @dev Checks if roots exists for the slot already. If there is, check for a conflict between
/// the given roots and the existing roots. If there is an existing header but no
/// conflict, do nothing. This avoids timestamp renewal DoS attacks.
function setSlotRoots(uint256 slot, bytes32 finalizedHeaderRoot, bytes32 executionStateRoot)
internal
{
if (headers[slot] != bytes32(0)) {
if (headers[slot] != finalizedHeaderRoot) {
consistent = false;
}
return;
}
if (executionStateRoots[slot] != bytes32(0)) {
if (executionStateRoots[slot] != executionStateRoot) {
consistent = false;
}
return;
}
head = slot;
headers[slot] = finalizedHeaderRoot;
executionStateRoots[slot] = executionStateRoot;
timestamps[slot] = block.timestamp;
emit HeadUpdate(slot, finalizedHeaderRoot);
}
/// @notice Sets the sync committee poseidon for a given period.
function setSyncCommitteePoseidon(uint256 period, bytes32 poseidon) internal {
if (
syncCommitteePoseidons[period] != bytes32(0)
&& syncCommitteePoseidons[period] != poseidon
) {
consistent = false;
return;
}
syncCommitteePoseidons[period] = poseidon;
emit SyncCommitteeUpdate(period, poseidon);
}
}