Skip to content

Commit a1d3409

Browse files
authored
Merge pull request #1567 from keep-network/terminated-group-tracking
Record and provide more information on groups We can't implement external rewards for beacon signers if we erase records of expired groups or delete members when they withdraw rewards. Here we pack an additional terminated flag in each group's information, and change the reward withdrawal logic to get all rewards for the named operator and flagging that operator as withdrawn. To prevent stub contract deployments from running out of gas, the operator contract and its libraries have been refactored slightly.
2 parents 1cfb448 + b8c074b commit a1d3409

11 files changed

Lines changed: 241 additions & 128 deletions

solidity/contracts/KeepRandomBeaconOperator.sol

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
155155
modifier onlyServiceContract() {
156156
require(
157157
serviceContracts.contains(msg.sender),
158-
"Caller is not an authorized contract"
158+
"Caller is not a service contract"
159159
);
160160
_;
161161
}
@@ -234,7 +234,7 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
234234

235235
// reimbursing a submitter that triggered group selection
236236
(bool success, ) = stakingContract.magpieOf(submitter).call.value(groupSelectionStartFee)("");
237-
require(success, "Failed reimbursing submitter for starting a group selection");
237+
require(success, "Group selection reimbursement failed");
238238
}
239239

240240
function startGroupSelection(uint256 _newEntry, uint256 _payment) internal {
@@ -689,23 +689,26 @@ contract KeepRandomBeaconOperator is ReentrancyGuard {
689689
}
690690

691691
/**
692-
* @dev Gets all indices in the provided group for a member.
692+
* @notice Return whether the given operator
693+
* has withdrawn their rewards from the given group.
693694
*/
694-
function getGroupMemberIndices(bytes memory groupPubKey, address member) public view returns (uint256[] memory indices) {
695-
return groups.getGroupMemberIndices(groupPubKey, member);
695+
function hasWithdrawnRewards(address operator, uint256 groupIndex)
696+
public view returns (bool) {
697+
return groups.hasWithdrawnRewards(operator, groupIndex);
696698
}
697699

698700
/**
699701
* @dev Withdraws accumulated group member rewards for operator
700-
* using the provided group index and member indices. Once the
701-
* accumulated reward is withdrawn from the selected group, member is
702-
* removed from it. Rewards can be withdrawn only from stale group.
702+
* using the provided group index.
703+
* Once the accumulated reward is withdrawn from the selected group,
704+
* the operator is flagged as withdrawn.
705+
* Rewards can be withdrawn only from stale group.
703706
* @param operator Operator address.
704707
* @param groupIndex Group index.
705-
* @param groupMemberIndices Array of member indices for the group member.
706708
*/
707-
function withdrawGroupMemberRewards(address operator, uint256 groupIndex, uint256[] memory groupMemberIndices) public nonReentrant {
708-
uint256 accumulatedRewards = groups.withdrawFromGroup(operator, groupIndex, groupMemberIndices);
709+
function withdrawGroupMemberRewards(address operator, uint256 groupIndex)
710+
public nonReentrant {
711+
uint256 accumulatedRewards = groups.withdrawFromGroup(operator, groupIndex);
709712
(bool success, ) = stakingContract.magpieOf(operator).call.value(accumulatedRewards)("");
710713
if (success) {
711714
emit GroupMemberRewardsWithdrawn(stakingContract.magpieOf(operator), operator, accumulatedRewards, groupIndex);

solidity/contracts/libraries/operator/Groups.sol

Lines changed: 70 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,19 @@ library Groups {
1010
using SafeMath for uint256;
1111
using BytesLib for bytes;
1212

13+
// The index of a group is flagged with the most significant bit set,
14+
// to distinguish the group `0` from null.
15+
// The flag is toggled with bitwise XOR (`^`)
16+
// which keeps all other bits intact but flips the flag bit.
17+
// The flag should be set before writing to `groupIndices`,
18+
// and unset after reading from `groupIndices`
19+
// before using the value.
20+
uint256 constant GROUP_INDEX_FLAG = 1 << 255;
21+
1322
struct Group {
1423
bytes groupPubKey;
15-
uint registrationBlockHeight;
24+
uint64 registrationBlockHeight;
25+
bool terminated;
1626
}
1727

1828
struct Storage {
@@ -23,8 +33,10 @@ library Groups {
2333
// The value is set when the operator contract is added.
2434
uint256 relayEntryTimeout;
2535

36+
// Mapping of `groupPubKey` to flagged `groupIndex`
37+
mapping (bytes => uint256) groupIndices;
2638
Group[] groups;
27-
uint256[] terminatedGroups;
39+
uint256[] activeTerminatedGroups;
2840
mapping (bytes => address[]) groupMembers;
2941

3042
// Sum of all group member rewards earned so far. The value is the same for
@@ -33,6 +45,10 @@ library Groups {
3345
// this value.
3446
mapping (bytes => uint256) groupMemberRewards;
3547

48+
// Mapping of `groupPubKey, operator`
49+
// to whether the operator has withdrawn rewards from that group.
50+
mapping(bytes => mapping(address => bool)) withdrawn;
51+
3652
// expiredGroupOffset is pointing to the first active group, it is also the
3753
// expired groups counter
3854
uint256 expiredGroupOffset;
@@ -47,7 +63,8 @@ library Groups {
4763
Storage storage self,
4864
bytes memory groupPubKey
4965
) internal {
50-
self.groups.push(Group(groupPubKey, block.number));
66+
self.groupIndices[groupPubKey] = (self.groups.length ^ GROUP_INDEX_FLAG);
67+
self.groups.push(Group(groupPubKey, uint64(block.number), false));
5168
}
5269

5370
/**
@@ -123,39 +140,15 @@ library Groups {
123140
return self.groupMembers[groupPubKey][memberIndex];
124141
}
125142

126-
/**
127-
* @dev Gets all indices in the provided group for a member.
128-
*/
129-
function getGroupMemberIndices(
130-
Storage storage self,
131-
bytes memory groupPubKey,
132-
address member
133-
) public view returns (uint256[] memory indices) {
134-
uint256 counter;
135-
for (uint i = 0; i < self.groupMembers[groupPubKey].length; i++) {
136-
if (self.groupMembers[groupPubKey][i] == member) {
137-
counter++;
138-
}
139-
}
140-
141-
indices = new uint256[](counter);
142-
counter = 0;
143-
for (uint i = 0; i < self.groupMembers[groupPubKey].length; i++) {
144-
if (self.groupMembers[groupPubKey][i] == member) {
145-
indices[counter] = i;
146-
counter++;
147-
}
148-
}
149-
}
150-
151143
/**
152144
* @dev Terminates group.
153145
*/
154146
function terminateGroup(
155147
Storage storage self,
156148
uint256 groupIndex
157149
) internal {
158-
self.terminatedGroups.push(groupIndex);
150+
self.groups[groupIndex].terminated = true;
151+
self.activeTerminatedGroups.push(groupIndex);
159152
}
160153

161154
/**
@@ -165,12 +158,7 @@ library Groups {
165158
Storage storage self,
166159
uint256 groupIndex
167160
) internal view returns(bool) {
168-
for (uint i = 0; i < self.terminatedGroups.length; i++) {
169-
if (self.terminatedGroups[i] == groupIndex) {
170-
return true;
171-
}
172-
}
173-
return false;
161+
return self.groups[groupIndex].terminated;
174162
}
175163

176164
/**
@@ -180,12 +168,9 @@ library Groups {
180168
Storage storage self,
181169
bytes memory groupPubKey
182170
) internal view returns(bool) {
183-
for (uint i = 0; i < self.groups.length; i++) {
184-
if (self.groups[i].groupPubKey.equalStorage(groupPubKey)) {
185-
return true;
186-
}
187-
}
188-
return false;
171+
// Values in `groupIndices` are flagged with `GROUP_INDEX_FLAG`
172+
// and thus nonzero, even for group 0
173+
return self.groupIndices[groupPubKey] > 0;
189174
}
190175

191176
/**
@@ -196,7 +181,7 @@ library Groups {
196181
Storage storage self,
197182
Group memory group
198183
) internal view returns(uint256) {
199-
return group.registrationBlockHeight.add(self.groupActiveTime);
184+
return uint256(group.registrationBlockHeight).add(self.groupActiveTime);
200185
}
201186

202187
/**
@@ -224,15 +209,12 @@ library Groups {
224209
Storage storage self,
225210
bytes memory groupPubKey
226211
) public view returns(bool) {
227-
for (uint i = 0; i < self.groups.length; i++) {
228-
if (self.groups[i].groupPubKey.equalStorage(groupPubKey)) {
229-
bool isExpired = self.expiredGroupOffset > i;
230-
bool isStale = groupStaleTime(self, self.groups[i]) < block.number;
231-
return isExpired && isStale;
232-
}
233-
}
234-
235-
revert("Group does not exist");
212+
uint256 flaggedIndex = self.groupIndices[groupPubKey];
213+
require(flaggedIndex != 0, "Group does not exist");
214+
uint256 index = flaggedIndex ^ GROUP_INDEX_FLAG;
215+
bool isExpired = self.expiredGroupOffset > index;
216+
bool isStale = groupStaleTime(self, self.groups[index]) < block.number;
217+
return isExpired && isStale;
236218
}
237219

238220
/**
@@ -258,7 +240,7 @@ library Groups {
258240
function numberOfGroups(
259241
Storage storage self
260242
) internal view returns(uint256) {
261-
return self.groups.length.sub(self.expiredGroupOffset).sub(self.terminatedGroups.length);
243+
return self.groups.length.sub(self.expiredGroupOffset).sub(self.activeTerminatedGroups.length);
262244
}
263245

264246
/**
@@ -273,14 +255,14 @@ library Groups {
273255
self.expiredGroupOffset++;
274256
}
275257

276-
// Go through all terminatedGroups and if some of the terminated
277-
// groups are expired, remove them from terminatedGroups collection.
258+
// Go through all activeTerminatedGroups and if some of the terminated
259+
// groups are expired, remove them from activeTerminatedGroups collection.
278260
// This is needed because we evaluate the shift of selected group index
279261
// based on how many non-expired groups has been terminated.
280-
for (uint i = 0; i < self.terminatedGroups.length; i++) {
281-
if (self.expiredGroupOffset > self.terminatedGroups[i]) {
282-
self.terminatedGroups[i] = self.terminatedGroups[self.terminatedGroups.length - 1];
283-
self.terminatedGroups.length--;
262+
for (uint i = 0; i < self.activeTerminatedGroups.length; i++) {
263+
if (self.expiredGroupOffset > self.activeTerminatedGroups[i]) {
264+
self.activeTerminatedGroups[i] = self.activeTerminatedGroups[self.activeTerminatedGroups.length - 1];
265+
self.activeTerminatedGroups.length--;
284266
}
285267
}
286268
}
@@ -297,7 +279,7 @@ library Groups {
297279
Storage storage self,
298280
uint256 seed
299281
) public returns(uint256) {
300-
require(numberOfGroups(self) > 0, "At least one active group required");
282+
require(numberOfGroups(self) > 0, "No active groups");
301283

302284
expireOldGroups(self);
303285
uint256 selectedGroup = seed % numberOfGroups(self);
@@ -324,8 +306,8 @@ library Groups {
324306
uint256 selectedIndex
325307
) internal view returns(uint256) {
326308
uint256 shiftedIndex = selectedIndex;
327-
for (uint i = 0; i < self.terminatedGroups.length; i++) {
328-
if (self.terminatedGroups[i] <= shiftedIndex) {
309+
for (uint i = 0; i < self.activeTerminatedGroups.length; i++) {
310+
if (self.activeTerminatedGroups[i] <= shiftedIndex) {
329311
shiftedIndex++;
330312
}
331313
}
@@ -335,35 +317,40 @@ library Groups {
335317

336318
/**
337319
* @dev Withdraws accumulated group member rewards for operator
338-
* using the provided group index and member indices. Once the
339-
* accumulated reward is withdrawn from the selected group, member is
340-
* removed from it. Rewards can be withdrawn only from stale group.
320+
* using the provided group index.
321+
* Once the accumulated reward is withdrawn from the selected group,
322+
* the operator is flagged as withdrawn.
323+
* Rewards can be withdrawn only from stale group.
341324
* @param operator Operator address.
342325
* @param groupIndex Group index.
343-
* @param groupMemberIndices Array of member indices for the group member.
344326
*/
345327
function withdrawFromGroup(
346328
Storage storage self,
347329
address operator,
348-
uint256 groupIndex,
349-
uint256[] memory groupMemberIndices
330+
uint256 groupIndex
350331
) public returns (uint256 rewards) {
351332
bool isExpired = self.expiredGroupOffset > groupIndex;
352333
bool isStale = isStaleGroup(self, groupIndex);
353334
require(isExpired && isStale, "Group must be expired and stale");
354335
bytes memory groupPublicKey = getGroupPublicKey(self, groupIndex);
355-
for (uint i = 0; i < groupMemberIndices.length; i++) {
356-
if (operator == self.groupMembers[groupPublicKey][groupMemberIndices[i]]) {
357-
delete self.groupMembers[groupPublicKey][groupMemberIndices[i]];
336+
require(
337+
!(self.withdrawn[groupPublicKey][operator]),
338+
"Rewards already withdrawn"
339+
);
340+
self.withdrawn[groupPublicKey][operator] = true;
341+
for (uint i = 0; i < self.groupMembers[groupPublicKey].length; i++) {
342+
if (operator == self.groupMembers[groupPublicKey][i]) {
358343
rewards = rewards.add(self.groupMemberRewards[groupPublicKey]);
359344
}
360345
}
361346
}
362347

363348
/**
364-
* @dev Returns addresses of all the members in the provided group.
349+
* @dev Returns members of the given group by group public key.
350+
*
351+
* @param groupPubKey Group public key.
365352
*/
366-
function membersOf(
353+
function getGroupMembers(
367354
Storage storage self,
368355
bytes memory groupPubKey
369356
) public view returns (address[] memory members) {
@@ -373,7 +360,7 @@ library Groups {
373360
/**
374361
* @dev Returns addresses of all the members in the provided group.
375362
*/
376-
function membersOf(
363+
function getGroupMembers(
377364
Storage storage self,
378365
uint256 groupIndex
379366
) public view returns (address[] memory members) {
@@ -407,7 +394,7 @@ library Groups {
407394
terminateGroup(self, groupIndex);
408395
self.stakingContract.seize(minimumStake, 100, msg.sender, self.groupMembers[groupPubKey]);
409396
} else {
410-
revert("Group is terminated or the signature is invalid");
397+
revert("Group terminated or sig invalid");
411398
}
412399
}
413400

@@ -421,15 +408,18 @@ library Groups {
421408
// Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see the Yellow Paper for more details.
422409
uint256 rewardAdjustment = uint256(20 * 100).div(groupSize); // Reward adjustment in percentage
423410
rewardAdjustment = rewardAdjustment > 100 ? 100:rewardAdjustment; // Reward adjustment can be 100% max
424-
self.stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, membersOf(self, groupIndex));
411+
self.stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, getGroupMembers(self, groupIndex));
425412
}
426413

427414
/**
428-
* @dev Returns members of the given group by group public key.
429-
*
430-
* @param groupPubKey Group public key.
415+
* @notice Return whether the given operator
416+
* has withdrawn their rewards from the given group.
431417
*/
432-
function getGroupMembers(Storage storage self, bytes memory groupPubKey) public view returns (address[] memory ) {
433-
return self.groupMembers[groupPubKey];
418+
function hasWithdrawnRewards(
419+
Storage storage self,
420+
address operator,
421+
uint256 groupIndex
422+
) public view returns (bool) {
423+
return self.withdrawn[getGroupPublicKey(self, groupIndex)][operator];
434424
}
435425
}

0 commit comments

Comments
 (0)