From f5b6b43f1308a7d01e7ca4ce218aa4cb33b70454 Mon Sep 17 00:00:00 2001 From: Nik G Date: Thu, 28 Nov 2019 15:23:06 +0300 Subject: [PATCH 1/6] Add a view to get members by group index --- .../solidity/contracts/libraries/operator/Groups.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/solidity/contracts/libraries/operator/Groups.sol b/contracts/solidity/contracts/libraries/operator/Groups.sol index 803e7114ec..8d0b5e439a 100644 --- a/contracts/solidity/contracts/libraries/operator/Groups.sol +++ b/contracts/solidity/contracts/libraries/operator/Groups.sol @@ -367,6 +367,17 @@ library Groups { return self.groupMembers[groupPubKey]; } + /** + * @dev Returns addresses of all the members in the provided group. + */ + function membersOf( + Storage storage self, + uint256 groupIndex + ) public view returns (address[] memory members) { + bytes memory groupPubKey = self.groups[groupIndex].groupPubKey; + return self.groupMembers[groupPubKey]; + } + /** * @dev Reports unauthorized signing for the provided group. Must provide * a valid signature of the group address as a message. Successful signature From 675ce14a05d44e2d5a95c3b47b2b06f24168144d Mon Sep 17 00:00:00 2001 From: Nik G Date: Thu, 28 Nov 2019 15:24:01 +0300 Subject: [PATCH 2/6] Implement slashing for relay entry timeout When a group fails to produce an entry, all of its members shall be subject to seizing. The submitter of the trigger transaction shall be treated as the tattletale, but the tattletale reward shall be limited to min(1, 20 / group_size) of the maximum, or effectively the minimum stake of a single member. This is to prevent actors in a lynchpin position from profitably stealing other stakers' funds. --- .../contracts/KeepRandomBeaconOperator.sol | 11 ++++++++++- .../test/TestKeepRandomBeaconOperatorSlashing.js | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol index 1f6f0f7501..5064b6cdd2 100644 --- a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol +++ b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol @@ -583,14 +583,23 @@ contract KeepRandomBeaconOperator is ReentrancyGuard { /** * @dev Function used to inform about the fact the currently ongoing * new relay entry generation operation timed out. As a result, the group - * which was supposed to produce a new relay entry is immediatelly + * which was supposed to produce a new relay entry is immediately * terminated and a new group is selected to produce a new relay entry. + * All members of the group are punished by seizing minimum stake of + * their tokens. The submitter of the transaction is rewarded with a + * tattletale reward which is limited to min(1, 20 / group_size) of the + * maximum tattletale reward. */ function reportRelayEntryTimeout() public { require(hasEntryTimedOut(), "Entry did not time out"); groups.terminateGroup(signingRequest.groupIndex); + // Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see RFC15 for more details. + uint256 rewardAdjustment = uint256(20 * 100).div(groupSize); // Reward adjustment in percentage + rewardAdjustment = rewardAdjustment > 100 ? 100:rewardAdjustment; // Reward adjustment can be 100% max + stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, groups.membersOf(signingRequest.groupIndex)); + // We could terminate the last active group. If that's the case, // do not try to execute signing again because there is no group // which can handle it. diff --git a/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js b/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js index 9fad7198d4..3077d6f553 100644 --- a/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js +++ b/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js @@ -3,6 +3,7 @@ import stakeDelegate from './helpers/stakeDelegate' import {createSnapshot, restoreSnapshot} from "./helpers/snapshot" import {bls} from './helpers/data' import expectThrowWithMessage from './helpers/expectThrowWithMessage' +import mineBlocks from './helpers/mineBlocks' contract('KeepRandomBeaconOperator', function(accounts) { let token, stakingContract, serviceContract, operatorContract, minimumStake, largeStake, entryFeeEstimate, groupIndex, @@ -89,4 +90,17 @@ contract('KeepRandomBeaconOperator', function(accounts) { assert.isTrue((await token.balanceOf(tattletale)).isZero(), "Unexpected tattletale balance") }) + + it("should be able to report failure to produce entry after relay entry timeout", async () => { + mineBlocks(20) + await operatorContract.reportRelayEntryTimeout({from: tattletale}) + + assert.equal((await stakingContract.balanceOf(operator1)).isZero(), true, "Unexpected operator 1 balance") + assert.equal((await stakingContract.balanceOf(operator2)).isZero(), true, "Unexpected operator 2 balance") + assert.equal((await stakingContract.balanceOf(operator3)).isZero(), true, "Unexpected operator 3 balance") + + // Expecting 5% of all the seized tokens with reward adjustment of (20 / 64) = 31% + let expectedTattletaleReward = minimumStake.muln(3).muln(5).divn(100).muln(31).divn(100) + assert.isTrue((await token.balanceOf(tattletale)).eq(expectedTattletaleReward), "Unexpected tattletale balance") + }) }) From 5e564e25d7f88c79f6693c4239a04f094870038e Mon Sep 17 00:00:00 2001 From: Nik G Date: Mon, 27 Jan 2020 10:50:56 -0500 Subject: [PATCH 3/6] Minor test improvements --- .../test/TestKeepRandomBeaconOperatorSlashing.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js b/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js index 3077d6f553..457b0ca696 100644 --- a/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js +++ b/contracts/solidity/test/TestKeepRandomBeaconOperatorSlashing.js @@ -92,12 +92,21 @@ contract('KeepRandomBeaconOperator', function(accounts) { }) it("should be able to report failure to produce entry after relay entry timeout", async () => { + let operator1balance = await stakingContract.balanceOf(operator1) + let operator2balance = await stakingContract.balanceOf(operator2) + let operator3balance = await stakingContract.balanceOf(operator3) + + await expectThrowWithMessage( + operatorContract.reportRelayEntryTimeout({from: tattletale}), + "Entry did not time out." + ) + mineBlocks(20) await operatorContract.reportRelayEntryTimeout({from: tattletale}) - assert.equal((await stakingContract.balanceOf(operator1)).isZero(), true, "Unexpected operator 1 balance") - assert.equal((await stakingContract.balanceOf(operator2)).isZero(), true, "Unexpected operator 2 balance") - assert.equal((await stakingContract.balanceOf(operator3)).isZero(), true, "Unexpected operator 3 balance") + assert.isTrue((await stakingContract.balanceOf(operator1)).eq(operator1balance.sub(minimumStake)), "Unexpected operator 1 balance") + assert.isTrue((await stakingContract.balanceOf(operator2)).eq(operator2balance.sub(minimumStake)), "Unexpected operator 2 balance") + assert.isTrue((await stakingContract.balanceOf(operator3)).eq(operator3balance.sub(minimumStake)), "Unexpected operator 3 balance") // Expecting 5% of all the seized tokens with reward adjustment of (20 / 64) = 31% let expectedTattletaleReward = minimumStake.muln(3).muln(5).divn(100).muln(31).divn(100) From 0d3979ce2903b4c18b988518c2c94d77ca9717d9 Mon Sep 17 00:00:00 2001 From: Nik G Date: Mon, 27 Jan 2020 11:00:15 -0500 Subject: [PATCH 4/6] Minor comment improvement --- contracts/solidity/contracts/KeepRandomBeaconOperator.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol index 5064b6cdd2..adde32cc86 100644 --- a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol +++ b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol @@ -595,7 +595,7 @@ contract KeepRandomBeaconOperator is ReentrancyGuard { groups.terminateGroup(signingRequest.groupIndex); - // Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see RFC15 for more details. + // Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see the Yellow Paper for more details. uint256 rewardAdjustment = uint256(20 * 100).div(groupSize); // Reward adjustment in percentage rewardAdjustment = rewardAdjustment > 100 ? 100:rewardAdjustment; // Reward adjustment can be 100% max stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, groups.membersOf(signingRequest.groupIndex)); From ae6466c81521f489db1190d30b308ff6d2ce3372 Mon Sep 17 00:00:00 2001 From: Nik G Date: Mon, 27 Jan 2020 11:00:56 -0500 Subject: [PATCH 5/6] Update operator contract max size --- contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js b/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js index 5eb69b7d80..41192750a5 100644 --- a/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js +++ b/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js @@ -16,7 +16,7 @@ contract('KeepRandomBeaconOperator', (_) => { let bytecodeSize = bytecode.length / 2; // size in bytes let deployedBytecodeSize = deployedBytecode.length / 2; // size in bytes - const maxSafeBytecodeSize = 22482 + const maxSafeBytecodeSize = 23112 console.log( "KeepRandomBeaconOperator size of bytecode in bytes = ", From d793dbba7584f4de67f275c9e346bebbc834f2ff Mon Sep 17 00:00:00 2001 From: Nik G Date: Mon, 27 Jan 2020 12:21:29 -0500 Subject: [PATCH 6/6] Move parts of code into groups library Moving seize call to groups library. ~500 bytes saved on contract size. --- .../solidity/contracts/KeepRandomBeaconOperator.sol | 8 +------- .../contracts/libraries/operator/Groups.sol | 13 +++++++++++++ .../test/TestKeepRandomBeaconOperatorSize.js | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol index adde32cc86..f275eb7631 100644 --- a/contracts/solidity/contracts/KeepRandomBeaconOperator.sol +++ b/contracts/solidity/contracts/KeepRandomBeaconOperator.sol @@ -592,13 +592,7 @@ contract KeepRandomBeaconOperator is ReentrancyGuard { */ function reportRelayEntryTimeout() public { require(hasEntryTimedOut(), "Entry did not time out"); - - groups.terminateGroup(signingRequest.groupIndex); - - // Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see the Yellow Paper for more details. - uint256 rewardAdjustment = uint256(20 * 100).div(groupSize); // Reward adjustment in percentage - rewardAdjustment = rewardAdjustment > 100 ? 100:rewardAdjustment; // Reward adjustment can be 100% max - stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, groups.membersOf(signingRequest.groupIndex)); + groups.reportRelayEntryTimeout(signingRequest.groupIndex, groupSize, minimumStake); // We could terminate the last active group. If that's the case, // do not try to execute signing again because there is no group diff --git a/contracts/solidity/contracts/libraries/operator/Groups.sol b/contracts/solidity/contracts/libraries/operator/Groups.sol index 8d0b5e439a..3f01ce728b 100644 --- a/contracts/solidity/contracts/libraries/operator/Groups.sol +++ b/contracts/solidity/contracts/libraries/operator/Groups.sol @@ -412,6 +412,19 @@ library Groups { } } + function reportRelayEntryTimeout( + Storage storage self, + uint256 groupIndex, + uint256 groupSize, + uint256 minimumStake + ) public { + terminateGroup(self, groupIndex); + // Reward is limited to min(1, 20 / group_size) of the maximum tattletale reward, see the Yellow Paper for more details. + uint256 rewardAdjustment = uint256(20 * 100).div(groupSize); // Reward adjustment in percentage + rewardAdjustment = rewardAdjustment > 100 ? 100:rewardAdjustment; // Reward adjustment can be 100% max + self.stakingContract.seize(minimumStake, rewardAdjustment, msg.sender, membersOf(self, groupIndex)); + } + /** * @dev Returns members of the given group by group public key. * diff --git a/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js b/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js index 41192750a5..6dc71ead68 100644 --- a/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js +++ b/contracts/solidity/test/TestKeepRandomBeaconOperatorSize.js @@ -16,7 +16,7 @@ contract('KeepRandomBeaconOperator', (_) => { let bytecodeSize = bytecode.length / 2; // size in bytes let deployedBytecodeSize = deployedBytecode.length / 2; // size in bytes - const maxSafeBytecodeSize = 23112 + const maxSafeBytecodeSize = 22546 console.log( "KeepRandomBeaconOperator size of bytecode in bytes = ",