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

Feat/subnets #136

Merged
merged 17 commits into from Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Expand Up @@ -18,15 +18,15 @@ commands:
- restore_cache:
name: Restore node_modules
keys:
- v2-dependencies-{{ checksum "yarn.lock" }}
- v3-dependencies-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install
- save_cache:
name: Save node_modules
paths:
- node_modules
key: v2-dependencies-{{ checksum "yarn.lock" }}
key: v3-dependencies-{{ checksum "yarn.lock" }}

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Expand Up @@ -26,7 +26,7 @@ coverage.json
scTopics
allFiredEvents
in_complete_tests
build/development
build
local_*
bindings.go
types
126 changes: 120 additions & 6 deletions contracts/DarknodeRegistry/DarknodeRegistry.sol
Expand Up @@ -10,10 +10,22 @@ import "../Governance/Claimable.sol";
import "../libraries/CanReclaimTokens.sol";
import "./DarknodeRegistryV1.sol";

contract DarknodeRegistryStateV2 {}
contract DarknodeRegistryStateV2 {
// RenVM can have a maximum of 255 subnets, and a darknodes inclusion or
// exclusion is set using the ith bit of the below subnet, 0th bit is used
// for RenVM and it should always be set to 1 when a darknode is registered
mapping(address => uint256) public subnets;
susruth marked this conversation as resolved.
Show resolved Hide resolved

// subnetLastUpdated tracks when the subnet was last updated, subnet can
// only be changed once per epoch
mapping(address => uint256) public subnetLastUpdated;
}

/// @notice DarknodeRegistry is responsible for the registration and
/// deregistration of Darknodes.
/// @dev Emits a LogDarknodeSubnetUpdated event to set the subnet to 0
/// during deregistration, and subnets map is set to 0 on refund. This is
/// to allow slasher to slash a DN that is deregistered
contract DarknodeRegistryLogicV2 is
Claimable,
CanReclaimTokens,
Expand Down Expand Up @@ -58,9 +70,18 @@ contract DarknodeRegistryLogicV2 is
address indexed _darknodeOperator,
address indexed _darknodeID,
address indexed _challenger,
uint8 _subnetID,
uint256 _percentage
);

/// @notice Emitted when a darknode joins a subnet.
/// @param _darknodeID The ID of the darknode that was registered.
/// @param _subnet The subnets the current darknode is part of.
event LogDarknodeSubnetUpdated(
address indexed _darknodeID,
uint256 indexed _subnet
);

/// @notice Emitted when a new epoch has begun.
event LogNewEpoch(uint256 indexed epochhash);

Expand Down Expand Up @@ -141,6 +162,17 @@ contract DarknodeRegistryLogicV2 is
_;
}

/// @notice Restrict a function to nodes on the specific subnet.
modifier onSubnet(address _darknodeID, uint8 _subnetID) {
require(
_subnetID == 0 ||
subnets[_darknodeID] & (2**uint256(_subnetID)) ==
2**uint256(_subnetID),
"DarknodeRegistry: darknode not part of the subnet"
);
_;
}

/// @notice The contract constructor.
///
/// @param _VERSION A string defining the contract version.
Expand Down Expand Up @@ -192,7 +224,7 @@ contract DarknodeRegistryLogicV2 is
/// caller of this method will be stored as the owner of the darknode.
///
/// @param _darknodeID The darknode ID that will be registered.
function registerNode(address _darknodeID)
function registerNode(address _darknodeID, uint256 _subnet)
public
onlyRefunded(_darknodeID)
{
Expand All @@ -207,6 +239,11 @@ contract DarknodeRegistryLogicV2 is
"DarknodeRegistry: bond transfer failed"
);

require(
_subnet % 2 == 1,
"DarknodeRegistry: can not remove RenVM inclusion"
);

// Flag this darknode for registration
store.appendDarknode(
_darknodeID,
Expand All @@ -221,14 +258,60 @@ contract DarknodeRegistryLogicV2 is

// Emit an event.
emit LogDarknodeRegistered(msg.sender, _darknodeID, minimumBond);
updateDarknodeSubnet(_darknodeID, _subnet);
}

/// @notice An alias for `registerNode` that includes the legacy public key
/// parameter.
/// @param _darknodeID The darknode ID that will be registered.
/// @param _publicKey Deprecated parameter - see `registerNode`.
function register(address _darknodeID, bytes calldata _publicKey) external {
return registerNode(_darknodeID);
// Default subnets to only the core RenVM subnet.
return registerNode(_darknodeID, 1);
}

/// @notice update the subnets the darknode is part of, can be used to join or leave subnets.
/// @param _darknodeID The darknode ID that will be registered.
/// @param _subnet The subnets the darknode want to be part of.
function updateSubnet(address _darknodeID, uint256 _subnet)
public
onlyDarknodeOperator(_darknodeID)
{
require(
_subnet % 2 == 1,
"DarknodeRegistry: can not remove RenVM inclusion"
);
uint256 currentHash = currentEpoch.epochhash;
require(
subnetLastUpdated[_darknodeID] != currentHash,
"DarknodeRegistry: can only update subnet once per epoch"
);
updateDarknodeSubnet(_darknodeID, _subnet);
}

/// @notice update the subnets the darknode is part of, can be used to join or leave subnets.
/// @param _darknodeIDs The list of darknode IDs that will be updated.
/// @param _subnet The subnets the darknode want to be part of.
function updateSubnetMultiple(
address[] calldata _darknodeIDs,
uint256 _subnet
) external {
require(
_subnet % 2 == 1,
"DarknodeRegistry: can not remove RenVM inclusion"
);

for (uint256 i = 0; i < _darknodeIDs.length; i++) {
require(
store.darknodeOperator(_darknodeIDs[i]) == msg.sender,
"DarknodeRegistry: can only be called by the darknode operator"
);
require(
subnetLastUpdated[_darknodeIDs[i]] != currentEpoch.epochhash,
"DarknodeRegistry: can only update subnet once per epoch"
);
updateDarknodeSubnet(_darknodeIDs[i], _subnet);
}
}

/// @notice Register multiple darknodes and transfer the bonds to this contract.
Expand All @@ -238,7 +321,10 @@ contract DarknodeRegistryLogicV2 is
/// will be stored as the owner of each darknode. If one registration fails, all
/// registrations fail.
/// @param _darknodeIDs The darknode IDs that will be registered.
function registerMultiple(address[] calldata _darknodeIDs) external {
/// @param _subnet The subnets the darknodes want to be part of.
function registerMultiple(address[] calldata _darknodeIDs, uint256 _subnet)
external
{
// Save variables in memory to prevent redundant reads from storage
DarknodeRegistryStore _store = store;
Epoch memory _currentEpoch = currentEpoch;
Expand All @@ -247,6 +333,11 @@ contract DarknodeRegistryLogicV2 is
);
uint256 _minimumBond = minimumBond;

require(
_subnet % 2 == 1,
"DarknodeRegistry: can not remove RenVM inclusion"
);

require(
ren.transferFrom(
msg.sender,
Expand Down Expand Up @@ -282,6 +373,8 @@ contract DarknodeRegistryLogicV2 is
);

emit LogDarknodeRegistered(msg.sender, darknodeID, _minimumBond);

updateDarknodeSubnet(darknodeID, _subnet);
}

numDarknodesNextEpoch = numDarknodesNextEpoch.add(_darknodeIDs.length);
Expand Down Expand Up @@ -336,6 +429,8 @@ contract DarknodeRegistryLogicV2 is

_store.updateDarknodeDeregisteredAt(darknodeID, nextDeregisteredAt);

emit LogDarknodeSubnetUpdated(darknodeID, 0);

emit LogDarknodeDeregistered(msg.sender, darknodeID);
}

Expand Down Expand Up @@ -464,10 +559,11 @@ contract DarknodeRegistryLogicV2 is
/// @param _challenger The challenger who should receive a portion of the bond as reward.
/// @param _percentage The total percentage of bond to be slashed.
function slash(
uint8 _subnetID,
address _guilty,
address _challenger,
uint256 _percentage
) external onlySlasher onlyDarknode(_guilty) {
) external onlySlasher onSubnet(_guilty, _subnetID) onlyDarknode(_guilty) {
require(_percentage <= 100, "DarknodeRegistry: invalid percent");

// If the darknode has not been deregistered then deregister it
Expand Down Expand Up @@ -498,6 +594,7 @@ contract DarknodeRegistryLogicV2 is
store.darknodeOperator(_guilty),
_guilty,
_challenger,
_subnetID,
_percentage
);
}
Expand All @@ -517,6 +614,10 @@ contract DarknodeRegistryLogicV2 is
// Erase the darknode from the registry
store.removeDarknode(_darknodeID);

// Remove from all subnets
subnets[_darknodeID] = 0;
delete(subnetLastUpdated[_darknodeID]);

// Refund the operator by transferring REN
require(
ren.transfer(msg.sender, amount),
Expand Down Expand Up @@ -568,6 +669,10 @@ contract DarknodeRegistryLogicV2 is
// Erase the darknode from the registry
_store.removeDarknode(darknodeID);

// Remove from all subnets
subnets[darknodeID] = 0;
delete(subnetLastUpdated[darknodeID]);

// Emit an event
emit LogDarknodeRefunded(msg.sender, darknodeID, amount);

Expand Down Expand Up @@ -900,10 +1005,19 @@ contract DarknodeRegistryLogicV2 is
);
numDarknodesNextEpoch = numDarknodesNextEpoch.sub(1);

emit LogDarknodeSubnetUpdated(_darknodeID, 0);
// Emit an event
emit LogDarknodeDeregistered(darknodeOperator, _darknodeID);
}

function updateDarknodeSubnet(address _darknodeID, uint256 _subnet)
private
{
subnets[_darknodeID] = _subnet;
subnetLastUpdated[_darknodeID] = currentEpoch.epochhash;
emit LogDarknodeSubnetUpdated(_darknodeID, _subnet);
}

function getDarknodeCountFromEpochs()
private
view
Expand Down Expand Up @@ -951,4 +1065,4 @@ contract DarknodeRegistryLogicV2 is
/* solium-disable-next-line no-empty-blocks */
contract DarknodeRegistryProxy is InitializableAdminUpgradeabilityProxy {

}
}
8 changes: 8 additions & 0 deletions contracts/slasher/IRenVMSignatureVerifier.sol
@@ -0,0 +1,8 @@
pragma solidity 0.5.17;

interface IRenVMSignatureVerifier {
function isValidSignature(bytes32 sigHash, bytes calldata signature)
external
view
returns (bytes4);
}
96 changes: 96 additions & 0 deletions contracts/slasher/Slasher.sol
@@ -0,0 +1,96 @@
pragma solidity 0.5.17;

import "@openzeppelin/contracts/cryptography/ECDSA.sol";
import "../DarknodeRegistry/DarknodeRegistry.sol";
import "./IRenVMSignatureVerifier.sol";

/// @notice Slasher is responsible for the slashing of darknode bonds if certain
/// darknodes misbehave.
/// @dev This contract is intentionally written to be simple, key params not updatable
/// (like DNR, bond, and signer) and not upgradeable. As if we need to change any logic
/// we should be deploying a new slasher and updating the slasher on the DNR, and not
/// updating the logic here.
contract Slasher {
using ECDSA for bytes32;

// See IERC1271
bytes4 constant CORRECT_SIGNATURE_RETURN_VALUE = 0x1626ba7e;

DarknodeRegistryLogicV2 dnr;
susruth marked this conversation as resolved.
Show resolved Hide resolved
IRenVMSignatureVerifier renVMSignatureVerifier;
uint256 challengeBond;

mapping(bytes32 => mapping(uint8 => address)) challenged;
mapping(bytes32 => mapping(address => address)) slashed;

event Challenged(address _challenger, bytes32 _epochHash, uint32 _subnetID);
susruth marked this conversation as resolved.
Show resolved Hide resolved

constructor(
DarknodeRegistryLogicV2 _dnr,
IRenVMSignatureVerifier _renVMSignatureVerifier,
uint256 _challengeBond
) public {
dnr = _dnr;
renVMSignatureVerifier = _renVMSignatureVerifier;
challengeBond = _challengeBond;
}

/// @notice Allows any user to challenge the correctness of one of the RenVM subnets.
/// @param _subnetID The subnet the user wants to challenge.
/// @param _epochHash The epoch the user wants to challenge.
function challenge(uint8 _subnetID, bytes32 _epochHash) public {
require(
challenged[_epochHash][_subnetID] == address(0x0),
"Slasher: this epoch has already been challenged"
);
dnr.ren().transferFrom(msg.sender, address(this), challengeBond);
challenged[_epochHash][_subnetID] = msg.sender;
emit Challenged(msg.sender, _epochHash, _subnetID);
}

/// @notice Allows RenVM to slash misbehaving darknodes, by submitting a signature.
/// It rewards the challenger with half of the amount being slashed
susruth marked this conversation as resolved.
Show resolved Hide resolved
/// @param _darknodes the list of darknodes that need to be slashed.
/// @param _percentages the list of percentages of bonds that need to be slashed.
/// @param _challenger the address of the challenger.
/// @param _subnetID The subnet the user wants to challenge.
/// @param _epochHash The epoch the user wants to challenge.
/// @param _signature The signature produced by RenVM.
function slash(
address[] calldata _darknodes,
uint256[] calldata _percentages,
address _challenger,
uint8 _subnetID,
bytes32 _epochHash,
bytes calldata _signature
) external {
require(
renVMSignatureVerifier.isValidSignature(
keccak256(
abi.encode(
_darknodes,
_percentages,
_challenger,
_epochHash
)
),
_signature
) == CORRECT_SIGNATURE_RETURN_VALUE,
"Slasher: invalid signature"
);
require(
_darknodes.length == _percentages.length,
"Slasher: invalid slash params"
);
for (uint256 i = 0; i < _darknodes.length; i++) {
address darknode = _darknodes[i];
require(
slashed[_epochHash][darknode] == address(0x0),
"Slasher: this epoch has already been slashed"
susruth marked this conversation as resolved.
Show resolved Hide resolved
);
dnr.slash(_subnetID, darknode, _challenger, _percentages[i]);
slashed[_epochHash][darknode] = _challenger;
}
dnr.ren().transfer(_challenger, challengeBond);
}
}