diff --git a/nucypher/blockchain/eth/sol/source/contracts/Coordinator.sol b/nucypher/blockchain/eth/sol/source/contracts/Coordinator.sol new file mode 100644 index 000000000..0e9ebbb5a --- /dev/null +++ b/nucypher/blockchain/eth/sol/source/contracts/Coordinator.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.0; + +import "zeppelin/ownership/Ownable.sol"; + +/** +* @title Coordinator +* @notice Coordination layer for DKG-TDec +*/ +contract Coordinator is Ownable { + + // Ritual + event StartRitual(uint32 indexed ritualId, address indexed initiator, address[] nodes); + event StartTranscriptRound(uint32 indexed ritualId); + event StartAggregationRound(uint32 indexed ritualId); + // TODO: Do we want the public key here? If so, we want 2 events or do we reuse this event? + event EndRitual(uint32 indexed ritualId, address indexed initiator, RitualState status); + + // Node + event TranscriptPosted(uint32 indexed ritualId, address indexed node, bytes32 transcriptDigest); + event AggregationPosted(uint32 indexed ritualId, address indexed node, bytes32 aggregatedTranscriptDigest); + + // Admin + event TimeoutChanged(uint32 oldTimeout, uint32 newTimeout); + event MaxDkgSizeChanged(uint32 oldSize, uint32 newSize); + + enum RitualState { + NON_INITIATED, + AWAITING_TRANSCRIPTS, + AWAITING_AGGREGATIONS, + TIMEOUT, + INVALID, + FINALIZED + } + + uint256 public constant PUBLIC_KEY_SIZE = 48; + + struct Participant { + address node; + bool aggregated; + bytes transcript; // TODO: Consider event processing complexity vs storage cost + } + + // TODO: Optimize layout + struct Ritual { + uint32 id; // TODO: Redundant? ID is index of rituals array + address initiator; + uint32 dkgSize; + uint32 initTimestamp; + uint32 totalTranscripts; + uint32 totalAggregations; + bytes32 aggregatedTranscriptHash; + bool aggregationMismatch; + bytes aggregatedTranscript; + bytes1[PUBLIC_KEY_SIZE] publicKey; + Participant[] participant; + } + + Ritual[] public rituals; + + uint32 public timeout; + uint32 public maxDkgSize; + + constructor(uint32 _timeout, uint32 _maxDkgSize) { + timeout = _timeout; + maxDkgSize = _maxDkgSize; + } + + function getRitualState(uint256 ritualId) external view returns (RitualState){ + // TODO: restrict to ritualID < rituals.length? + return getRitualState(rituals[ritualId]); + } + + function getRitualState(Ritual storage ritual) internal view returns (RitualState){ + uint32 t0 = ritual.initTimestamp; + uint32 deadline = t0 + timeout; + if(t0 == 0){ + return RitualState.NON_INITIATED; + } else if (ritual.publicKey[0] != 0x0){ // TODO: Improve check + return RitualState.FINALIZED; + } else if (ritual.aggregationMismatch){ + return RitualState.INVALID; + } else if (block.timestamp > deadline){ + return RitualState.TIMEOUT; + } else if (ritual.totalTranscripts < ritual.dkgSize) { + return RitualState.AWAITING_TRANSCRIPTS; + } else if (ritual.totalAggregations < ritual.dkgSize) { + return RitualState.AWAITING_AGGREGATIONS; + } else { + // TODO: Is it possible to reach this state? + // - No public key + // - All transcripts and all aggregations + // - Still within the deadline + } + } + + + function setTimeout(uint32 newTimeout) external onlyOwner { + emit TimeoutChanged(timeout, newTimeout); + timeout = newTimeout; + } + + function setMaxDkgSize(uint32 newSize) external onlyOwner { + emit MaxDkgSizeChanged(maxDkgSize, newSize); + maxDkgSize = newSize; + } + + function numberOfRituals() external view returns(uint256) { + return rituals.length; + } + + function getParticipants(uint32 ritualId) external view returns(Participant[] memory) { + Ritual storage ritual = rituals[ritualId]; + return ritual.participant; + } + + function initiateRitual(address[] calldata nodes) external returns (uint32) { + // TODO: Validate service fees, expiration dates, threshold + require(nodes.length <= maxDkgSize, "Invalid number of nodes"); + + uint32 id = uint32(rituals.length); + Ritual storage ritual = rituals.push(); + ritual.id = id; // TODO: Possibly redundant + ritual.initiator = msg.sender; // TODO: Consider sponsor model + ritual.dkgSize = uint32(nodes.length); + ritual.initTimestamp = uint32(block.timestamp); + + address previousNode = address(0); + for(uint256 i=0; i < nodes.length; i++){ + Participant storage newParticipant = ritual.participant.push(); + address currentNode = nodes[i]; + newParticipant.node = currentNode; + require(previousNode < currentNode, "Nodes must be sorted"); + previousNode = currentNode; + // TODO: Check nodes are eligible (staking, etc) + } + // TODO: Compute cohort fingerprint as hash(nodes) + + emit StartRitual(id, msg.sender, nodes); + emit StartTranscriptRound(id); + return ritual.id; + } + + function postTranscript(uint32 ritualId, uint256 nodeIndex, bytes calldata transcript) external { + Ritual storage ritual = rituals[ritualId]; + require( + getRitualState(ritual) == RitualState.AWAITING_TRANSCRIPTS, + "Not waiting for transcripts" + ); + Participant storage participant = ritual.participant[nodeIndex]; + require( + participant.node == msg.sender, + "Node not part of ritual" + ); + require( + participant.transcript.length == 0, + "Node already posted transcript" + ); + + // TODO: Validate transcript size based on dkg size + + // Nodes commit to their transcript + bytes32 transcriptDigest = keccak256(transcript); + participant.transcript = transcript; // TODO: ??? + emit TranscriptPosted(ritualId, msg.sender, transcriptDigest); + ritual.totalTranscripts++; + + // end round + if (ritual.totalTranscripts == ritual.dkgSize){ + emit StartAggregationRound(ritualId); + } + } + + function postAggregation(uint32 ritualId, uint256 nodeIndex, bytes calldata aggregatedTranscript) external { + Ritual storage ritual = rituals[ritualId]; + require( + getRitualState(ritual) == RitualState.AWAITING_AGGREGATIONS, + "Not waiting for aggregations" + ); + Participant storage participant = ritual.participant[nodeIndex]; + require( + participant.node == msg.sender, + "Node not part of ritual" + ); + require( + !participant.aggregated, + "Node already posted aggregation" + ); + + // nodes commit to their aggregation result + bytes32 aggregatedTranscriptDigest = keccak256(aggregatedTranscript); + participant.aggregated = true; + emit AggregationPosted(ritualId, msg.sender, aggregatedTranscriptDigest); + + if (ritual.aggregatedTranscriptHash == bytes32(0)){ + ritual.aggregatedTranscriptHash = aggregatedTranscriptDigest; + } else if (ritual.aggregatedTranscriptHash != aggregatedTranscriptDigest){ + ritual.aggregationMismatch = true; + emit EndRitual(ritualId, ritual.initiator, RitualState.INVALID); + // TODO: Invalid ritual + // TODO: Consider freeing ritual storage + return; + } + + ritual.totalAggregations++; + + // end round - Last node posting aggregation will finalize + if (ritual.totalAggregations == ritual.dkgSize){ + emit EndRitual(ritualId, ritual.initiator, RitualState.FINALIZED); + // TODO: Last node extracts public key bytes from aggregated transcript + // and store in ritual.publicKey + ritual.publicKey[0] = bytes1(0x42); + } + } +} diff --git a/nucypher/blockchain/eth/sol/source/contracts/CoordinatorV2.sol b/nucypher/blockchain/eth/sol/source/contracts/CoordinatorV2.sol deleted file mode 100644 index 53a03b32e..000000000 --- a/nucypher/blockchain/eth/sol/source/contracts/CoordinatorV2.sol +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.0; - -/** -* @title CoordinatorV1 -* @notice Coordination layer for DKG-TDec -*/ -contract CoordinatorV1 { - - uint32 public constant DKG_SIZE = 8; - uint32 public TIMEOUT = 9600; - - event StartRitual(uint32 indexed ritualId, address[] nodes); - event StartTranscriptRound(uint32 indexed ritualId); - event StartConfirmationRound(uint32 indexed ritualId); - event RitualEnded(uint32 indexed ritualId); - - event TranscriptPosted(uint32 indexed ritualId, address indexed node, bytes32 transcriptDigest); - event ConfirmationPosted(uint32 indexed ritualId, address indexed node, address[] confirmedNodes); - - event TimeoutChanged(uint32 timeout); - event DkgSizeChanged(uint8 dkgSize); - - enum RitualStatus { - WAITING_FOR_CHECKINS, - WAITING_FOR_TRANSCRIPTS, - WAITING_FOR_CONFIRMATIONS, - COMPLETED, - FAILED - } - - // TODO: Find better name - struct Performance { - address node; - uint32 checkinTimestamp; - uint96 confirmedBy; - bytes32 transcript; - } - - struct Ritual { - uint32 id; - uint32 initTimestamp; - uint32 totalCheckins; - uint32 totalTranscripts; - uint32 totalConfirmations; - RitualStatus status; - Performance[DKG_SIZE] performance; - } - - Ritual[] public rituals; - - function numberOfRituals() external view returns(uint256){ - return rituals.length; - } - - function getPerformances(uint32 ritualId) external view returns(Performance[] memory){ - Performance[] memory performances = new Performance[](rituals[ritualId].performance.length); - for(uint32 i=0; i < rituals[ritualId].performance.length; i++){ - performances[i] = rituals[ritualId].performance[i]; - } - return performances; - } - - function setTimeout(uint32 timeout) external { - TIMEOUT = timeout; - emit TimeoutChanged(timeout); - } - - function initiateRitual(address[] calldata nodes) external { - // TODO: Check for payment - // TODO: Check for expiration time - // TODO: Improve DKG size choices - require(nodes.length == DKG_SIZE, "Invalid number of nodes"); - - uint32 id = uint32(rituals.length); - Ritual storage ritual = rituals.push(); - ritual.id = id; - ritual.initTimestamp = uint32(block.timestamp); - ritual.status = RitualStatus.WAITING_FOR_CHECKINS; - ritual.totalTranscripts = 0; - ritual.totalConfirmations = 0; - ritual.totalCheckins = 0; - - address previousNode = nodes[0]; - ritual.performance[0].node = previousNode; - address currentNode; - for(uint256 i=1; i < nodes.length; i++){ - currentNode = nodes[i]; - require(currentNode > previousNode, "Nodes must be sorted"); - ritual.performance[i].node = currentNode; - previousNode = currentNode; - // TODO: Check nodes are eligible (staking, etc) - } - - emit StartRitual(id, nodes); - } - - function checkIn(uint32 ritualId, uint256 nodeIndex) external { - Ritual storage ritual = rituals[ritualId]; - require(ritual.status == RitualStatus.WAITING_FOR_CHECKINS, "Not waiting for check-ins"); - require(ritual.performance[nodeIndex].node == msg.sender, "Node not part of ritual"); - if ((uint32(block.timestamp) - ritual.initTimestamp) > TIMEOUT) { - ritual.status = RitualStatus.FAILED; - emit RitualEnded(ritualId); - revert("Ritual timed out"); - } - require(ritual.performance[nodeIndex].checkinTimestamp == 0, "Node already checked in"); - ritual.performance[nodeIndex].checkinTimestamp = uint32(block.timestamp); - ritual.totalCheckins++; - if (ritual.totalCheckins == DKG_SIZE){ - ritual.status = RitualStatus.WAITING_FOR_TRANSCRIPTS; - emit StartTranscriptRound(ritualId); - } - } - - function postTranscript(uint32 ritualId, uint256 nodeIndex, bytes calldata transcript) external { - Ritual storage ritual = rituals[ritualId]; - require(ritual.status == RitualStatus.WAITING_FOR_TRANSCRIPTS, "Not waiting for transcripts"); - require(ritual.performance[nodeIndex].node == msg.sender, "Node not part of ritual"); - if ((uint32(block.timestamp) - ritual.initTimestamp) > TIMEOUT) { - ritual.status = RitualStatus.FAILED; - emit RitualEnded(ritualId); - revert("Ritual timed out"); - } - require(ritual.performance[nodeIndex].transcript == bytes32(0), "Node already posted transcript"); - - // Nodes commit to their transcript - bytes32 transcriptDigest = keccak256(transcript); - ritual.performance[nodeIndex].transcript = transcriptDigest; - ritual.totalTranscripts++; - if (ritual.totalTranscripts == DKG_SIZE){ - ritual.status = RitualStatus.WAITING_FOR_CONFIRMATIONS; - emit StartConfirmationRound(ritualId); - } - emit TranscriptPosted(ritualId, msg.sender, transcriptDigest); - } - - function postConfirmation(uint32 ritualId, uint256 nodeIndex, uint256[] calldata confirmedNodesIndexes) external { - Ritual storage ritual = rituals[ritualId]; - require(ritual.status == RitualStatus.WAITING_FOR_CONFIRMATIONS, "Not waiting for confirmations"); - require( - ritual.performance[nodeIndex].node == msg.sender && - ritual.performance[nodeIndex].transcript != bytes32(0), - "Node not part of ritual" - ); - - require(confirmedNodesIndexes.length <= DKG_SIZE, "Invalid number of confirmations"); - if ((uint32(block.timestamp) - ritual.initTimestamp) > TIMEOUT) { - ritual.status = RitualStatus.FAILED; - emit RitualEnded(ritualId); - revert("Ritual timed out"); - } - - address[] memory confirmedNodes = new address[](confirmedNodesIndexes.length); - - // First, node adds itself to its list of confirmers - uint96 caller = uint96(2 ** nodeIndex); - ritual.performance[nodeIndex].confirmedBy |= caller; - for(uint256 i=0; i < confirmedNodesIndexes.length; i++){ - uint256 confirmedNodeIndex = confirmedNodesIndexes[i]; - require(confirmedNodeIndex < DKG_SIZE, "Invalid node index"); - // We add caller to the list of confirmations of each confirmed node - ritual.performance[confirmedNodeIndex].confirmedBy |= caller; - confirmedNodes[i] = ritual.performance[confirmedNodeIndex].node; - } - ritual.totalConfirmations++; - if (ritual.totalConfirmations == DKG_SIZE){ - ritual.status = RitualStatus.COMPLETED; - emit RitualEnded(ritualId); - } - emit ConfirmationPosted(ritualId, msg.sender, confirmedNodes); - } - -} diff --git a/nucypher/blockchain/eth/sol/source/contracts/CoordinatorV3.sol b/nucypher/blockchain/eth/sol/source/contracts/CoordinatorV3.sol deleted file mode 100644 index d067b1d46..000000000 --- a/nucypher/blockchain/eth/sol/source/contracts/CoordinatorV3.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -pragma solidity ^0.8.0; - -import "./proxy/Upgradeable.sol"; - -/** -* @title CoordinatorV3 -* @notice Coordination layer for DKG-TDec -*/ -contract CoordinatorV3 is Upgradeable { - - // Ritual - event StartRitual(uint32 indexed ritualId, address[] nodes, address initiator); - event StartTranscriptRound(uint32 indexed ritualId); - event StartAggregationRound(uint32 indexed ritualId); - event EndRitual(uint32 indexed ritualId, RitualStatus status, address initiator); - - // Node - event TranscriptPosted(uint32 indexed ritualId, address indexed node, bytes32 transcriptDigest); - event AggregationPosted(uint32 indexed ritualId, address indexed node, bytes32 aggregatedTranscriptDigest); - - // Admin - event TimeoutChanged(uint32 oldTimeout, uint32 newTimeout); - event MaxDkgSizeChanged(uint32 oldSize, uint32 newSize); - - enum RitualStatus { - AWAITING_TRANSCRIPTS, - AWAITING_AGGREGATIONS, - AWAITING_FINALIZATION, - TIMED_OUT, - INVALID, - FINALIZED - } - - struct Rite { - address node; - bool aggregated; - bytes transcript; - } - - struct Ritual { - uint32 id; - address initiator; - uint32 dkgSize; - uint32 threshold; - bytes32 publicMaterial; - uint32 initTimestamp; - uint32 totalTranscripts; - uint32 totalAggregations; - RitualStatus status; - Rite[] rite; - } - - Ritual[] public rituals; - - uint32 public timeout; - uint32 public maxDkgSize; - - constructor(uint32 _timeout, uint32 _maxDkgSize) { - timeout = _timeout; - maxDkgSize = _maxDkgSize; - } - - function _checkActiveRitual(Ritual storage _ritual) internal { - uint32 delta = uint32(block.timestamp) - _ritual.initTimestamp; - if (delta > timeout) { - _ritual.status = RitualStatus.TIMED_OUT; - emit EndRitual(_ritual.id, _ritual.status); // penalty hook, missing nodes can be known at this stage - revert("Ritual timed out"); - } - } - - function checkActiveRitual(uint32 ritualId) external { - Ritual storage ritual = rituals[ritualId]; - _checkActiveRitual(ritual); - } - - function setTimeout(uint32 newTimeout) external onlyOwner { - uint32 oldTimeout = timeout; - timeout = newTimeout; - emit TimeoutChanged(oldTimeout, newTimeout); - } - - function setMaxDkgSize(uint32 newSize) external onlyOwner { - uint32 oldSize = maxDkgSize; - maxDkgSize = newSize; - emit MaxDkgSizeChanged(oldSize, newSize); - } - - function numberOfRituals() external view returns(uint256) { - return rituals.length; - } - - function getRites(uint32 ritualId) external view returns(Rite[] memory) { - Rite[] memory rites = new Rite[](rituals[ritualId].rite.length); - for(uint32 i=0; i < rituals[ritualId].rite.length; i++){ - rites[i] = rituals[ritualId].rite[i]; - } - return rites; - } - - function initiateRitual(address[] calldata nodes) external returns (uint32) { - require(nodes.length <= maxDkgSize, "Invalid number of nodes"); - - uint32 id = uint32(rituals.length); - Ritual storage ritual = rituals.push(); - ritual.id = id; - ritual.initiator = msg.sender; - ritual.threshold = threshold; - ritual.dkgSize = uint32(nodes.length); - ritual.initTimestamp = uint32(block.timestamp); - ritual.status = RitualStatus.AWAITING_TRANSCRIPTS; - - address previousNode = nodes[0]; - ritual.rite[0].node = previousNode; - address currentNode; - for(uint256 i=1; i < nodes.length; i++){ - currentNode = nodes[i]; - ritual.rite[i].node = currentNode; - previousNode = currentNode; - // TODO: Check nodes are eligible (staking, etc) - } - - emit StartRitual(id, nodes, msg.sender); - return ritual.id; - } - - function postTranscript(uint32 ritualId, uint256 nodeIndex, bytes calldata transcript) external { - Ritual storage ritual = rituals[ritualId]; - require(ritual.rite[nodeIndex].node == msg.sender, "Node not part of ritual"); - require(ritual.status == RitualStatus.AWAITING_TRANSCRIPTS, "Not waiting for transcripts"); - require(ritual.rite[nodeIndex].transcript.length == 0, "Node already posted transcript"); - require(ritual.rite[nodeIndex].aggregated == false, "Node already posted aggregation"); - _checkActiveRitual(ritual); - - // Nodes commit to their transcript - bytes32 transcriptDigest = keccak256(transcript); - ritual.rite[nodeIndex].transcript = transcript; - emit TranscriptPosted(ritualId, msg.sender, transcriptDigest); - ritual.totalTranscripts++; - - // end round - if (ritual.totalTranscripts == ritual.dkgSize){ - ritual.status = RitualStatus.AWAITING_AGGREGATIONS; - emit StartAggregationRound(ritualId); - } - } - - function postAggregation(uint32 ritualId, uint256 nodeIndex, bytes calldata aggregatedTranscripts) external { - Ritual storage ritual = rituals[ritualId]; - require(ritual.status == RitualStatus.AWAITING_AGGREGATIONS, "Not waiting for confirmations"); - require(ritual.rite[nodeIndex].node == msg.sender, "Node not part of ritual"); - _checkActiveRitual(ritual); - - // nodes commit to their aggregation result - bytes32 aggregatedTranscriptDigest = keccak256(aggregatedTranscripts); - ritual.rite[nodeIndex].transcript = aggregatedTranscriptDigest; - ritual.rite[nodeIndex].aggregated = true; - emit AggregationPosted(ritualId, msg.sender, aggregatedTranscripts); - ritual.totalAggregations++; - - // end round - if (ritual.totalAggregations == ritual.dkgSize){ - ritual.status = RitualStatus.AWAITING_FINALIZATION; - emit EndRitual(ritualId, ritual.status, ritual.initiator); - } - } - - function finalizeRitual(uint32 ritualId) public { - Ritual storage ritual = rituals[ritualId]; - require(ritual.status == RitualStatus.AWAITING_FINALIZATION, 'ritual cannot be finalized'); - - bytes32 firstRiteDigest = keccak256(ritual.rite[0].transcript); - for(uint32 i=1; i < ritual.rite.length; i++){ - bytes32 currentRiteDigest = keccak256(ritual.rite[i].transcript); - if (firstRiteDigest != currentRiteDigest) { - ritual.status = RitualStatus.INVALID; - emit EndRitual(ritualId, ritual.status, ritual.initiator); - revert('aggregated transcripts do not match'); - } - } - - ritual.publicMaterial = firstRiteDigest; - ritual.status = RitualStatus.FINALIZED; - emit EndRitual(ritualId, ritual.status, ritual.initiator); - } - -} diff --git a/nucypher/crypto/dkg.py b/nucypher/crypto/ferveo/dkg.py similarity index 100% rename from nucypher/crypto/dkg.py rename to nucypher/crypto/ferveo/dkg.py