Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stagger allowed miner responses during a dispute #842

Merged
merged 18 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/colony/IColony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -816,12 +816,14 @@ contract IColony is ColonyDataTypes, IRecovery {
/// @param _user User allowing their tokens to be obligated.
/// @param _obligator Address of the account we are willing to let obligate us.
/// @param _domainId Domain in which we are willing to be obligated.
/// @return approval The amount the user has approved
function getApproval(address _user, address _obligator, uint256 _domainId) public view returns (uint256 approval);

/// @notice View an obligation of tokens.
/// @param _user User whose tokens are obligated.
/// @param _obligator Address of the account who obligated us.
/// @param _domainId Domain in which we are obligated.
/// @return obligation The amount that is currently obligated
function getObligation(address _user, address _obligator, uint256 _domainId) public view returns (uint256 obligation);

/// @notice Get the domain corresponding to a funding pot
Expand Down
167 changes: 87 additions & 80 deletions contracts/reputationMiningCycle/IReputationMiningCycle.sol

Large diffs are not rendered by default.

467 changes: 153 additions & 314 deletions contracts/reputationMiningCycle/ReputationMiningCycle.sol

Large diffs are not rendered by default.

187 changes: 187 additions & 0 deletions contracts/reputationMiningCycle/ReputationMiningCycleBinarySearch.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
This file is part of The Colony Network.

The Colony Network is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The Colony Network is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with The Colony Network. If not, see <http://www.gnu.org/licenses/>.
*/

pragma solidity 0.5.8;
pragma experimental "ABIEncoderV2";

import "../../lib/dappsys/math.sol";
import "../colonyNetwork/IColonyNetwork.sol";
import "../patriciaTree/PatriciaTreeProofs.sol";
import "../tokenLocking/ITokenLocking.sol";
import "./ReputationMiningCycleStorage.sol";
import "./ReputationMiningCycleCommon.sol";


contract ReputationMiningCycleBinarySearch is ReputationMiningCycleCommon {
function respondToBinarySearchForChallenge(
uint256 _round,
uint256 _idx,
bytes memory _jhIntermediateValue,
bytes32[] memory _siblings
) public
{
require(_idx < disputeRounds[_round].length, "colony-reputation-mining-index-beyond-round-length");
require(disputeRounds[_round][_idx].lowerBound != disputeRounds[_round][_idx].upperBound, "colony-reputation-mining-challenge-not-active");
require(
responsePossible(DisputeStages.BinarySearchResponse, disputeRounds[_round][_idx].lastResponseTimestamp),
"colony-reputation-mining-user-ineligible-to-respond"
);

uint256 targetNode = disputeRounds[_round][_idx].lowerBound;
bytes32 targetHashDuringSearch = disputeRounds[_round][_idx].targetHashDuringSearch;
bytes32 impliedRoot;
bytes32[2] memory lastSiblings;

Submission storage submission = reputationHashSubmissions[disputeRounds[_round][_idx].firstSubmitter];
// Check proof is the right length
uint256 expectedLength = expectedProofLength(submission.jrhNLeaves, disputeRounds[_round][_idx].lowerBound) -
(disputeRounds[_round][_idx].challengeStepCompleted - 1); // We expect shorter proofs the more chanllenge rounds we've done so far
require(expectedLength == _siblings.length, "colony-reputation-mining-invalid-binary-search-proof-length");
// Because branchmasks are used from the end, we can just get the whole branchmask. We will run out of siblings before we run out of
// branchmask, if everything is working right.
uint256 branchMask = expectedBranchMask(submission.jrhNLeaves, disputeRounds[_round][_idx].lowerBound);

(impliedRoot, lastSiblings) = getFinalPairAndImpliedRootNoHash(
bytes32(targetNode),
_jhIntermediateValue,
branchMask,
_siblings
);
require(impliedRoot == targetHashDuringSearch, "colony-reputation-mining-invalid-binary-search-response");
// If require hasn't thrown, proof is correct.
// Process the consequences
processBinaryChallengeSearchResponse(_round, _idx, _jhIntermediateValue, lastSiblings);
// Reward the user
rewardResponder(msg.sender);

emit BinarySearchStep(submission.proposedNewRootHash, submission.nLeaves, submission.jrh);
}

function confirmBinarySearchResult(
uint256 _round,
uint256 _idx,
bytes memory _jhIntermediateValue,
bytes32[] memory _siblings
) public
{
require(_idx < disputeRounds[_round].length, "colony-reputation-mining-index-beyond-round-length");
Submission storage submission = reputationHashSubmissions[disputeRounds[_round][_idx].firstSubmitter];
require(submission.jrhNLeaves != 0, "colony-reputation-jrh-hash-not-verified");
require(disputeRounds[_round][_idx].lowerBound == disputeRounds[_round][_idx].upperBound, "colony-reputation-binary-search-incomplete");
require(
2**(disputeRounds[_round][_idx].challengeStepCompleted - 2) <= submission.jrhNLeaves,
"colony-reputation-binary-search-result-already-confirmed"
);
require(
responsePossible(DisputeStages.BinarySearchConfirm, disputeRounds[_round][_idx].lastResponseTimestamp),
"colony-reputation-mining-user-ineligible-to-respond"
);

// uint256 targetNode = disputeRounds[round][idx].lowerBound;
uint256 branchMask = expectedBranchMask(submission.jrhNLeaves, disputeRounds[_round][_idx].lowerBound);
bytes32 impliedRoot = getImpliedRootNoHashKey(bytes32(disputeRounds[_round][_idx].lowerBound), _jhIntermediateValue, branchMask, _siblings);
require(impliedRoot == submission.jrh, "colony-reputation-mining-invalid-binary-search-confirmation");
bytes32 intermediateReputationHash;
uint256 intermediateReputationNLeaves;
assembly {
intermediateReputationHash := mload(add(_jhIntermediateValue, 0x20))
intermediateReputationNLeaves := mload(add(_jhIntermediateValue, 0x40))
}
disputeRounds[_round][_idx].intermediateReputationHash = intermediateReputationHash;
disputeRounds[_round][_idx].intermediateReputationNLeaves = intermediateReputationNLeaves;
while (2**(disputeRounds[_round][_idx].challengeStepCompleted - 2) <= submission.jrhNLeaves) {
disputeRounds[_round][_idx].challengeStepCompleted += 1;
}
disputeRounds[_round][_idx].lastResponseTimestamp = now;

rewardResponder(msg.sender);

emit BinarySearchConfirmed(submission.proposedNewRootHash, submission.nLeaves, submission.jrh, disputeRounds[_round][_idx].lowerBound);
}

function processBinaryChallengeSearchResponse(
uint256 _round,
uint256 _idx,
bytes memory _jhIntermediateValue,
bytes32[2] memory _lastSiblings
) internal
{
disputeRounds[_round][_idx].lastResponseTimestamp = now;
disputeRounds[_round][_idx].challengeStepCompleted += 1;
// Save our intermediate hash
bytes32 intermediateReputationHash;
uint256 intermediateReputationNLeaves;
assembly {
intermediateReputationHash := mload(add(_jhIntermediateValue, 0x20))
intermediateReputationNLeaves := mload(add(_jhIntermediateValue, 0x40))
}
disputeRounds[_round][_idx].intermediateReputationHash = intermediateReputationHash;
disputeRounds[_round][_idx].intermediateReputationNLeaves = intermediateReputationNLeaves;

disputeRounds[_round][_idx].hash1 = _lastSiblings[0];
disputeRounds[_round][_idx].hash2 = _lastSiblings[1];

uint256 opponentIdx = getOpponentIdx(_idx);
if (disputeRounds[_round][opponentIdx].challengeStepCompleted == disputeRounds[_round][_idx].challengeStepCompleted ) {
// Our opponent answered this challenge already.
// Compare our intermediateReputationHash to theirs to establish how to move the bounds.
processBinaryChallengeSearchStep(_round, _idx);
}
}

function processBinaryChallengeSearchStep(uint256 _round, uint256 _idx) internal {
uint256 opponentIdx = getOpponentIdx(_idx);
uint256 searchWidth = (disputeRounds[_round][_idx].upperBound - disputeRounds[_round][_idx].lowerBound) + 1;
uint256 searchWidthNextPowerOfTwo = nextPowerOfTwoInclusive(searchWidth);
if (
disputeRounds[_round][opponentIdx].hash1 == disputeRounds[_round][_idx].hash1
)
{
disputeRounds[_round][_idx].lowerBound += searchWidthNextPowerOfTwo/2;
disputeRounds[_round][opponentIdx].lowerBound += searchWidthNextPowerOfTwo/2;
disputeRounds[_round][_idx].targetHashDuringSearch = disputeRounds[_round][_idx].hash2;
disputeRounds[_round][opponentIdx].targetHashDuringSearch = disputeRounds[_round][opponentIdx].hash2;
} else {
disputeRounds[_round][_idx].upperBound -= (searchWidth - searchWidthNextPowerOfTwo/2);
disputeRounds[_round][opponentIdx].upperBound -= (searchWidth - searchWidthNextPowerOfTwo/2);
disputeRounds[_round][_idx].targetHashDuringSearch = disputeRounds[_round][_idx].hash1;
disputeRounds[_round][opponentIdx].targetHashDuringSearch = disputeRounds[_round][opponentIdx].hash1;
}
// We need to keep the intermediate hashes so that we can figure out what type of dispute we are resolving later
// If the number of nodes in the reputation state are different, then we are disagreeing on whether this log entry
// corresponds to an existing reputation entry or not.
// If the hashes are different, then it's a calculation error.
// However, the intermediate hashes saved might not be the ones that correspond to the first disagreement, based on how exactly the last
// step of the binary challenge came to be.

// If complete, mark that the binary search is completed (but the intermediate hashes may or may not be correct) by setting
// challengeStepCompleted to the maximum it could be for the number of nodes we had to search through, plus one to indicate
// they've submitted their jrh
Submission storage submission = reputationHashSubmissions[disputeRounds[_round][_idx].firstSubmitter];
if (disputeRounds[_round][_idx].lowerBound == disputeRounds[_round][_idx].upperBound) {
if (2**(disputeRounds[_round][_idx].challengeStepCompleted-1) < submission.jrhNLeaves) {
disputeRounds[_round][_idx].challengeStepCompleted += 1;
disputeRounds[_round][opponentIdx].challengeStepCompleted += 1;
}
}

// Our opponent responded to this step of the challenge before we did, so we should
// reset their 'last response' time to now, as they aren't able to respond
// to the next challenge before they know what it is!
disputeRounds[_round][opponentIdx].lastResponseTimestamp = now;
}
}
98 changes: 84 additions & 14 deletions contracts/reputationMiningCycle/ReputationMiningCycleCommon.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ contract ReputationMiningCycleCommon is ReputationMiningCycleStorage, PatriciaTr
/// @notice Size of mining window in seconds
uint256 constant MINING_WINDOW_SIZE = 60 * 60 * 24; // 24 hours

function expectedBranchMask(uint256 _nNodes, uint256 _node) public pure returns (uint256) {
// Gets the expected branchmask for a patricia tree which has nNodes, with keys from 0 to nNodes -1
// i.e. the tree is 'full' - there are no missing nodes
uint256 mask = sub(_nNodes, 1); // Every branchmask in a full tree has at least these 1s set
uint256 xored = mask ^ _node; // Where do mask and node differ?
// Set every bit in the mask from the first bit where they differ to 1
uint256 remainderMask = sub(nextPowerOfTwoInclusive(add(xored, 1)), 1);
return mask | remainderMask;
}

function rewardResponder(address _responder) internal returns (bytes32) {
respondedToChallenge[_responder] = true;
uint256 reward = disputeRewardSize();
Expand All @@ -42,7 +52,6 @@ contract ReputationMiningCycleCommon is ReputationMiningCycleStorage, PatriciaTr
}

function disputeRewardSize() internal returns (uint256) {
// TODO: Is this worth calculating once, and then saving? Seems quite likely.
uint256 nLogEntries = reputationUpdateLog.length;

// If there's no log, it must be one of the first two reputation cycles - no reward.
Expand Down Expand Up @@ -85,19 +94,19 @@ contract ReputationMiningCycleCommon is ReputationMiningCycleStorage, PatriciaTr

// https://ethereum.stackexchange.com/questions/8086/logarithm-math-operation-in-solidity
// Some impressive de Bruijn sequence magic here...
function log2Ceiling(uint x) internal pure returns (uint y) {
function log2Ceiling(uint _x) internal pure returns (uint y) {
assembly {
let arg := x
x := sub(x,1)
x := or(x, div(x, 0x02))
x := or(x, div(x, 0x04))
x := or(x, div(x, 0x10))
x := or(x, div(x, 0x100))
x := or(x, div(x, 0x10000))
x := or(x, div(x, 0x100000000))
x := or(x, div(x, 0x10000000000000000))
x := or(x, div(x, 0x100000000000000000000000000000000))
x := add(x, 1)
let arg := _x
_x := sub(_x,1)
_x := or(_x, div(_x, 0x02))
_x := or(_x, div(_x, 0x04))
_x := or(_x, div(_x, 0x10))
_x := or(_x, div(_x, 0x100))
_x := or(_x, div(_x, 0x10000))
_x := or(_x, div(_x, 0x100000000))
_x := or(_x, div(_x, 0x10000000000000000))
_x := or(_x, div(_x, 0x100000000000000000000000000000000))
_x := add(_x, 1)
let m := mload(0x40)
mstore(m, 0xf8f9cbfae6cc78fbefe7cdc3a1793dfcf4f0e8bbd8cec470b6a28a7a5a3e1efd)
mstore(add(m,0x20), 0xf5ecf1b3e9debc68e1d9cfabc5997135bfb7a7a3938b7b606b5b4b3f2f1f0ffe)
Expand All @@ -110,9 +119,70 @@ contract ReputationMiningCycleCommon is ReputationMiningCycleStorage, PatriciaTr
mstore(0x40, add(m, 0x100))
let magic := 0x818283848586878898a8b8c8d8e8f929395969799a9b9d9e9faaeb6bedeeff
let shift := 0x100000000000000000000000000000000000000000000000000000000000000
let a := div(mul(x, magic), shift)
let a := div(mul(_x, magic), shift)
y := div(mload(add(m,sub(255,a))), shift)
y := add(y, mul(256, gt(arg, 0x8000000000000000000000000000000000000000000000000000000000000000)))
}
}

uint256 constant UINT256_MAX = 2**256 - 1;
uint256 constant SUBMITTER_ONLY_WINDOW_DURATION = 60 * 10;
uint256 constant Y = UINT256_MAX / SUBMITTER_ONLY_WINDOW_DURATION;

function responsePossible(DisputeStages _stage, uint256 _responseWindowOpened) internal view returns (bool) {
if (_responseWindowOpened > now) {
return false;
}

uint256 windowOpenFor = now - _responseWindowOpened;

if (windowOpenFor <= SUBMITTER_ONLY_WINDOW_DURATION) {
// require user made a submission
if (reputationHashSubmissions[msg.sender].proposedNewRootHash == bytes32(0x00)) {
return false;
}
uint256 target = windowOpenFor * Y;
if (uint256(keccak256(abi.encodePacked(msg.sender, _stage))) > target) {
return false;
}
}
return true;
}

function nextPowerOfTwoInclusive(uint256 _v) internal pure returns (uint) { // solium-disable-line security/no-assign-params
// Returns the next power of two, or v if v is already a power of two.
// Doesn't work for zero.
_v = sub(_v, 1);
_v |= _v >> 1;
_v |= _v >> 2;
_v |= _v >> 4;
_v |= _v >> 8;
_v |= _v >> 16;
_v |= _v >> 32;
_v |= _v >> 64;
_v |= _v >> 128;
_v = add(_v, 1);
return _v;
}

function expectedProofLength(uint256 _nNodes, uint256 _node) internal pure returns (uint256) { // solium-disable-line security/no-assign-params
_nNodes -= 1;
uint256 nextPowerOfTwo = nextPowerOfTwoInclusive(_nNodes + 1);
uint256 layers = 0;
while (_nNodes != 0 && (_node+1 > nextPowerOfTwo / 2)) {
_nNodes -= nextPowerOfTwo/2;
_node -= nextPowerOfTwo/2;
layers += 1;
nextPowerOfTwo = nextPowerOfTwoInclusive(_nNodes + 1);
}
while (nextPowerOfTwo > 1) {
layers += 1;
nextPowerOfTwo >>= 1;
}
return layers;
}

function getOpponentIdx(uint256 _idx) internal pure returns (uint256) {
return _idx % 2 == 1 ? _idx - 1 : _idx + 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,18 @@ contract ReputationMiningCycleDataTypes {
// Justification tree.
}


enum DisputeStages { ConfirmJRH, BinarySearchResponse, BinarySearchConfirm, RespondToChallenge, InvalidateHash, ConfirmNewHash }

event ReputationRootHashSubmitted(address _miner, bytes32 _newHash, uint256 _nLeaves, bytes32 _jrh, uint256 _entryIndex);
event JustificationRootHashConfirmed(bytes32 _newHash, uint256 _nLeaves, bytes32 _jrh);
event BinarySearchConfirmed(bytes32 _newHash, uint256 _nLeaves, bytes32 _jrh, uint256 _firstDisagreeIdx);
event ChallengeCompleted(bytes32 _newHash, uint256 _nLeaves, bytes32 _jrh);
event HashInvalidated(bytes32 _newHash, uint256 _nLeaves, bytes32 _jrh);

event BinarySearchStep(bytes32 _newHash, uint256 _nLeaves, bytes32 _jrh);

/// @notice Event logged when a reputation UID is proven to be correct in a challenge
event ProveUIDSuccess(uint256 previousNewReputationUID, uint256 _disagreeStateReputationUID, bool existingUID);
event ProveUIDSuccess(uint256 previousNewReputationUID, uint256 _disagreeStateReputationUID, bool _existingUID);

/// @notice Event logged when a reputation value is proven to be correct in a challenge
event ProveValueSuccess(int256 _agreeStateReputationValue, int256 _disagreeStateReputationValue, int256 _originReputationValue);
Expand Down
Loading