-
Notifications
You must be signed in to change notification settings - Fork 12
Extract handover functions to the new contract #431
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
base: main
Are you sure you want to change the base?
Changes from all commits
136a375
b0ebee7
b4dcc7c
7d350db
f47fda6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,307 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "@openzeppelin-upgradeable/contracts/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; | ||
| import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; | ||
| import "../../threshold/ITACoChildApplication.sol"; | ||
| import "./Coordinator.sol"; | ||
|
|
||
| /** | ||
| * @title HandoverCoordinator | ||
| * @notice Coordination layer for Handover protocol | ||
| */ | ||
| contract HandoverCoordinator is Initializable, AccessControlDefaultAdminRulesUpgradeable { | ||
| event ReimbursementPoolSet(address indexed pool); | ||
| event HandoverRequest( | ||
| uint32 indexed ritualId, | ||
| address indexed departingParticipant, | ||
| address indexed incomingParticipant | ||
| ); | ||
| event HandoverTranscriptPosted( | ||
| uint32 indexed ritualId, | ||
| address indexed departingParticipant, | ||
| address indexed incomingParticipant | ||
| ); | ||
| event BlindedSharePosted(uint32 indexed ritualId, address indexed departingParticipant); | ||
| event HandoverCanceled( | ||
| uint32 indexed ritualId, | ||
| address indexed departingParticipant, | ||
| address indexed incomingParticipant | ||
| ); | ||
| event HandoverFinalized( | ||
| uint32 indexed ritualId, | ||
| address indexed departingParticipant, | ||
| address indexed incomingParticipant | ||
| ); | ||
|
|
||
| enum HandoverState { | ||
| NON_INITIATED, | ||
| HANDOVER_AWAITING_TRANSCRIPT, | ||
| HANDOVER_AWAITING_BLINDED_SHARE, | ||
| HANDOVER_AWAITING_FINALIZATION, | ||
| HANDOVER_TIMEOUT | ||
| } | ||
|
|
||
| struct Handover { | ||
| uint32 requestTimestamp; | ||
| address incomingProvider; | ||
| bytes transcript; | ||
| bytes decryptionRequestStaticKey; | ||
| bytes blindedShare; | ||
| } | ||
|
|
||
| bytes32 public constant HANDOVER_SUPERVISOR_ROLE = keccak256("HANDOVER_SUPERVISOR_ROLE"); | ||
|
|
||
| ITACoChildApplication public immutable application; | ||
| Coordinator public immutable coordinator; | ||
| uint32 public immutable handoverTimeout; | ||
| uint96 private immutable minAuthorization; // TODO use child app for checking eligibility | ||
|
|
||
| IReimbursementPool internal reimbursementPool; | ||
| mapping(bytes32 handoverKey => Handover handover) public handovers; | ||
| // Note: Adjust the __preSentinelGap size if more contract variables are added | ||
|
|
||
| uint256[20] internal __gap; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not understanding what is this variable.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in case we inherit contract and then upgrade it we want to have ability to add new variables to that contract, so we reduce gap and add new variable without any changes in child contract. |
||
|
|
||
| constructor( | ||
| ITACoChildApplication _application, | ||
| Coordinator _coordinator, | ||
| uint32 _handoverTimeout | ||
| ) { | ||
| application = _application; | ||
| coordinator = _coordinator; | ||
| handoverTimeout = _handoverTimeout; | ||
| minAuthorization = _application.minimumAuthorization(); // TODO use child app for checking eligibility | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Initialize function for using with OpenZeppelin proxy | ||
| */ | ||
| function initialize(address _admin) external initializer { | ||
| __AccessControlDefaultAdminRules_init(0, _admin); | ||
| } | ||
|
|
||
| function setReimbursementPool(IReimbursementPool pool) external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
| require( | ||
| address(pool) == address(0) || pool.isAuthorized(address(this)), | ||
| "Invalid ReimbursementPool" | ||
| ); | ||
| reimbursementPool = pool; | ||
| emit ReimbursementPoolSet(address(pool)); | ||
| } | ||
|
|
||
| function processReimbursement(uint256 initialGasLeft) internal { | ||
| if (address(reimbursementPool) != address(0)) { | ||
| // For calldataGasCost calculation, see https://github.com/nucypher/nucypher-contracts/issues/328 | ||
| uint256 calldataGasCost = (msg.data.length - 128) * 16 + 128 * 4; | ||
| uint256 gasUsed = initialGasLeft - gasleft() + calldataGasCost; | ||
| try reimbursementPool.refund(gasUsed, msg.sender) { | ||
| return; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function getHandoverKey( | ||
| uint32 ritualId, | ||
| address departingProvider | ||
| ) public view returns (bytes32) { | ||
| return keccak256(abi.encode(ritualId, departingProvider)); | ||
| } | ||
|
|
||
| function getHandoverState( | ||
| uint32 ritualId, | ||
| address departingParticipant | ||
| ) external view returns (HandoverState) { | ||
| Handover storage handover = handovers[getHandoverKey(ritualId, departingParticipant)]; | ||
| return getHandoverState(handover); | ||
| } | ||
|
|
||
| function getHandoverState(Handover storage handover) internal view returns (HandoverState) { | ||
| uint32 t0 = handover.requestTimestamp; | ||
| uint32 deadline = t0 + handoverTimeout; | ||
| if (t0 == 0) { | ||
| return HandoverState.NON_INITIATED; | ||
| } else if (block.timestamp > deadline) { | ||
| // Handover failed due to timeout | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably I'm misunderstanding how handovers work, but if we reached the timeout, it doesn't necessary means that the handover failed, right? The handover could be succesful AND we have reached the timeout.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if it's outside timeout it means that blinded share and transcript were not provided in time which means something wrong with one of the nodes
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right, right. When handover is finished |
||
| return HandoverState.HANDOVER_TIMEOUT; | ||
| } else if (handover.transcript.length == 0) { | ||
| return HandoverState.HANDOVER_AWAITING_TRANSCRIPT; | ||
| } else if (handover.blindedShare.length == 0) { | ||
| return HandoverState.HANDOVER_AWAITING_BLINDED_SHARE; | ||
| } else { | ||
| return HandoverState.HANDOVER_AWAITING_FINALIZATION; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Calculates position of blinded share for particular participant | ||
| * @param index Participant index | ||
| * @param threshold Threshold | ||
| * @dev See https://github.com/nucypher/nucypher-contracts/issues/400 | ||
| */ | ||
| function blindedSharePosition(uint256 index, uint16 threshold) public pure returns (uint256) { | ||
| return 32 + index * BLS12381.G2_POINT_SIZE + threshold * BLS12381.G1_POINT_SIZE; | ||
| } | ||
|
|
||
| function handoverRequest( | ||
| uint32 ritualId, | ||
| address departingParticipant, | ||
| address incomingParticipant | ||
| ) external onlyRole(HANDOVER_SUPERVISOR_ROLE) { | ||
| require(coordinator.isRitualActive(ritualId), "Ritual is not active"); | ||
| require( | ||
| coordinator.isParticipant(ritualId, departingParticipant), | ||
| "Departing node must be a participant" | ||
| ); | ||
| require( | ||
| !coordinator.isParticipant(ritualId, incomingParticipant), | ||
| "Incoming node cannot be a participant" | ||
| ); | ||
|
|
||
| Handover storage handover = handovers[getHandoverKey(ritualId, departingParticipant)]; | ||
| HandoverState state = getHandoverState(handover); | ||
|
|
||
| require( | ||
| state == HandoverState.NON_INITIATED || state == HandoverState.HANDOVER_TIMEOUT, | ||
| "Handover already requested" | ||
| ); | ||
| require( | ||
| coordinator.isProviderKeySet(incomingParticipant), | ||
| "Incoming provider has not set public key" | ||
| ); | ||
| require( | ||
| application.authorizedStake(incomingParticipant) >= minAuthorization, | ||
| "Not enough authorization" | ||
| ); | ||
| handover.requestTimestamp = uint32(block.timestamp); | ||
| handover.incomingProvider = incomingParticipant; | ||
| delete handover.blindedShare; | ||
| delete handover.transcript; | ||
| delete handover.decryptionRequestStaticKey; | ||
| emit HandoverRequest(ritualId, departingParticipant, incomingParticipant); | ||
| } | ||
|
|
||
| function postHandoverTranscript( | ||
| uint32 ritualId, | ||
| address departingParticipant, | ||
| bytes calldata transcript, | ||
| bytes calldata decryptionRequestStaticKey | ||
| ) external { | ||
| uint256 initialGasLeft = gasleft(); | ||
| require(coordinator.isRitualActive(ritualId), "Ritual is not active"); | ||
| require(transcript.length > 0, "Parameters can't be empty"); | ||
| require( | ||
| decryptionRequestStaticKey.length == 42, | ||
| "Invalid length for decryption request static key" | ||
| ); | ||
|
|
||
| Handover storage handover = handovers[getHandoverKey(ritualId, departingParticipant)]; | ||
| require( | ||
| getHandoverState(handover) == HandoverState.HANDOVER_AWAITING_TRANSCRIPT, | ||
| "Not waiting for transcript" | ||
| ); | ||
| address provider = application.operatorToStakingProvider(msg.sender); | ||
| require(handover.incomingProvider == provider, "Wrong incoming provider"); | ||
|
|
||
| handover.transcript = transcript; | ||
| handover.decryptionRequestStaticKey = decryptionRequestStaticKey; | ||
| emit HandoverTranscriptPosted(ritualId, departingParticipant, provider); | ||
| processReimbursement(initialGasLeft); | ||
| } | ||
|
|
||
| function postBlindedShare(uint32 ritualId, bytes calldata blindedShare) external { | ||
| uint256 initialGasLeft = gasleft(); | ||
| require(coordinator.isRitualActive(ritualId), "Ritual is not active"); | ||
|
|
||
| address provider = application.operatorToStakingProvider(msg.sender); | ||
| Handover storage handover = handovers[getHandoverKey(ritualId, provider)]; | ||
| require( | ||
| getHandoverState(handover) == HandoverState.HANDOVER_AWAITING_BLINDED_SHARE, | ||
| "Not waiting for blinded share" | ||
| ); | ||
| require(blindedShare.length == BLS12381.G2_POINT_SIZE, "Wrong size of blinded share"); | ||
|
|
||
| handover.blindedShare = blindedShare; | ||
| emit BlindedSharePosted(ritualId, provider); | ||
| processReimbursement(initialGasLeft); | ||
| } | ||
|
|
||
| function cancelHandover( | ||
| uint32 ritualId, | ||
| address departingParticipant | ||
| ) external onlyRole(HANDOVER_SUPERVISOR_ROLE) { | ||
| Handover storage handover = handovers[getHandoverKey(ritualId, departingParticipant)]; | ||
| address incomingParticipant = handover.incomingProvider; | ||
|
|
||
| require( | ||
| getHandoverState(handover) != HandoverState.NON_INITIATED, | ||
| "Handover not requested" | ||
| ); | ||
| handover.requestTimestamp = 0; | ||
| handover.incomingProvider = address(0); | ||
| delete handover.blindedShare; | ||
| delete handover.transcript; | ||
| delete handover.decryptionRequestStaticKey; | ||
|
|
||
| emit HandoverCanceled(ritualId, departingParticipant, incomingParticipant); | ||
| } | ||
|
|
||
| function finalizeHandover( | ||
| uint32 ritualId, | ||
| address departingParticipant | ||
| ) external onlyRole(HANDOVER_SUPERVISOR_ROLE) { | ||
| require(coordinator.isRitualActive(ritualId), "Ritual is not active"); | ||
|
|
||
| Handover storage handover = handovers[getHandoverKey(ritualId, departingParticipant)]; | ||
| require( | ||
| getHandoverState(handover) == HandoverState.HANDOVER_AWAITING_FINALIZATION, | ||
| "Not waiting for finalization" | ||
| ); | ||
| address incomingParticipant = handover.incomingProvider; | ||
|
|
||
| Coordinator.Participant[] memory participants = coordinator.getParticipants(ritualId); | ||
| uint256 participantIndex = findParticipant(participants, departingParticipant); | ||
| coordinator.updateParticipant( | ||
| ritualId, | ||
| departingParticipant, | ||
| incomingParticipant, | ||
| true, | ||
| new bytes(0), | ||
| handover.decryptionRequestStaticKey | ||
| ); | ||
|
|
||
| uint16 threshold = coordinator.getThreshold(ritualId); | ||
| uint256 startIndex = blindedSharePosition(participantIndex, threshold); | ||
| coordinator.replaceAggregatedTranscriptBytes( | ||
| ritualId, | ||
| incomingParticipant, | ||
| handover.blindedShare, | ||
| startIndex | ||
| ); | ||
|
|
||
| handover.requestTimestamp = 0; | ||
| handover.incomingProvider = address(0); | ||
| delete handover.blindedShare; | ||
| delete handover.transcript; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not the scope of this PR, but we should consider deleting the transcripts of all participants since these are no longer valid (can't recreate an transcript aggregation after a handover). See #427 |
||
| delete handover.decryptionRequestStaticKey; | ||
|
|
||
| emit HandoverFinalized(ritualId, departingParticipant, incomingParticipant); | ||
| application.release(departingParticipant); | ||
| } | ||
|
|
||
| function findParticipant( | ||
| Coordinator.Participant[] memory participants, | ||
| address provider | ||
| ) internal view returns (uint256 index) { | ||
| for (uint256 i = 0; i < participants.length; i++) { | ||
| Coordinator.Participant memory participant = participants[i]; | ||
| if (participant.provider == provider) { | ||
| return i; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| deployment: | ||
| name: lynx-upgrade-handover-coordinator | ||
| chain_id: 80002 | ||
|
|
||
| artifacts: | ||
| dir: ./deployment/artifacts/ | ||
| filename: lynx-upgrade-handover-coordinator.json | ||
|
|
||
| constants: | ||
| TACO_CHILD_APPLICATION: "0x42F30AEc1A36995eEFaf9536Eb62BD751F982D32" | ||
| COORDINATOR: "0xE9e94499bB0f67b9DBD75506ec1735486DE57770" | ||
| DKG_TIMEOUT_SECONDS: 3600 # 1 hour | ||
| HANDOVER_TIMEOUT_SECONDS: 900 # 15 minutes | ||
|
|
||
| contracts: | ||
| - HandoverCoordinator: | ||
| constructor: | ||
| _application: $TACoChildApplication | ||
| _coordinator: $Coordinator | ||
| _handoverTimeout: $HANDOVER_TIMEOUT_SECONDS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is not the purpouse of this PR, but I'm curious: why making handoverTimeout immutable and not something that we can modify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cheaper on gas, basically it's constant that can be changed by upgrade