From 315b77b77f70c471377ef9be7cc0b11913e07070 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 19 Sep 2025 20:41:37 +0500 Subject: [PATCH 01/88] chore: copy over contracts from tokenomics branch --- .gitmodules | 2 +- .../contracts/interfaces/IBondingRegistry.sol | 392 +++++++++++ .../interfaces/ICiphernodeRegistry.sol | 75 +++ .../contracts/interfaces/IRegistryFilter.sol | 10 + .../contracts/interfaces/ISlashVerifier.sol | 18 + .../contracts/interfaces/ISlashingManager.sol | 294 +++++++++ .../contracts/registry/BondingRegistry.sol | 607 ++++++++++++++++++ .../registry/CiphernodeRegistryOwnable.sol | 134 +++- .../registry/NaiveRegistryFilter.sol | 31 +- .../contracts/slashing/SlashingManager.sol | 429 +++++++++++++ .../contracts/test/MockRegistryFilter.sol | 34 +- .../contracts/test/MockSlashingVerifier.sol | 19 + .../contracts/token/EnclaveToken.sol | 233 +++++++ 13 files changed, 2245 insertions(+), 33 deletions(-) create mode 100644 packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol create mode 100644 packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol create mode 100644 packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol create mode 100644 packages/enclave-contracts/contracts/registry/BondingRegistry.sol create mode 100644 packages/enclave-contracts/contracts/slashing/SlashingManager.sol create mode 100644 packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol create mode 100644 packages/enclave-contracts/contracts/token/EnclaveToken.sol diff --git a/.gitmodules b/.gitmodules index b663ca7ab4..7517ddce4c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/risc0/risc0-ethereum [submodule "templates/default/lib/risc0-ethereum"] path = templates/default/lib/risc0-ethereum - url = https://github.com/gnosisguild/risc0-ethereum + url = https://github.com/gnosisguild/risc0-ethereum diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol new file mode 100644 index 0000000000..2d16ce10d2 --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +/** + * @title IBondingRegistry + * @notice Interface for the main bonding registry that holds operator balance and license bonds + */ +interface IBondingRegistry { + // ====================== + // Custom Errors + // ====================== + + // General + error ZeroAddress(); + error ZeroAmount(); + error Unauthorized(); + error InsufficientBalance(); + error ActiveCommittee(); + error NotLicensed(); + error AlreadyRegistered(); + error NotRegistered(); + error ExitInProgress(); + error ExitNotReady(); + error InvalidAmount(); + error InvalidConfiguration(); + error NoPendingDeregistration(); + + // ====================== + // Events (Protocol-Named) + // ====================== + + /** + * @notice Emitted when operator's ticket balance changes + * @param operator Address of the operator + * @param delta Change in balance (positive for increase, negative for decrease) + * @param newBalance New total balance + * @param reason Reason for the change (e.g., "DEPOSIT", "WITHDRAW", slash reason) + */ + event TicketBalanceUpdated( + address indexed operator, + int256 delta, + uint256 newBalance, + bytes32 indexed reason + ); + + /** + * @notice Emitted when operator's license bond changes + * @param operator Address of the operator + * @param delta Change in bond (positive for increase, negative for decrease) + * @param newBond New total license bond + * @param reason Reason for the change (e.g., "BOND", "UNBOND", slash reason) + */ + event LicenseBondUpdated( + address indexed operator, + int256 delta, + uint256 newBond, + bytes32 indexed reason + ); + + /** + * @notice Emitted when operator requests deregistration from the protocol + * @param operator Address of the operator + * @param unlockAt Timestamp when deregistration can be finalized + */ + event CiphernodeDeregistrationRequested( + address indexed operator, + uint64 unlockAt + ); + + /** + * @notice Emitted when operator's deregistration is finalized + * @param operator Address of the operator + * @param ticketRefund Amount of ticket balance refunded + * @param licenseRefund Amount of license bond refunded + */ + event DeregistrationFinalized( + address indexed operator, + uint256 ticketRefund, + uint256 licenseRefund + ); + + /** + * @notice Emitted when operator registration status changes + * @param operator Address of the operator + * @param registered True if registered, false if deregistered + */ + event OperatorRegistrationChanged( + address indexed operator, + bool registered + ); + + /** + * @notice Emitted when operator active status changes + * @param operator Address of the operator + * @param active True if active, false if inactive + */ + event OperatorActivationChanged(address indexed operator, bool active); + + /** + * @notice Emitted when configuration is updated + * @param parameter Name of the parameter + * @param oldValue Previous value + * @param newValue New value + */ + event ConfigurationUpdated( + bytes32 indexed parameter, + uint256 oldValue, + uint256 newValue + ); + + /** + * @notice Emitted when treasury withdraws slashed funds + * @param to Treasury address + * @param ticketAmount Amount of slashed ticket balance withdrawn + * @param licenseAmount Amount of slashed license bond withdrawn + */ + event SlashedFundsWithdrawn( + address indexed to, + uint256 ticketAmount, + uint256 licenseAmount + ); + + // ====================== + // View Functions + // ====================== + + /** + * @notice Get operator's current ticket balance + * @param operator Address of the operator + * @return Current collateral balance + */ + function getTicketBalance(address operator) external view returns (uint256); + + /** + * @notice Get operator's current license bond + * @param operator Address of the operator + * @return Current license bond + */ + function getLicenseBond(address operator) external view returns (uint256); + + /** + * @notice Get current ticket price + * @return Price per ticket in collateral token units + */ + function ticketPrice() external view returns (uint256); + + /** + * @notice Calculate available tickets for an operator + * @param operator Address of the operator + * @return Number of tickets available (floor(balance / ticketPrice)) + */ + function availableTickets(address operator) external view returns (uint256); + + /** + * @notice Check if operator is licensed + * @param operator Address of the operator + * @return True if operator has sufficient license bond + */ + function isLicensed(address operator) external view returns (bool); + + /** + * @notice Check if operator is registered + * @param operator Address of the operator + * @return True if operator is registered + */ + function isRegistered(address operator) external view returns (bool); + + /** + * @notice Check if operator is active + * @param operator Address of the operator + * @return True if operator is active (licensed, registered, and has min tickets) + */ + function isActive(address operator) external view returns (bool); + + /** + * @notice Check if operator has deregistration in progress + * @param operator Address of the operator + * @return True if exit requested but not finalized + */ + function hasExitInProgress(address operator) external view returns (bool); + + /** + * @notice Get license bond price required + * @return License bond price amount + */ + function licenseRequiredBond() external view returns (uint256); + + /** + * @notice Get minimum ticket balance required for activation + * @return Minimum number of tickets required + */ + function minTicketBalance() external view returns (uint256); + + /** + * @notice Get exit delay period + * @return Number of seconds operators must wait after requesting exit + */ + function exitDelay() external view returns (uint64); + + /** + * @notice Get slashed funds treasury address + * @return Address where slashed funds are sent + */ + function slashedFundsTreasury() external view returns (address); + + /** + * @notice Get total slashed ticket balance + * @return Amount of ticket balance slashed and available for treasury withdrawal + */ + function slashedTicketBalance() external view returns (uint256); + + /** + * @notice Get total slashed license bond + * @return Amount of license bond slashed and available for treasury withdrawal + */ + function slashedLicenseBond() external view returns (uint256); + + // ====================== + // Operator Functions + // ====================== + + /** + * @notice Increase operator's ticket balance by depositing tokens + * @param amount Amount of ticket tokens to deposit + * @dev Requires approval for ticket token transfer + */ + function addTicketBalance(uint256 amount) external; + + /** + * @notice Decrease operator's ticket balance by withdrawing tokens + * @param amount Amount of ticket tokens to withdraw + * @dev Reverts if operator is in any active committee + */ + function removeTicketBalance(uint256 amount) external; + + /** + * @notice Bond license tokens to become eligible for registration + * @param amount Amount of license tokens to bond + * @dev Requires approval for license token transfer + */ + function bondLicense(uint256 amount) external; + + /** + * @notice Unbond license tokens + * @param amount Amount of license tokens to unbond + * @dev Reverts if operator is in any active committee or still registered + */ + function unbondLicense(uint256 amount) external; + + /** + * @notice Register as an operator (callable by licensed operators) + * @dev Requires sufficient license bond and calls registry + */ + function registerOperator() external; + + /** + * @notice Deregister as an operator and remove from IMT + * @param siblingNodes Sibling node proofs for IMT removal + * @dev Requires operator to provide sibling nodes for immediate IMT removal + */ + function deregisterOperator(uint256[] calldata siblingNodes) external; + + /** + * @notice Finalize deregistration and withdraw all remaining funds + * @dev Can only be called after deregistration delay has passed + */ + function finalizeDeregistration() external; + + // ====================== + // Slashing Functions (Role-Restricted) + // ====================== + + /** + * @notice Slash operator's ticket balance by absolute amount + * @param operator Address of the operator to slash + * @param amount Amount to slash + * @param reason Reason for slashing (stored in event) + * @dev Only callable by authorized slashing manager + */ + function slashTicketBalance( + address operator, + uint256 amount, + bytes32 reason + ) external; + + /** + * @notice Slash operator's license bond by absolute amount + * @param operator Address of the operator to slash + * @param amount Amount to slash + * @param reason Reason for slashing (stored in event) + * @dev Only callable by authorized slashing manager + */ + function slashLicenseBond( + address operator, + uint256 amount, + bytes32 reason + ) external; + + /** + * @notice Slash operator's license bond by percentage (basis points) + * @param operator Address of the operator to slash + * @param bps Percentage to slash in basis points (0-10000) + * @param reason Reason for slashing (stored in event) + * @dev Only callable by authorized slashing manager + */ + function slashLicenseBps( + address operator, + uint256 bps, + bytes32 reason + ) external; + + // ====================== + // Admin Functions + // ====================== + + /** + * @notice Set ticket price + * @param newTicketPrice New price per ticket + * @dev Only callable by contract owner + */ + function setTicketPrice(uint256 newTicketPrice) external; + + /** + * @notice Set license bond price required + * @param newlicenseRequiredBond New license bond price + * @dev Only callable by contract owner + */ + function setlicenseRequiredBond(uint256 newlicenseRequiredBond) external; + + /** + * @notice Set minimum ticket balance required for activation + * @param newMinTicketBalance New minimum ticket balance + * @dev Only callable by contract owner + */ + function setMinTicketBalance(uint256 newMinTicketBalance) external; + + /** + * @notice Set exit delay period + * @param newExitDelay New exit delay in seconds + * @dev Only callable by contract owner + */ + function setExitDelay(uint64 newExitDelay) external; + + /** + * @notice Set slashed funds treasury address + * @param newSlashedFundsTreasury New slashed funds treasury address + * @dev Only callable by contract owner + */ + function setSlashedFundsTreasury(address newSlashedFundsTreasury) external; + + /** + * @notice Set registry address + * @param newRegistry New registry contract address + * @dev Only callable by contract owner + */ + function setRegistry(address newRegistry) external; + + /** + * @notice Set slashing manager address + * @param newSlashingManager New slashing manager contract address + * @dev Only callable by contract owner + */ + function setSlashingManager(address newSlashingManager) external; + + /** + * @notice Withdraw slashed funds to treasury + * @param ticketAmount Amount of slashed ticket balance to withdraw + * @param licenseAmount Amount of slashed license bond to withdraw + * @dev Only callable by contract owner, sends to treasury address + */ + function withdrawSlashedFunds( + uint256 ticketAmount, + uint256 licenseAmount + ) external; + + /** + * @notice Emergency pause the contract + * @dev Only callable by contract owner + */ + function pause() external; + + /** + * @notice Unpause the contract + * @dev Only callable by contract owner + */ + function unpause() external; +} diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index afa2c2bec2..5fff38159a 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -5,6 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +import { IRegistryFilter } from "./IRegistryFilter.sol"; + interface ICiphernodeRegistry { /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. @@ -21,6 +23,11 @@ interface ICiphernodeRegistry { /// @param publicKey Public key of the committee. event CommitteePublished(uint256 indexed e3Id, bytes publicKey); + /// @notice This event MUST be emitted when a committee's active status changes. + /// @param e3Id ID of the E3 for which the committee status changed. + /// @param active True if committee is now active, false if completed. + event CommitteeActivationChanged(uint256 indexed e3Id, bool active); + /// @notice This event MUST be emitted when `enclave` is set. /// @param enclave Address of the enclave contract. event EnclaveSet(address indexed enclave); @@ -51,6 +58,23 @@ interface ICiphernodeRegistry { function isCiphernodeEligible(address ciphernode) external returns (bool); + /// @notice Check if a ciphernode is enabled in the registry + /// @param node Address of the ciphernode + /// @return enabled Whether the ciphernode is enabled + function isEnabled(address node) external view returns (bool enabled); + + /// @notice Add a ciphernode to the registry + /// @param node Address of the ciphernode to add + function addCiphernode(address node) external; + + /// @notice Remove a ciphernode from the registry + /// @param node Address of the ciphernode to remove + /// @param siblingNodes Array of sibling node indices for tree operations + function removeCiphernode( + address node, + uint256[] calldata siblingNodes + ) external; + /// @notice Initiates the committee selection process for a specified E3. /// @dev This function MUST revert when not called by the Enclave contract. /// @param e3Id ID of the E3 for which to select the committee. @@ -81,4 +105,55 @@ interface ICiphernodeRegistry { function committeePublicKey( uint256 e3Id ) external view returns (bytes32 publicKeyHash); + + /// @notice This function should be called by the Enclave contract to get the filter for a given E3. + /// @dev This function MUST revert if no filter has been requested for the given E3. + /// @param e3Id ID of the E3 for which to get the filter. + /// @return filter The filter for the given E3. + function getFilter(uint256 e3Id) external view returns (address filter); + + /// @notice This function should be called by the Enclave contract to get the committee for a given E3. + /// @dev This function MUST revert if no committee has been requested for the given E3. + /// @param e3Id ID of the E3 for which to get the committee. + /// @return committee The committee for the given E3. + function getCommittee( + uint256 e3Id + ) external view returns (IRegistryFilter.Committee memory committee); + + /// @notice Mark a committee as active when a job starts + /// @param e3Id ID of the E3 committee + /// @param members Array of committee member addresses + /// @dev Should be called by authorized entities when job execution begins + function markCommitteeActive( + uint256 e3Id, + address[] calldata members + ) external; + + /// @notice Mark a committee as completed when a job ends + /// @param e3Id ID of the E3 committee + /// @param members Array of committee member addresses + /// @dev Should be called by authorized entities when job execution completes + function markCommitteeCompleted( + uint256 e3Id, + address[] calldata members + ) external; + + /// @notice Check if a node is active in any committee + /// @param node Address of the node to check + /// @return True if the node is currently active in at least one committee + function isNodeActiveInAnyCommittee( + address node + ) external view returns (bool); + + /// @notice Get the number of active committees a node is in + /// @param node Address of the node to check + /// @return Number of active committees the node is participating in + function activeCommitteeCountOf( + address node + ) external view returns (uint256); + + /// @notice Check if a specific committee is active + /// @param e3Id ID of the E3 committee to check + /// @return True if the committee is currently active + function isCommitteeActive(uint256 e3Id) external view returns (bool); } diff --git a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol b/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol index b7aeb0c63d..1dce174abb 100644 --- a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol +++ b/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol @@ -6,8 +6,18 @@ pragma solidity >=0.8.27; interface IRegistryFilter { + struct Committee { + address[] nodes; + uint32[2] threshold; + bytes32 publicKey; + } + function requestCommittee( uint256 e3Id, uint32[2] calldata threshold ) external returns (bool success); + + function getCommittee( + uint256 e3Id + ) external view returns (Committee memory); } diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol new file mode 100644 index 0000000000..328723acbb --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +pragma solidity >=0.8.27; + +interface ISlashVerifier { + /// @notice This function should be called by the SlashingManager contract to verify the + /// proof of a slash. + /// @param proposalId ID of the proposal. + /// @param proof ABI encoded proof of the given proposal. + /// @return success Whether or not the proof was successfully verified. + function verify( + uint256 proposalId, + bytes memory proof + ) external view returns (bool success); +} diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol new file mode 100644 index 0000000000..2242d1585d --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +import { IBondingRegistry } from "./IBondingRegistry.sol"; + +/** + * @title ISlashingManager + * @notice Interface for managing slashing proposals, appeals, and execution + * @dev Maintains policy table and handles slash workflows with appeals + */ +interface ISlashingManager { + // ====================== + // Structs + // ====================== + + /** + * @notice Slashing policy configuration for different slash reasons + */ + struct SlashPolicy { + uint256 ticketPenalty; // Amount or BPS for ticket collateral penalty + uint256 licensePenalty; // Amount or BPS for license stake penalty + bool useTicketBps; // True if ticket penalty is in BPS, false if absolute amount + bool useLicenseBps; // True if license penalty is in BPS, false if absolute amount + bool requiresProof; // True if slash requires verifier proof + address proofVerifier; // Address of the verifier contract for proof verification + bool banNode; // True if this slash type would result in banning + uint256 appealWindow; // Seconds operators have to appeal (0 = immediate execution) + bool enabled; // True if this slash type is currently enabled + } + + /** + * @notice Slash proposal details + */ + struct SlashProposal { + address operator; // Address being slashed + bytes32 reason; // Reason hash (maps to SlashPolicy) + uint256 ticketAmount; // Calculated ticket penalty amount + uint256 licenseAmount; // Calculated license penalty amount + bool executedTicket; // True if ticket penalty executed + bool executedLicense; // True if license penalty executed + bool appealed; // True if operator filed appeal + bool resolved; // True if appeal was resolved + bool approved; // True if appeal was approved (penalty cancelled) + uint256 proposedAt; // Timestamp when proposed + uint256 executableAt; // Timestamp when execution is allowed + address proposer; // Address that proposed the slash + bytes32 proofHash; // Hash of the proof data + bool proofVerified; // True if proof was verified + } + + // ====================== + // Errors + // ====================== + + error ZeroAddress(); + error Unauthorized(); + error InvalidPolicy(); + error InvalidProposal(); + error ProofRequired(); + error InvalidProof(); + error AppealWindowExpired(); + error AppealWindowActive(); + error AlreadyAppealed(); + error AlreadyExecuted(); + error AlreadyResolved(); + error SlashReasonNotFound(); + error SlashReasonDisabled(); + error CiphernodeBanned(); + + // ====================== + // Events + // ====================== + + /** + * @notice Emitted when a slash policy is updated + */ + event SlashPolicyUpdated(bytes32 indexed reason, SlashPolicy policy); + + /** + * @notice Emitted when a slash is proposed + */ + event SlashProposed( + uint256 indexed proposalId, + address indexed operator, + bytes32 indexed reason, + uint256 ticketAmount, + uint256 licenseAmount, + uint256 executableAt, + address proposer + ); + + /** + * @notice Emitted when a slash is executed + */ + event SlashExecuted( + uint256 indexed proposalId, + address indexed operator, + bytes32 indexed reason, + uint256 ticketAmount, + uint256 licenseAmount, + bool ticketExecuted, + bool licenseExecuted + ); + + /** + * @notice Emitted when an appeal is filed + */ + event AppealFiled( + uint256 indexed proposalId, + address indexed operator, + bytes32 indexed reason, + string evidence + ); + + /** + * @notice Emitted when an appeal is resolved + */ + event AppealResolved( + uint256 indexed proposalId, + address indexed operator, + bool approved, + address resolver, + string resolution + ); + + /** + * @notice Emitted when a node is banned + */ + event NodeBanned( + address indexed node, + bytes32 indexed reason, + address banner + ); + + /** + * @notice Emitted when a node is unbanned + */ + event NodeUnbanned(address indexed node, address unbanner); + + // ====================== + // View Functions + // ====================== + + /** + * @notice Get slash policy for a reason + */ + function getSlashPolicy( + bytes32 reason + ) external view returns (SlashPolicy memory); + + /** + * @notice Get slash proposal details + */ + function getSlashProposal( + uint256 proposalId + ) external view returns (SlashProposal memory); + + /** + * @notice Get total number of proposals + */ + function totalProposals() external view returns (uint256); + + /** + * @notice Check if a node is banned + */ + function isBanned(address node) external view returns (bool); + + /** + * @notice Get bonding vault contract + */ + function bondingRegistry() external view returns (IBondingRegistry); + + // ====================== + // Admin Functions + // ====================== + + /** + * @notice Set slash policy for a reason + * @param reason Reason hash to set policy for + * @param policy Policy configuration + */ + function setSlashPolicy( + bytes32 reason, + SlashPolicy calldata policy + ) external; + + /** + * @notice Set bonding vault address + * @param newBondingRegistry New bonding vault contract address + */ + function setBondingRegistry(address newBondingRegistry) external; + + /** + * @notice Add authorized slasher + * @param slasher Address to authorize for slashing + */ + function addSlasher(address slasher) external; + + /** + * @notice Remove authorized slasher + * @param slasher Address to remove from slashing authorization + */ + function removeSlasher(address slasher) external; + + /** + * @notice Add authorized verifier + * @param verifier Address to authorize for proof verification + */ + function addVerifier(address verifier) external; + + /** + * @notice Remove authorized verifier + * @param verifier Address to remove from verification authorization + */ + function removeVerifier(address verifier) external; + + // ====================== + // Slashing Functions + // ====================== + + /** + * @notice Propose a slash with proof + * @param operator Address to slash + * @param reason Slash reason (must have configured policy) + * @param proof Proof data (if required by policy) + * @return proposalId ID of the created proposal + */ + function proposeSlash( + address operator, + bytes32 reason, + bytes calldata proof + ) external returns (uint256 proposalId); + + /** + * @notice Execute a slash proposal + * @param proposalId ID of the proposal to execute + */ + function executeSlash(uint256 proposalId) external; + + // ====================== + // Appeal Functions + // ====================== + + /** + * @notice File an appeal for a slash proposal + * @param proposalId ID of the proposal to appeal + * @param evidence Evidence string supporting the appeal + */ + function fileAppeal(uint256 proposalId, string calldata evidence) external; + + /** + * @notice Resolve an appeal (governance only) + * @param proposalId ID of the proposal with appeal + * @param approved True to approve appeal (cancel slash), false to deny + * @param resolution Resolution explanation string + */ + function resolveAppeal( + uint256 proposalId, + bool approved, + string calldata resolution + ) external; + + // ====================== + // Ban Management + // ====================== + + /** + * @notice Ban a node (governance only) + * @param node Address to ban + * @param reason Reason for banning + */ + function banNode(address node, bytes32 reason) external; + + /** + * @notice Unban a node (governance only) + * @param node Address to unban + */ + function unbanNode(address node) external; + + /** + * @notice Emergency pause slashing operations + */ + function pause() external; + + /** + * @notice Unpause slashing operations + */ + function unpause() external; +} diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol new file mode 100644 index 0000000000..697f257dd3 --- /dev/null +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +import { + UUPSUpgradeable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + PausableUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; +import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; + +/** + * @title BondingRegistry + * @notice Main registry for operator balance and license bonds + */ +contract BondingRegistry is + Initializable, + UUPSUpgradeable, + OwnableUpgradeable, + PausableUpgradeable, + IBondingRegistry +{ + using SafeERC20 for IERC20; + + // ====================== + // Constants + // ====================== + + uint256 private constant BPS_DENOMINATOR = 10_000; + bytes32 private constant REASON_DEPOSIT = bytes32("DEPOSIT"); + bytes32 private constant REASON_WITHDRAW = bytes32("WITHDRAW"); + bytes32 private constant REASON_BOND = bytes32("BOND"); + bytes32 private constant REASON_UNBOND = bytes32("UNBOND"); + + // ====================== + // Storage + // ====================== + + /// @notice Ticket token (USDC) + IERC20 public ticketToken; + + /// @notice License token (ENCL) + IERC20 public licenseToken; + + /// @notice Registry contract for committee membership checks + ICiphernodeRegistry public registry; + + /// @notice Authorized slashing manager + address public slashingManager; + + /// @notice Treasury address for slashed funds + address public slashedFundsTreasury; + + // Configuration + uint256 public ticketPrice; + uint256 public licenseRequiredBond; + uint256 public minTicketBalance; + uint64 public exitDelay; + + // Operator data structure + struct Operator { + uint256 ticketBalance; + uint256 licenseBond; + uint64 exitUnlocksAt; + bool registered; + bool exitRequested; + bool active; + } + + mapping(address operator => Operator data) internal operators; + + // Total slashed funds available for treasury withdrawal + uint256 public slashedTicketBalance; + uint256 public slashedLicenseBond; + + // ====================== + // Storage Gaps for Upgrades + // ====================== + + uint256[50] private __gap; + + // ====================== + // Modifiers + // ====================== + + modifier onlySlashingManager() { + if (msg.sender != slashingManager) revert Unauthorized(); + _; + } + + modifier notInActiveCommittee(address operator) { + if ( + address(registry) != address(0) && + registry.isNodeActiveInAnyCommittee(operator) + ) { + revert ActiveCommittee(); + } + _; + } + + modifier noExitInProgress(address operator) { + if (operators[operator].exitRequested) revert ExitInProgress(); + _; + } + + // ====================== + // Initialization + // ====================== + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param owner Contract owner + * @param _ticketToken Ticket token contract + * @param _licenseToken License token contract + * @param _registry Registry contract + * @param _slashedFundsTreasury Slashed funds treasury address + * @param _ticketPrice Initial ticket price + * @param _licenseRequiredBond Initial license bond price + * @param _minTicketBalance Initial minimum ticket balance for activation + * @param _exitDelay Initial exit delay period + */ + function initialize( + address owner, + IERC20 _ticketToken, + IERC20 _licenseToken, + address _registry, + address _slashedFundsTreasury, + uint256 _ticketPrice, + uint256 _licenseRequiredBond, + uint256 _minTicketBalance, + uint64 _exitDelay + ) external initializer { + __Ownable_init(owner); + __Pausable_init(); + __UUPSUpgradeable_init(); + + require(address(_ticketToken) != address(0), ZeroAddress()); + require(address(_licenseToken) != address(0), ZeroAddress()); + require(_slashedFundsTreasury != address(0), ZeroAddress()); + require(_ticketPrice != 0, InvalidConfiguration()); + require(_licenseRequiredBond != 0, InvalidConfiguration()); + + ticketToken = _ticketToken; + licenseToken = _licenseToken; + registry = ICiphernodeRegistry(_registry); + slashedFundsTreasury = _slashedFundsTreasury; + ticketPrice = _ticketPrice; + licenseRequiredBond = _licenseRequiredBond; + minTicketBalance = _minTicketBalance; + exitDelay = _exitDelay; + } + + // ====================== + // View Functions + // ====================== + + function getTicketBalance( + address operator + ) external view returns (uint256) { + return operators[operator].ticketBalance; + } + + function getLicenseBond(address operator) external view returns (uint256) { + return operators[operator].licenseBond; + } + + function availableTickets( + address operator + ) external view returns (uint256) { + if (ticketPrice == 0) return 0; + return operators[operator].ticketBalance / ticketPrice; + } + + function isLicensed(address operator) external view returns (bool) { + return + operators[operator].licenseBond >= (licenseRequiredBond * 80) / 100; + } + + function isRegistered(address operator) external view returns (bool) { + return operators[operator].registered; + } + + function isActive(address operator) external view returns (bool) { + Operator storage op = operators[operator]; + return + op.registered && + op.licenseBond >= (licenseRequiredBond * 80) / 100 && + (ticketPrice == 0 || + op.ticketBalance / ticketPrice >= minTicketBalance); + } + + function hasExitInProgress(address operator) external view returns (bool) { + return operators[operator].exitRequested; + } + + // ====================== + // Operator Functions + // ====================== + + function addTicketBalance( + uint256 amount + ) external whenNotPaused noExitInProgress(msg.sender) { + require(amount != 0, ZeroAmount()); + require(operators[msg.sender].registered, NotRegistered()); + + uint256 balanceBefore = ticketToken.balanceOf(address(this)); + ticketToken.safeTransferFrom(msg.sender, address(this), amount); + uint256 actualReceived = ticketToken.balanceOf(address(this)) - + balanceBefore; + + operators[msg.sender].ticketBalance += actualReceived; + + emit TicketBalanceUpdated( + msg.sender, + int256(actualReceived), + operators[msg.sender].ticketBalance, + REASON_DEPOSIT + ); + + _updateOperatorStatus(msg.sender); + } + + function removeTicketBalance( + uint256 amount + ) + external + whenNotPaused + noExitInProgress(msg.sender) + notInActiveCommittee(msg.sender) + { + require(amount != 0, ZeroAmount()); + require(operators[msg.sender].registered, NotRegistered()); + require( + operators[msg.sender].ticketBalance >= amount, + InsufficientBalance() + ); + + operators[msg.sender].ticketBalance -= amount; + ticketToken.safeTransfer(msg.sender, amount); + + emit TicketBalanceUpdated( + msg.sender, + -int256(amount), + operators[msg.sender].ticketBalance, + REASON_WITHDRAW + ); + + _updateOperatorStatus(msg.sender); + } + + function bondLicense( + uint256 amount + ) external whenNotPaused noExitInProgress(msg.sender) { + require(amount != 0, ZeroAmount()); + + uint256 balanceBefore = licenseToken.balanceOf(address(this)); + licenseToken.safeTransferFrom(msg.sender, address(this), amount); + uint256 actualReceived = licenseToken.balanceOf(address(this)) - + balanceBefore; + + operators[msg.sender].licenseBond += actualReceived; + + emit LicenseBondUpdated( + msg.sender, + int256(actualReceived), + operators[msg.sender].licenseBond, + REASON_BOND + ); + + _updateOperatorStatus(msg.sender); + } + + function unbondLicense( + uint256 amount + ) + external + whenNotPaused + noExitInProgress(msg.sender) + notInActiveCommittee(msg.sender) + { + require(amount != 0, ZeroAmount()); + require( + operators[msg.sender].licenseBond >= amount, + InsufficientBalance() + ); + + operators[msg.sender].licenseBond -= amount; + licenseToken.safeTransfer(msg.sender, amount); + + emit LicenseBondUpdated( + msg.sender, + -int256(amount), + operators[msg.sender].licenseBond, + REASON_UNBOND + ); + + _updateOperatorStatus(msg.sender); + } + + function registerOperator() + external + whenNotPaused + noExitInProgress(msg.sender) + { + require(!operators[msg.sender].registered, AlreadyRegistered()); + require( + operators[msg.sender].licenseBond >= licenseRequiredBond, + NotLicensed() + ); + + operators[msg.sender].registered = true; + + if (address(registry) != address(0)) { + registry.addCiphernode(msg.sender); + } + + emit OperatorRegistrationChanged(msg.sender, true); + _updateOperatorStatus(msg.sender); + } + + /** + * @notice Deregister as an operator and remove from IMT + * @param siblingNodes Sibling node proofs for IMT removal + * @dev Requires operator to provide sibling nodes for immediate IMT removal + */ + function deregisterOperator( + uint256[] calldata siblingNodes + ) + external + whenNotPaused + noExitInProgress(msg.sender) + notInActiveCommittee(msg.sender) + { + require(operators[msg.sender].registered, NotRegistered()); + + operators[msg.sender].registered = false; + operators[msg.sender].exitRequested = true; + operators[msg.sender].exitUnlocksAt = + uint64(block.timestamp) + + exitDelay; + + if (address(registry) != address(0)) { + registry.removeCiphernode(msg.sender, siblingNodes); + } + + emit CiphernodeDeregistrationRequested( + msg.sender, + operators[msg.sender].exitUnlocksAt + ); + _updateOperatorStatus(msg.sender); + } + + function finalizeDeregistration() external { + require(operators[msg.sender].exitRequested, ExitInProgress()); + require( + block.timestamp >= operators[msg.sender].exitUnlocksAt, + ExitNotReady() + ); + + uint256 ticketRefund = operators[msg.sender].ticketBalance; + uint256 licenseRefund = operators[msg.sender].licenseBond; + + operators[msg.sender].ticketBalance = 0; + operators[msg.sender].licenseBond = 0; + operators[msg.sender].exitRequested = false; + operators[msg.sender].exitUnlocksAt = 0; + + if (ticketRefund > 0) { + ticketToken.safeTransfer(msg.sender, ticketRefund); + } + if (licenseRefund > 0) { + licenseToken.safeTransfer(msg.sender, licenseRefund); + } + + emit DeregistrationFinalized(msg.sender, ticketRefund, licenseRefund); + _updateOperatorStatus(msg.sender); + } + + // ====================== + // Slashing Functions + // ====================== + + function slashTicketBalance( + address operator, + uint256 amount, + bytes32 reason + ) external onlySlashingManager { + require(amount != 0, ZeroAmount()); + + uint256 currentBalance = operators[operator].ticketBalance; + uint256 slashAmount = Math.min(amount, currentBalance); + + if (slashAmount > 0) { + operators[operator].ticketBalance -= slashAmount; + slashedTicketBalance += slashAmount; + + emit TicketBalanceUpdated( + operator, + -int256(slashAmount), + operators[operator].ticketBalance, + reason + ); + + _updateOperatorStatus(operator); + } + } + + function slashLicenseBond( + address operator, + uint256 amount, + bytes32 reason + ) external onlySlashingManager { + require(amount != 0, ZeroAmount()); + + uint256 currentBond = operators[operator].licenseBond; + uint256 slashAmount = Math.min(amount, currentBond); + + if (slashAmount > 0) { + operators[operator].licenseBond -= slashAmount; + slashedLicenseBond += slashAmount; + + emit LicenseBondUpdated( + operator, + -int256(slashAmount), + operators[operator].licenseBond, + reason + ); + + if ( + operators[operator].licenseBond < + (licenseRequiredBond * 80) / 100 && + operators[operator].registered + ) { + operators[operator].registered = false; + emit OperatorRegistrationChanged(operator, false); + } + + _updateOperatorStatus(operator); + } + } + + function slashLicenseBps( + address operator, + uint256 bps, + bytes32 reason + ) external onlySlashingManager { + require(bps != 0, ZeroAmount()); + require(bps <= BPS_DENOMINATOR, InvalidAmount()); + + uint256 currentBond = operators[operator].licenseBond; + uint256 slashAmount = (currentBond * bps) / BPS_DENOMINATOR; + + if (slashAmount > 0) { + operators[operator].licenseBond -= slashAmount; + slashedLicenseBond += slashAmount; + + emit LicenseBondUpdated( + operator, + -int256(slashAmount), + operators[operator].licenseBond, + reason + ); + + _updateOperatorStatus(operator); + } + } + + // ====================== + // Admin Functions + // ====================== + + function setTicketPrice(uint256 newTicketPrice) external onlyOwner { + require(newTicketPrice != 0, InvalidConfiguration()); + + uint256 oldValue = ticketPrice; + ticketPrice = newTicketPrice; + + emit ConfigurationUpdated("ticketPrice", oldValue, newTicketPrice); + } + + function setlicenseRequiredBond( + uint256 newlicenseRequiredBond + ) external onlyOwner { + require(newlicenseRequiredBond != 0, InvalidConfiguration()); + + uint256 oldValue = licenseRequiredBond; + licenseRequiredBond = newlicenseRequiredBond; + + emit ConfigurationUpdated( + "licenseRequiredBond", + oldValue, + newlicenseRequiredBond + ); + } + + function setMinTicketBalance( + uint256 newMinTicketBalance + ) external onlyOwner { + uint256 oldValue = minTicketBalance; + minTicketBalance = newMinTicketBalance; + + emit ConfigurationUpdated( + "minTicketBalance", + oldValue, + newMinTicketBalance + ); + } + + function setExitDelay(uint64 newExitDelay) external onlyOwner { + uint256 oldValue = uint256(exitDelay); + exitDelay = newExitDelay; + + emit ConfigurationUpdated("exitDelay", oldValue, uint256(newExitDelay)); + } + + function setSlashedFundsTreasury( + address newSlashedFundsTreasury + ) external onlyOwner { + require(newSlashedFundsTreasury != address(0), ZeroAddress()); + slashedFundsTreasury = newSlashedFundsTreasury; + } + + function setRegistry(address newRegistry) external onlyOwner { + registry = ICiphernodeRegistry(newRegistry); + } + + function setSlashingManager(address newSlashingManager) external onlyOwner { + slashingManager = newSlashingManager; + } + + function withdrawSlashedFunds( + uint256 ticketAmount, + uint256 licenseAmount + ) external onlyOwner { + if (ticketAmount > slashedTicketBalance) revert InsufficientBalance(); + if (licenseAmount > slashedLicenseBond) revert InsufficientBalance(); + + if (ticketAmount > 0) { + slashedTicketBalance -= ticketAmount; + ticketToken.safeTransfer(slashedFundsTreasury, ticketAmount); + } + + if (licenseAmount > 0) { + slashedLicenseBond -= licenseAmount; + licenseToken.safeTransfer(slashedFundsTreasury, licenseAmount); + } + + emit SlashedFundsWithdrawn( + slashedFundsTreasury, + ticketAmount, + licenseAmount + ); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + // ====================== + // Internal Functions + // ====================== + + function _updateOperatorStatus(address operator) internal { + bool newActiveStatus = operators[operator].registered && + operators[operator].licenseBond >= + (licenseRequiredBond * 80) / 100 && + (ticketPrice == 0 || + operators[operator].ticketBalance / ticketPrice >= + minTicketBalance); + + if (operators[operator].active != newActiveStatus) { + operators[operator].active = newActiveStatus; + emit OperatorActivationChanged(operator, newActiveStatus); + } + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} +} diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 59724bf828..d080bbf9b6 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -7,6 +7,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -18,6 +19,14 @@ import { contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { using InternalLeanIMT for LeanIMTData; + //////////////////////////////////////////////////////////// + // // + // Events // + // // + //////////////////////////////////////////////////////////// + + event BondingRegistrySet(address indexed bondingRegistry); + //////////////////////////////////////////////////////////// // // // Storage Variables // @@ -25,6 +34,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { //////////////////////////////////////////////////////////// address public enclave; + address public bondingRegistry; uint256 public numCiphernodes; LeanIMTData public ciphernodes; @@ -32,6 +42,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { mapping(uint256 e3Id => uint256 root) public roots; mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; + // Committee tracking for active job management + mapping(uint256 e3Id => bool) public committeeActive; + mapping(address node => uint256 count) public activeCommitteeCount; + //////////////////////////////////////////////////////////// // // // Errors // @@ -44,6 +58,12 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { error CommitteeNotPublished(); error CiphernodeNotEnabled(address node); error OnlyEnclave(); + error OnlyBondingRegistry(); + error NotOwnerOrBondingRegistry(); + error NodeNotBonded(address node); + error ZeroAddress(); + error BondingRegistryNotSet(); + error Unauthorized(); //////////////////////////////////////////////////////////// // // @@ -56,6 +76,19 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { _; } + modifier onlyBondingRegistry() { + require(msg.sender == bondingRegistry, OnlyBondingRegistry()); + _; + } + + modifier onlyOwnerOrBondingVault() { + require( + msg.sender == owner() || msg.sender == bondingRegistry, + NotOwnerOrBondingRegistry() + ); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -67,6 +100,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } function initialize(address _owner, address _enclave) public initializer { + require(_owner != address(0), ZeroAddress()); + require(_enclave != address(0), ZeroAddress()); + __Ownable_init(msg.sender); setEnclave(_enclave); if (_owner != owner()) transferOwnership(_owner); @@ -107,7 +143,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit CommitteePublished(e3Id, publicKey); } - function addCiphernode(address node) external onlyOwner { + function addCiphernode(address node) external onlyOwnerOrBondingVault { + if (isEnabled(node)) { + return; + } + uint160 ciphernode = uint160(node); ciphernodes._insert(ciphernode); numCiphernodes++; @@ -122,7 +162,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { function removeCiphernode( address node, uint256[] calldata siblingNodes - ) external onlyOwner { + ) external onlyOwnerOrBondingVault { + require(isEnabled(node), CiphernodeNotEnabled(node)); + uint160 ciphernode = uint160(node); uint256 index = ciphernodes._indexOf(ciphernode); ciphernodes._remove(ciphernode, siblingNodes); @@ -137,10 +179,17 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { //////////////////////////////////////////////////////////// function setEnclave(address _enclave) public onlyOwner { + require(_enclave != address(0), ZeroAddress()); enclave = _enclave; emit EnclaveSet(_enclave); } + function setBondingRegistry(address _bondingRegistry) public onlyOwner { + require(_bondingRegistry != address(0), ZeroAddress()); + bondingRegistry = _bondingRegistry; + emit BondingRegistrySet(_bondingRegistry); + } + //////////////////////////////////////////////////////////// // // // Get Functions // @@ -155,7 +204,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } function isCiphernodeEligible(address node) external view returns (bool) { - return isEnabled(node); + if (!isEnabled(node)) return false; + + require(bondingRegistry != address(0), BondingRegistryNotSet()); + return IBondingRegistry(bondingRegistry).isActive(node); } function isEnabled(address node) public view returns (bool) { @@ -170,11 +222,83 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { return roots[e3Id]; } - function getFilter(uint256 e3Id) public view returns (IRegistryFilter) { - return registryFilters[e3Id]; + function getFilter(uint256 e3Id) public view returns (address filter) { + return address(registryFilters[e3Id]); + } + + function getCommittee( + uint256 e3Id + ) public view returns (IRegistryFilter.Committee memory) { + return registryFilters[e3Id].getCommittee(e3Id); } function treeSize() public view returns (uint256) { return ciphernodes.size; } + + function getBondingRegistry() external view returns (address) { + return bondingRegistry; + } + + //////////////////////////////////////////////////////////// + // // + // Committee Tracking // + // // + //////////////////////////////////////////////////////////// + + function markCommitteeActive( + uint256 e3Id, + address[] calldata members + ) external { + require(msg.sender == enclave || msg.sender == owner(), Unauthorized()); + + // Idempotent: only process if not already active + if (!committeeActive[e3Id]) { + committeeActive[e3Id] = true; + + // Increment active committee count for each member + for (uint256 i = 0; i < members.length; i++) { + activeCommitteeCount[members[i]]++; + } + + emit CommitteeActivationChanged(e3Id, true); + } + } + + function markCommitteeCompleted( + uint256 e3Id, + address[] calldata members + ) external { + require(msg.sender == enclave || msg.sender == owner(), Unauthorized()); + + // Only process if currently active + if (committeeActive[e3Id]) { + committeeActive[e3Id] = false; + + // Decrement active committee count for each member + for (uint256 i = 0; i < members.length; i++) { + if (activeCommitteeCount[members[i]] > 0) { + activeCommitteeCount[members[i]]--; + } + } + + emit CommitteeActivationChanged(e3Id, false); + } + } + + function isNodeActiveInAnyCommittee( + address node + ) external view returns (bool) { + return activeCommitteeCount[node] > 0; + } + + function activeCommitteeCountOf( + address node + ) external view returns (uint256) { + return activeCommitteeCount[node]; + } + + function isCommitteeActive(uint256 e3Id) external view returns (bool) { + return committeeActive[e3Id]; + } } diff --git a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol index 0c7d4d4c04..a7774dc097 100644 --- a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol +++ b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol @@ -12,12 +12,6 @@ import { } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { - struct Committee { - address[] nodes; - uint32[2] threshold; - bytes32 publicKey; - } - //////////////////////////////////////////////////////////// // // // Storage Variables // @@ -26,7 +20,8 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { address public registry; - mapping(uint256 e3 => Committee committee) public committees; + mapping(uint256 e3 => IRegistryFilter.Committee committee) + public committees; //////////////////////////////////////////////////////////// // // @@ -52,6 +47,15 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { _; } + modifier onlyOwnerOrCiphernode() { + require( + msg.sender == owner() || + ICiphernodeRegistry(registry).isCiphernodeEligible(msg.sender), + CiphernodeNotEnabled(msg.sender) + ); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -85,14 +89,13 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { function publishCommittee( uint256 e3Id, - address[] calldata nodes, - bytes calldata publicKey + address[] memory nodes, + bytes memory publicKey ) external onlyOwner { - Committee storage committee = committees[e3Id]; + IRegistryFilter.Committee storage committee = committees[e3Id]; require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); committee.nodes = nodes; committee.publicKey = keccak256(publicKey); - ICiphernodeRegistry(registry).publishCommittee( e3Id, abi.encode(nodes), @@ -118,7 +121,9 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { function getCommittee( uint256 e3Id - ) external view returns (Committee memory) { - return committees[e3Id]; + ) external view returns (IRegistryFilter.Committee memory) { + IRegistryFilter.Committee memory committee = committees[e3Id]; + require(committee.publicKey != bytes32(0), CommitteeNotPublished()); + return committee; } } diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol new file mode 100644 index 0000000000..c75a93a79b --- /dev/null +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +import { + UUPSUpgradeable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { + AccessControlUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { + PausableUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { + ReentrancyGuardUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; +import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; + +/** + * @title SlashingManager + * @notice Manages slashing proposals, appeals, and execution for the bonding system + * @dev UUPS upgradeable contract with role-based access control + */ +contract SlashingManager is + Initializable, + UUPSUpgradeable, + AccessControlUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable, + ISlashingManager +{ + // ====================== + // Constants & Roles + // ====================== + + bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); + bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + + uint256 private constant BPS_DENOMINATOR = 10_000; + + // ====================== + // Storage + // ====================== + + /// @notice Bonding registry contract + IBondingRegistry public bondingRegistry; + + /// @notice Slash policies by reason hash + mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; + + /// @notice All slash proposals + mapping(uint256 proposalId => SlashProposal proposal) public proposals; + + /// @notice Total number of proposals created + uint256 public totalProposals; + + /// @notice Banned nodes + mapping(address node => bool banned) public banned; + + // ====================== + // Storage Gaps for Upgrades + // ====================== + + uint256[50] private __gap; + + // ====================== + // Modifiers + // ====================== + + modifier onlySlasher() { + if (!hasRole(SLASHER_ROLE, msg.sender)) revert Unauthorized(); + _; + } + + modifier onlyVerifier() { + if (!hasRole(VERIFIER_ROLE, msg.sender)) revert Unauthorized(); + _; + } + + modifier onlyGovernance() { + if (!hasRole(GOVERNANCE_ROLE, msg.sender)) revert Unauthorized(); + _; + } + + modifier notBanned(address node) { + if (banned[node]) revert CiphernodeBanned(); + _; + } + + // ====================== + // Initialization + // ====================== + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param admin Contract admin (gets DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE) + * @param _bondingRegistry Bonding registry contract address + */ + function initialize( + address admin, + address _bondingRegistry + ) external initializer { + __AccessControl_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + + require(admin != address(0), ZeroAddress()); + require(_bondingRegistry != address(0), ZeroAddress()); + + bondingRegistry = IBondingRegistry(_bondingRegistry); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + } + + // ====================== + // View Functions + // ====================== + + function getSlashPolicy( + bytes32 reason + ) external view returns (SlashPolicy memory) { + return slashPolicies[reason]; + } + + function getSlashProposal( + uint256 proposalId + ) external view returns (SlashProposal memory) { + if (proposalId >= totalProposals) revert InvalidProposal(); + return proposals[proposalId]; + } + + function isBanned(address node) external view returns (bool) { + return banned[node]; + } + + // ====================== + // Admin Functions + // ====================== + + function setSlashPolicy( + bytes32 reason, + SlashPolicy calldata policy + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(reason != bytes32(0), InvalidPolicy()); + if (policy.useTicketBps && policy.ticketPenalty > BPS_DENOMINATOR) + revert InvalidPolicy(); + if (policy.useLicenseBps && policy.licensePenalty > BPS_DENOMINATOR) + revert InvalidPolicy(); + + slashPolicies[reason] = policy; + emit SlashPolicyUpdated(reason, policy); + } + + function setBondingRegistry( + address newBondingRegistry + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(newBondingRegistry != address(0), ZeroAddress()); + bondingRegistry = IBondingRegistry(newBondingRegistry); + } + + function addSlasher(address slasher) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(slasher != address(0), ZeroAddress()); + _grantRole(SLASHER_ROLE, slasher); + } + + function removeSlasher( + address slasher + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + _revokeRole(SLASHER_ROLE, slasher); + } + + function addVerifier( + address verifier + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(verifier != address(0), ZeroAddress()); + _grantRole(VERIFIER_ROLE, verifier); + } + + function removeVerifier( + address verifier + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + _revokeRole(VERIFIER_ROLE, verifier); + } + + // ====================== + // Slashing Functions + // ====================== + + function proposeSlash( + address operator, + bytes32 reason, + bytes calldata proof + ) + external + onlySlasher + whenNotPaused + notBanned(operator) + returns (uint256 proposalId) + { + SlashPolicy storage policy = slashPolicies[reason]; + require(policy.enabled, SlashReasonDisabled()); + require(!(policy.requiresProof && proof.length == 0), ProofRequired()); + + uint256 ticketAmount = 0; + uint256 licenseAmount = 0; + + if (policy.ticketPenalty > 0) { + if (policy.useTicketBps) { + uint256 ticketBalance = bondingRegistry.getTicketBalance( + operator + ); + ticketAmount = + (ticketBalance * policy.ticketPenalty) / + BPS_DENOMINATOR; + } else { + ticketAmount = policy.ticketPenalty; + } + } + + if (policy.licensePenalty > 0) { + if (policy.useLicenseBps) { + uint256 bond = bondingRegistry.getLicenseBond(operator); + licenseAmount = + (bond * policy.licensePenalty) / + BPS_DENOMINATOR; + } else { + licenseAmount = policy.licensePenalty; + } + } + + proposalId = totalProposals++; + uint256 executableAt = block.timestamp + policy.appealWindow; + + bool proofVerified = false; + if (policy.requiresProof) { + proofVerified = ISlashVerifier(policy.proofVerifier).verify( + proposalId, + proof + ); + } + + proposals[proposalId] = SlashProposal({ + operator: operator, + reason: reason, + ticketAmount: ticketAmount, + licenseAmount: licenseAmount, + executedTicket: false, + executedLicense: false, + appealed: false, + resolved: false, + approved: false, + proposedAt: block.timestamp, + executableAt: executableAt, + proposer: msg.sender, + proofHash: keccak256(proof), + proofVerified: proofVerified + }); + + emit SlashProposed( + proposalId, + operator, + reason, + ticketAmount, + licenseAmount, + executableAt, + msg.sender + ); + } + + function executeSlash( + uint256 proposalId + ) external onlySlasher whenNotPaused nonReentrant { + require(proposalId < totalProposals, InvalidProposal()); + + SlashProposal storage proposal = proposals[proposalId]; + + require( + !(proposal.appealed && !proposal.resolved), + AppealWindowActive() + ); + require(!(proposal.resolved && proposal.approved), AlreadyExecuted()); + require(block.timestamp >= proposal.executableAt, AppealWindowActive()); + require( + !(proposal.executedTicket && proposal.executedLicense), + AlreadyExecuted() + ); + + bool ticketExecuted = proposal.executedTicket; + bool licenseExecuted = proposal.executedLicense; + + // Ticket Slash + if (!proposal.executedTicket && proposal.ticketAmount > 0) { + bondingRegistry.slashTicketBalance( + proposal.operator, + proposal.ticketAmount, + proposal.reason + ); + proposal.executedTicket = true; + ticketExecuted = true; + } + + // License bond slash + if (!proposal.executedLicense && proposal.licenseAmount > 0) { + bondingRegistry.slashLicenseBond( + proposal.operator, + proposal.licenseAmount, + proposal.reason + ); + proposal.executedLicense = true; + licenseExecuted = true; + } + + SlashPolicy storage policy = slashPolicies[proposal.reason]; + if (policy.banNode) { + banned[proposal.operator] = true; + emit NodeBanned(proposal.operator, proposal.reason, address(this)); + } + + emit SlashExecuted( + proposalId, + proposal.operator, + proposal.reason, + proposal.ticketAmount, + proposal.licenseAmount, + ticketExecuted, + licenseExecuted + ); + } + + // ====================== + // Appeal Functions + // ====================== + + function fileAppeal( + uint256 proposalId, + string calldata evidence + ) external whenNotPaused { + require(proposalId < totalProposals, InvalidProposal()); + + SlashProposal storage proposal = proposals[proposalId]; + require(msg.sender == proposal.operator, Unauthorized()); + require(!proposal.appealed, AlreadyAppealed()); + require(block.timestamp < proposal.executableAt, AppealWindowExpired()); + + proposal.appealed = true; + + emit AppealFiled( + proposalId, + proposal.operator, + proposal.reason, + evidence + ); + } + + function resolveAppeal( + uint256 proposalId, + bool approved, + string calldata resolution + ) external onlyGovernance { + require(proposalId < totalProposals, InvalidProposal()); + + SlashProposal storage proposal = proposals[proposalId]; + require(proposal.appealed, InvalidProposal()); + require(!proposal.resolved, AlreadyResolved()); + + proposal.resolved = true; + proposal.approved = approved; + + emit AppealResolved( + proposalId, + proposal.operator, + approved, + msg.sender, + resolution + ); + } + + // ====================== + // Ban Management + // ====================== + + function banNode(address node, bytes32 reason) external onlyGovernance { + require(node != address(0), ZeroAddress()); + + banned[node] = true; + emit NodeBanned(node, reason, msg.sender); + } + + function unbanNode(address node) external onlyGovernance { + require(node != address(0), ZeroAddress()); + + banned[node] = false; + emit NodeUnbanned(node, msg.sender); + } + + function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _pause(); + } + + function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { + _unpause(); + } + + // ====================== + // Internal Functions + // ====================== + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol b/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol index a8a6d390eb..e9f32f2d6f 100644 --- a/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol +++ b/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol @@ -19,12 +19,6 @@ interface IRegistry { } contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { - struct Committee { - address[] nodes; - uint32[2] threshold; - bytes publicKey; - } - //////////////////////////////////////////////////////////// // // // Storage Variables // @@ -33,7 +27,8 @@ contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { address public registry; - mapping(uint256 e3 => Committee committee) public committees; + mapping(uint256 e3 => IRegistryFilter.Committee committee) + public committees; //////////////////////////////////////////////////////////// // // @@ -84,7 +79,7 @@ contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { uint256 e3Id, uint32[2] calldata threshold ) external onlyRegistry returns (bool success) { - Committee storage committee = committees[e3Id]; + IRegistryFilter.Committee storage committee = committees[e3Id]; require(committee.threshold.length == 0, CommitteeAlreadyExists()); committee.threshold = threshold; success = true; @@ -95,13 +90,10 @@ contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { address[] memory nodes, bytes memory publicKey ) external onlyOwner { - Committee storage committee = committees[e3Id]; - require( - keccak256(committee.publicKey) == keccak256(hex""), - CommitteeAlreadyPublished() - ); + IRegistryFilter.Committee storage committee = committees[e3Id]; + require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); committee.nodes = nodes; - committee.publicKey = publicKey; + committee.publicKey = keccak256(publicKey); IRegistry(registry).publishCommittee(e3Id, nodes, publicKey); } @@ -114,4 +106,18 @@ contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { function setRegistry(address _registry) public onlyOwner { registry = _registry; } + + //////////////////////////////////////////////////////////// + // // + // Get Functions // + // // + //////////////////////////////////////////////////////////// + + function getCommittee( + uint256 e3Id + ) external view returns (IRegistryFilter.Committee memory) { + IRegistryFilter.Committee memory committee = committees[e3Id]; + require(committee.publicKey != bytes32(0), CommitteeNotPublished()); + return committee; + } } diff --git a/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol new file mode 100644 index 0000000000..9a134afbea --- /dev/null +++ b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +pragma solidity >=0.8.27; + +import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; + +contract MockSlashingVerifier is ISlashVerifier { + function verify( + uint256, + bytes memory data + ) external pure returns (bool success) { + data; + + if (data.length > 0) success = true; + } +} diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol new file mode 100644 index 0000000000..597c6d56d1 --- /dev/null +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { + ERC20Permit, + Nonces +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { + ERC20Votes +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { + AccessControl +} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title EnclaveToken + */ +contract EnclaveToken is + ERC20, + ERC20Permit, + ERC20Votes, + Ownable, + AccessControl +{ + // Custom errors + error ZeroAddress(); + error ZeroAmount(); + error ExceedsTotalSupply(); + error ArrayLengthMismatch(); + error TransferNotAllowed(); + + /// @dev Maximum supply of the token (18 decimals). + uint256 public constant TOTAL_SUPPLY = 1_200_000_000e18; + + /// @dev Role allowing accounts to mint new tokens. + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @dev Tracks the amount of tokens minted so far. + uint256 public totalMinted; + + /// @dev Mapping of addresses allowed to transfer when restrictions are active. + mapping(address account => bool allowed) public transferWhitelisted; + + /// @dev Whether transfers are currently restricted. + bool public transfersRestricted; + + /// @dev Emitted when tokens are minted as part of an allocation. + event AllocationMinted( + address indexed recipient, + uint256 amount, + string allocation + ); + + /// @dev Emitted whenever the transfer restriction flag is updated. + event TransferRestrictionUpdated(bool restricted); + + /// @dev Emitted when an address is added to or removed from the whitelist. + event TransferWhitelistUpdated(address indexed account, bool whitelisted); + + /** + * @notice Deploy the Enclave token. + * @param _owner Address that will initially own the contract and have admin rights. + */ + constructor( + address _owner + ) ERC20("Enclave", "ENCL") ERC20Permit("Enclave") Ownable(_owner) { + // Grant the deployer all admin roles. + _grantRole(DEFAULT_ADMIN_ROLE, _owner); + _grantRole(MINTER_ROLE, _owner); + + // Initialise state variables. + totalMinted = 0; + transfersRestricted = true; + transferWhitelisted[_owner] = true; + + emit TransferRestrictionUpdated(true); + emit TransferWhitelistUpdated(_owner, true); + } + + /** + * @notice Mint an allocation of tokens to a recipient. + * @dev Only accounts with the MINTER_ROLE may call this function. + * @param recipient Address to receive the minted tokens. + * @param amount Amount of tokens to mint (18 decimals). + * @param allocation Description of the allocation for off-chain bookkeeping. + */ + function mintAllocation( + address recipient, + uint256 amount, + string memory allocation + ) external onlyRole(MINTER_ROLE) { + if (recipient == address(0)) revert ZeroAddress(); + if (amount == 0) revert ZeroAmount(); + // Ensure we do not exceed the total supply. + if (totalMinted + amount > TOTAL_SUPPLY) revert ExceedsTotalSupply(); + + _mint(recipient, amount); + totalMinted += amount; + emit AllocationMinted(recipient, amount, allocation); + } + + /** + * @notice Mint multiple allocations in a batch. + * @dev Only accounts with the MINTER_ROLE may call this function. + * @param recipients Array of addresses to receive tokens. + * @param amounts Corresponding array of amounts to mint. + * @param allocations Array of allocation descriptions. + */ + function batchMintAllocations( + address[] memory recipients, + uint256[] memory amounts, + string[] memory allocations + ) external onlyRole(MINTER_ROLE) { + if ( + recipients.length != amounts.length || + amounts.length != allocations.length + ) revert ArrayLengthMismatch(); + + uint256 totalAmount = 0; + for (uint256 i = 0; i < amounts.length; i++) { + totalAmount += amounts[i]; + } + if (totalMinted + totalAmount > TOTAL_SUPPLY) + revert ExceedsTotalSupply(); + + for (uint256 i = 0; i < recipients.length; i++) { + address rec = recipients[i]; + uint256 amt = amounts[i]; + if (rec == address(0)) revert ZeroAddress(); + if (amt == 0) revert ZeroAmount(); + + _mint(rec, amt); + emit AllocationMinted(rec, amt, allocations[i]); + } + totalMinted += totalAmount; + } + + /** + * @notice Enable or disable transfer restrictions. + * @dev Only the owner can toggle this flag. + * @param restricted Whether transfers should be restricted. + */ + function setTransferRestriction(bool restricted) external onlyOwner { + transfersRestricted = restricted; + emit TransferRestrictionUpdated(restricted); + } + + /** + * @notice Add or remove an address from the transfer whitelist. + * @dev Only the owner may call this. + * @param account Address whose whitelist status is to be updated. + * @param whitelisted Whether the address should be whitelisted. + */ + function setTransferWhitelist( + address account, + bool whitelisted + ) external onlyOwner { + transferWhitelisted[account] = whitelisted; + emit TransferWhitelistUpdated(account, whitelisted); + } + + /** + * @notice Toggle an account's whitelist status. + * @dev Only the owner may call this. + * @param account Address whose whitelist status should be toggled. + */ + function toggleTransferWhitelist(address account) external onlyOwner { + bool newStatus = !transferWhitelisted[account]; + transferWhitelisted[account] = newStatus; + emit TransferWhitelistUpdated(account, newStatus); + } + + /** + * @notice Whitelist contracts that are allowed to transfer while restricted. + * @dev Convenience function for whitelisting middleware contracts. + * @param bondingManager BondingManager contract to whitelist. + * @param vestingEscrow VestingEscrow contract to whitelist. + */ + function whitelistContracts( + address bondingManager, + address vestingEscrow + ) external onlyOwner { + if (bondingManager != address(0)) { + transferWhitelisted[bondingManager] = true; + emit TransferWhitelistUpdated(bondingManager, true); + } + if (vestingEscrow != address(0)) { + transferWhitelisted[vestingEscrow] = true; + emit TransferWhitelistUpdated(vestingEscrow, true); + } + } + + /** + * @dev Override ERC20Votes update hook to enforce transfer restrictions. + */ + function _update( + address from, + address to, + uint256 value + ) internal override(ERC20, ERC20Votes) { + // When transfers are restricted, only whitelisted addresses can send or receive. + if (from != address(0) && to != address(0) && transfersRestricted) { + if (!transferWhitelisted[from] && !transferWhitelisted[to]) + revert TransferNotAllowed(); + } + super._update(from, to, value); + } + + /** + * @dev Expose ERC165 interface support via AccessControl. + */ + function supportsInterface( + bytes4 interfaceId + ) public view override(AccessControl) returns (bool) { + return super.supportsInterface(interfaceId); + } + + /** + * @dev Expose permit nonces via both ERC20Permit and OpenZeppelin Nonces. + */ + function nonces( + address owner + ) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} From 030e0ff59150a93f733e47866b92d3d2627c3e62 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 24 Sep 2025 13:18:54 +0500 Subject: [PATCH 02/88] feat: policy can ban nodes --- .../contracts/interfaces/IBondingRegistry.sol | 35 +--- .../contracts/interfaces/ISlashingManager.sol | 9 +- .../contracts/registry/BondingRegistry.sol | 136 +++++++------- .../contracts/slashing/SlashingManager.sol | 176 ++++++++---------- 4 files changed, 161 insertions(+), 195 deletions(-) diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index 2d16ce10d2..141b0ed151 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -18,6 +18,7 @@ interface IBondingRegistry { // General error ZeroAddress(); error ZeroAmount(); + error CiphernodeBanned(); error Unauthorized(); error InsufficientBalance(); error ActiveCommittee(); @@ -84,16 +85,6 @@ interface IBondingRegistry { uint256 licenseRefund ); - /** - * @notice Emitted when operator registration status changes - * @param operator Address of the operator - * @param registered True if registered, false if deregistered - */ - event OperatorRegistrationChanged( - address indexed operator, - bool registered - ); - /** * @notice Emitted when operator active status changes * @param operator Address of the operator @@ -301,19 +292,6 @@ interface IBondingRegistry { bytes32 reason ) external; - /** - * @notice Slash operator's license bond by percentage (basis points) - * @param operator Address of the operator to slash - * @param bps Percentage to slash in basis points (0-10000) - * @param reason Reason for slashing (stored in event) - * @dev Only callable by authorized slashing manager - */ - function slashLicenseBps( - address operator, - uint256 bps, - bytes32 reason - ) external; - // ====================== // Admin Functions // ====================== @@ -327,10 +305,17 @@ interface IBondingRegistry { /** * @notice Set license bond price required - * @param newlicenseRequiredBond New license bond price + * @param newLicenseRequiredBond New license bond price + * @dev Only callable by contract owner + */ + function setLicenseRequiredBond(uint256 newLicenseRequiredBond) external; + + /** + * @notice Set license active BPS + * @param newBps New license active BPS * @dev Only callable by contract owner */ - function setlicenseRequiredBond(uint256 newlicenseRequiredBond) external; + function setLicenseActiveBps(uint256 newBps) external; /** * @notice Set minimum ticket balance required for activation diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index 2242d1585d..d2271ebccc 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -22,10 +22,8 @@ interface ISlashingManager { * @notice Slashing policy configuration for different slash reasons */ struct SlashPolicy { - uint256 ticketPenalty; // Amount or BPS for ticket collateral penalty - uint256 licensePenalty; // Amount or BPS for license stake penalty - bool useTicketBps; // True if ticket penalty is in BPS, false if absolute amount - bool useLicenseBps; // True if license penalty is in BPS, false if absolute amount + uint256 ticketPenalty; // Amount for ticket collateral penalty + uint256 licensePenalty; // Amount for license stake penalty bool requiresProof; // True if slash requires verifier proof address proofVerifier; // Address of the verifier contract for proof verification bool banNode; // True if this slash type would result in banning @@ -63,6 +61,8 @@ interface ISlashingManager { error InvalidProposal(); error ProofRequired(); error InvalidProof(); + error AppealUpheld(); + error AppealPending(); error AppealWindowExpired(); error AppealWindowActive(); error AlreadyAppealed(); @@ -71,6 +71,7 @@ interface ISlashingManager { error SlashReasonNotFound(); error SlashReasonDisabled(); error CiphernodeBanned(); + error VerifierNotSet(); // ====================== // Events diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 697f257dd3..8e7ce601f6 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -26,6 +26,7 @@ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; /** * @title BondingRegistry @@ -44,7 +45,6 @@ contract BondingRegistry is // Constants // ====================== - uint256 private constant BPS_DENOMINATOR = 10_000; bytes32 private constant REASON_DEPOSIT = bytes32("DEPOSIT"); bytes32 private constant REASON_WITHDRAW = bytes32("WITHDRAW"); bytes32 private constant REASON_BOND = bytes32("BOND"); @@ -74,6 +74,7 @@ contract BondingRegistry is uint256 public licenseRequiredBond; uint256 public minTicketBalance; uint64 public exitDelay; + uint256 public licenseActiveBps = 8_000; // 80% // Operator data structure struct Operator { @@ -195,8 +196,7 @@ contract BondingRegistry is } function isLicensed(address operator) external view returns (bool) { - return - operators[operator].licenseBond >= (licenseRequiredBond * 80) / 100; + return operators[operator].licenseBond >= _minLicenseBond(); } function isRegistered(address operator) external view returns (bool) { @@ -207,7 +207,7 @@ contract BondingRegistry is Operator storage op = operators[operator]; return op.registered && - op.licenseBond >= (licenseRequiredBond * 80) / 100 && + op.licenseBond >= _minLicenseBond() && (ticketPrice == 0 || op.ticketBalance / ticketPrice >= minTicketBalance); } @@ -325,6 +325,10 @@ contract BondingRegistry is whenNotPaused noExitInProgress(msg.sender) { + require( + !ISlashingManager(slashingManager).isBanned(msg.sender), + CiphernodeBanned() + ); require(!operators[msg.sender].registered, AlreadyRegistered()); require( operators[msg.sender].licenseBond >= licenseRequiredBond, @@ -334,10 +338,10 @@ contract BondingRegistry is operators[msg.sender].registered = true; if (address(registry) != address(0)) { + // CiphernodeRegistry already emits an event when a ciphernode is added registry.addCiphernode(msg.sender); } - emit OperatorRegistrationChanged(msg.sender, true); _updateOperatorStatus(msg.sender); } @@ -363,6 +367,7 @@ contract BondingRegistry is exitDelay; if (address(registry) != address(0)) { + // CiphernodeRegistry already emits an event when a ciphernode is removed registry.removeCiphernode(msg.sender, siblingNodes); } @@ -374,19 +379,17 @@ contract BondingRegistry is } function finalizeDeregistration() external { - require(operators[msg.sender].exitRequested, ExitInProgress()); - require( - block.timestamp >= operators[msg.sender].exitUnlocksAt, - ExitNotReady() - ); + Operator storage op = operators[msg.sender]; + require(op.exitRequested, ExitInProgress()); + require(block.timestamp >= op.exitUnlocksAt, ExitNotReady()); - uint256 ticketRefund = operators[msg.sender].ticketBalance; - uint256 licenseRefund = operators[msg.sender].licenseBond; + uint256 ticketRefund = op.ticketBalance; + uint256 licenseRefund = op.licenseBond; - operators[msg.sender].ticketBalance = 0; - operators[msg.sender].licenseBond = 0; - operators[msg.sender].exitRequested = false; - operators[msg.sender].exitUnlocksAt = 0; + op.ticketBalance = 0; + op.licenseBond = 0; + op.exitRequested = false; + op.exitUnlocksAt = 0; if (ticketRefund > 0) { ticketToken.safeTransfer(msg.sender, ticketRefund); @@ -395,6 +398,18 @@ contract BondingRegistry is licenseToken.safeTransfer(msg.sender, licenseRefund); } + emit TicketBalanceUpdated( + msg.sender, + -int256(ticketRefund), + 0, + REASON_WITHDRAW + ); + emit LicenseBondUpdated( + msg.sender, + -int256(licenseRefund), + 0, + REASON_UNBOND + ); emit DeregistrationFinalized(msg.sender, ticketRefund, licenseRefund); _updateOperatorStatus(msg.sender); } @@ -410,17 +425,18 @@ contract BondingRegistry is ) external onlySlashingManager { require(amount != 0, ZeroAmount()); - uint256 currentBalance = operators[operator].ticketBalance; + Operator storage op = operators[operator]; + uint256 currentBalance = op.ticketBalance; uint256 slashAmount = Math.min(amount, currentBalance); if (slashAmount > 0) { - operators[operator].ticketBalance -= slashAmount; + op.ticketBalance -= slashAmount; slashedTicketBalance += slashAmount; emit TicketBalanceUpdated( operator, -int256(slashAmount), - operators[operator].ticketBalance, + op.ticketBalance, reason ); @@ -435,52 +451,18 @@ contract BondingRegistry is ) external onlySlashingManager { require(amount != 0, ZeroAmount()); - uint256 currentBond = operators[operator].licenseBond; + Operator storage op = operators[operator]; + uint256 currentBond = op.licenseBond; uint256 slashAmount = Math.min(amount, currentBond); if (slashAmount > 0) { - operators[operator].licenseBond -= slashAmount; - slashedLicenseBond += slashAmount; - - emit LicenseBondUpdated( - operator, - -int256(slashAmount), - operators[operator].licenseBond, - reason - ); - - if ( - operators[operator].licenseBond < - (licenseRequiredBond * 80) / 100 && - operators[operator].registered - ) { - operators[operator].registered = false; - emit OperatorRegistrationChanged(operator, false); - } - - _updateOperatorStatus(operator); - } - } - - function slashLicenseBps( - address operator, - uint256 bps, - bytes32 reason - ) external onlySlashingManager { - require(bps != 0, ZeroAmount()); - require(bps <= BPS_DENOMINATOR, InvalidAmount()); - - uint256 currentBond = operators[operator].licenseBond; - uint256 slashAmount = (currentBond * bps) / BPS_DENOMINATOR; - - if (slashAmount > 0) { - operators[operator].licenseBond -= slashAmount; + op.licenseBond -= slashAmount; slashedLicenseBond += slashAmount; emit LicenseBondUpdated( operator, -int256(slashAmount), - operators[operator].licenseBond, + op.licenseBond, reason ); @@ -501,21 +483,30 @@ contract BondingRegistry is emit ConfigurationUpdated("ticketPrice", oldValue, newTicketPrice); } - function setlicenseRequiredBond( - uint256 newlicenseRequiredBond + function setLicenseRequiredBond( + uint256 newLicenseRequiredBond ) external onlyOwner { - require(newlicenseRequiredBond != 0, InvalidConfiguration()); + require(newLicenseRequiredBond != 0, InvalidConfiguration()); uint256 oldValue = licenseRequiredBond; - licenseRequiredBond = newlicenseRequiredBond; + licenseRequiredBond = newLicenseRequiredBond; emit ConfigurationUpdated( "licenseRequiredBond", oldValue, - newlicenseRequiredBond + newLicenseRequiredBond ); } + function setLicenseActiveBps(uint256 newBps) external onlyOwner { + require(newBps > 0 && newBps <= 10_000, InvalidConfiguration()); + + uint256 oldValue = licenseActiveBps; + licenseActiveBps = newBps; + + emit ConfigurationUpdated("licenseActiveBps", oldValue, newBps); + } + function setMinTicketBalance( uint256 newMinTicketBalance ) external onlyOwner { @@ -555,8 +546,8 @@ contract BondingRegistry is uint256 ticketAmount, uint256 licenseAmount ) external onlyOwner { - if (ticketAmount > slashedTicketBalance) revert InsufficientBalance(); - if (licenseAmount > slashedLicenseBond) revert InsufficientBalance(); + require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); + require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); if (ticketAmount > 0) { slashedTicketBalance -= ticketAmount; @@ -588,19 +579,22 @@ contract BondingRegistry is // ====================== function _updateOperatorStatus(address operator) internal { - bool newActiveStatus = operators[operator].registered && - operators[operator].licenseBond >= - (licenseRequiredBond * 80) / 100 && + Operator storage op = operators[operator]; + bool newActiveStatus = op.registered && + op.licenseBond >= _minLicenseBond() && (ticketPrice == 0 || - operators[operator].ticketBalance / ticketPrice >= - minTicketBalance); + op.ticketBalance / ticketPrice >= minTicketBalance); - if (operators[operator].active != newActiveStatus) { - operators[operator].active = newActiveStatus; + if (op.active != newActiveStatus) { + op.active = newActiveStatus; emit OperatorActivationChanged(operator, newActiveStatus); } } + function _minLicenseBond() internal view returns (uint256) { + return (licenseRequiredBond * licenseActiveBps) / 10_000; + } + function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index c75a93a79b..3358afdb93 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -47,8 +47,6 @@ contract SlashingManager is bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE"); bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); - uint256 private constant BPS_DENOMINATOR = 10_000; - // ====================== // Storage // ====================== @@ -143,7 +141,7 @@ contract SlashingManager is function getSlashProposal( uint256 proposalId ) external view returns (SlashProposal memory) { - if (proposalId >= totalProposals) revert InvalidProposal(); + require(proposalId < totalProposals, InvalidProposal()); return proposals[proposalId]; } @@ -158,12 +156,20 @@ contract SlashingManager is function setSlashPolicy( bytes32 reason, SlashPolicy calldata policy - ) external onlyRole(DEFAULT_ADMIN_ROLE) { + ) external onlyRole(GOVERNANCE_ROLE) { require(reason != bytes32(0), InvalidPolicy()); - if (policy.useTicketBps && policy.ticketPenalty > BPS_DENOMINATOR) - revert InvalidPolicy(); - if (policy.useLicenseBps && policy.licensePenalty > BPS_DENOMINATOR) - revert InvalidPolicy(); + require(policy.enabled, InvalidPolicy()); + require( + policy.ticketPenalty > 0 || policy.licensePenalty > 0, + InvalidPolicy() + ); + + if (policy.requiresProof) { + require(policy.proofVerifier != address(0), VerifierNotSet()); + require(policy.appealWindow == 0, InvalidPolicy()); + } else { + require(policy.appealWindow > 0, InvalidPolicy()); + } slashPolicies[reason] = policy; emit SlashPolicyUpdated(reason, policy); @@ -215,53 +221,30 @@ contract SlashingManager is notBanned(operator) returns (uint256 proposalId) { + require(operator != address(0), ZeroAddress()); + SlashPolicy storage policy = slashPolicies[reason]; require(policy.enabled, SlashReasonDisabled()); - require(!(policy.requiresProof && proof.length == 0), ProofRequired()); - - uint256 ticketAmount = 0; - uint256 licenseAmount = 0; - - if (policy.ticketPenalty > 0) { - if (policy.useTicketBps) { - uint256 ticketBalance = bondingRegistry.getTicketBalance( - operator - ); - ticketAmount = - (ticketBalance * policy.ticketPenalty) / - BPS_DENOMINATOR; - } else { - ticketAmount = policy.ticketPenalty; - } - } - - if (policy.licensePenalty > 0) { - if (policy.useLicenseBps) { - uint256 bond = bondingRegistry.getLicenseBond(operator); - licenseAmount = - (bond * policy.licensePenalty) / - BPS_DENOMINATOR; - } else { - licenseAmount = policy.licensePenalty; - } - } - - proposalId = totalProposals++; - uint256 executableAt = block.timestamp + policy.appealWindow; + uint256 nextId = totalProposals; bool proofVerified = false; + if (policy.requiresProof) { + require(proof.length != 0, ProofRequired()); proofVerified = ISlashVerifier(policy.proofVerifier).verify( - proposalId, + nextId, proof ); + require(proofVerified, InvalidProof()); } - proposals[proposalId] = SlashProposal({ + uint256 executableAt = block.timestamp + policy.appealWindow; + + proposals[nextId] = SlashProposal({ operator: operator, reason: reason, - ticketAmount: ticketAmount, - licenseAmount: licenseAmount, + ticketAmount: policy.ticketPenalty, + licenseAmount: policy.licensePenalty, executedTicket: false, executedLicense: false, appealed: false, @@ -275,71 +258,76 @@ contract SlashingManager is }); emit SlashProposed( - proposalId, + nextId, operator, reason, - ticketAmount, - licenseAmount, + policy.ticketPenalty, + policy.licensePenalty, executableAt, msg.sender ); + + totalProposals = nextId + 1; + return nextId; } function executeSlash( uint256 proposalId ) external onlySlasher whenNotPaused nonReentrant { require(proposalId < totalProposals, InvalidProposal()); + SlashProposal storage p = proposals[proposalId]; - SlashProposal storage proposal = proposals[proposalId]; + // Has already been executed? + require(!(p.executedTicket && p.executedLicense), AlreadyExecuted()); - require( - !(proposal.appealed && !proposal.resolved), - AppealWindowActive() - ); - require(!(proposal.resolved && proposal.approved), AlreadyExecuted()); - require(block.timestamp >= proposal.executableAt, AppealWindowActive()); - require( - !(proposal.executedTicket && proposal.executedLicense), - AlreadyExecuted() - ); + SlashPolicy storage policy = slashPolicies[p.reason]; - bool ticketExecuted = proposal.executedTicket; - bool licenseExecuted = proposal.executedLicense; + if (policy.requiresProof) { + // Appeal window is 0 by policy validation, so we dont check for appeal gating + require(p.proofVerified, InvalidProof()); + } else { + // Evidence mode with appeals + require(block.timestamp >= p.executableAt, AppealWindowActive()); + if (p.appealed) { + require(p.resolved, AppealPending()); + require(!p.approved, AppealUpheld()); // approved = appeal upheld => cancel slash, maybe we return here instead + } + } - // Ticket Slash - if (!proposal.executedTicket && proposal.ticketAmount > 0) { + bool ticketExecuted = p.executedTicket; + bool licenseExecuted = p.executedLicense; + + if (!p.executedTicket && p.ticketAmount > 0) { bondingRegistry.slashTicketBalance( - proposal.operator, - proposal.ticketAmount, - proposal.reason + p.operator, + p.ticketAmount, + p.reason ); - proposal.executedTicket = true; + p.executedTicket = true; ticketExecuted = true; } - // License bond slash - if (!proposal.executedLicense && proposal.licenseAmount > 0) { + if (!p.executedLicense && p.licenseAmount > 0) { bondingRegistry.slashLicenseBond( - proposal.operator, - proposal.licenseAmount, - proposal.reason + p.operator, + p.licenseAmount, + p.reason ); - proposal.executedLicense = true; + p.executedLicense = true; licenseExecuted = true; } - SlashPolicy storage policy = slashPolicies[proposal.reason]; if (policy.banNode) { - banned[proposal.operator] = true; - emit NodeBanned(proposal.operator, proposal.reason, address(this)); + banned[p.operator] = true; + emit NodeBanned(p.operator, p.reason, address(this)); } emit SlashExecuted( proposalId, - proposal.operator, - proposal.reason, - proposal.ticketAmount, - proposal.licenseAmount, + p.operator, + p.reason, + p.ticketAmount, + p.licenseAmount, ticketExecuted, licenseExecuted ); @@ -354,20 +342,18 @@ contract SlashingManager is string calldata evidence ) external whenNotPaused { require(proposalId < totalProposals, InvalidProposal()); + SlashProposal storage p = proposals[proposalId]; - SlashProposal storage proposal = proposals[proposalId]; - require(msg.sender == proposal.operator, Unauthorized()); - require(!proposal.appealed, AlreadyAppealed()); - require(block.timestamp < proposal.executableAt, AppealWindowExpired()); + // Only the accused can appeal + require(msg.sender == p.operator, Unauthorized()); + // Only in the window + require(block.timestamp < p.executableAt, AppealWindowExpired()); + // Only once + require(!p.appealed, AlreadyAppealed()); - proposal.appealed = true; + p.appealed = true; - emit AppealFiled( - proposalId, - proposal.operator, - proposal.reason, - evidence - ); + emit AppealFiled(proposalId, p.operator, p.reason, evidence); } function resolveAppeal( @@ -376,17 +362,17 @@ contract SlashingManager is string calldata resolution ) external onlyGovernance { require(proposalId < totalProposals, InvalidProposal()); + SlashProposal storage p = proposals[proposalId]; - SlashProposal storage proposal = proposals[proposalId]; - require(proposal.appealed, InvalidProposal()); - require(!proposal.resolved, AlreadyResolved()); + require(p.appealed, InvalidProposal()); + require(!p.resolved, AlreadyResolved()); - proposal.resolved = true; - proposal.approved = approved; + p.resolved = true; + p.approved = approved; // true => cancel slash, false => slash stands emit AppealResolved( proposalId, - proposal.operator, + p.operator, approved, msg.sender, resolution From 03632d58204b8713397a4603f52fad1acea8857f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 25 Sep 2025 19:07:51 +0500 Subject: [PATCH 03/88] feat: ticket checkpoints --- .../contracts/interfaces/IBondingRegistry.sol | 1 - .../contracts/registry/BondingRegistry.sol | 60 ++++++++++--------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index 141b0ed151..9aab8b853b 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -21,7 +21,6 @@ interface IBondingRegistry { error CiphernodeBanned(); error Unauthorized(); error InsufficientBalance(); - error ActiveCommittee(); error NotLicensed(); error AlreadyRegistered(); error NotRegistered(); diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 8e7ce601f6..3a2a093384 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -23,6 +23,10 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { + Checkpoints +} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; @@ -40,6 +44,8 @@ contract BondingRegistry is IBondingRegistry { using SafeERC20 for IERC20; + using Checkpoints for Checkpoints.Trace208; + using SafeCast for uint256; // ====================== // Constants @@ -86,8 +92,13 @@ contract BondingRegistry is bool active; } + // Operator data mapping(address operator => Operator data) internal operators; + // Per-operator ticket balance checkpoints (key = block.number) + mapping(address operator => Checkpoints.Trace208 ticketCkpts) + private _ticketCkpts; + // Total slashed funds available for treasury withdrawal uint256 public slashedTicketBalance; uint256 public slashedLicenseBond; @@ -107,16 +118,6 @@ contract BondingRegistry is _; } - modifier notInActiveCommittee(address operator) { - if ( - address(registry) != address(0) && - registry.isNodeActiveInAnyCommittee(operator) - ) { - revert ActiveCommittee(); - } - _; - } - modifier noExitInProgress(address operator) { if (operators[operator].exitRequested) revert ExitInProgress(); _; @@ -184,6 +185,13 @@ contract BondingRegistry is return operators[operator].ticketBalance; } + function getTicketBalanceAtBlock( + address operator, + uint256 blockNumber + ) external view returns (uint256) { + return uint256(_ticketCkpts[operator].upperLookup(uint48(blockNumber))); + } + function getLicenseBond(address operator) external view returns (uint256) { return operators[operator].licenseBond; } @@ -232,6 +240,7 @@ contract BondingRegistry is balanceBefore; operators[msg.sender].ticketBalance += actualReceived; + _writeTicketCheckpoint(msg.sender); emit TicketBalanceUpdated( msg.sender, @@ -245,12 +254,7 @@ contract BondingRegistry is function removeTicketBalance( uint256 amount - ) - external - whenNotPaused - noExitInProgress(msg.sender) - notInActiveCommittee(msg.sender) - { + ) external whenNotPaused noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( @@ -260,6 +264,7 @@ contract BondingRegistry is operators[msg.sender].ticketBalance -= amount; ticketToken.safeTransfer(msg.sender, amount); + _writeTicketCheckpoint(msg.sender); emit TicketBalanceUpdated( msg.sender, @@ -295,12 +300,7 @@ contract BondingRegistry is function unbondLicense( uint256 amount - ) - external - whenNotPaused - noExitInProgress(msg.sender) - notInActiveCommittee(msg.sender) - { + ) external whenNotPaused noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require( operators[msg.sender].licenseBond >= amount, @@ -352,12 +352,7 @@ contract BondingRegistry is */ function deregisterOperator( uint256[] calldata siblingNodes - ) - external - whenNotPaused - noExitInProgress(msg.sender) - notInActiveCommittee(msg.sender) - { + ) external whenNotPaused noExitInProgress(msg.sender) { require(operators[msg.sender].registered, NotRegistered()); operators[msg.sender].registered = false; @@ -397,6 +392,7 @@ contract BondingRegistry is if (licenseRefund > 0) { licenseToken.safeTransfer(msg.sender, licenseRefund); } + _writeTicketCheckpoint(msg.sender); emit TicketBalanceUpdated( msg.sender, @@ -432,6 +428,7 @@ contract BondingRegistry is if (slashAmount > 0) { op.ticketBalance -= slashAmount; slashedTicketBalance += slashAmount; + _writeTicketCheckpoint(operator); emit TicketBalanceUpdated( operator, @@ -591,6 +588,13 @@ contract BondingRegistry is } } + function _writeTicketCheckpoint(address operator) internal { + _ticketCkpts[operator].push( + uint48(block.number), + operators[operator].ticketBalance.toUint208() + ); + } + function _minLicenseBond() internal view returns (uint256) { return (licenseRequiredBond * licenseActiveBps) / 10_000; } From 7677604bdaa227623d8bee387314b3d4260bce85 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 26 Sep 2025 19:05:18 +0500 Subject: [PATCH 04/88] feat: ticket usdc wrapper with votes: --- .../contracts/interfaces/IBondingRegistry.sol | 18 - .../contracts/lib/ExitQueueLib.sol | 340 ++++++++++++++++ .../contracts/registry/BondingRegistry.sol | 385 ++++++++++-------- .../registry/CiphernodeRegistryOwnable.sol | 2 +- .../contracts/slashing/SlashingManager.sol | 3 +- .../contracts/token/EnclaveTicket.sol | 207 ++++++++++ 6 files changed, 771 insertions(+), 184 deletions(-) create mode 100644 packages/enclave-contracts/contracts/lib/ExitQueueLib.sol create mode 100644 packages/enclave-contracts/contracts/token/EnclaveTicket.sol diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index 9aab8b853b..83629227d6 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -72,18 +72,6 @@ interface IBondingRegistry { uint64 unlockAt ); - /** - * @notice Emitted when operator's deregistration is finalized - * @param operator Address of the operator - * @param ticketRefund Amount of ticket balance refunded - * @param licenseRefund Amount of license bond refunded - */ - event DeregistrationFinalized( - address indexed operator, - uint256 ticketRefund, - uint256 licenseRefund - ); - /** * @notice Emitted when operator active status changes * @param operator Address of the operator @@ -255,12 +243,6 @@ interface IBondingRegistry { */ function deregisterOperator(uint256[] calldata siblingNodes) external; - /** - * @notice Finalize deregistration and withdraw all remaining funds - * @dev Can only be called after deregistration delay has passed - */ - function finalizeDeregistration() external; - // ====================== // Slashing Functions (Role-Restricted) // ====================== diff --git a/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol new file mode 100644 index 0000000000..3e59cc6ad3 --- /dev/null +++ b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +library ExitQueueLib { + struct ExitTranche { + uint64 unlockTimestamp; + uint256 ticketAmount; + uint256 licenseAmount; + } + + struct PendingAmounts { + uint256 ticketAmount; + uint256 licenseAmount; + } + + struct ExitQueueState { + mapping(address operator => ExitTranche[] operatorQueues) operatorQueues; + mapping(address operator => uint256 queueHeadIndex) queueHeadIndex; + mapping(address operator => PendingAmounts operatorPendings) pendingTotals; + } + + enum AssetType { + Ticket, + License + } + + event AssetsQueuedForExit( + address indexed operator, + uint256 ticketAmount, + uint256 licenseAmount, + uint64 unlockTimestamp + ); + + event AssetsClaimed( + address indexed operator, + uint256 ticketAmount, + uint256 licenseAmount + ); + + event PendingAssetsSlashed( + address indexed operator, + uint256 ticketAmount, + uint256 licenseAmount, + bool includedLockedAssets + ); + + error ZeroAmountNotAllowed(); + error TimestampOverflow(); + error IndexOutOfBounds(); + + function queueAssetsForExit( + ExitQueueState storage state, + address operator, + uint64 exitDelaySeconds, + uint256 ticketAmount, + uint256 licenseAmount + ) internal { + if (ticketAmount == 0 && licenseAmount == 0) { + return; + } + + uint64 currentTimestamp = uint64(block.timestamp); + require( + currentTimestamp <= (type(uint64).max - exitDelaySeconds), + TimestampOverflow() + ); + uint64 unlockTimestamp = currentTimestamp + exitDelaySeconds; + + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + + uint256 len = operatorQueue.length; + bool merged; + if (len != 0) { + ExitTranche storage lastTranche = operatorQueue[len - 1]; + if (lastTranche.unlockTimestamp == unlockTimestamp) { + if (ticketAmount != 0) lastTranche.ticketAmount += ticketAmount; + if (licenseAmount != 0) + lastTranche.licenseAmount += licenseAmount; + merged = true; + } + } + + if (!merged) { + operatorQueue.push(); + ExitTranche storage t = operatorQueue[len]; + t.unlockTimestamp = unlockTimestamp; + t.ticketAmount = ticketAmount; + t.licenseAmount = licenseAmount; + } + + _updatePendingTotals( + state, + operator, + ticketAmount, + licenseAmount, + true + ); + + emit AssetsQueuedForExit( + operator, + ticketAmount, + licenseAmount, + unlockTimestamp + ); + } + + function queueTicketsForExit( + ExitQueueState storage state, + address operator, + uint64 exitDelaySeconds, + uint256 ticketAmount + ) internal { + queueAssetsForExit(state, operator, exitDelaySeconds, ticketAmount, 0); + } + + function queueLicensesForExit( + ExitQueueState storage state, + address operator, + uint64 exitDelaySeconds, + uint256 licenseAmount + ) internal { + queueAssetsForExit(state, operator, exitDelaySeconds, 0, licenseAmount); + } + + function getPendingAmounts( + ExitQueueState storage state, + address operator + ) internal view returns (uint256 ticketAmount, uint256 licenseAmount) { + PendingAmounts storage pending = state.pendingTotals[operator]; + return (pending.ticketAmount, pending.licenseAmount); + } + + function previewClaimableAmounts( + ExitQueueState storage state, + address operator + ) internal view returns (uint256 ticketAmount, uint256 licenseAmount) { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 currentIndex = state.queueHeadIndex[operator]; + + for (uint256 i = currentIndex; i < operatorQueue.length; i++) { + ExitTranche storage tranche = operatorQueue[i]; + + if (block.timestamp < tranche.unlockTimestamp) { + break; + } + + ticketAmount += tranche.ticketAmount; + licenseAmount += tranche.licenseAmount; + } + } + + function claimAssets( + ExitQueueState storage state, + address operator, + uint256 maxTicketAmount, + uint256 maxLicenseAmount + ) internal returns (uint256 ticketsClaimed, uint256 licensesClaimed) { + if (maxTicketAmount > 0) { + ticketsClaimed = _takeAssetsFromQueue( + state, + operator, + maxTicketAmount, + AssetType.Ticket, + false + ); + if (ticketsClaimed > 0) { + state.pendingTotals[operator].ticketAmount -= ticketsClaimed; + } + } + + if (maxLicenseAmount > 0) { + licensesClaimed = _takeAssetsFromQueue( + state, + operator, + maxLicenseAmount, + AssetType.License, + false + ); + if (licensesClaimed > 0) { + state.pendingTotals[operator].licenseAmount -= licensesClaimed; + } + } + + if (ticketsClaimed > 0 || licensesClaimed > 0) { + _cleanupEmptyTranches(state, operator); + emit AssetsClaimed(operator, ticketsClaimed, licensesClaimed); + } + } + + function slashPendingAssets( + ExitQueueState storage state, + address operator, + uint256 ticketAmountToSlash, + uint256 licenseAmountToSlash, + bool includeLockedAssets + ) internal returns (uint256 ticketsSlashed, uint256 licensesSlashed) { + if (ticketAmountToSlash > 0) { + ticketsSlashed = _takeAssetsFromQueue( + state, + operator, + ticketAmountToSlash, + AssetType.Ticket, + includeLockedAssets + ); + if (ticketsSlashed > 0) { + state.pendingTotals[operator].ticketAmount -= ticketsSlashed; + } + } + + if (licenseAmountToSlash > 0) { + licensesSlashed = _takeAssetsFromQueue( + state, + operator, + licenseAmountToSlash, + AssetType.License, + includeLockedAssets + ); + if (licensesSlashed > 0) { + state.pendingTotals[operator].licenseAmount -= licensesSlashed; + } + } + + if (ticketsSlashed > 0 || licensesSlashed > 0) { + _cleanupEmptyTranches(state, operator); + emit PendingAssetsSlashed( + operator, + ticketsSlashed, + licensesSlashed, + includeLockedAssets + ); + } + } + + function _updatePendingTotals( + ExitQueueState storage state, + address operator, + uint256 ticketAmountDelta, + uint256 licenseAmountDelta, + bool isIncrease + ) private { + if ((ticketAmountDelta | licenseAmountDelta) == 0) return; + + PendingAmounts storage pending = state.pendingTotals[operator]; + + if (isIncrease) { + if (ticketAmountDelta != 0) + pending.ticketAmount += ticketAmountDelta; + if (licenseAmountDelta != 0) + pending.licenseAmount += licenseAmountDelta; + } else { + if (ticketAmountDelta != 0) + pending.ticketAmount -= ticketAmountDelta; + if (licenseAmountDelta != 0) + pending.licenseAmount -= licenseAmountDelta; + } + } + + function _cleanupEmptyTranches( + ExitQueueState storage state, + address operator + ) private { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 currentIndex = state.queueHeadIndex[operator]; + + while (currentIndex < operatorQueue.length) { + ExitTranche storage tranche = operatorQueue[currentIndex]; + if (tranche.ticketAmount == 0 && tranche.licenseAmount == 0) { + currentIndex++; + } else { + break; + } + } + + state.queueHeadIndex[operator] = currentIndex; + } + + function _takeAssetsFromQueue( + ExitQueueState storage state, + address operator, + uint256 wantedAmount, + AssetType assetType, + bool includeLockedAssets + ) private returns (uint256 takenAmount) { + if (wantedAmount == 0) { + return 0; + } + + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 currentIndex = state.queueHeadIndex[operator]; + uint256 queueLength = operatorQueue.length; + uint256 remainingWanted = wantedAmount; + + while (remainingWanted > 0 && currentIndex < queueLength) { + ExitTranche storage tranche = operatorQueue[currentIndex]; + + if ( + !includeLockedAssets && + block.timestamp < tranche.unlockTimestamp + ) { + break; + } + + uint256 availableAmount; + if (assetType == AssetType.Ticket) { + availableAmount = tranche.ticketAmount; + } else { + availableAmount = tranche.licenseAmount; + } + + if (availableAmount == 0) { + currentIndex++; + continue; + } + + uint256 amountToTake = remainingWanted < availableAmount + ? remainingWanted + : availableAmount; + + if (assetType == AssetType.Ticket) { + tranche.ticketAmount -= amountToTake; + } else { + tranche.licenseAmount -= amountToTake; + } + + remainingWanted -= amountToTake; + takenAmount += amountToTake; + + if (tranche.ticketAmount == 0 && tranche.licenseAmount == 0) { + currentIndex++; + } + } + + state.queueHeadIndex[operator] = currentIndex; + } +} diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 3a2a093384..1178b40292 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -23,14 +23,12 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; -import { - Checkpoints -} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { ExitQueueLib } from "../lib/ExitQueueLib.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; +import { EnclaveTicketToken } from "../token/EnclaveTicket.sol"; /** * @title BondingRegistry @@ -44,8 +42,7 @@ contract BondingRegistry is IBondingRegistry { using SafeERC20 for IERC20; - using Checkpoints for Checkpoints.Trace208; - using SafeCast for uint256; + using ExitQueueLib for ExitQueueLib.ExitQueueState; // ====================== // Constants @@ -60,8 +57,8 @@ contract BondingRegistry is // Storage // ====================== - /// @notice Ticket token (USDC) - IERC20 public ticketToken; + /// @notice ticket token (ETK (Underlying USDC)) + EnclaveTicketToken public ticketToken; /// @notice License token (ENCL) IERC20 public licenseToken; @@ -84,7 +81,6 @@ contract BondingRegistry is // Operator data structure struct Operator { - uint256 ticketBalance; uint256 licenseBond; uint64 exitUnlocksAt; bool registered; @@ -95,14 +91,15 @@ contract BondingRegistry is // Operator data mapping(address operator => Operator data) internal operators; - // Per-operator ticket balance checkpoints (key = block.number) - mapping(address operator => Checkpoints.Trace208 ticketCkpts) - private _ticketCkpts; - // Total slashed funds available for treasury withdrawal uint256 public slashedTicketBalance; uint256 public slashedLicenseBond; + // ====================== + // Exit Queue library state + // ====================== + ExitQueueLib.ExitQueueState private _exits; + // ====================== // Storage Gaps for Upgrades // ====================== @@ -119,7 +116,9 @@ contract BondingRegistry is } modifier noExitInProgress(address operator) { - if (operators[operator].exitRequested) revert ExitInProgress(); + Operator storage op = operators[operator]; + if (op.exitRequested && block.timestamp < op.exitUnlocksAt) + revert ExitInProgress(); _; } @@ -146,7 +145,7 @@ contract BondingRegistry is */ function initialize( address owner, - IERC20 _ticketToken, + EnclaveTicketToken _ticketToken, IERC20 _licenseToken, address _registry, address _slashedFundsTreasury, @@ -182,25 +181,37 @@ contract BondingRegistry is function getTicketBalance( address operator ) external view returns (uint256) { - return operators[operator].ticketBalance; + return ticketToken.balanceOf(operator); + } + + function getLicenseBond(address operator) external view returns (uint256) { + return operators[operator].licenseBond; + } + + function availableTickets( + address operator + ) external view returns (uint256) { + if (ticketPrice == 0) return 0; + return ticketToken.balanceOf(operator) / ticketPrice; } function getTicketBalanceAtBlock( address operator, uint256 blockNumber ) external view returns (uint256) { - return uint256(_ticketCkpts[operator].upperLookup(uint48(blockNumber))); + return ticketToken.getPastVotes(operator, blockNumber); } - function getLicenseBond(address operator) external view returns (uint256) { - return operators[operator].licenseBond; + function pendingExits( + address operator + ) external view returns (uint256 ticket, uint256 license) { + return _exits.getPendingAmounts(operator); } - function availableTickets( + function previewClaimable( address operator - ) external view returns (uint256) { - if (ticketPrice == 0) return 0; - return operators[operator].ticketBalance / ticketPrice; + ) external view returns (uint256 ticket, uint256 license) { + return _exits.previewClaimableAmounts(operator); } function isLicensed(address operator) external view returns (bool) { @@ -217,35 +228,111 @@ contract BondingRegistry is op.registered && op.licenseBond >= _minLicenseBond() && (ticketPrice == 0 || - op.ticketBalance / ticketPrice >= minTicketBalance); + ticketToken.balanceOf(operator) / ticketPrice >= + minTicketBalance); } function hasExitInProgress(address operator) external view returns (bool) { - return operators[operator].exitRequested; + Operator storage op = operators[operator]; + return op.exitRequested && block.timestamp < op.exitUnlocksAt; } // ====================== // Operator Functions // ====================== + function registerOperator() + external + whenNotPaused + noExitInProgress(msg.sender) + { + // Clear previous exit request + if (operators[msg.sender].exitRequested) { + operators[msg.sender].exitRequested = false; + operators[msg.sender].exitUnlocksAt = 0; + } + + require( + !ISlashingManager(slashingManager).isBanned(msg.sender), + CiphernodeBanned() + ); + require(!operators[msg.sender].registered, AlreadyRegistered()); + require( + operators[msg.sender].licenseBond >= licenseRequiredBond, + NotLicensed() + ); + + operators[msg.sender].registered = true; + + if (address(registry) != address(0)) { + // CiphernodeRegistry already emits an event when a ciphernode is added + registry.addCiphernode(msg.sender); + } + + _updateOperatorStatus(msg.sender); + } + + function deregisterOperator( + uint256[] calldata siblingNodes + ) external whenNotPaused noExitInProgress(msg.sender) { + Operator storage op = operators[msg.sender]; + require(op.registered, NotRegistered()); + + op.registered = false; + op.exitRequested = true; + op.exitUnlocksAt = uint64(block.timestamp) + exitDelay; + + uint256 ticketOut = ticketToken.balanceOf(msg.sender); + uint256 licenseOut = op.licenseBond; + if (ticketOut != 0) { + ticketToken.lockForExit(msg.sender, ticketOut); + emit TicketBalanceUpdated( + msg.sender, + -int256(ticketOut), + 0, + REASON_WITHDRAW + ); + } + if (licenseOut != 0) { + op.licenseBond = 0; + emit LicenseBondUpdated( + msg.sender, + -int256(licenseOut), + 0, + REASON_UNBOND + ); + } + + if (ticketOut != 0 || licenseOut != 0) { + _exits.queueAssetsForExit( + msg.sender, + exitDelay, + ticketOut, + licenseOut + ); + } + + if (address(registry) != address(0)) { + // CiphernodeRegistry already emits an event when a ciphernode is removed + registry.removeCiphernode(msg.sender, siblingNodes); + } + + emit CiphernodeDeregistrationRequested(msg.sender, op.exitUnlocksAt); + _updateOperatorStatus(msg.sender); + } + function addTicketBalance( uint256 amount ) external whenNotPaused noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); - uint256 balanceBefore = ticketToken.balanceOf(address(this)); - ticketToken.safeTransferFrom(msg.sender, address(this), amount); - uint256 actualReceived = ticketToken.balanceOf(address(this)) - - balanceBefore; - - operators[msg.sender].ticketBalance += actualReceived; - _writeTicketCheckpoint(msg.sender); + ticketToken.depositFrom(msg.sender, msg.sender, amount); emit TicketBalanceUpdated( msg.sender, - int256(actualReceived), - operators[msg.sender].ticketBalance, + int256(amount), + ticketToken.balanceOf(msg.sender), REASON_DEPOSIT ); @@ -258,18 +345,17 @@ contract BondingRegistry is require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( - operators[msg.sender].ticketBalance >= amount, + ticketToken.balanceOf(msg.sender) >= amount, InsufficientBalance() ); - operators[msg.sender].ticketBalance -= amount; - ticketToken.safeTransfer(msg.sender, amount); - _writeTicketCheckpoint(msg.sender); + ticketToken.lockForExit(msg.sender, amount); + _exits.queueTicketsForExit(msg.sender, exitDelay, amount); emit TicketBalanceUpdated( msg.sender, -int256(amount), - operators[msg.sender].ticketBalance, + ticketToken.balanceOf(msg.sender), REASON_WITHDRAW ); @@ -308,7 +394,7 @@ contract BondingRegistry is ); operators[msg.sender].licenseBond -= amount; - licenseToken.safeTransfer(msg.sender, amount); + _exits.queueLicensesForExit(msg.sender, exitDelay, amount); emit LicenseBondUpdated( msg.sender, @@ -320,94 +406,24 @@ contract BondingRegistry is _updateOperatorStatus(msg.sender); } - function registerOperator() - external - whenNotPaused - noExitInProgress(msg.sender) - { - require( - !ISlashingManager(slashingManager).isBanned(msg.sender), - CiphernodeBanned() - ); - require(!operators[msg.sender].registered, AlreadyRegistered()); - require( - operators[msg.sender].licenseBond >= licenseRequiredBond, - NotLicensed() - ); - - operators[msg.sender].registered = true; - - if (address(registry) != address(0)) { - // CiphernodeRegistry already emits an event when a ciphernode is added - registry.addCiphernode(msg.sender); - } - - _updateOperatorStatus(msg.sender); - } - - /** - * @notice Deregister as an operator and remove from IMT - * @param siblingNodes Sibling node proofs for IMT removal - * @dev Requires operator to provide sibling nodes for immediate IMT removal - */ - function deregisterOperator( - uint256[] calldata siblingNodes - ) external whenNotPaused noExitInProgress(msg.sender) { - require(operators[msg.sender].registered, NotRegistered()); - - operators[msg.sender].registered = false; - operators[msg.sender].exitRequested = true; - operators[msg.sender].exitUnlocksAt = - uint64(block.timestamp) + - exitDelay; - - if (address(registry) != address(0)) { - // CiphernodeRegistry already emits an event when a ciphernode is removed - registry.removeCiphernode(msg.sender, siblingNodes); - } + // ====================== + // Claim Functions + // ====================== - emit CiphernodeDeregistrationRequested( + function claimExits( + uint256 maxTicketAmount, + uint256 maxLicenseAmount + ) external whenNotPaused { + (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( msg.sender, - operators[msg.sender].exitUnlocksAt + maxTicketAmount, + maxLicenseAmount ); - _updateOperatorStatus(msg.sender); - } - - function finalizeDeregistration() external { - Operator storage op = operators[msg.sender]; - require(op.exitRequested, ExitInProgress()); - require(block.timestamp >= op.exitUnlocksAt, ExitNotReady()); - - uint256 ticketRefund = op.ticketBalance; - uint256 licenseRefund = op.licenseBond; - - op.ticketBalance = 0; - op.licenseBond = 0; - op.exitRequested = false; - op.exitUnlocksAt = 0; + require(ticketClaim > 0 || licenseClaim > 0, ExitNotReady()); - if (ticketRefund > 0) { - ticketToken.safeTransfer(msg.sender, ticketRefund); - } - if (licenseRefund > 0) { - licenseToken.safeTransfer(msg.sender, licenseRefund); - } - _writeTicketCheckpoint(msg.sender); - - emit TicketBalanceUpdated( - msg.sender, - -int256(ticketRefund), - 0, - REASON_WITHDRAW - ); - emit LicenseBondUpdated( - msg.sender, - -int256(licenseRefund), - 0, - REASON_UNBOND - ); - emit DeregistrationFinalized(msg.sender, ticketRefund, licenseRefund); - _updateOperatorStatus(msg.sender); + if (ticketClaim > 0) ticketToken.payout(msg.sender, ticketClaim); + if (licenseClaim > 0) + licenseToken.safeTransfer(msg.sender, licenseClaim); } // ====================== @@ -416,55 +432,102 @@ contract BondingRegistry is function slashTicketBalance( address operator, - uint256 amount, - bytes32 reason + uint256 requestedSlashAmount, + bytes32 slashReason ) external onlySlashingManager { - require(amount != 0, ZeroAmount()); + require(requestedSlashAmount != 0, ZeroAmount()); - Operator storage op = operators[operator]; - uint256 currentBalance = op.ticketBalance; - uint256 slashAmount = Math.min(amount, currentBalance); + (uint256 pendingTicketBalance, ) = _exits.getPendingAmounts(operator); + uint256 activeBalance = ticketToken.balanceOf(operator); + uint256 totalAvailableBalance = activeBalance + pendingTicketBalance; + + uint256 actualSlashAmount = Math.min( + requestedSlashAmount, + totalAvailableBalance + ); - if (slashAmount > 0) { - op.ticketBalance -= slashAmount; - slashedTicketBalance += slashAmount; - _writeTicketCheckpoint(operator); + if (actualSlashAmount == 0) { + return; + } - emit TicketBalanceUpdated( + // Slash from active balance first + uint256 slashedFromActiveBalance = Math.min( + actualSlashAmount, + activeBalance + ); + if (slashedFromActiveBalance > 0) { + ticketToken.slash(operator, slashedFromActiveBalance); + } + + // Slash remaining amount from pending queue + uint256 remainingToSlash = actualSlashAmount - slashedFromActiveBalance; + if (remainingToSlash > 0) { + _exits.slashPendingAssets( operator, - -int256(slashAmount), - op.ticketBalance, - reason + remainingToSlash, + 0, // licenseAmount + true ); - - _updateOperatorStatus(operator); } + + slashedTicketBalance += actualSlashAmount; + emit TicketBalanceUpdated( + operator, + -int256(actualSlashAmount), + ticketToken.balanceOf(operator), + slashReason + ); + + _updateOperatorStatus(operator); } function slashLicenseBond( address operator, - uint256 amount, - bytes32 reason + uint256 requestedSlashAmount, + bytes32 slashReason ) external onlySlashingManager { - require(amount != 0, ZeroAmount()); + require(requestedSlashAmount != 0, ZeroAmount()); + + Operator storage operatorData = operators[operator]; + (, uint256 pendingLicenseBalance) = _exits.getPendingAmounts(operator); + uint256 totalAvailableBalance = operatorData.licenseBond + + pendingLicenseBalance; + uint256 actualSlashAmount = Math.min( + requestedSlashAmount, + totalAvailableBalance + ); - Operator storage op = operators[operator]; - uint256 currentBond = op.licenseBond; - uint256 slashAmount = Math.min(amount, currentBond); + if (actualSlashAmount == 0) return; - if (slashAmount > 0) { - op.licenseBond -= slashAmount; - slashedLicenseBond += slashAmount; + // Slash from active balance first + uint256 slashedFromActiveBalance = Math.min( + actualSlashAmount, + operatorData.licenseBond + ); + if (slashedFromActiveBalance > 0) { + operatorData.licenseBond -= slashedFromActiveBalance; + } - emit LicenseBondUpdated( + // Slash remaining amount from pending queue + uint256 remainingToSlash = actualSlashAmount - slashedFromActiveBalance; + if (remainingToSlash > 0) { + _exits.slashPendingAssets( operator, - -int256(slashAmount), - op.licenseBond, - reason + 0, // ticketAmount + remainingToSlash, + true ); - - _updateOperatorStatus(operator); } + + slashedLicenseBond += actualSlashAmount; + emit LicenseBondUpdated( + operator, + -int256(actualSlashAmount), + operatorData.licenseBond, + slashReason + ); + + _updateOperatorStatus(operator); } // ====================== @@ -548,7 +611,7 @@ contract BondingRegistry is if (ticketAmount > 0) { slashedTicketBalance -= ticketAmount; - ticketToken.safeTransfer(slashedFundsTreasury, ticketAmount); + ticketToken.payout(slashedFundsTreasury, ticketAmount); } if (licenseAmount > 0) { @@ -580,7 +643,8 @@ contract BondingRegistry is bool newActiveStatus = op.registered && op.licenseBond >= _minLicenseBond() && (ticketPrice == 0 || - op.ticketBalance / ticketPrice >= minTicketBalance); + ticketToken.balanceOf(operator) / ticketPrice >= + minTicketBalance); if (op.active != newActiveStatus) { op.active = newActiveStatus; @@ -588,13 +652,6 @@ contract BondingRegistry is } } - function _writeTicketCheckpoint(address operator) internal { - _ticketCkpts[operator].push( - uint48(block.number), - operators[operator].ticketBalance.toUint208() - ); - } - function _minLicenseBond() internal view returns (uint256) { return (licenseRequiredBond * licenseActiveBps) / 10_000; } diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index d080bbf9b6..4ef0b20dd0 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -43,7 +43,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; // Committee tracking for active job management - mapping(uint256 e3Id => bool) public committeeActive; + mapping(uint256 e3Id => bool active) public committeeActive; mapping(address node => uint256 count) public activeCommitteeCount; //////////////////////////////////////////////////////////// diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 3358afdb93..f67bb3215e 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -290,7 +290,7 @@ contract SlashingManager is require(block.timestamp >= p.executableAt, AppealWindowActive()); if (p.appealed) { require(p.resolved, AppealPending()); - require(!p.approved, AppealUpheld()); // approved = appeal upheld => cancel slash, maybe we return here instead + require(!p.approved, AppealUpheld()); // approved = appeal upheld => cancel slash, return? } } @@ -409,6 +409,7 @@ contract SlashingManager is // Internal Functions // ====================== + // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade( address newImplementation ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicket.sol b/packages/enclave-contracts/contracts/token/EnclaveTicket.sol new file mode 100644 index 0000000000..f111d3c962 --- /dev/null +++ b/packages/enclave-contracts/contracts/token/EnclaveTicket.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { + ERC20Wrapper +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; +import { + ERC20Permit +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { + ERC20Votes +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; + +/** + * @title EnclaveTicketToken (ETK) + * @notice Non-transferable ERC20Votes wrapper over USDC for operator staking + * @dev Features: + * - Only BondingRegistry can deposit/withdraw + * - Auto self-delegation on first mint for voting + * - Slashing burns shares and sends USDC to treasury + * - Non-transferable between users + */ +contract EnclaveTicketToken is + ERC20, + ERC20Permit, + ERC20Votes, + Ownable, + ERC20Wrapper +{ + address public registry; + address public slashedFundsTreasury; + + error NotRegistry(); + error TransferNotAllowed(); + error ZeroAddress(); + error DelegationLocked(); + + modifier onlyRegistry() { + if (msg.sender != registry) revert NotRegistry(); + _; + } + + constructor( + IERC20 underlyingUSDC, + address registry_, + address treasury_, + address initialOwner_ + ) + ERC20("Enclave Ticket Token", "ETK") + ERC20Permit("Enclave Ticket Token") + ERC20Wrapper(underlyingUSDC) + Ownable(initialOwner_) + { + require(registry_ != address(0), ZeroAddress()); + require(treasury_ != address(0), ZeroAddress()); + registry = registry_; + slashedFundsTreasury = treasury_; + } + + function setRegistry(address newRegistry) external onlyOwner { + require(newRegistry != address(0), ZeroAddress()); + registry = newRegistry; + } + + function setTreasury(address newTreasury) external onlyOwner { + require(newTreasury != address(0), ZeroAddress()); + slashedFundsTreasury = newTreasury; + } + + /** + * @notice Deposit USDC and mint ticket tokens to operator + * @param operator Address to receive the ticket tokens + * @param amount Amount of USDC to wrap + * @return success True if successful + */ + function depositFor( + address operator, + uint256 amount + ) public override onlyRegistry returns (bool success) { + success = super.depositFor(operator, amount); + + // Auto-delegate on first deposit to track voting power + if (delegates(operator) == address(0)) { + _delegate(operator, operator); + } + } + + /** + * @notice Deposit USDC from an account and mint ticket tokens to an account + * @param from Address to deposit from + * @param to Address to mint to + * @param amount Amount of USDC to deposit + * @return success True if successful + */ + function depositFrom( + address from, + address to, + uint256 amount + ) external onlyRegistry returns (bool) { + IERC20(address(underlying())).transferFrom(from, address(this), amount); + _mint(to, amount); + if (delegates(to) == address(0)) _delegate(to, to); + return true; + } + + /** + * @notice Burn ticket tokens and transfer USDC to receiver + * @dev Registry must have approval or use permit before calling + * @param receiver Address to receive the USDC + * @param amount Amount of ticket tokens to burn + * @return success True if successful + */ + function withdrawTo( + address receiver, + uint256 amount + ) public override onlyRegistry returns (bool success) { + return super.withdrawTo(receiver, amount); + } + + /** + * @notice Lock ticket tokens for exit + * @param operator Address to lock from + * @param amount Amount of ticket tokens to lock + */ + function lockForExit( + address operator, + uint256 amount + ) external onlyRegistry { + _burn(operator, amount); + } + + /** + * @notice Payout ticket tokens to an address + * @param to Address to payout to + * @param amount Amount of ticket tokens to payout + */ + function payout(address to, uint256 amount) external onlyRegistry { + IERC20(address(underlying())).transfer(to, amount); + } + + /** + * @notice Slash ticket tokens by burning shares and transferring USDC to treasury + * @param operator Operator to slash + * @param amount Amount to slash + */ + function slash(address operator, uint256 amount) external onlyRegistry { + _burn(operator, amount); + } + + /** + * @notice Prevent transfers between users (only mint/burn allowed) + */ + function _update( + address from, + address to, + uint256 value + ) internal override(ERC20, ERC20Votes) { + if (from != address(0) && to != address(0)) { + revert TransferNotAllowed(); + } + super._update(from, to, value); + } + + /** + * @notice Delegate voting power to an address. + * @dev This function is locked and cannot be used. + */ + + function delegate(address) public pure override { + revert DelegationLocked(); + } + + /** + * @notice Delegate voting power to an address using a signature. + * @dev This function is locked and cannot be used. + */ + + function delegateBySig( + address, + uint256, + uint256, + uint8, + bytes32, + bytes32 + ) public pure override { + revert DelegationLocked(); + } + + function decimals() + public + view + override(ERC20, ERC20Wrapper) + returns (uint8) + { + return super.decimals(); + } + + function nonces( + address owner + ) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} From f777154e034bceb8bbb847e746c2d835352358a1 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 26 Sep 2025 23:14:19 +0500 Subject: [PATCH 05/88] feat: rewards distribution --- .../enclave-contracts/contracts/Enclave.sol | 82 ++++++++++++++++++- .../contracts/interfaces/IBondingRegistry.sol | 54 +++++++++--- .../contracts/interfaces/IEnclave.sol | 18 +++- .../contracts/registry/BondingRegistry.sol | 38 ++++++++- .../contracts/test/MockStableToken.sol | 28 +++++++ .../contracts/token/EnclaveTicket.sol | 39 ++------- 6 files changed, 212 insertions(+), 47 deletions(-) create mode 100644 packages/enclave-contracts/contracts/test/MockStableToken.sol diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 2bc492bba7..196828dda5 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -8,10 +8,16 @@ pragma solidity >=0.8.27; import { IEnclave, E3, IE3Program } from "./interfaces/IEnclave.sol"; import { IInputValidator } from "./interfaces/IInputValidator.sol"; import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; +import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; +import { IRegistryFilter } from "./interfaces/IRegistryFilter.sol"; import { IDecryptionVerifier } from "./interfaces/IDecryptionVerifier.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { InternalLeanIMT, LeanIMTData, @@ -20,6 +26,7 @@ import { contract Enclave is IEnclave, OwnableUpgradeable { using InternalLeanIMT for LeanIMTData; + using SafeERC20 for IERC20; //////////////////////////////////////////////////////////// // // @@ -28,6 +35,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { //////////////////////////////////////////////////////////// ICiphernodeRegistry public ciphernodeRegistry; // address of the Ciphernode registry. + IBondingRegistry public bondingRegistry; // address of the Bonding registry. + IERC20 public usdcToken; // address of the USDC token. uint256 public maxDuration; // maximum duration of a computation in seconds. uint256 public nexte3Id; // ID of the next E3. @@ -50,6 +59,9 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// Mapping that stores the valid E3 program ABI encoded parameter sets (e.g., BFV). mapping(bytes e3ProgramParams => bool allowed) public e3ProgramsParams; + // Mapping of E3 payments. + mapping(uint256 e3Id => uint256 e3Payment) public e3Payments; + //////////////////////////////////////////////////////////// // // // Errors // @@ -79,6 +91,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { error CiphertextOutputNotPublished(uint256 e3Id); error PaymentRequired(uint256 value); error PlaintextOutputAlreadyPublished(uint256 e3Id); + error InsufficientBalance(); + error InsufficientAllowance(); //////////////////////////////////////////////////////////// // // @@ -92,12 +106,16 @@ contract Enclave is IEnclave, OwnableUpgradeable { constructor( address _owner, ICiphernodeRegistry _ciphernodeRegistry, + IBondingRegistry _bondingRegistry, + IERC20 _usdcToken, uint256 _maxDuration, bytes[] memory _e3ProgramsParams ) { initialize( _owner, _ciphernodeRegistry, + _bondingRegistry, + _usdcToken, _maxDuration, _e3ProgramsParams ); @@ -110,12 +128,16 @@ contract Enclave is IEnclave, OwnableUpgradeable { function initialize( address _owner, ICiphernodeRegistry _ciphernodeRegistry, + IBondingRegistry _bondingRegistry, + IERC20 _usdcToken, uint256 _maxDuration, bytes[] memory _e3ProgramsParams ) public initializer { __Ownable_init(msg.sender); setMaxDuration(_maxDuration); setCiphernodeRegistry(_ciphernodeRegistry); + setBondingRegistry(_bondingRegistry); + setUsdcToken(_usdcToken); setE3ProgramsParams(_e3ProgramsParams); if (_owner != owner()) transferOwnership(_owner); } @@ -129,9 +151,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { function request( E3RequestParams calldata requestParams ) external payable returns (uint256 e3Id, E3 memory e3) { - // TODO: allow for other payment methods or only native tokens? - // TODO: should payment checks be somewhere else? Perhaps in the E3 Program or ciphernode registry? - require(msg.value > 0, PaymentRequired(msg.value)); + uint256 e3Fee = getE3Quote(requestParams); + require(e3Fee > 0, PaymentRequired(e3Fee)); require( requestParams.threshold[1] >= requestParams.threshold[0] && requestParams.threshold[0] > 0, @@ -198,6 +219,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { e3s[e3Id] = e3; + usdcToken.safeTransferFrom(msg.sender, address(this), e3Fee); + require( ciphernodeRegistry.requestCommittee( e3Id, @@ -324,9 +347,40 @@ contract Enclave is IEnclave, OwnableUpgradeable { ); require(success, InvalidOutput(plaintextOutput)); + _distributeRewards(e3Id); + emit PlaintextOutputPublished(e3Id, plaintextOutput); } + //////////////////////////////////////////////////////////// + // // + // Internal Functions // + // // + //////////////////////////////////////////////////////////// + + function _distributeRewards(uint256 e3Id) internal { + IRegistryFilter.Committee memory committee = ciphernodeRegistry + .getCommittee(e3Id); + + uint256[] memory amounts = new uint256[](committee.nodes.length); + + // We might need to pay different amounts to different nodes. + // For now, we'll pay the same amount to all nodes. + uint256 amount = e3Payments[e3Id] / committee.nodes.length; + for (uint256 i = 0; i < committee.nodes.length; i++) { + amounts[i] = amount; + } + + // Approve the BondingRegistry to spend the USDC tokens + usdcToken.approve(address(bondingRegistry), e3Payments[e3Id]); + // Distribute rewards to the committee + bondingRegistry.distributeRewards(usdcToken, committee.nodes, amounts); + // Zero out the payment + e3Payments[e3Id] = 0; + // Where does dust go? Treasury maybe? + usdcToken.approve(address(bondingRegistry), 0); + } + //////////////////////////////////////////////////////////// // // // Set Functions // @@ -354,6 +408,22 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit CiphernodeRegistrySet(address(_ciphernodeRegistry)); } + function setBondingRegistry( + IBondingRegistry _bondingRegistry + ) public onlyOwner returns (bool success) { + bondingRegistry = _bondingRegistry; + success = true; + emit BondingRegistrySet(address(_bondingRegistry)); + } + + function setUsdcToken( + IERC20 _usdcToken + ) public onlyOwner returns (bool success) { + usdcToken = _usdcToken; + success = true; + emit UsdcTokenSet(address(_usdcToken)); + } + function enableE3Program( IE3Program e3Program ) public onlyOwner returns (bool success) { @@ -437,6 +507,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { return InternalLeanIMT._root(inputs[e3Id]); } + function getE3Quote( + E3RequestParams calldata + ) public pure returns (uint256 fee) { + fee = 1 * 10 ** 18; + } + function getDecryptionVerifier( bytes32 encryptionSchemeId ) public view returns (IDecryptionVerifier) { diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index 83629227d6..be6fef9fb3 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -6,6 +6,8 @@ pragma solidity >=0.8.27; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + /** * @title IBondingRegistry * @notice Interface for the main bonding registry that holds operator balance and license bonds @@ -29,6 +31,8 @@ interface IBondingRegistry { error InvalidAmount(); error InvalidConfiguration(); error NoPendingDeregistration(); + error OnlyRewardDistributor(); + error ArrayLengthMismatch(); // ====================== // Events (Protocol-Named) @@ -202,6 +206,19 @@ interface IBondingRegistry { // Operator Functions // ====================== + /** + * @notice Register as an operator (callable by licensed operators) + * @dev Requires sufficient license bond and calls registry + */ + function registerOperator() external; + + /** + * @notice Deregister as an operator and remove from IMT + * @param siblingNodes Sibling node proofs for IMT removal + * @dev Requires operator to provide sibling nodes for immediate IMT removal + */ + function deregisterOperator(uint256[] calldata siblingNodes) external; + /** * @notice Increase operator's ticket balance by depositing tokens * @param amount Amount of ticket tokens to deposit @@ -230,21 +247,22 @@ interface IBondingRegistry { */ function unbondLicense(uint256 amount) external; - /** - * @notice Register as an operator (callable by licensed operators) - * @dev Requires sufficient license bond and calls registry - */ - function registerOperator() external; + // ====================== + // Claim Functions + // ====================== /** - * @notice Deregister as an operator and remove from IMT - * @param siblingNodes Sibling node proofs for IMT removal - * @dev Requires operator to provide sibling nodes for immediate IMT removal + * @notice Claim operator's ticket balance and license bond + * @param maxTicketAmount Maximum amount of ticket tokens to claim + * @param maxLicenseAmount Maximum amount of license tokens to claim */ - function deregisterOperator(uint256[] calldata siblingNodes) external; + function claimExits( + uint256 maxTicketAmount, + uint256 maxLicenseAmount + ) external; // ====================== - // Slashing Functions (Role-Restricted) + // Slashing Functions // ====================== /** @@ -273,6 +291,22 @@ interface IBondingRegistry { bytes32 reason ) external; + // ====================== + // Reward Distribution Functions + // ====================== + /** + * @notice Distribute rewards to operators + * @param rewardToken Reward token contract + * @param operators Addresses of the operators to distribute rewards to + * @param amounts Amounts of rewards to distribute to each operator + * @dev Only callable by contract owner + */ + function distributeRewards( + IERC20 rewardToken, + address[] calldata operators, + uint256[] calldata amounts + ) external; + // ====================== // Admin Functions // ====================== diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 7f3feeb8ff..5870d895a9 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -70,6 +70,14 @@ interface IEnclave { /// @param ciphernodeRegistry The address of the CiphernodeRegistry contract. event CiphernodeRegistrySet(address ciphernodeRegistry); + /// @notice This event MUST be emitted any time the BondingRegistry is set. + /// @param bondingRegistry The address of the BondingRegistry contract. + event BondingRegistrySet(address bondingRegistry); + + /// @notice This event MUST be emitted any time the USDC token is set. + /// @param usdcToken The address of the USDC token. + event UsdcTokenSet(address usdcToken); + /// @notice The event MUST be emitted any time an encryption scheme is enabled. /// @param encryptionSchemeId The ID of the encryption scheme that was enabled. event EncryptionSchemeEnabled(bytes32 encryptionSchemeId); @@ -125,7 +133,7 @@ interface IEnclave { /// @return e3Id ID of the E3. /// @return e3 The E3 struct. function request( - E3RequestParams memory requestParams + E3RequestParams calldata requestParams ) external payable returns (uint256 e3Id, E3 memory e3); /// @notice This function should be called to activate an Encrypted Execution Environment (E3) once it has been @@ -220,4 +228,12 @@ interface IEnclave { /// @param e3Id ID of the E3. /// @return root The root of the input merkle tree. function getInputRoot(uint256 e3Id) external view returns (uint256 root); + + /// @notice This function returns the fee of an E3 + /// @dev This function MUST revert if the E3 parameters are invalid. + /// @param e3Params the struct representing the E3 request parameters + /// @return fee the fee of the E3 + function getE3Quote( + E3RequestParams memory e3Params + ) external view returns (uint256 fee); } diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 1178b40292..abe8687f00 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -69,6 +69,9 @@ contract BondingRegistry is /// @notice Authorized slashing manager address public slashingManager; + /// @notice Authorized reward distributor + address public rewardDistributor; + /// @notice Treasury address for slashed funds address public slashedFundsTreasury; @@ -285,7 +288,7 @@ contract BondingRegistry is uint256 ticketOut = ticketToken.balanceOf(msg.sender); uint256 licenseOut = op.licenseBond; if (ticketOut != 0) { - ticketToken.lockForExit(msg.sender, ticketOut); + ticketToken.burnTickets(msg.sender, ticketOut); emit TicketBalanceUpdated( msg.sender, -int256(ticketOut), @@ -349,7 +352,7 @@ contract BondingRegistry is InsufficientBalance() ); - ticketToken.lockForExit(msg.sender, amount); + ticketToken.burnTickets(msg.sender, amount); _exits.queueTicketsForExit(msg.sender, exitDelay, amount); emit TicketBalanceUpdated( @@ -456,7 +459,7 @@ contract BondingRegistry is activeBalance ); if (slashedFromActiveBalance > 0) { - ticketToken.slash(operator, slashedFromActiveBalance); + ticketToken.burnTickets(operator, slashedFromActiveBalance); } // Slash remaining amount from pending queue @@ -530,6 +533,29 @@ contract BondingRegistry is _updateOperatorStatus(operator); } + // ====================== + // Reward Distribution Functions + // ====================== + + function distributeRewards( + IERC20 rewardToken, + address[] calldata recipients, + uint256[] calldata amounts + ) external { + require(msg.sender == rewardDistributor, OnlyRewardDistributor()); + require(recipients.length == amounts.length, ArrayLengthMismatch()); + + for (uint256 i = 0; i < recipients.length; i++) { + if (amounts[i] > 0 && operators[recipients[i]].registered) { + rewardToken.safeTransferFrom( + rewardDistributor, + recipients[i], + amounts[i] + ); + } + } + } + // ====================== // Admin Functions // ====================== @@ -602,6 +628,12 @@ contract BondingRegistry is slashingManager = newSlashingManager; } + function setRewardDistributor( + address newRewardDistributor + ) external onlyOwner { + rewardDistributor = newRewardDistributor; + } + function withdrawSlashedFunds( uint256 ticketAmount, uint256 licenseAmount diff --git a/packages/enclave-contracts/contracts/test/MockStableToken.sol b/packages/enclave-contracts/contracts/test/MockStableToken.sol new file mode 100644 index 0000000000..0f8a8614ed --- /dev/null +++ b/packages/enclave-contracts/contracts/test/MockStableToken.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +pragma solidity >=0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +contract TestUSDC is ERC20, Ownable { + uint8 private _decimals; + + constructor( + uint256 initialSupply + ) ERC20("USD Coin", "USDC") Ownable(msg.sender) { + _decimals = 6; + _mint(msg.sender, initialSupply * 10 ** _decimals); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } +} diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicket.sol b/packages/enclave-contracts/contracts/token/EnclaveTicket.sol index f111d3c962..50187fbcae 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveTicket.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveTicket.sol @@ -1,4 +1,8 @@ // SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -17,12 +21,7 @@ import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; /** * @title EnclaveTicketToken (ETK) - * @notice Non-transferable ERC20Votes wrapper over USDC for operator staking - * @dev Features: - * - Only BondingRegistry can deposit/withdraw - * - Auto self-delegation on first mint for voting - * - Slashing burns shares and sends USDC to treasury - * - Non-transferable between users + * @notice Non-transferable non-delegatable ERC20Votes wrapper over USDC for operator staking */ contract EnclaveTicketToken is ERC20, @@ -32,7 +31,6 @@ contract EnclaveTicketToken is ERC20Wrapper { address public registry; - address public slashedFundsTreasury; error NotRegistry(); error TransferNotAllowed(); @@ -47,7 +45,6 @@ contract EnclaveTicketToken is constructor( IERC20 underlyingUSDC, address registry_, - address treasury_, address initialOwner_ ) ERC20("Enclave Ticket Token", "ETK") @@ -56,9 +53,7 @@ contract EnclaveTicketToken is Ownable(initialOwner_) { require(registry_ != address(0), ZeroAddress()); - require(treasury_ != address(0), ZeroAddress()); registry = registry_; - slashedFundsTreasury = treasury_; } function setRegistry(address newRegistry) external onlyOwner { @@ -66,11 +61,6 @@ contract EnclaveTicketToken is registry = newRegistry; } - function setTreasury(address newTreasury) external onlyOwner { - require(newTreasury != address(0), ZeroAddress()); - slashedFundsTreasury = newTreasury; - } - /** * @notice Deposit USDC and mint ticket tokens to operator * @param operator Address to receive the ticket tokens @@ -122,11 +112,11 @@ contract EnclaveTicketToken is } /** - * @notice Lock ticket tokens for exit - * @param operator Address to lock from - * @param amount Amount of ticket tokens to lock + * @notice Burn ticket tokens + * @param operator Address to burn from + * @param amount Amount of ticket tokens to burn */ - function lockForExit( + function burnTickets( address operator, uint256 amount ) external onlyRegistry { @@ -142,15 +132,6 @@ contract EnclaveTicketToken is IERC20(address(underlying())).transfer(to, amount); } - /** - * @notice Slash ticket tokens by burning shares and transferring USDC to treasury - * @param operator Operator to slash - * @param amount Amount to slash - */ - function slash(address operator, uint256 amount) external onlyRegistry { - _burn(operator, amount); - } - /** * @notice Prevent transfers between users (only mint/burn allowed) */ @@ -169,7 +150,6 @@ contract EnclaveTicketToken is * @notice Delegate voting power to an address. * @dev This function is locked and cannot be used. */ - function delegate(address) public pure override { revert DelegationLocked(); } @@ -178,7 +158,6 @@ contract EnclaveTicketToken is * @notice Delegate voting power to an address using a signature. * @dev This function is locked and cannot be used. */ - function delegateBySig( address, uint256, From 4acd3a45ce5055201ab91978915bf555e078b37e Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 29 Sep 2025 12:16:10 +0500 Subject: [PATCH 06/88] feat: add ignition modules --- .../enclave-contracts/contracts/Enclave.sol | 1 + .../contracts/interfaces/IBondingRegistry.sol | 30 +-- .../interfaces/ICiphernodeRegistry.sol | 37 ---- .../contracts/interfaces/IEnclave.sol | 2 +- .../contracts/interfaces/ISlashingManager.sol | 10 - .../contracts/registry/BondingRegistry.sol | 182 +++++++----------- .../registry/CiphernodeRegistryOwnable.sol | 66 ------- .../contracts/slashing/SlashingManager.sol | 88 +-------- .../contracts/test/MockCiphernodeRegistry.sol | 47 +++++ .../contracts/test/MockStableToken.sol | 2 +- ...claveTicket.sol => EnclaveTicketToken.sol} | 0 .../ignition/modules/bondingRegistry.ts | 34 ++++ .../ignition/modules/enclave.ts | 4 +- .../ignition/modules/enclaveTicketToken.ts | 22 +++ .../ignition/modules/enclaveToken.ts | 16 ++ .../ignition/modules/mockSlashingVerifier.ts | 14 ++ .../ignition/modules/mockStableToken.ts | 16 ++ .../ignition/modules/slashingManager.ts | 20 ++ 18 files changed, 273 insertions(+), 318 deletions(-) rename packages/enclave-contracts/contracts/token/{EnclaveTicket.sol => EnclaveTicketToken.sol} (100%) create mode 100644 packages/enclave-contracts/ignition/modules/bondingRegistry.ts create mode 100644 packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts create mode 100644 packages/enclave-contracts/ignition/modules/enclaveToken.ts create mode 100644 packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts create mode 100644 packages/enclave-contracts/ignition/modules/mockStableToken.ts create mode 100644 packages/enclave-contracts/ignition/modules/slashingManager.ts diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 196828dda5..c2ef016c30 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -218,6 +218,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { e3.decryptionVerifier = decryptionVerifier; e3s[e3Id] = e3; + e3Payments[e3Id] = e3Fee; usdcToken.safeTransferFrom(msg.sender, address(this), e3Fee); diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index be6fef9fb3..c0816b87fa 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -7,6 +7,8 @@ pragma solidity >=0.8.27; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ICiphernodeRegistry } from "./ICiphernodeRegistry.sol"; +import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; /** * @title IBondingRegistry @@ -346,6 +348,20 @@ interface IBondingRegistry { */ function setExitDelay(uint64 newExitDelay) external; + /** + * @notice Set ticket token + * @param newTicketToken New ticket token + * @dev Only callable by contract owner + */ + function setTicketToken(EnclaveTicketToken newTicketToken) external; + + /** + * @notice Set license token + * @param newLicenseToken New license token + * @dev Only callable by contract owner + */ + function setLicenseToken(IERC20 newLicenseToken) external; + /** * @notice Set slashed funds treasury address * @param newSlashedFundsTreasury New slashed funds treasury address @@ -358,7 +374,7 @@ interface IBondingRegistry { * @param newRegistry New registry contract address * @dev Only callable by contract owner */ - function setRegistry(address newRegistry) external; + function setRegistry(ICiphernodeRegistry newRegistry) external; /** * @notice Set slashing manager address @@ -377,16 +393,4 @@ interface IBondingRegistry { uint256 ticketAmount, uint256 licenseAmount ) external; - - /** - * @notice Emergency pause the contract - * @dev Only callable by contract owner - */ - function pause() external; - - /** - * @notice Unpause the contract - * @dev Only callable by contract owner - */ - function unpause() external; } diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 5fff38159a..f44f45abf8 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -119,41 +119,4 @@ interface ICiphernodeRegistry { function getCommittee( uint256 e3Id ) external view returns (IRegistryFilter.Committee memory committee); - - /// @notice Mark a committee as active when a job starts - /// @param e3Id ID of the E3 committee - /// @param members Array of committee member addresses - /// @dev Should be called by authorized entities when job execution begins - function markCommitteeActive( - uint256 e3Id, - address[] calldata members - ) external; - - /// @notice Mark a committee as completed when a job ends - /// @param e3Id ID of the E3 committee - /// @param members Array of committee member addresses - /// @dev Should be called by authorized entities when job execution completes - function markCommitteeCompleted( - uint256 e3Id, - address[] calldata members - ) external; - - /// @notice Check if a node is active in any committee - /// @param node Address of the node to check - /// @return True if the node is currently active in at least one committee - function isNodeActiveInAnyCommittee( - address node - ) external view returns (bool); - - /// @notice Get the number of active committees a node is in - /// @param node Address of the node to check - /// @return Number of active committees the node is participating in - function activeCommitteeCountOf( - address node - ) external view returns (uint256); - - /// @notice Check if a specific committee is active - /// @param e3Id ID of the E3 committee to check - /// @return True if the committee is currently active - function isCommitteeActive(uint256 e3Id) external view returns (bool); } diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 5870d895a9..1982780e78 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -234,6 +234,6 @@ interface IEnclave { /// @param e3Params the struct representing the E3 request parameters /// @return fee the fee of the E3 function getE3Quote( - E3RequestParams memory e3Params + E3RequestParams calldata e3Params ) external view returns (uint256 fee); } diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index d2271ebccc..69b9592926 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -282,14 +282,4 @@ interface ISlashingManager { * @param node Address to unban */ function unbanNode(address node) external; - - /** - * @notice Emergency pause slashing operations - */ - function pause() external; - - /** - * @notice Unpause slashing operations - */ - function unpause() external; } diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index abe8687f00..81296dd57a 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -6,18 +6,9 @@ pragma solidity >=0.8.27; -import { - UUPSUpgradeable -} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - Initializable -} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - PausableUpgradeable -} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 @@ -28,19 +19,13 @@ import { ExitQueueLib } from "../lib/ExitQueueLib.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; -import { EnclaveTicketToken } from "../token/EnclaveTicket.sol"; +import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; /** * @title BondingRegistry * @notice Main registry for operator balance and license bonds */ -contract BondingRegistry is - Initializable, - UUPSUpgradeable, - OwnableUpgradeable, - PausableUpgradeable, - IBondingRegistry -{ +contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { using SafeERC20 for IERC20; using ExitQueueLib for ExitQueueLib.ExitQueueState; @@ -103,12 +88,6 @@ contract BondingRegistry is // ====================== ExitQueueLib.ExitQueueState private _exits; - // ====================== - // Storage Gaps for Upgrades - // ====================== - - uint256[50] private __gap; - // ====================== // Modifiers // ====================== @@ -125,56 +104,57 @@ contract BondingRegistry is _; } - // ====================== - // Initialization - // ====================== + //////////////////////////////////////////////////////////// + // // + // Initialization // + // // + //////////////////////////////////////////////////////////// + + constructor( + address _owner, + EnclaveTicketToken _ticketToken, + IERC20 _licenseToken, + ICiphernodeRegistry _registry, + address _slashedFundsTreasury, + uint256 _ticketPrice, + uint256 _licenseRequiredBond, + uint256 _minTicketBalance, + uint64 _exitDelay + ) { + initialize( + _owner, + _ticketToken, + _licenseToken, + _registry, + _slashedFundsTreasury, + _ticketPrice, + _licenseRequiredBond, + _minTicketBalance, + _exitDelay + ); + } - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /** - * @notice Initialize the contract - * @param owner Contract owner - * @param _ticketToken Ticket token contract - * @param _licenseToken License token contract - * @param _registry Registry contract - * @param _slashedFundsTreasury Slashed funds treasury address - * @param _ticketPrice Initial ticket price - * @param _licenseRequiredBond Initial license bond price - * @param _minTicketBalance Initial minimum ticket balance for activation - * @param _exitDelay Initial exit delay period - */ function initialize( - address owner, + address _owner, EnclaveTicketToken _ticketToken, IERC20 _licenseToken, - address _registry, + ICiphernodeRegistry _registry, address _slashedFundsTreasury, uint256 _ticketPrice, uint256 _licenseRequiredBond, uint256 _minTicketBalance, uint64 _exitDelay - ) external initializer { - __Ownable_init(owner); - __Pausable_init(); - __UUPSUpgradeable_init(); - - require(address(_ticketToken) != address(0), ZeroAddress()); - require(address(_licenseToken) != address(0), ZeroAddress()); - require(_slashedFundsTreasury != address(0), ZeroAddress()); - require(_ticketPrice != 0, InvalidConfiguration()); - require(_licenseRequiredBond != 0, InvalidConfiguration()); - - ticketToken = _ticketToken; - licenseToken = _licenseToken; - registry = ICiphernodeRegistry(_registry); - slashedFundsTreasury = _slashedFundsTreasury; - ticketPrice = _ticketPrice; - licenseRequiredBond = _licenseRequiredBond; - minTicketBalance = _minTicketBalance; - exitDelay = _exitDelay; + ) public initializer { + __Ownable_init(msg.sender); + setTicketToken(_ticketToken); + setLicenseToken(_licenseToken); + setRegistry(_registry); + setSlashedFundsTreasury(_slashedFundsTreasury); + setTicketPrice(_ticketPrice); + setLicenseRequiredBond(_licenseRequiredBond); + setMinTicketBalance(_minTicketBalance); + setExitDelay(_exitDelay); + if (_owner != owner()) transferOwnership(_owner); } // ====================== @@ -226,13 +206,7 @@ contract BondingRegistry is } function isActive(address operator) external view returns (bool) { - Operator storage op = operators[operator]; - return - op.registered && - op.licenseBond >= _minLicenseBond() && - (ticketPrice == 0 || - ticketToken.balanceOf(operator) / ticketPrice >= - minTicketBalance); + return operators[operator].active; } function hasExitInProgress(address operator) external view returns (bool) { @@ -244,11 +218,7 @@ contract BondingRegistry is // Operator Functions // ====================== - function registerOperator() - external - whenNotPaused - noExitInProgress(msg.sender) - { + function registerOperator() external noExitInProgress(msg.sender) { // Clear previous exit request if (operators[msg.sender].exitRequested) { operators[msg.sender].exitRequested = false; @@ -277,7 +247,7 @@ contract BondingRegistry is function deregisterOperator( uint256[] calldata siblingNodes - ) external whenNotPaused noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) { Operator storage op = operators[msg.sender]; require(op.registered, NotRegistered()); @@ -326,7 +296,7 @@ contract BondingRegistry is function addTicketBalance( uint256 amount - ) external whenNotPaused noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); @@ -344,7 +314,7 @@ contract BondingRegistry is function removeTicketBalance( uint256 amount - ) external whenNotPaused noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( @@ -365,9 +335,7 @@ contract BondingRegistry is _updateOperatorStatus(msg.sender); } - function bondLicense( - uint256 amount - ) external whenNotPaused noExitInProgress(msg.sender) { + function bondLicense(uint256 amount) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); uint256 balanceBefore = licenseToken.balanceOf(address(this)); @@ -389,7 +357,7 @@ contract BondingRegistry is function unbondLicense( uint256 amount - ) external whenNotPaused noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require( operators[msg.sender].licenseBond >= amount, @@ -416,7 +384,7 @@ contract BondingRegistry is function claimExits( uint256 maxTicketAmount, uint256 maxLicenseAmount - ) external whenNotPaused { + ) external { (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( msg.sender, maxTicketAmount, @@ -560,7 +528,7 @@ contract BondingRegistry is // Admin Functions // ====================== - function setTicketPrice(uint256 newTicketPrice) external onlyOwner { + function setTicketPrice(uint256 newTicketPrice) public onlyOwner { require(newTicketPrice != 0, InvalidConfiguration()); uint256 oldValue = ticketPrice; @@ -571,7 +539,7 @@ contract BondingRegistry is function setLicenseRequiredBond( uint256 newLicenseRequiredBond - ) external onlyOwner { + ) public onlyOwner { require(newLicenseRequiredBond != 0, InvalidConfiguration()); uint256 oldValue = licenseRequiredBond; @@ -584,7 +552,7 @@ contract BondingRegistry is ); } - function setLicenseActiveBps(uint256 newBps) external onlyOwner { + function setLicenseActiveBps(uint256 newBps) public onlyOwner { require(newBps > 0 && newBps <= 10_000, InvalidConfiguration()); uint256 oldValue = licenseActiveBps; @@ -593,9 +561,7 @@ contract BondingRegistry is emit ConfigurationUpdated("licenseActiveBps", oldValue, newBps); } - function setMinTicketBalance( - uint256 newMinTicketBalance - ) external onlyOwner { + function setMinTicketBalance(uint256 newMinTicketBalance) public onlyOwner { uint256 oldValue = minTicketBalance; minTicketBalance = newMinTicketBalance; @@ -606,7 +572,7 @@ contract BondingRegistry is ); } - function setExitDelay(uint64 newExitDelay) external onlyOwner { + function setExitDelay(uint64 newExitDelay) public onlyOwner { uint256 oldValue = uint256(exitDelay); exitDelay = newExitDelay; @@ -615,29 +581,39 @@ contract BondingRegistry is function setSlashedFundsTreasury( address newSlashedFundsTreasury - ) external onlyOwner { + ) public onlyOwner { require(newSlashedFundsTreasury != address(0), ZeroAddress()); slashedFundsTreasury = newSlashedFundsTreasury; } - function setRegistry(address newRegistry) external onlyOwner { - registry = ICiphernodeRegistry(newRegistry); + function setTicketToken( + EnclaveTicketToken newTicketToken + ) public onlyOwner { + ticketToken = newTicketToken; } - function setSlashingManager(address newSlashingManager) external onlyOwner { + function setLicenseToken(IERC20 newLicenseToken) public onlyOwner { + licenseToken = newLicenseToken; + } + + function setRegistry(ICiphernodeRegistry newRegistry) public onlyOwner { + registry = newRegistry; + } + + function setSlashingManager(address newSlashingManager) public onlyOwner { slashingManager = newSlashingManager; } function setRewardDistributor( address newRewardDistributor - ) external onlyOwner { + ) public onlyOwner { rewardDistributor = newRewardDistributor; } function withdrawSlashedFunds( uint256 ticketAmount, uint256 licenseAmount - ) external onlyOwner { + ) public onlyOwner { require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); @@ -658,14 +634,6 @@ contract BondingRegistry is ); } - function pause() external onlyOwner { - _pause(); - } - - function unpause() external onlyOwner { - _unpause(); - } - // ====================== // Internal Functions // ====================== @@ -687,8 +655,4 @@ contract BondingRegistry is function _minLicenseBond() internal view returns (uint256) { return (licenseRequiredBond * licenseActiveBps) / 10_000; } - - function _authorizeUpgrade( - address newImplementation - ) internal override onlyOwner {} } diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 4ef0b20dd0..5a1bf85d73 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -42,10 +42,6 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { mapping(uint256 e3Id => uint256 root) public roots; mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; - // Committee tracking for active job management - mapping(uint256 e3Id => bool active) public committeeActive; - mapping(address node => uint256 count) public activeCommitteeCount; - //////////////////////////////////////////////////////////// // // // Errors // @@ -239,66 +235,4 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { function getBondingRegistry() external view returns (address) { return bondingRegistry; } - - //////////////////////////////////////////////////////////// - // // - // Committee Tracking // - // // - //////////////////////////////////////////////////////////// - - function markCommitteeActive( - uint256 e3Id, - address[] calldata members - ) external { - require(msg.sender == enclave || msg.sender == owner(), Unauthorized()); - - // Idempotent: only process if not already active - if (!committeeActive[e3Id]) { - committeeActive[e3Id] = true; - - // Increment active committee count for each member - for (uint256 i = 0; i < members.length; i++) { - activeCommitteeCount[members[i]]++; - } - - emit CommitteeActivationChanged(e3Id, true); - } - } - - function markCommitteeCompleted( - uint256 e3Id, - address[] calldata members - ) external { - require(msg.sender == enclave || msg.sender == owner(), Unauthorized()); - - // Only process if currently active - if (committeeActive[e3Id]) { - committeeActive[e3Id] = false; - - // Decrement active committee count for each member - for (uint256 i = 0; i < members.length; i++) { - if (activeCommitteeCount[members[i]] > 0) { - activeCommitteeCount[members[i]]--; - } - } - - emit CommitteeActivationChanged(e3Id, false); - } - } - - function isNodeActiveInAnyCommittee( - address node - ) external view returns (bool) { - return activeCommitteeCount[node] > 0; - } - - function activeCommitteeCountOf( - address node - ) external view returns (uint256) { - return activeCommitteeCount[node]; - } - - function isCommitteeActive(uint256 e3Id) external view returns (bool) { - return committeeActive[e3Id]; - } } diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index f67bb3215e..ec28c7bacb 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -7,21 +7,8 @@ pragma solidity >=0.8.27; import { - UUPSUpgradeable -} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { - Initializable -} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import { - AccessControlUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import { - PausableUpgradeable -} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import { - ReentrancyGuardUpgradeable -} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; - + AccessControl +} from "@openzeppelin/contracts/access/AccessControl.sol"; import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; @@ -31,14 +18,7 @@ import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; * @notice Manages slashing proposals, appeals, and execution for the bonding system * @dev UUPS upgradeable contract with role-based access control */ -contract SlashingManager is - Initializable, - UUPSUpgradeable, - AccessControlUpgradeable, - PausableUpgradeable, - ReentrancyGuardUpgradeable, - ISlashingManager -{ +contract SlashingManager is ISlashingManager, AccessControl { // ====================== // Constants & Roles // ====================== @@ -66,12 +46,6 @@ contract SlashingManager is /// @notice Banned nodes mapping(address node => bool banned) public banned; - // ====================== - // Storage Gaps for Upgrades - // ====================== - - uint256[50] private __gap; - // ====================== // Modifiers // ====================== @@ -97,28 +71,10 @@ contract SlashingManager is } // ====================== - // Initialization + // Constructor // ====================== - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /** - * @notice Initialize the contract - * @param admin Contract admin (gets DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE) - * @param _bondingRegistry Bonding registry contract address - */ - function initialize( - address admin, - address _bondingRegistry - ) external initializer { - __AccessControl_init(); - __Pausable_init(); - __ReentrancyGuard_init(); - __UUPSUpgradeable_init(); - + constructor(address admin, address _bondingRegistry) { require(admin != address(0), ZeroAddress()); require(_bondingRegistry != address(0), ZeroAddress()); @@ -214,13 +170,7 @@ contract SlashingManager is address operator, bytes32 reason, bytes calldata proof - ) - external - onlySlasher - whenNotPaused - notBanned(operator) - returns (uint256 proposalId) - { + ) external onlySlasher notBanned(operator) returns (uint256 proposalId) { require(operator != address(0), ZeroAddress()); SlashPolicy storage policy = slashPolicies[reason]; @@ -271,9 +221,7 @@ contract SlashingManager is return nextId; } - function executeSlash( - uint256 proposalId - ) external onlySlasher whenNotPaused nonReentrant { + function executeSlash(uint256 proposalId) external onlySlasher { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = proposals[proposalId]; @@ -337,10 +285,7 @@ contract SlashingManager is // Appeal Functions // ====================== - function fileAppeal( - uint256 proposalId, - string calldata evidence - ) external whenNotPaused { + function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = proposals[proposalId]; @@ -396,21 +341,4 @@ contract SlashingManager is banned[node] = false; emit NodeUnbanned(node, msg.sender); } - - function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { - _pause(); - } - - function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { - _unpause(); - } - - // ====================== - // Internal Functions - // ====================== - - // solhint-disable-next-line no-empty-blocks - function _authorizeUpgrade( - address newImplementation - ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index a04f31d896..cbd6107a87 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -6,6 +6,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { function requestCommittee( @@ -20,6 +21,14 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { } } + function addCiphernode(address) external {} + + function isEnabled(address) external pure returns (bool) { + return true; + } + + function removeCiphernode(address, uint256[] calldata) external {} + // solhint-disable no-empty-blocks function publishCommittee( uint256, @@ -38,6 +47,21 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { function isCiphernodeEligible(address) external pure returns (bool) { return false; } + + function getFilter(uint256) external pure returns (address) { + return address(0); + } + + function getCommittee( + uint256 + ) external pure returns (IRegistryFilter.Committee memory) { + return + IRegistryFilter.Committee( + new address[](0), + [uint32(0), uint32(0)], + bytes32(0) + ); + } } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { @@ -53,6 +77,14 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { } } + function addCiphernode(address) external {} + + function isEnabled(address) external pure returns (bool) { + return true; + } + + function removeCiphernode(address, uint256[] calldata) external {} + // solhint-disable no-empty-blocks function publishCommittee( uint256, @@ -60,6 +92,21 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { bytes calldata ) external {} // solhint-disable-line no-empty-blocks + function getFilter(uint256) external pure returns (address) { + return address(0); + } + + function getCommittee( + uint256 + ) external pure returns (IRegistryFilter.Committee memory) { + return + IRegistryFilter.Committee( + new address[](0), + [uint32(0), uint32(0)], + bytes32(0) + ); + } + function committeePublicKey(uint256) external pure returns (bytes32) { return bytes32(0); } diff --git a/packages/enclave-contracts/contracts/test/MockStableToken.sol b/packages/enclave-contracts/contracts/test/MockStableToken.sol index 0f8a8614ed..dcfc978518 100644 --- a/packages/enclave-contracts/contracts/test/MockStableToken.sol +++ b/packages/enclave-contracts/contracts/test/MockStableToken.sol @@ -8,7 +8,7 @@ pragma solidity >=0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -contract TestUSDC is ERC20, Ownable { +contract MockUSDC is ERC20, Ownable { uint8 private _decimals; constructor( diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicket.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol similarity index 100% rename from packages/enclave-contracts/contracts/token/EnclaveTicket.sol rename to packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol diff --git a/packages/enclave-contracts/ignition/modules/bondingRegistry.ts b/packages/enclave-contracts/ignition/modules/bondingRegistry.ts new file mode 100644 index 0000000000..de79a9ad2d --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/bondingRegistry.ts @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("BondingRegistry", (m) => { + const ticketToken = m.getParameter("ticketToken"); + const licenseToken = m.getParameter("licenseToken"); + const registry = m.getParameter("registry"); + const slashedFundsTreasury = m.getParameter("slashedFundsTreasury"); + const ticketPrice = m.getParameter("ticketPrice"); + const licenseRequiredBond = m.getParameter("licenseRequiredBond"); + const minTicketBalance = m.getParameter("minTicketBalance"); + const exitDelay = m.getParameter("exitDelay"); + const owner = m.getParameter("owner"); + + const bondingRegistry = m.contract("BondingRegistry", [ + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + ]); + + return { bondingRegistry }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/enclave.ts b/packages/enclave-contracts/ignition/modules/enclave.ts index eec6c0da2f..5ec3528c79 100644 --- a/packages/enclave-contracts/ignition/modules/enclave.ts +++ b/packages/enclave-contracts/ignition/modules/enclave.ts @@ -12,12 +12,14 @@ export default buildModule("Enclave", (m) => { const owner = m.getParameter("owner"); const maxDuration = m.getParameter("maxDuration"); const registry = m.getParameter("registry"); + const bondingRegistry = m.getParameter("bondingRegistry"); + const usdcToken = m.getParameter("usdcToken"); const poseidonT3 = m.library("PoseidonT3"); const enclave = m.contract( "Enclave", - [owner, registry, maxDuration, [params]], + [owner, registry, bondingRegistry, usdcToken, maxDuration, [params]], { libraries: { PoseidonT3: poseidonT3, diff --git a/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts b/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts new file mode 100644 index 0000000000..476903cf14 --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("EnclaveTicketToken", (m) => { + const underlyingUSDC = m.getParameter("underlyingUSDC"); + const registry = m.getParameter("registry"); + const owner = m.getParameter("owner"); + + const enclaveTicketToken = m.contract("EnclaveTicketToken", [ + underlyingUSDC, + registry, + owner, + ]); + + return { enclaveTicketToken }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/enclaveToken.ts b/packages/enclave-contracts/ignition/modules/enclaveToken.ts new file mode 100644 index 0000000000..b8baaf9cfb --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/enclaveToken.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("EnclaveToken", (m) => { + const owner = m.getParameter("owner"); + + const enclaveToken = m.contract("EnclaveToken", [owner]); + + return { enclaveToken }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts new file mode 100644 index 0000000000..5e9d0e1431 --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("MockSlashingVerifier", (m) => { + const mockSlashingVerifier = m.contract("MockSlashingVerifier"); + + return { mockSlashingVerifier }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/mockStableToken.ts b/packages/enclave-contracts/ignition/modules/mockStableToken.ts new file mode 100644 index 0000000000..480389be16 --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/mockStableToken.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("MockUSDC", (m) => { + const initialSupply = m.getParameter("initialSupply"); + + const mockUSDC = m.contract("MockUSDC", [initialSupply]); + + return { mockUSDC }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/slashingManager.ts b/packages/enclave-contracts/ignition/modules/slashingManager.ts new file mode 100644 index 0000000000..0d5919900e --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/slashingManager.ts @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("SlashingManager", (m) => { + const bondingRegistry = m.getParameter("bondingRegistry"); + const admin = m.getParameter("admin"); + + const slashingManager = m.contract("SlashingManager", [ + admin, + bondingRegistry, + ]); + + return { slashingManager }; +}) as any; From 7aace0b1495438d98a6014726440e4acdeb2042b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 29 Sep 2025 12:36:13 +0500 Subject: [PATCH 07/88] feat: add bonding registry tests --- packages/enclave-contracts/hardhat.config.ts | 1 + .../test/BondingRegistry.spec.ts | 1239 +++++++++++++++++ .../enclave-contracts/test/Enclave.spec.ts | 288 +++- 3 files changed, 1454 insertions(+), 74 deletions(-) create mode 100644 packages/enclave-contracts/test/BondingRegistry.spec.ts diff --git a/packages/enclave-contracts/hardhat.config.ts b/packages/enclave-contracts/hardhat.config.ts index 0aae4886ef..7cb04b8eb7 100644 --- a/packages/enclave-contracts/hardhat.config.ts +++ b/packages/enclave-contracts/hardhat.config.ts @@ -164,6 +164,7 @@ const config: HardhatUserConfig = { enabled: true, runs: 800, }, + viaIR: true, }, }, ], diff --git a/packages/enclave-contracts/test/BondingRegistry.spec.ts b/packages/enclave-contracts/test/BondingRegistry.spec.ts new file mode 100644 index 0000000000..a02081a989 --- /dev/null +++ b/packages/enclave-contracts/test/BondingRegistry.spec.ts @@ -0,0 +1,1239 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import { LeanIMT } from "@zk-kit/lean-imt"; +import { expect } from "chai"; +import { network } from "hardhat"; +import { poseidon2 } from "poseidon-lite"; + +import BondingRegistryModule from "../ignition/modules/bondingRegistry"; +import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../ignition/modules/enclaveToken"; +import MockCiphernodeRegistryModule from "../ignition/modules/mockCiphernodeRegistry"; +import MockStableTokenModule from "../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, +} from "../types"; + +const AddressOne = "0x0000000000000000000000000000000000000001"; +const AddressTwo = "0x0000000000000000000000000000000000000002"; +const AddressThree = "0x0000000000000000000000000000000000000003"; + +const { ethers, networkHelpers, ignition } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +// Hash function used to compute the tree nodes for ciphernode registry. +const hash = (a: bigint, b: bigint) => poseidon2([a, b]); + +// Reason constants matching the contract +const REASON_DEPOSIT = ethers.encodeBytes32String("DEPOSIT"); +const REASON_WITHDRAW = ethers.encodeBytes32String("WITHDRAW"); +const REASON_BOND = ethers.encodeBytes32String("BOND"); +const REASON_UNBOND = ethers.encodeBytes32String("UNBOND"); + +describe("BondingRegistry", function () { + const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60; + const TICKET_PRICE = ethers.parseUnits("10", 6); // 10 USDC per ticket (6 decimals) + const LICENSE_REQUIRED_BOND = ethers.parseEther("1000"); // 1000 ENCL required + const MIN_TICKET_BALANCE = 5; // minimum 5 tickets + + async function setup() { + const [owner, operator1, operator2, treasury, notTheOwner] = + await ethers.getSigners(); + + const ownerAddress = await owner.getAddress(); + const operator1Address = await operator1.getAddress(); + const operator2Address = await operator2.getAddress(); + const treasuryAddress = await treasury.getAddress(); + + // Deploy USDC mock + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, // 1M USDC (with 6 decimals) + }, + }, + }); + + // Deploy ENCL token + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + // Deploy CiphernodeRegistry for testing + const ciphernodeRegistryContract = await ignition.deploy( + MockCiphernodeRegistryModule, + { + parameters: { + CiphernodeRegistry: { + enclaveAddress: ownerAddress, + owner: ownerAddress, + }, + }, + }, + ); + + // Deploy EnclaveTicketToken + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + underlyingUSDC: await usdcContract.mockUSDC.getAddress(), + registry: AddressOne, // temporary, will be updated + owner: ownerAddress, + }, + }, + }, + ); + + // Deploy SlashingManager + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: AddressOne, // temporary, will be updated + }, + }, + }, + ); + + // Deploy BondingRegistry + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: + await ciphernodeRegistryContract.mockCiphernodeRegistry.getAddress(), + slashedFundsTreasury: treasuryAddress, + ticketPrice: TICKET_PRICE, + licenseRequiredBond: LICENSE_REQUIRED_BOND, + minTicketBalance: MIN_TICKET_BALANCE, + exitDelay: SEVEN_DAYS_IN_SECONDS, + }, + }, + }, + ); + + // Connect to deployed contracts + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + const ticketToken = EnclaveTicketTokenFactory.connect( + await ticketTokenContract.enclaveTicketToken.getAddress(), + owner, + ); + const licenseToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + const slashingManager = SlashingManagerFactory.connect( + await slashingManagerContract.slashingManager.getAddress(), + owner, + ); + const ciphernodeRegistry = CiphernodeRegistryOwnableFactory.connect( + await ciphernodeRegistryContract.mockCiphernodeRegistry.getAddress(), + owner, + ); + + // Update contract references with actual addresses + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), + ); + + // Setup initial token balances and approvals + await usdcToken.mint(ownerAddress, ethers.parseUnits("100000", 6)); // 100k USDC + await usdcToken.mint(operator1Address, ethers.parseUnits("100000", 6)); // 100k USDC + await usdcToken.mint(operator2Address, ethers.parseUnits("100000", 6)); // 100k USDC + + await licenseToken.mintAllocation( + ownerAddress, + ethers.parseEther("100000"), + "Test allocation", + ); + await licenseToken.mintAllocation( + operator1Address, + ethers.parseEther("100000"), + "Test allocation", + ); + await licenseToken.mintAllocation( + operator2Address, + ethers.parseEther("100000"), + "Test allocation", + ); + + // Enable transfers for testing + await licenseToken.setTransferRestriction(false); + + // Setup Merkle tree for ciphernode registry + const tree = new LeanIMT(hash); + + return { + bondingRegistry, + ticketToken, + licenseToken, + usdcToken, + slashingManager, + ciphernodeRegistry, + tree, + owner, + operator1, + operator2, + treasury, + notTheOwner, + ownerAddress, + operator1Address, + operator2Address, + treasuryAddress, + }; + } + + describe("constructor / initialize()", function () { + it("correctly sets initial parameters", async function () { + const { bondingRegistry, ticketToken, licenseToken, treasuryAddress } = + await loadFixture(setup); + + expect(await bondingRegistry.ticketToken()).to.equal( + await ticketToken.getAddress(), + ); + expect(await bondingRegistry.licenseToken()).to.equal( + await licenseToken.getAddress(), + ); + expect(await bondingRegistry.slashedFundsTreasury()).to.equal( + treasuryAddress, + ); + expect(await bondingRegistry.ticketPrice()).to.equal(TICKET_PRICE); + expect(await bondingRegistry.licenseRequiredBond()).to.equal( + LICENSE_REQUIRED_BOND, + ); + expect(await bondingRegistry.minTicketBalance()).to.equal( + MIN_TICKET_BALANCE, + ); + expect(await bondingRegistry.exitDelay()).to.equal(SEVEN_DAYS_IN_SECONDS); + expect(await bondingRegistry.licenseActiveBps()).to.equal(8000); // 80% + }); + }); + + describe("bondLicense()", function () { + it("allows operators to bond license tokens", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + + await expect(bondingRegistry.connect(operator1).bondLicense(bondAmount)) + .to.emit(bondingRegistry, "LicenseBondUpdated") + .withArgs( + await operator1.getAddress(), + bondAmount, + bondAmount, + REASON_BOND, + ); + + expect( + await bondingRegistry.getLicenseBond(await operator1.getAddress()), + ).to.equal(bondAmount); + }); + + it("reverts if amount is zero", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).bondLicense(0), + ).to.be.revertedWithCustomError(bondingRegistry, "ZeroAmount"); + }); + + it("reverts if exit is in progress", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond initial license + const bondAmount = ethers.parseEther("1000"); + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + // Register operator + await bondingRegistry.connect(operator1).registerOperator(); + + // Start deregistration (which triggers exit) + await bondingRegistry.connect(operator1).deregisterOperator([]); + + // Try to bond more during exit + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await expect( + bondingRegistry.connect(operator1).bondLicense(bondAmount), + ).to.be.revertedWithCustomError(bondingRegistry, "ExitInProgress"); + }); + + it("accumulates multiple bond amounts", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount1 = ethers.parseEther("500"); + const bondAmount2 = ethers.parseEther("300"); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount1); + await bondingRegistry.connect(operator1).bondLicense(bondAmount1); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount2); + await bondingRegistry.connect(operator1).bondLicense(bondAmount2); + + expect( + await bondingRegistry.getLicenseBond(await operator1.getAddress()), + ).to.equal(bondAmount1 + bondAmount2); + }); + }); + + describe("unbondLicense()", function () { + it("allows operators to unbond license tokens", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + const unbondAmount = ethers.parseEther("200"); + + // Bond first + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + // Unbond + await expect( + bondingRegistry.connect(operator1).unbondLicense(unbondAmount), + ) + .to.emit(bondingRegistry, "LicenseBondUpdated") + .withArgs( + await operator1.getAddress(), + -unbondAmount, + bondAmount - unbondAmount, + REASON_UNBOND, + ); + + expect( + await bondingRegistry.getLicenseBond(await operator1.getAddress()), + ).to.equal(bondAmount - unbondAmount); + }); + + it("reverts if amount is zero", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).unbondLicense(0), + ).to.be.revertedWithCustomError(bondingRegistry, "ZeroAmount"); + }); + + it("reverts if insufficient balance", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry + .connect(operator1) + .unbondLicense(ethers.parseEther("100")), + ).to.be.revertedWithCustomError(bondingRegistry, "InsufficientBalance"); + }); + + it("queues license tokens for exit", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + const unbondAmount = ethers.parseEther("200"); + + // Bond first + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + // Unbond + await bondingRegistry.connect(operator1).unbondLicense(unbondAmount); + + const [ticketPending, licensePending] = + await bondingRegistry.pendingExits(await operator1.getAddress()); + expect(licensePending).to.equal(unbondAmount); + }); + }); + + describe("registerOperator()", function () { + it("allows properly licensed operators to register", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond enough license tokens + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + await bondingRegistry.connect(operator1).registerOperator(); + + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; // no tickets yet + }); + + it("reverts if not properly licensed", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).registerOperator(), + ).to.be.revertedWithCustomError(bondingRegistry, "NotLicensed"); + }); + + it("reverts if already registered", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Try to register again + await expect( + bondingRegistry.connect(operator1).registerOperator(), + ).to.be.revertedWithCustomError(bondingRegistry, "AlreadyRegistered"); + }); + + it("clears previous exit request when re-registering", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Deregister + await bondingRegistry.connect(operator1).deregisterOperator([]); + + // Wait for exit delay to pass + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + // Re-bond and register + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + expect( + await bondingRegistry.hasExitInProgress(await operator1.getAddress()), + ).to.be.false; + }); + }); + + describe("deregisterOperator()", function () { + it("allows registered operators to deregister", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const latestTime = await time.latest(); + await expect(bondingRegistry.connect(operator1).deregisterOperator([])) + .to.emit(bondingRegistry, "CiphernodeDeregistrationRequested") + .withArgs( + await operator1.getAddress(), + latestTime + SEVEN_DAYS_IN_SECONDS + 1, + ); + + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.false; + expect( + await bondingRegistry.hasExitInProgress(await operator1.getAddress()), + ).to.be.true; + }); + + it("reverts if not registered", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).deregisterOperator([]), + ).to.be.revertedWithCustomError(bondingRegistry, "NotRegistered"); + }); + + it("queues assets for exit when deregistering", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Bond license + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Add tickets + const ticketAmount = ethers.parseUnits("100", 6); // 100 USDC + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + // Deregister + await bondingRegistry.connect(operator1).deregisterOperator([]); + + const [ticketPending, licensePending] = + await bondingRegistry.pendingExits(await operator1.getAddress()); + expect(ticketPending).to.equal(ticketAmount); + expect(licensePending).to.equal(bondAmount); + }); + }); + + describe("addTicketBalance()", function () { + it("allows registered operators to add ticket balance", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Add tickets + const ticketAmount = ethers.parseUnits("100", 6); // 100 USDC + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + + await expect( + bondingRegistry.connect(operator1).addTicketBalance(ticketAmount), + ) + .to.emit(bondingRegistry, "TicketBalanceUpdated") + .withArgs( + await operator1.getAddress(), + ticketAmount, + ticketAmount, + REASON_DEPOSIT, + ); + + expect( + await bondingRegistry.getTicketBalance(await operator1.getAddress()), + ).to.equal(ticketAmount); + }); + + it("activates operator when minimum balance is reached", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Add enough tickets to become active (5 tickets * 10 USDC each = 50 USDC) + const ticketAmount = ethers.parseUnits("50", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + + await expect( + bondingRegistry.connect(operator1).addTicketBalance(ticketAmount), + ) + .to.emit(bondingRegistry, "OperatorActivationChanged") + .withArgs(await operator1.getAddress(), true); + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .true; + }); + + it("reverts if not registered", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry + .connect(operator1) + .addTicketBalance(ethers.parseUnits("100", 6)), + ).to.be.revertedWithCustomError(bondingRegistry, "NotRegistered"); + }); + + it("reverts if amount is zero", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await expect( + bondingRegistry.connect(operator1).addTicketBalance(0), + ).to.be.revertedWithCustomError(bondingRegistry, "ZeroAmount"); + }); + }); + + describe("removeTicketBalance()", function () { + it("allows operators to remove ticket balance", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Setup operator with tickets + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + // Remove some tickets + const removeAmount = ethers.parseUnits("30", 6); + await expect( + bondingRegistry.connect(operator1).removeTicketBalance(removeAmount), + ) + .to.emit(bondingRegistry, "TicketBalanceUpdated") + .withArgs( + await operator1.getAddress(), + -removeAmount, + ticketAmount - removeAmount, + REASON_WITHDRAW, + ); + + expect( + await bondingRegistry.getTicketBalance(await operator1.getAddress()), + ).to.equal(ticketAmount - removeAmount); + }); + + it("queues removed tickets for exit", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Setup operator with tickets + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + // Remove some tickets + const removeAmount = ethers.parseUnits("30", 6); + await bondingRegistry + .connect(operator1) + .removeTicketBalance(removeAmount); + + const [ticketPending, licensePending] = + await bondingRegistry.pendingExits(await operator1.getAddress()); + expect(ticketPending).to.equal(removeAmount); + }); + + it("deactivates operator if balance falls below minimum", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Setup active operator + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("60", 6); // 6 tickets worth + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + // Remove enough to go below minimum (remove 2 tickets worth, leaving 4 < 5 minimum) + const removeAmount = ethers.parseUnits("20", 6); + await expect( + bondingRegistry.connect(operator1).removeTicketBalance(removeAmount), + ) + .to.emit(bondingRegistry, "OperatorActivationChanged") + .withArgs(await operator1.getAddress(), false); + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + }); + + it("reverts if insufficient balance", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await expect( + bondingRegistry + .connect(operator1) + .removeTicketBalance(ethers.parseUnits("100", 6)), + ).to.be.revertedWithCustomError(bondingRegistry, "InsufficientBalance"); + }); + }); + + describe("claimExits()", function () { + it("allows claiming after exit delay", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Setup operator and bond/tickets + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + // Deregister to queue assets for exit + await bondingRegistry.connect(operator1).deregisterOperator([]); + + // Wait for exit delay + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + // Claim exits + const initialUSDCBalance = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const initialENCLBalance = await licenseToken.balanceOf( + await operator1.getAddress(), + ); + + await bondingRegistry + .connect(operator1) + .claimExits(ticketAmount, bondAmount); + + expect(await usdcToken.balanceOf(await operator1.getAddress())).to.equal( + initialUSDCBalance + ticketAmount, + ); + expect( + await licenseToken.balanceOf(await operator1.getAddress()), + ).to.equal(initialENCLBalance + bondAmount); + }); + + it("reverts if exit not ready", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Deregister but don't wait + await bondingRegistry.connect(operator1).deregisterOperator([]); + + await expect( + bondingRegistry.connect(operator1).claimExits(0, bondAmount), + ).to.be.revertedWithCustomError(bondingRegistry, "ExitNotReady"); + }); + + it("allows partial claims", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Setup and queue assets for exit + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + // Wait for exit delay + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + // Claim partial amounts + const partialTickets = ethers.parseUnits("50", 6); + const partialLicense = ethers.parseEther("500"); + + const initialUSDCBalance = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const initialENCLBalance = await licenseToken.balanceOf( + await operator1.getAddress(), + ); + + await bondingRegistry + .connect(operator1) + .claimExits(partialTickets, partialLicense); + + expect(await usdcToken.balanceOf(await operator1.getAddress())).to.equal( + initialUSDCBalance + partialTickets, + ); + expect( + await licenseToken.balanceOf(await operator1.getAddress()), + ).to.equal(initialENCLBalance + partialLicense); + + // Check remaining pending amounts + const [remainingTickets, remainingLicense] = + await bondingRegistry.pendingExits(await operator1.getAddress()); + expect(remainingTickets).to.equal(ticketAmount - partialTickets); + expect(remainingLicense).to.equal(bondAmount - partialLicense); + }); + }); + + describe("isLicensed()", function () { + it("returns true when operator has minimum license bond", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond exactly the minimum required (80% of LICENSE_REQUIRED_BOND) + const minBond = (LICENSE_REQUIRED_BOND * 8000n) / 10000n; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), minBond); + await bondingRegistry.connect(operator1).bondLicense(minBond); + + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.true; + }); + + it("returns false when operator has insufficient license bond", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + // Bond less than minimum required + const insufficientBond = (LICENSE_REQUIRED_BOND * 7999n) / 10000n; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), insufficientBond); + await bondingRegistry.connect(operator1).bondLicense(insufficientBond); + + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.false; + }); + }); + + describe("availableTickets()", function () { + it("calculates available tickets correctly", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Bond and register + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Add tickets worth exactly 10 tickets (10 * 10 USDC = 100 USDC) + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + expect( + await bondingRegistry.availableTickets(await operator1.getAddress()), + ).to.equal(10); + }); + + it("returns 0 when ticket price is 0", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + // This test should check the internal logic - if ticketPrice were 0, it would return 0 + // Since we can't set ticketPrice to 0 via setTicketPrice due to validation, + // we just verify that a fresh operator has 0 available tickets + expect( + await bondingRegistry.availableTickets(await operator1.getAddress()), + ).to.equal(0); + }); + }); + + describe("Admin Functions", function () { + describe("setTicketPrice()", function () { + it("allows owner to set ticket price", async function () { + const { bondingRegistry } = await loadFixture(setup); + + const newPrice = ethers.parseUnits("15", 6); + await expect(bondingRegistry.setTicketPrice(newPrice)) + .to.emit(bondingRegistry, "ConfigurationUpdated") + .withArgs( + ethers.encodeBytes32String("ticketPrice"), + TICKET_PRICE, + newPrice, + ); + + expect(await bondingRegistry.ticketPrice()).to.equal(newPrice); + }); + + it("reverts if price is zero", async function () { + const { bondingRegistry } = await loadFixture(setup); + + await expect( + bondingRegistry.setTicketPrice(0), + ).to.be.revertedWithCustomError( + bondingRegistry, + "InvalidConfiguration", + ); + }); + + it("reverts if not owner", async function () { + const { bondingRegistry, notTheOwner } = await loadFixture(setup); + + await expect( + bondingRegistry + .connect(notTheOwner) + .setTicketPrice(ethers.parseEther("15")), + ).to.be.revertedWithCustomError( + bondingRegistry, + "OwnableUnauthorizedAccount", + ); + }); + }); + + describe("setLicenseActiveBps()", function () { + it("allows owner to set license active basis points", async function () { + const { bondingRegistry } = await loadFixture(setup); + + const newBps = 9000; // 90% + await expect(bondingRegistry.setLicenseActiveBps(newBps)) + .to.emit(bondingRegistry, "ConfigurationUpdated") + .withArgs( + ethers.encodeBytes32String("licenseActiveBps"), + 8000, + newBps, + ); + + expect(await bondingRegistry.licenseActiveBps()).to.equal(newBps); + }); + + it("reverts if bps is 0", async function () { + const { bondingRegistry } = await loadFixture(setup); + + await expect( + bondingRegistry.setLicenseActiveBps(0), + ).to.be.revertedWithCustomError( + bondingRegistry, + "InvalidConfiguration", + ); + }); + + it("reverts if bps is greater than 10000", async function () { + const { bondingRegistry } = await loadFixture(setup); + + await expect( + bondingRegistry.setLicenseActiveBps(10001), + ).to.be.revertedWithCustomError( + bondingRegistry, + "InvalidConfiguration", + ); + }); + }); + + describe("withdrawSlashedFunds()", function () { + it("allows owner to withdraw slashed funds", async function () { + const { bondingRegistry, treasury } = await loadFixture(setup); + + // Simulate some slashed funds (would normally come from slashing operations) + // For testing, we'll directly set the slashed balances by calling internal slashing functions + // This would normally be done by the slashing manager + + // Test that the function exists and can be called (even with 0 amounts) + await expect(bondingRegistry.withdrawSlashedFunds(0, 0)) + .to.emit(bondingRegistry, "SlashedFundsWithdrawn") + .withArgs(await treasury.getAddress(), 0, 0); + }); + + it("reverts if not owner", async function () { + const { bondingRegistry, notTheOwner } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(notTheOwner).withdrawSlashedFunds(0, 0), + ).to.be.revertedWithCustomError( + bondingRegistry, + "OwnableUnauthorizedAccount", + ); + }); + }); + }); + + describe("Edge Cases and Complex Scenarios", function () { + it("handles operator becoming inactive due to license reduction", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // Setup active operator + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .true; + + // Unbond enough license to fall below 80% threshold + const unbondAmount = LICENSE_REQUIRED_BOND / 5n; // Remove 20%, leaving 80% exactly at threshold + await bondingRegistry.connect(operator1).unbondLicense(unbondAmount + 1n); // Remove just 1 wei more + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.false; + }); + + it("handles multiple operators with different states", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + operator2, + } = await loadFixture(setup); + + // Operator 1: Licensed but not active (no tickets) + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + // Operator 2: Licensed and active + await licenseToken + .connect(operator2) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator2).bondLicense(bondAmount); + await bondingRegistry.connect(operator2).registerOperator(); + + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator2) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator2).addTicketBalance(ticketAmount); + + // Check states + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + + expect(await bondingRegistry.isRegistered(await operator2.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator2.getAddress())).to.be + .true; + }); + + it("handles the complete operator lifecycle", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + // 1. Bond license + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.true; + + // 2. Register + await bondingRegistry.connect(operator1).registerOperator(); + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + + // 3. Add tickets to become active + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .true; + + // 4. Deregister + await bondingRegistry.connect(operator1).deregisterOperator([]); + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.false; + expect( + await bondingRegistry.hasExitInProgress(await operator1.getAddress()), + ).to.be.true; + + // 5. Wait and claim + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + const initialUSDCBalance = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const initialENCLBalance = await licenseToken.balanceOf( + await operator1.getAddress(), + ); + + await bondingRegistry + .connect(operator1) + .claimExits(ticketAmount, bondAmount); + + expect(await usdcToken.balanceOf(await operator1.getAddress())).to.equal( + initialUSDCBalance + ticketAmount, + ); + expect( + await licenseToken.balanceOf(await operator1.getAddress()), + ).to.equal(initialENCLBalance + bondAmount); + + // 6. Re-register after claiming + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + }); + }); +}); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 878966410b..66f95b016c 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -8,14 +8,19 @@ import { expect } from "chai"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; +import BondingRegistryModule from "../ignition/modules/bondingRegistry"; import EnclaveModule from "../ignition/modules/enclave"; +import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../ignition/modules/enclaveToken"; import MockCiphernodeRegistryModule from "../ignition/modules/mockCiphernodeRegistry"; import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; import mockComputeProviderModule from "../ignition/modules/mockComputeProvider"; import MockDecryptionVerifierModule from "../ignition/modules/mockDecryptionVerifier"; import MockE3ProgramModule from "../ignition/modules/mockE3Program"; import MockInputValidatorModule from "../ignition/modules/mockInputValidator"; +import MockStableTokenModule from "../ignition/modules/mockStableToken"; import NaiveRegistryFilterModule from "../ignition/modules/naiveRegistryFilter"; +import SlashingManagerModule from "../ignition/modules/slashingManager"; import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, Enclave__factory as EnclaveFactory, @@ -53,30 +58,124 @@ describe("Enclave", function () { // Hash function used to compute the tree nodes. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); + // Helper function to approve USDC and make request + const makeRequest = async ( + enclave: any, + usdcToken: any, + requestParams: any, + signer?: any, + ) => { + const fee = await enclave.getE3Quote(requestParams); + const tokenContract = signer ? usdcToken.connect(signer) : usdcToken; + const enclaveContract = signer ? enclave.connect(signer) : enclave; + + await tokenContract.approve(await enclave.getAddress(), fee); + return enclaveContract.request(requestParams); + }; + const setup = async () => { const [owner, notTheOwner] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); + // Deploy PoseidonT3 library first + const poseidonFactory = await ethers.getContractFactory("PoseidonT3"); + const poseidonDeployment = await poseidonFactory.deploy(); + + // Deploy USDC mock + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, // 1M USDC (with 6 decimals) + }, + }, + }); + + // Deploy ENCL token + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + // Deploy EnclaveTicketToken + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + underlyingUSDC: await usdcContract.mockUSDC.getAddress(), + registry: addressOne, // temporary, will be updated + owner: ownerAddress, + }, + }, + }, + ); + + // Deploy SlashingManager + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: addressOne, // temporary, will be updated + }, + }, + }, + ); + + // Deploy BondingRegistry + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: addressOne, // will be updated when ciphernode registry is ready + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseEther("10"), // 10 USDC per ticket (scaled to 18 decimals for calculation) + licenseRequiredBond: ethers.parseEther("1000"), // 1000 ENCL required + minTicketBalance: 5, // minimum 5 tickets + exitDelay: 7 * 24 * 60 * 60, // 7 days in seconds + }, + }, + }, + ); + const enclaveContract = await ignition.deploy(EnclaveModule, { parameters: { Enclave: { params: encodedE3ProgramParams, owner: ownerAddress, maxDuration: THIRTY_DAYS_IN_SECONDS, - registry: addressOne, + registry: addressOne, // will be updated when ciphernode registry is ready + bondingRegistry: + await bondingRegistryContract.bondingRegistry.getAddress(), + usdcToken: await usdcContract.mockUSDC.getAddress(), }, }, }); const enclaveAddress = await enclaveContract.enclave.getAddress(); - const ciphernodeRegistry = await ignition.deploy( - MockCiphernodeRegistryModule, + // Deploy MockCiphernodeRegistry manually + const ciphernodeRegistryFactory = await ethers.getContractFactory( + "MockCiphernodeRegistry", + { + libraries: { + PoseidonT3: await poseidonDeployment.getAddress(), + }, + }, ); - + const ciphernodeRegistryContract = await ciphernodeRegistryFactory.deploy(); const ciphernodeRegistryAddress = - await ciphernodeRegistry.mockCiphernodeRegistry.getAddress(); + await ciphernodeRegistryContract.getAddress(); const naiveRegistryFilter = await ignition.deploy( NaiveRegistryFilterModule, @@ -94,10 +193,11 @@ describe("Enclave", function () { await naiveRegistryFilter.naiveRegistryFilter.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); - const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( - ciphernodeRegistryAddress, - owner, - ); + const ciphernodeRegistryOwnableContract = + CiphernodeRegistryOwnableFactory.connect( + ciphernodeRegistryAddress, + owner, + ); const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( naiveRegistryFilterAddress, owner, @@ -108,6 +208,20 @@ describe("Enclave", function () { await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); } + // Update contract references with actual addresses + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + await bondingRegistryContract.bondingRegistry.setRegistry( + ciphernodeRegistryAddress, + ); + await bondingRegistryContract.bondingRegistry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); + await slashingManagerContract.slashingManager.setBondingRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + const mockComputeProvider = await ignition.deploy( mockComputeProviderModule, ); @@ -137,7 +251,7 @@ describe("Enclave", function () { const request = { filter: await naiveRegistryFilterContract.getAddress(), threshold: [2, 2] as [number, number], - startTime: [await time.latest(), (await time.latest()) + 100] as [ + startWindow: [await time.latest(), (await time.latest()) + 100] as [ number, number, ], @@ -150,10 +264,35 @@ describe("Enclave", function () { ), }; + // Setup initial token balances + await usdcContract.mockUSDC.mint( + ownerAddress, + ethers.parseUnits("10000", 6), + ); // 10k USDC + await usdcContract.mockUSDC.mint( + await notTheOwner.getAddress(), + ethers.parseUnits("10000", 6), + ); // 10k USDC + await enclTokenContract.enclaveToken.mintAllocation( + ownerAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await enclTokenContract.enclaveToken.mintAllocation( + await notTheOwner.getAddress(), + ethers.parseEther("10000"), + "Test allocation", + ); + return { enclave, - ciphernodeRegistryContract, + ciphernodeRegistryContract: ciphernodeRegistryOwnableContract, naiveRegistryFilterContract, + bondingRegistry: bondingRegistryContract.bondingRegistry, + ticketToken: ticketTokenContract.enclaveTicketToken, + licenseToken: enclTokenContract.enclaveToken, + usdcToken: usdcContract.mockUSDC, + slashingManager: slashingManagerContract.slashingManager, mocks: { decryptionVerifier: decryptionVerifier.mockDecryptionVerifier, inputValidator: inputValidator.mockInputValidator, @@ -339,7 +478,7 @@ describe("Enclave", function () { { filter: await naiveRegistryFilterContract.getAddress(), threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -583,13 +722,13 @@ describe("Enclave", function () { }); describe("request()", function () { - it("reverts if msg.value is 0", async function () { + it("reverts if USDC allowance is insufficient", async function () { const { enclave, request } = await loadFixture(setup); await expect( enclave.request({ filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -598,20 +737,27 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "PaymentRequired"); }); it("reverts if threshold is 0", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, owner } = await loadFixture(setup); + const fee = await enclave.getE3Quote({ + filter: request.filter, + threshold: [0, 2], + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); + await usdcToken.approve(await enclave.getAddress(), fee); await expect( - enclave.request( - { - filter: request.filter, - threshold: [0, 2], - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), + enclave.request({ + filter: request.filter, + threshold: [0, 2], + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }), ).to.be.revertedWithCustomError(enclave, "InvalidThreshold"); }); it("reverts if threshold is greater than number", async function () { @@ -621,7 +767,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: [3, 2], - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -638,7 +784,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: 0, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -655,7 +801,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: time.duration.days(31), e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -672,7 +818,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: ethers.ZeroAddress, e3ProgramParams: request.e3ProgramParams, @@ -692,7 +838,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -712,7 +858,7 @@ describe("Enclave", function () { { filter: AddressTwo, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -723,20 +869,17 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "CommitteeSelectionFailed"); }); it("instantiates a new E3", async function () { - const { enclave, request, mocks } = await loadFixture(setup); - - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + const { enclave, request, mocks, usdcToken } = await loadFixture(setup); + + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3 = await enclave.getE3(0); const block = await ethers.provider.getBlock("latest").catch((e) => e); @@ -756,19 +899,16 @@ describe("Enclave", function () { expect(e3.plaintextOutput).to.equal("0x"); }); it("emits E3Requested event", async function () { - const { enclave, request } = await loadFixture(setup); - const tx = await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + const { enclave, request, usdcToken } = await loadFixture(setup); + const tx = await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3 = await enclave.getE3(0); await expect(tx) @@ -792,7 +932,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -917,7 +1057,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -953,7 +1093,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -981,7 +1121,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1003,7 +1143,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1037,7 +1177,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1063,7 +1203,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1085,7 +1225,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1113,7 +1253,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1130,7 +1270,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1152,7 +1292,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1179,7 +1319,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1209,7 +1349,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, @@ -1246,7 +1386,7 @@ describe("Enclave", function () { { filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, From 85cf66e40c782b5eb30716aeca0131e4c47fb46c Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 29 Sep 2025 16:41:44 +0500 Subject: [PATCH 08/88] fix: stack too deep error --- .../ICiphernodeRegistry.json | 126 +++++++++++++++++- .../interfaces/IEnclave.sol/IEnclave.json | 84 +++++++++++- .../NaiveRegistryFilter.json | 10 +- .../contracts/slashing/SlashingManager.sol | 52 ++++---- packages/enclave-contracts/hardhat.config.ts | 1 - 5 files changed, 238 insertions(+), 35 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 63ec1552b3..2dbedad471 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -65,6 +65,25 @@ "name": "CiphernodeRemoved", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "name": "CommitteeActivationChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -122,6 +141,19 @@ "name": "EnclaveSet", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "addCiphernode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -141,6 +173,61 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getCommittee", + "outputs": [ + { + "components": [ + { + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + }, + { + "internalType": "uint32[2]", + "name": "threshold", + "type": "uint32[2]" + }, + { + "internalType": "bytes32", + "name": "publicKey", + "type": "bytes32" + } + ], + "internalType": "struct IRegistryFilter.Committee", + "name": "committee", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getFilter", + "outputs": [ + { + "internalType": "address", + "name": "filter", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -160,6 +247,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -183,6 +289,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "siblingNodes", + "type": "uint256[]" + } + ], + "name": "removeCiphernode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -219,5 +343,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_27-8287ebf5be05227965814498ddef24fc9009e2f0" + "buildInfoId": "solc-0_8_27-57dc158663b619d7ef07ffec146d462efa6e9350" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 524089c238..59ab1bb790 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -16,6 +16,19 @@ "name": "AllowedE3ProgramsParamsSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "bondingRegistry", + "type": "address" + } + ], + "name": "BondingRegistrySet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -291,6 +304,19 @@ "name": "PlaintextOutputPublished", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "usdcToken", + "type": "address" + } + ], + "name": "UsdcTokenSet", + "type": "event" + }, { "inputs": [ { @@ -444,6 +470,62 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "filter", + "type": "address" + }, + { + "internalType": "uint32[2]", + "name": "threshold", + "type": "uint32[2]" + }, + { + "internalType": "uint256[2]", + "name": "startWindow", + "type": "uint256[2]" + }, + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "internalType": "contract IE3Program", + "name": "e3Program", + "type": "address" + }, + { + "internalType": "bytes", + "name": "e3ProgramParams", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "computeProviderParams", + "type": "bytes" + } + ], + "internalType": "struct IEnclave.E3RequestParams", + "name": "e3Params", + "type": "tuple" + } + ], + "name": "getE3Quote", + "outputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -704,5 +786,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_27-8287ebf5be05227965814498ddef24fc9009e2f0" + "buildInfoId": "solc-0_8_27-57dc158663b619d7ef07ffec146d462efa6e9350" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json index e41dbde5d7..d72e4fba9f 100644 --- a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +++ b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json @@ -166,7 +166,7 @@ "type": "bytes32" } ], - "internalType": "struct NaiveRegistryFilter.Committee", + "internalType": "struct IRegistryFilter.Committee", "name": "", "type": "tuple" } @@ -299,11 +299,11 @@ "type": "function" } ], - "bytecode": "0x608060405234801561000f575f5ffd5b50604051610f91380380610f9183398101604081905261002e916102f6565b610038828261003f565b5050610327565b5f516020610f715f395f51905f52805468010000000000000000810460ff1615906001600160401b03165f811580156100755750825b90505f826001600160401b031660011480156100905750303b155b90508115801561009e575080155b156100bc5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156100ea57845460ff60401b1916680100000000000000001785555b6100f333610175565b6100fc86610189565b5f516020610f515f395f51905f52546001600160a01b0388811691161461012657610126876101b2565b831561016c57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b61017d6101f1565b6101868161022e565b50565b610191610236565b5f80546001600160a01b0319166001600160a01b0392909216919091179055565b6101ba610236565b6001600160a01b0381166101e857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101868161027e565b5f516020610f715f395f51905f525468010000000000000000900460ff1661022c57604051631afcd79f60e31b815260040160405180910390fd5b565b6101ba6101f1565b336102555f516020610f515f395f51905f52546001600160a01b031690565b6001600160a01b03161461022c5760405163118cdaa760e01b81523360048201526024016101df565b5f516020610f515f395f51905f5280546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80516001600160a01b03811681146102f1575f5ffd5b919050565b5f5f60408385031215610307575f5ffd5b610310836102db565b915061031e602084016102db565b90509250929050565b610c1d806103345f395ff3fe608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca3660046108e3565b6101e8565b6040516100dc91906108fa565b60405180910390f35b6100f86100f33660046109dc565b6102d3565b005b61010d610108366004610a87565b6103be565b60405190151581526020016100dc565b6100f861012b366004610ad2565b61044e565b6100f86105b7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b03565b6105ca565b6100f86101b3366004610b03565b610600565b6101da6101c63660046108e3565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f0610781565b5f828152600160209081526040918290208251815460809381028201840190945260608101848152909391928492849184018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d579050505050505081526020016002820154815250509050919050565b6102db610642565b5f85815260016020526040902060028101541561030b5760405163632a22bb60e01b815260040160405180910390fd5b6103168186866107a7565b508282604051610327929190610b23565b60405190819003812060028301555f546001600160a01b03169063d9bbec959088906103599089908990602001610b32565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103899493929190610b7c565b5f604051808303815f87803b1580156103a0575f5ffd5b505af11580156103b2573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b031633146103e9576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610427576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104449101836002610815565b5060019392505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff165f811580156104985750825b90505f8267ffffffffffffffff1660011480156104b45750303b155b9050811580156104c2575080155b156104e05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051457845468ff00000000000000001916680100000000000000001785555b61051d3361069d565b610526866105ca565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105635761056387610600565b83156105ae57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105bf610642565b6105c85f6106ae565b565b6105d2610642565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610608610642565b6001600160a01b03811661063657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61063f816106ae565b50565b336106747f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105c85760405163118cdaa760e01b815233600482015260240161062d565b6106a561072b565b61063f81610779565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005468010000000000000000900460ff166105c857604051631afcd79f60e31b815260040160405180910390fd5b61060861072b565b60405180606001604052806060815260200161079b6108b1565b81526020015f81525090565b828054828255905f5260205f20908101928215610805579160200282015b8281111561080557815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107c5565b506108119291506108cf565b5090565b600183019183908215610805579160200282015f5b8382111561087457833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261082a565b80156108a45782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610874565b50506108119291506108cf565b60405180604001604052806002906020820280368337509192915050565b5b80821115610811575f81556001016108d0565b5f602082840312156108f3575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b8083101561094b576001600160a01b03845116825260208201915060208401935060018301925061091f565b50602086015192506040850191505f5b600281101561098057835163ffffffff1683526020938401939092019160010161095b565b506040860151608086015280935050505092915050565b5f5f83601f8401126109a7575f5ffd5b50813567ffffffffffffffff8111156109be575f5ffd5b6020830191508360208285010111156109d5575f5ffd5b9250929050565b5f5f5f5f5f606086880312156109f0575f5ffd5b85359450602086013567ffffffffffffffff811115610a0d575f5ffd5b8601601f81018813610a1d575f5ffd5b803567ffffffffffffffff811115610a33575f5ffd5b8860208260051b8401011115610a47575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a6a575f5ffd5b610a7688828901610997565b969995985093965092949392505050565b5f5f60608385031215610a98575f5ffd5b8235915060608301841015610aab575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610acd575f5ffd5b919050565b5f5f60408385031215610ae3575f5ffd5b610aec83610ab7565b9150610afa60208401610ab7565b90509250929050565b5f60208284031215610b13575f5ffd5b610b1c82610ab7565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b72576001600160a01b03610b5d84610ab7565b16825260209283019290910190600101610b44565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea26469706673582212209cae0da68f48355c857d12fda7dedc1a308b96025595fea9c7f98c34a8d61ee664736f6c634300081b00339016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca3660046108e3565b6101e8565b6040516100dc91906108fa565b60405180910390f35b6100f86100f33660046109dc565b6102d3565b005b61010d610108366004610a87565b6103be565b60405190151581526020016100dc565b6100f861012b366004610ad2565b61044e565b6100f86105b7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b03565b6105ca565b6100f86101b3366004610b03565b610600565b6101da6101c63660046108e3565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f0610781565b5f828152600160209081526040918290208251815460809381028201840190945260608101848152909391928492849184018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d579050505050505081526020016002820154815250509050919050565b6102db610642565b5f85815260016020526040902060028101541561030b5760405163632a22bb60e01b815260040160405180910390fd5b6103168186866107a7565b508282604051610327929190610b23565b60405190819003812060028301555f546001600160a01b03169063d9bbec959088906103599089908990602001610b32565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103899493929190610b7c565b5f604051808303815f87803b1580156103a0575f5ffd5b505af11580156103b2573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b031633146103e9576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610427576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104449101836002610815565b5060019392505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff165f811580156104985750825b90505f8267ffffffffffffffff1660011480156104b45750303b155b9050811580156104c2575080155b156104e05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051457845468ff00000000000000001916680100000000000000001785555b61051d3361069d565b610526866105ca565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105635761056387610600565b83156105ae57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105bf610642565b6105c85f6106ae565b565b6105d2610642565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610608610642565b6001600160a01b03811661063657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61063f816106ae565b50565b336106747f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105c85760405163118cdaa760e01b815233600482015260240161062d565b6106a561072b565b61063f81610779565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005468010000000000000000900460ff166105c857604051631afcd79f60e31b815260040160405180910390fd5b61060861072b565b60405180606001604052806060815260200161079b6108b1565b81526020015f81525090565b828054828255905f5260205f20908101928215610805579160200282015b8281111561080557815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107c5565b506108119291506108cf565b5090565b600183019183908215610805579160200282015f5b8382111561087457833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261082a565b80156108a45782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610874565b50506108119291506108cf565b60405180604001604052806002906020820280368337509192915050565b5b80821115610811575f81556001016108d0565b5f602082840312156108f3575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b8083101561094b576001600160a01b03845116825260208201915060208401935060018301925061091f565b50602086015192506040850191505f5b600281101561098057835163ffffffff1683526020938401939092019160010161095b565b506040860151608086015280935050505092915050565b5f5f83601f8401126109a7575f5ffd5b50813567ffffffffffffffff8111156109be575f5ffd5b6020830191508360208285010111156109d5575f5ffd5b9250929050565b5f5f5f5f5f606086880312156109f0575f5ffd5b85359450602086013567ffffffffffffffff811115610a0d575f5ffd5b8601601f81018813610a1d575f5ffd5b803567ffffffffffffffff811115610a33575f5ffd5b8860208260051b8401011115610a47575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a6a575f5ffd5b610a7688828901610997565b969995985093965092949392505050565b5f5f60608385031215610a98575f5ffd5b8235915060608301841015610aab575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610acd575f5ffd5b919050565b5f5f60408385031215610ae3575f5ffd5b610aec83610ab7565b9150610afa60208401610ab7565b90509250929050565b5f60208284031215610b13575f5ffd5b610b1c82610ab7565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b72576001600160a01b03610b5d84610ab7565b16825260209283019290910190600101610b44565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea26469706673582212209cae0da68f48355c857d12fda7dedc1a308b96025595fea9c7f98c34a8d61ee664736f6c634300081b0033", + "bytecode": "0x608060405234801561000f575f5ffd5b5060405161104338038061104383398101604081905261002e916102f6565b610038828261003f565b5050610327565b5f5160206110235f395f51905f52805468010000000000000000810460ff1615906001600160401b03165f811580156100755750825b90505f826001600160401b031660011480156100905750303b155b90508115801561009e575080155b156100bc5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156100ea57845460ff60401b1916680100000000000000001785555b6100f333610175565b6100fc86610189565b5f5160206110035f395f51905f52546001600160a01b0388811691161461012657610126876101b2565b831561016c57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b61017d6101f1565b6101868161022e565b50565b610191610236565b5f80546001600160a01b0319166001600160a01b0392909216919091179055565b6101ba610236565b6001600160a01b0381166101e857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101868161027e565b5f5160206110235f395f51905f525468010000000000000000900460ff1661022c57604051631afcd79f60e31b815260040160405180910390fd5b565b6101ba6101f1565b336102555f5160206110035f395f51905f52546001600160a01b031690565b6001600160a01b03161461022c5760405163118cdaa760e01b81523360048201526024016101df565b5f5160206110035f395f51905f5280546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80516001600160a01b03811681146102f1575f5ffd5b919050565b5f5f60408385031215610307575f5ffd5b610310836102db565b915061031e602084016102db565b90509250929050565b610ccf806103345f395ff3fe608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610908565b6101e8565b6040516100dc919061091f565b60405180910390f35b6100f86100f3366004610a88565b6102f8565b005b61010d610108366004610b6b565b6103d9565b60405190151581526020016100dc565b6100f861012b366004610b9b565b610469565b6100f86105bd565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610bcc565b6105d0565b6100f86101b3366004610bcc565b610606565b6101da6101c6366004610908565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107a4565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610648565b5f8381526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b825161034290829060208601906107ca565b50815160208084019190912060028301555f546040516001600160a01b039091169163d9bbec9591879161037891889101610bec565b604051602081830303815290604052856040518463ffffffff1660e01b81526004016103a693929190610c65565b5f604051808303815f87803b1580156103bd575f5ffd5b505af11580156103cf573d5f5f3e3d5ffd5b5050505050505050565b5f80546001600160a01b03163314610404576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610442576040516334c2a65d60e11b815260040160405180910390fd5b5f83815260016020819052604090912061045f910183600261083a565b5060019392505050565b5f6104726106a3565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f8115801561049e5750825b90505f8267ffffffffffffffff1660011480156104ba5750303b155b9050811580156104c8575080155b156104e65760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051a57845468ff00000000000000001916680100000000000000001785555b610523336106cb565b61052c866105d0565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105695761056987610606565b83156105b457845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105c5610648565b6105ce5f6106dc565b565b6105d8610648565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b61060e610648565b6001600160a01b03811661063c57604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b610645816106dc565b50565b3361067a7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105ce5760405163118cdaa760e01b8152336004820152602401610633565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106d3610759565b6106458161077e565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b610761610786565b6105ce57604051631afcd79f60e31b815260040160405180910390fd5b61060e610759565b5f61078f6106a3565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107be6108d6565b81526020015f81525090565b828054828255905f5260205f2090810192821561082a579160200282015b8281111561082a578251825473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b039091161782556020909201916001909101906107e8565b506108369291506108f4565b5090565b60018301918390821561082a579160200282015f5b8382111561089957833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261084f565b80156108c95782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610899565b50506108369291506108f4565b60405180604001604052806002906020820280368337509192915050565b5b80821115610836575f81556001016108f5565b5f60208284031215610918575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610970576001600160a01b038451168252602082019150602084019350600183019250610944565b50602086015192506040850191505f5b60028110156109a557835163ffffffff16835260209384019390920191600101610980565b506040860151608086015280935050505092915050565b634e487b7160e01b5f52604160045260245ffd5b604051601f8201601f1916810167ffffffffffffffff811182821017156109f9576109f96109bc565b604052919050565b80356001600160a01b0381168114610a17575f5ffd5b919050565b5f82601f830112610a2b575f5ffd5b813567ffffffffffffffff811115610a4557610a456109bc565b610a58601f8201601f19166020016109d0565b818152846020838601011115610a6c575f5ffd5b816020850160208301375f918101602001919091529392505050565b5f5f5f60608486031215610a9a575f5ffd5b83359250602084013567ffffffffffffffff811115610ab7575f5ffd5b8401601f81018613610ac7575f5ffd5b803567ffffffffffffffff811115610ae157610ae16109bc565b8060051b610af1602082016109d0565b91825260208184018101929081019089841115610b0c575f5ffd5b6020850194505b83851015610b3557610b2485610a01565b825260209485019490910190610b13565b95505050506040850135905067ffffffffffffffff811115610b55575f5ffd5b610b6186828701610a1c565b9150509250925092565b5f5f60608385031215610b7c575f5ffd5b8235915060608301841015610b8f575f5ffd5b50926020919091019150565b5f5f60408385031215610bac575f5ffd5b610bb583610a01565b9150610bc360208401610a01565b90509250929050565b5f60208284031215610bdc575f5ffd5b610be582610a01565b9392505050565b602080825282518282018190525f918401906040840190835b81811015610c2c5783516001600160a01b0316835260209384019390920191600101610c05565b509095945050505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b838152606060208201525f610c7d6060830185610c37565b8281036040840152610c8f8185610c37565b969550505050505056fea264697066735822122002870213daa8b2b1ad455a064f9487408ab2f4be0fab8576883c54f9b425485164736f6c634300081b00339016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610908565b6101e8565b6040516100dc919061091f565b60405180910390f35b6100f86100f3366004610a88565b6102f8565b005b61010d610108366004610b6b565b6103d9565b60405190151581526020016100dc565b6100f861012b366004610b9b565b610469565b6100f86105bd565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610bcc565b6105d0565b6100f86101b3366004610bcc565b610606565b6101da6101c6366004610908565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107a4565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610648565b5f8381526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b825161034290829060208601906107ca565b50815160208084019190912060028301555f546040516001600160a01b039091169163d9bbec9591879161037891889101610bec565b604051602081830303815290604052856040518463ffffffff1660e01b81526004016103a693929190610c65565b5f604051808303815f87803b1580156103bd575f5ffd5b505af11580156103cf573d5f5f3e3d5ffd5b5050505050505050565b5f80546001600160a01b03163314610404576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610442576040516334c2a65d60e11b815260040160405180910390fd5b5f83815260016020819052604090912061045f910183600261083a565b5060019392505050565b5f6104726106a3565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f8115801561049e5750825b90505f8267ffffffffffffffff1660011480156104ba5750303b155b9050811580156104c8575080155b156104e65760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051a57845468ff00000000000000001916680100000000000000001785555b610523336106cb565b61052c866105d0565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105695761056987610606565b83156105b457845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105c5610648565b6105ce5f6106dc565b565b6105d8610648565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b61060e610648565b6001600160a01b03811661063c57604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b610645816106dc565b50565b3361067a7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105ce5760405163118cdaa760e01b8152336004820152602401610633565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106d3610759565b6106458161077e565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b610761610786565b6105ce57604051631afcd79f60e31b815260040160405180910390fd5b61060e610759565b5f61078f6106a3565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107be6108d6565b81526020015f81525090565b828054828255905f5260205f2090810192821561082a579160200282015b8281111561082a578251825473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b039091161782556020909201916001909101906107e8565b506108369291506108f4565b5090565b60018301918390821561082a579160200282015f5b8382111561089957833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261084f565b80156108c95782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610899565b50506108369291506108f4565b60405180604001604052806002906020820280368337509192915050565b5b80821115610836575f81556001016108f5565b5f60208284031215610918575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610970576001600160a01b038451168252602082019150602084019350600183019250610944565b50602086015192506040850191505f5b60028110156109a557835163ffffffff16835260209384019390920191600101610980565b506040860151608086015280935050505092915050565b634e487b7160e01b5f52604160045260245ffd5b604051601f8201601f1916810167ffffffffffffffff811182821017156109f9576109f96109bc565b604052919050565b80356001600160a01b0381168114610a17575f5ffd5b919050565b5f82601f830112610a2b575f5ffd5b813567ffffffffffffffff811115610a4557610a456109bc565b610a58601f8201601f19166020016109d0565b818152846020838601011115610a6c575f5ffd5b816020850160208301375f918101602001919091529392505050565b5f5f5f60608486031215610a9a575f5ffd5b83359250602084013567ffffffffffffffff811115610ab7575f5ffd5b8401601f81018613610ac7575f5ffd5b803567ffffffffffffffff811115610ae157610ae16109bc565b8060051b610af1602082016109d0565b91825260208184018101929081019089841115610b0c575f5ffd5b6020850194505b83851015610b3557610b2485610a01565b825260209485019490910190610b13565b95505050506040850135905067ffffffffffffffff811115610b55575f5ffd5b610b6186828701610a1c565b9150509250925092565b5f5f60608385031215610b7c575f5ffd5b8235915060608301841015610b8f575f5ffd5b50926020919091019150565b5f5f60408385031215610bac575f5ffd5b610bb583610a01565b9150610bc360208401610a01565b90509250929050565b5f60208284031215610bdc575f5ffd5b610be582610a01565b9392505050565b602080825282518282018190525f918401906040840190835b81811015610c2c5783516001600160a01b0316835260209384019390920191600101610c05565b509095945050505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b838152606060208201525f610c7d6060830185610c37565b8281036040840152610c8f8185610c37565b969550505050505056fea264697066735822122002870213daa8b2b1ad455a064f9487408ab2f4be0fab8576883c54f9b425485164736f6c634300081b0033", "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/registry/NaiveRegistryFilter.sol", - "buildInfoId": "solc-0_8_27-e09265851ba98e1f1077468e6dbf08ff116b5fb9" -} + "buildInfoId": "solc-0_8_27-57dc158663b619d7ef07ffec146d462efa6e9350" +} \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index ec28c7bacb..51bf2de170 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -38,7 +38,7 @@ contract SlashingManager is ISlashingManager, AccessControl { mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; /// @notice All slash proposals - mapping(uint256 proposalId => SlashProposal proposal) public proposals; + mapping(uint256 proposalId => SlashProposal proposal) internal _proposals; /// @notice Total number of proposals created uint256 public totalProposals; @@ -98,7 +98,7 @@ contract SlashingManager is ISlashingManager, AccessControl { uint256 proposalId ) external view returns (SlashProposal memory) { require(proposalId < totalProposals, InvalidProposal()); - return proposals[proposalId]; + return _proposals[proposalId]; } function isBanned(address node) external view returns (bool) { @@ -176,39 +176,38 @@ contract SlashingManager is ISlashingManager, AccessControl { SlashPolicy storage policy = slashPolicies[reason]; require(policy.enabled, SlashReasonDisabled()); - uint256 nextId = totalProposals; + proposalId = totalProposals; bool proofVerified = false; if (policy.requiresProof) { require(proof.length != 0, ProofRequired()); proofVerified = ISlashVerifier(policy.proofVerifier).verify( - nextId, + proposalId, proof ); require(proofVerified, InvalidProof()); } uint256 executableAt = block.timestamp + policy.appealWindow; - - proposals[nextId] = SlashProposal({ - operator: operator, - reason: reason, - ticketAmount: policy.ticketPenalty, - licenseAmount: policy.licensePenalty, - executedTicket: false, - executedLicense: false, - appealed: false, - resolved: false, - approved: false, - proposedAt: block.timestamp, - executableAt: executableAt, - proposer: msg.sender, - proofHash: keccak256(proof), - proofVerified: proofVerified - }); + SlashProposal storage p = _proposals[proposalId]; + + p.operator = operator; + p.reason = reason; + p.ticketAmount = policy.ticketPenalty; + p.licenseAmount = policy.licensePenalty; + p.executedTicket = false; + p.executedLicense = false; + p.appealed = false; + p.resolved = false; + p.approved = false; + p.proposedAt = block.timestamp; + p.executableAt = executableAt; + p.proposer = msg.sender; + p.proofHash = keccak256(proof); + p.proofVerified = proofVerified; emit SlashProposed( - nextId, + proposalId, operator, reason, policy.ticketPenalty, @@ -217,13 +216,12 @@ contract SlashingManager is ISlashingManager, AccessControl { msg.sender ); - totalProposals = nextId + 1; - return nextId; + totalProposals = proposalId + 1; } function executeSlash(uint256 proposalId) external onlySlasher { require(proposalId < totalProposals, InvalidProposal()); - SlashProposal storage p = proposals[proposalId]; + SlashProposal storage p = _proposals[proposalId]; // Has already been executed? require(!(p.executedTicket && p.executedLicense), AlreadyExecuted()); @@ -287,7 +285,7 @@ contract SlashingManager is ISlashingManager, AccessControl { function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); - SlashProposal storage p = proposals[proposalId]; + SlashProposal storage p = _proposals[proposalId]; // Only the accused can appeal require(msg.sender == p.operator, Unauthorized()); @@ -307,7 +305,7 @@ contract SlashingManager is ISlashingManager, AccessControl { string calldata resolution ) external onlyGovernance { require(proposalId < totalProposals, InvalidProposal()); - SlashProposal storage p = proposals[proposalId]; + SlashProposal storage p = _proposals[proposalId]; require(p.appealed, InvalidProposal()); require(!p.resolved, AlreadyResolved()); diff --git a/packages/enclave-contracts/hardhat.config.ts b/packages/enclave-contracts/hardhat.config.ts index 7cb04b8eb7..0aae4886ef 100644 --- a/packages/enclave-contracts/hardhat.config.ts +++ b/packages/enclave-contracts/hardhat.config.ts @@ -164,7 +164,6 @@ const config: HardhatUserConfig = { enabled: true, runs: 800, }, - viaIR: true, }, }, ], From c9cce4e3268d0c9765e85f1ccce185d49a55f8c3 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 00:06:23 +0500 Subject: [PATCH 09/88] fix: enclave tests --- .../enclave-contracts/contracts/Enclave.sol | 2 +- .../CiphernodeRegistryOwnable.spec.ts | 14 +- .../NaiveRegistryFilter.spec.ts | 16 +- .../enclave-contracts/test/Enclave.spec.ts | 1043 ++++++++--------- 4 files changed, 541 insertions(+), 534 deletions(-) diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index c2ef016c30..13aa52df79 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -511,7 +511,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { function getE3Quote( E3RequestParams calldata ) public pure returns (uint256 fee) { - fee = 1 * 10 ** 18; + fee = 1 * 10 ** 6; } function getDecryptionVerifier( diff --git a/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts index 0f6548e4f3..1acec05b61 100644 --- a/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts @@ -237,14 +237,14 @@ describe("CiphernodeRegistryOwnable", function () { describe("addCiphernode()", function () { it("reverts if the caller is not the owner", async function () { const { registry, notTheOwner } = await loadFixture(setup); - await expect(registry.connect(notTheOwner).addCiphernode(AddressThree)) - .to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount") - .withArgs(await notTheOwner.getAddress()); + await expect( + registry.connect(notTheOwner).addCiphernode(AddressThree), + ).to.be.revertedWithCustomError(registry, "NotOwnerOrBondingRegistry"); }); it("adds the ciphernode to the registry", async function () { const { registry } = await loadFixture(setup); expect(await registry.addCiphernode(AddressThree)); - expect(await registry.isCiphernodeEligible(AddressThree)).to.be.true; + expect(await registry.isEnabled(AddressThree)).to.be.true; }); it("increments numCiphernodes", async function () { const { registry } = await loadFixture(setup); @@ -274,9 +274,7 @@ describe("CiphernodeRegistryOwnable", function () { const { registry, notTheOwner } = await loadFixture(setup); await expect( registry.connect(notTheOwner).removeCiphernode(AddressOne, []), - ) - .to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount") - .withArgs(await notTheOwner.getAddress()); + ).to.be.revertedWithCustomError(registry, "NotOwnerOrBondingRegistry"); }); it("removes the ciphernode from the registry", async function () { const { registry } = await loadFixture(setup); @@ -366,7 +364,7 @@ describe("CiphernodeRegistryOwnable", function () { describe("isCiphernodeEligible()", function () { it("returns true if the ciphernode is in the registry", async function () { const { registry } = await loadFixture(setup); - expect(await registry.isCiphernodeEligible(AddressOne)).to.be.true; + expect(await registry.isEnabled(AddressOne)).to.be.true; }); it("returns false if the ciphernode is not in the registry", async function () { const { registry } = await loadFixture(setup); diff --git a/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts b/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts index af32a57bea..83eeaadd25 100644 --- a/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts +++ b/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts @@ -104,9 +104,19 @@ describe("NaiveRegistryFilter", function () { ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyExists"); }); it("should set the threshold for the requested committee", async function () { - const { filter, owner, request } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - await filter.requestCommittee(request.e3Id, request.threshold); + const { filter, registry, request } = await loadFixture(setup); + await filter.setRegistry(await registry.getAddress()); + + await registry.requestCommittee( + request.e3Id, + await filter.getAddress(), + request.threshold, + ); + + const nodes = [AddressOne, AddressTwo]; + const publicKey = "0x1234567890abcdef"; + await filter.publishCommittee(request.e3Id, nodes, publicKey); + const committee = await filter.getCommittee(request.e3Id); expect(committee.threshold).to.deep.equal(request.threshold); }); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 66f95b016c..5e8479f25b 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -9,10 +9,10 @@ import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; import BondingRegistryModule from "../ignition/modules/bondingRegistry"; +import CiphernodeRegistryModule from "../ignition/modules/ciphernodeRegistry"; import EnclaveModule from "../ignition/modules/enclave"; import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../ignition/modules/enclaveToken"; -import MockCiphernodeRegistryModule from "../ignition/modules/mockCiphernodeRegistry"; import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; import mockComputeProviderModule from "../ignition/modules/mockComputeProvider"; import MockDecryptionVerifierModule from "../ignition/modules/mockDecryptionVerifier"; @@ -78,20 +78,14 @@ describe("Enclave", function () { const ownerAddress = await owner.getAddress(); - // Deploy PoseidonT3 library first - const poseidonFactory = await ethers.getContractFactory("PoseidonT3"); - const poseidonDeployment = await poseidonFactory.deploy(); - - // Deploy USDC mock const usdcContract = await ignition.deploy(MockStableTokenModule, { parameters: { MockUSDC: { - initialSupply: 1000000, // 1M USDC (with 6 decimals) + initialSupply: 1000000, }, }, }); - // Deploy ENCL token const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { parameters: { EnclaveToken: { @@ -100,34 +94,31 @@ describe("Enclave", function () { }, }); - // Deploy EnclaveTicketToken const ticketTokenContract = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { EnclaveTicketToken: { underlyingUSDC: await usdcContract.mockUSDC.getAddress(), - registry: addressOne, // temporary, will be updated + registry: addressOne, owner: ownerAddress, }, }, }, ); - // Deploy SlashingManager const slashingManagerContract = await ignition.deploy( SlashingManagerModule, { parameters: { SlashingManager: { admin: ownerAddress, - bondingRegistry: addressOne, // temporary, will be updated + bondingRegistry: addressOne, }, }, }, ); - // Deploy BondingRegistry const bondingRegistryContract = await ignition.deploy( BondingRegistryModule, { @@ -137,12 +128,12 @@ describe("Enclave", function () { ticketToken: await ticketTokenContract.enclaveTicketToken.getAddress(), licenseToken: await enclTokenContract.enclaveToken.getAddress(), - registry: addressOne, // will be updated when ciphernode registry is ready + registry: addressOne, slashedFundsTreasury: ownerAddress, - ticketPrice: ethers.parseEther("10"), // 10 USDC per ticket (scaled to 18 decimals for calculation) - licenseRequiredBond: ethers.parseEther("1000"), // 1000 ENCL required - minTicketBalance: 5, // minimum 5 tickets - exitDelay: 7 * 24 * 60 * 60, // 7 days in seconds + ticketPrice: ethers.parseEther("10"), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: 7 * 24 * 60 * 60, }, }, }, @@ -154,7 +145,7 @@ describe("Enclave", function () { params: encodedE3ProgramParams, owner: ownerAddress, maxDuration: THIRTY_DAYS_IN_SECONDS, - registry: addressOne, // will be updated when ciphernode registry is ready + registry: addressOne, bondingRegistry: await bondingRegistryContract.bondingRegistry.getAddress(), usdcToken: await usdcContract.mockUSDC.getAddress(), @@ -164,18 +155,17 @@ describe("Enclave", function () { const enclaveAddress = await enclaveContract.enclave.getAddress(); - // Deploy MockCiphernodeRegistry manually - const ciphernodeRegistryFactory = await ethers.getContractFactory( - "MockCiphernodeRegistry", - { - libraries: { - PoseidonT3: await poseidonDeployment.getAddress(), + const ciphernodeRegistry = await ignition.deploy(CiphernodeRegistryModule, { + parameters: { + CiphernodeRegistry: { + enclaveAddress: enclaveAddress, + owner: ownerAddress, }, }, - ); - const ciphernodeRegistryContract = await ciphernodeRegistryFactory.deploy(); + }); + const ciphernodeRegistryAddress = - await ciphernodeRegistryContract.getAddress(); + await ciphernodeRegistry.cipherNodeRegistry.getAddress(); const naiveRegistryFilter = await ignition.deploy( NaiveRegistryFilterModule, @@ -193,11 +183,10 @@ describe("Enclave", function () { await naiveRegistryFilter.naiveRegistryFilter.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); - const ciphernodeRegistryOwnableContract = - CiphernodeRegistryOwnableFactory.connect( - ciphernodeRegistryAddress, - owner, - ); + const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( + ciphernodeRegistryAddress, + owner, + ); const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( naiveRegistryFilterAddress, owner, @@ -208,7 +197,6 @@ describe("Enclave", function () { await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); } - // Update contract references with actual addresses await ticketTokenContract.enclaveTicketToken.setRegistry( await bondingRegistryContract.bondingRegistry.getAddress(), ); @@ -222,6 +210,18 @@ describe("Enclave", function () { await bondingRegistryContract.bondingRegistry.getAddress(), ); + await bondingRegistryContract.bondingRegistry.setRewardDistributor( + enclaveAddress, + ); + + const tree = new LeanIMT(hash); + + const testCiphernodes = [addressOne, AddressTwo]; + for (const ciphernodeAddress of testCiphernodes) { + await ciphernodeRegistryContract.addCiphernode(ciphernodeAddress); + tree.insert(BigInt(ciphernodeAddress)); + } + const mockComputeProvider = await ignition.deploy( mockComputeProviderModule, ); @@ -264,15 +264,14 @@ describe("Enclave", function () { ), }; - // Setup initial token balances await usdcContract.mockUSDC.mint( ownerAddress, - ethers.parseUnits("10000", 6), - ); // 10k USDC + ethers.parseUnits("1000000", 6), + ); await usdcContract.mockUSDC.mint( await notTheOwner.getAddress(), - ethers.parseUnits("10000", 6), - ); // 10k USDC + ethers.parseUnits("1000000", 6), + ); await enclTokenContract.enclaveToken.mintAllocation( ownerAddress, ethers.parseEther("10000"), @@ -286,13 +285,14 @@ describe("Enclave", function () { return { enclave, - ciphernodeRegistryContract: ciphernodeRegistryOwnableContract, + ciphernodeRegistryContract, naiveRegistryFilterContract, bondingRegistry: bondingRegistryContract.bondingRegistry, ticketToken: ticketTokenContract.enclaveTicketToken, licenseToken: enclTokenContract.enclaveToken, usdcToken: usdcContract.mockUSDC, slashingManager: slashingManagerContract.slashingManager, + tree, mocks: { decryptionVerifier: decryptionVerifier.mockDecryptionVerifier, inputValidator: inputValidator.mockInputValidator, @@ -471,21 +471,23 @@ describe("Enclave", function () { }); it("returns correct E3 details", async function () { - const { enclave, request, mocks, naiveRegistryFilterContract } = - await loadFixture(setup); + const { + enclave, + request, + mocks, + naiveRegistryFilterContract, + usdcToken, + } = await loadFixture(setup); - await enclave.request( - { - filter: await naiveRegistryFilterContract.getAddress(), - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: await naiveRegistryFilterContract.getAddress(), + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3 = await enclave.getE3(0); @@ -723,7 +725,7 @@ describe("Enclave", function () { describe("request()", function () { it("reverts if USDC allowance is insufficient", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await expect( enclave.request({ filter: request.filter, @@ -734,7 +736,7 @@ describe("Enclave", function () { e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, }), - ).to.be.revertedWithCustomError(enclave, "PaymentRequired"); + ).to.be.revertedWithCustomError(usdcToken, "ERC20InsufficientAllowance"); }); it("reverts if threshold is 0", async function () { const { enclave, request, usdcToken, owner } = await loadFixture(setup); @@ -852,21 +854,16 @@ describe("Enclave", function () { }); it("reverts if committee selection fails", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); + + const invalidFilterAddress = "0x0000000000000000000000000000000000000002"; + await expect( - enclave.request( - { - filter: AddressTwo, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "CommitteeSelectionFailed"); + makeRequest(enclave, usdcToken, { + ...request, + filter: invalidFilterAddress, + }), + ).to.be.revert(ethers); }); it("instantiates a new E3", async function () { const { enclave, request, mocks, usdcToken } = await loadFixture(setup); @@ -926,145 +923,131 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if E3 has already been activated", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); + + await naiveRegistryFilterContract.publishCommittee( + 0, + [addressOne, AddressTwo], + data, ); await expect(enclave.getE3(0)).to.not.be.revert(ethers); - await expect(enclave.activate(0, ethers.ZeroHash)).to.not.be.revert( - ethers, - ); - await expect(enclave.activate(0, ethers.ZeroHash)) + await expect(enclave.activate(0, data)).to.not.be.revert(ethers); + await expect(enclave.activate(0, data)) .to.be.revertedWithCustomError(enclave, "E3AlreadyActivated") .withArgs(0); }); it("reverts if E3 is not yet ready to start", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const startTime = [ (await time.latest()) + 1000, (await time.latest()) + 2000, ] as [number, number]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: startTime, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); await expect( enclave.activate(0, ethers.ZeroHash), ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request } = await loadFixture(setup); - const startTime = [ - (await time.latest()) + 1, - (await time.latest()) + 1000, - ] as [number, number]; + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); + const e3Id = 0; + const currentTime = await time.latest(); + const startTime = [currentTime + 10, currentTime + 100] as [ + number, + number, + ]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: startTime, + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); await mine(2, { interval: 2000 }); - await expect( - enclave.activate(0, ethers.ZeroHash), - ).to.be.revertedWithCustomError(enclave, "E3Expired"); + await expect(enclave.activate(e3Id, data)).to.be.revertedWithCustomError( + enclave, + "E3Expired", + ); }); it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const startTime = [ (await time.latest()) + 1000, (await time.latest()) + 2000, ] as [number, number]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: startTime, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); await expect( enclave.activate(0, ethers.ZeroHash), ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request } = await loadFixture(setup); - const startTime = [await time.latest(), (await time.latest()) + 1] as [ - number, - number, - ]; + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); + const e3Id = 0; + const currentTime = await time.latest(); + const startTime = [currentTime + 5, currentTime + 50] as [number, number]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: startTime, + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await mine(1, { interval: 1000 }); + await time.increaseTo(currentTime + request.duration + 100); - await expect( - enclave.activate(0, ethers.ZeroHash), - ).to.be.revertedWithCustomError(enclave, "E3Expired"); + await expect(enclave.activate(e3Id, data)).to.be.revertedWithCustomError( + enclave, + "E3Expired", + ); }); it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request, naiveRegistryFilterContract } = + const { enclave, request, naiveRegistryFilterContract, usdcToken } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, request); const prevRegistry = await enclave.ciphernodeRegistry(); @@ -1080,81 +1063,102 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "CommitteeSelectionFailed"); await enclave.setCiphernodeRegistry(prevRegistry); - await expect(enclave.activate(0, ethers.ZeroHash)).not.to.be.revert( - ethers, + await naiveRegistryFilterContract.setRegistry(prevRegistry); + + await naiveRegistryFilterContract.publishCommittee( + 0, + [addressOne, AddressTwo], + data, ); + + await expect(enclave.activate(0, data)).not.to.be.revert(ethers); }); it("sets committeePublicKey correctly", async () => { - const { enclave, request, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + ciphernodeRegistryContract, + usdcToken, + naiveRegistryFilterContract, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3Id = 0; + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, + ); + const publicKey = await ciphernodeRegistryContract.committeePublicKey(e3Id); let e3 = await enclave.getE3(e3Id); - expect(e3.committeePublicKey).to.not.equal(ethers.keccak256(publicKey)); + expect(e3.committeePublicKey).to.not.equal(publicKey); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); e3 = await enclave.getE3(e3Id); expect(e3.committeePublicKey).to.equal(publicKey); }); it("returns true if E3 is activated successfully", async () => { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3Id = 0; - expect( - await enclave.activate.staticCall(e3Id, ethers.ZeroHash), - ).to.be.equal(true); + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, + ); + + expect(await enclave.activate.staticCall(e3Id, data)).to.be.equal(true); }); it("emits E3Activated event", async () => { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3Id = 0; - await expect(enclave.activate(e3Id, ethers.ZeroHash)).to.emit( + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, + ); + + await expect(enclave.activate(e3Id, data)).to.emit( enclave, "E3Activated", ); @@ -1171,70 +1175,71 @@ describe("Enclave", function () { }); it("reverts if E3 has not been activated", async function () { - const { enclave, request } = await loadFixture(setup); - - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); - - const inputData = abiCoder.encode(["bytes32"], [ethers.ZeroHash]); + const { enclave, request, usdcToken } = await loadFixture(setup); - await expect(enclave.getE3(0)).to.not.be.revert(ethers); - await expect(enclave.publishInput(0, inputData)) - .to.be.revertedWithCustomError(enclave, "E3NotActivated") - .withArgs(0); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); + + const inputData = abiCoder.encode(["bytes32"], [ethers.ZeroHash]); - await enclave.activate(0, ethers.ZeroHash); + await expect(enclave.getE3(0)).to.not.be.revert(ethers); + await expect(enclave.publishInput(0, inputData)) + .to.be.revertedWithCustomError(enclave, "E3NotActivated") + .withArgs(0); }); it("reverts if input is not valid", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); - await enclave.activate(0, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + 0, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(0, data); await expect( enclave.publishInput(0, "0xaabbcc"), ).to.be.revertedWithCustomError(enclave, "InvalidInput"); }); it("reverts if outside of input window", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); - await enclave.activate(0, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + 0, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(0, data); await mine(2, { interval: request.duration }); @@ -1246,62 +1251,67 @@ describe("Enclave", function () { it("it allows publishing input to different requests", async function () { const fixtureSetup = () => setup(); - const { enclave, request } = await loadFixture(fixtureSetup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(fixtureSetup); const inputData = "0x12345678"; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); - await enclave.activate(0, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + 0, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(0, data); await enclave.publishInput(0, inputData); - // Make a new request, activate and call - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); - await enclave.activate( + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); + + await naiveRegistryFilterContract.publishCommittee( 1, - "0x0000000000000000000000000000000000000000000000000000000000000001", + [addressOne, AddressTwo], + data, ); + await enclave.activate(1, data); await enclave.publishInput(1, inputData); }); it("returns true if input is published successfully", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const inputData = "0x12345678"; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); - await enclave.activate(0, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + 0, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(0, data); expect(await enclave.publishInput.staticCall(0, inputData)).to.equal( true, @@ -1309,28 +1319,30 @@ describe("Enclave", function () { }); it("adds inputHash to merkle tree", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); - // To create an instance of a LeanIMT, you must provide the hash function. const tree = new LeanIMT(hash); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3Id = 0; - await enclave.activate(e3Id, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(e3Id, data); tree.insert(hash(BigInt(ethers.keccak256(inputData)), BigInt(0))); @@ -1343,25 +1355,28 @@ describe("Enclave", function () { expect(await enclave.getInputRoot(e3Id)).to.equal(tree.root); }); it("emits InputPublished event", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); const e3Id = 0; const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); - await enclave.activate(e3Id, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(e3Id, data); const expectedHash = hash(BigInt(ethers.keccak256(inputData)), BigInt(0)); await expect(enclave.publishInput(e3Id, inputData)) @@ -1379,67 +1394,64 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if E3 has not been activated", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); await expect(enclave.publishCiphertextOutput(e3Id, "0x", "0x")) .to.be.revertedWithCustomError(enclave, "E3NotActivated") .withArgs(e3Id); }); it("reverts if input deadline has not passed", async function () { - const { enclave, request } = await loadFixture(setup); - const tx = await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); - const block = await tx.getBlock(); - const timestamp = block ? block.timestamp : await time.latest(); - const expectedExpiration = timestamp + request.duration + 1; + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); + const currentTime = await time.latest(); + const tx = await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [currentTime, currentTime + 100], + }); const e3Id = 0; - await enclave.activate(e3Id, ethers.ZeroHash); + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, + ); + await enclave.activate(e3Id, data); - await expect(enclave.publishCiphertextOutput(e3Id, "0x", "0x")) - .to.be.revertedWithCustomError(enclave, "InputDeadlineNotPassed") - .withArgs(e3Id, expectedExpiration); + await expect( + enclave.publishCiphertextOutput(e3Id, "0x", "0x"), + ).to.be.revertedWithCustomError(enclave, "InputDeadlineNotPassed"); }); it("reverts if output has already been published", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: [await time.latest(), (await time.latest()) + 100], + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); expect(await enclave.publishCiphertextOutput(e3Id, data, proof)); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) @@ -1450,88 +1462,89 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + filter: request.filter, + threshold: request.threshold, + startWindow: [await time.latest(), (await time.latest()) + 100], + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await expect( enclave.publishCiphertextOutput(e3Id, "0x", "0x"), ).to.be.revertedWithCustomError(enclave, "InvalidOutput"); }); it("sets ciphertextOutput correctly", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); expect(await enclave.publishCiphertextOutput(e3Id, data, proof)); const e3 = await enclave.getE3(e3Id); expect(e3.ciphertextOutput).to.equal(dataHash); }); it("returns true if output is published successfully", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); expect( await enclave.publishCiphertextOutput.staticCall(e3Id, data, proof), ).to.equal(true); }); it("emits CiphertextOutputPublished event", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) .to.emit(enclave, "CiphertextOutputPublished") @@ -1549,63 +1562,53 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if E3 has not been activated", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) .to.be.revertedWithCustomError(enclave, "E3NotActivated") .withArgs(e3Id); }); it("reverts if ciphertextOutput has not been published", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) .to.be.revertedWithCustomError(enclave, "CiphertextOutputNotPublished") .withArgs(e3Id); }); it("reverts if plaintextOutput has already been published", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await enclave.publishPlaintextOutput(e3Id, data, proof); @@ -1617,22 +1620,21 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) @@ -1640,22 +1642,21 @@ describe("Enclave", function () { .withArgs(data); }); it("sets plaintextOutput correctly", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); expect(await enclave.publishPlaintextOutput(e3Id, data, proof)); @@ -1664,22 +1665,21 @@ describe("Enclave", function () { expect(e3.plaintextOutput).to.equal(data); }); it("returns true if output is published successfully", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); expect( @@ -1687,22 +1687,21 @@ describe("Enclave", function () { ).to.equal(true); }); it("emits PlaintextOutputPublished event", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken, naiveRegistryFilterContract } = + await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await naiveRegistryFilterContract.publishCommittee( + e3Id, + [addressOne, AddressTwo], + data, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(await enclave.publishPlaintextOutput(e3Id, data, proof)) From 6089fa52e3cb6be9fc045380335ec65c4d5fc8c4 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 14:30:10 +0500 Subject: [PATCH 10/88] feat: deployment scripts --- .../scripts/deployAndSave/bondingRegistry.ts | 140 ++ .../ciphernodeRegistryOwnable.ts | 1 + .../scripts/deployAndSave/enclave.ts | 21 +- .../deployAndSave/enclaveTicketToken.ts | 101 ++ .../scripts/deployAndSave/enclaveToken.ts | 85 ++ .../scripts/deployAndSave/mockStableToken.ts | 84 ++ .../scripts/deployAndSave/slashingManager.ts | 95 ++ .../scripts/deployEnclave.ts | 132 +- packages/enclave-contracts/scripts/index.ts | 5 + .../test/BondingRegistry.spec.ts | 126 +- .../enclave-contracts/test/Enclave.spec.ts | 34 +- .../test/SlashingManager.spec.ts | 1193 +++++++++++++++++ 12 files changed, 1878 insertions(+), 139 deletions(-) create mode 100644 packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts create mode 100644 packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts create mode 100644 packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts create mode 100644 packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts create mode 100644 packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts create mode 100644 packages/enclave-contracts/test/SlashingManager.spec.ts diff --git a/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts b/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts new file mode 100644 index 0000000000..59d3c9d2b6 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import { + BondingRegistry, + BondingRegistry__factory as BondingRegistryFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveBondingRegistry function + */ +export interface BondingRegistryArgs { + owner?: string; + ticketToken?: string; + licenseToken?: string; + registry?: string; + slashedFundsTreasury?: string; + ticketPrice?: string; + licenseRequiredBond?: string; + minTicketBalance?: number; + exitDelay?: number; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the BondingRegistry contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed BondingRegistry contract + */ +export const deployAndSaveBondingRegistry = async ({ + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + hre, +}: BondingRegistryArgs): Promise<{ + bondingRegistry: BondingRegistry; +}> => { + const { ignition, ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("BondingRegistry", chain); + + if ( + !owner || + !ticketToken || + !licenseToken || + !registry || + !slashedFundsTreasury || + !ticketPrice || + !licenseRequiredBond || + minTicketBalance === undefined || + exitDelay === undefined || + (preDeployedArgs?.constructorArgs?.owner === owner && + preDeployedArgs?.constructorArgs?.ticketToken === ticketToken && + preDeployedArgs?.constructorArgs?.licenseToken === licenseToken && + preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.slashedFundsTreasury === + slashedFundsTreasury && + preDeployedArgs?.constructorArgs?.ticketPrice === ticketPrice && + preDeployedArgs?.constructorArgs?.licenseRequiredBond === + licenseRequiredBond && + preDeployedArgs?.constructorArgs?.minTicketBalance === + minTicketBalance.toString() && + preDeployedArgs?.constructorArgs?.exitDelay === exitDelay.toString()) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "BondingRegistry address not found, it must be deployed first", + ); + } + const bondingRegistryContract = BondingRegistryFactory.connect( + preDeployedArgs.address, + signer, + ); + return { bondingRegistry: bondingRegistryContract }; + } + + const bondingRegistry = await ignition.deploy(BondingRegistryModule, { + parameters: { + BondingRegistry: { + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + }, + }, + }); + + await bondingRegistry.bondingRegistry.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const bondingRegistryAddress = + await bondingRegistry.bondingRegistry.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance: minTicketBalance.toString(), + exitDelay: exitDelay.toString(), + }, + blockNumber, + address: bondingRegistryAddress, + }, + "BondingRegistry", + chain, + ); + + const bondingRegistryContract = BondingRegistryFactory.connect( + bondingRegistryAddress, + signer, + ); + + return { bondingRegistry: bondingRegistryContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts index 7154c3638e..a2b5f6a337 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts @@ -85,6 +85,7 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, + signer, ); return { ciphernodeRegistry: ciphernodeRegistryContract }; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index dd0c5440b2..f3e2df9a33 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -17,6 +17,8 @@ export interface EnclaveArgs { owner?: string; maxDuration?: string; registry?: string; + bondingRegistry?: string; + usdcToken?: string; hre: HardhatRuntimeEnvironment; } @@ -30,6 +32,8 @@ export const deployAndSaveEnclave = async ({ owner, maxDuration, registry, + bondingRegistry, + usdcToken, hre, }: EnclaveArgs): Promise<{ enclave: Enclave }> => { const { ignition, ethers } = await hre.network.connect(); @@ -44,10 +48,14 @@ export const deployAndSaveEnclave = async ({ !owner || !maxDuration || !registry || + !bondingRegistry || + !usdcToken || (preDeployedArgs?.constructorArgs?.params === params && preDeployedArgs?.constructorArgs?.owner === owner && preDeployedArgs?.constructorArgs?.maxDuration === maxDuration && - preDeployedArgs?.constructorArgs?.registry === registry) + preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.usdcToken === usdcToken) ) { if (!preDeployedArgs?.address) { throw new Error("Enclave address not found, it must be deployed first"); @@ -66,6 +74,8 @@ export const deployAndSaveEnclave = async ({ owner, maxDuration, registry, + bondingRegistry, + usdcToken, }, }, }); @@ -77,7 +87,14 @@ export const deployAndSaveEnclave = async ({ storeDeploymentArgs( { - constructorArgs: { params, owner, maxDuration, registry }, + constructorArgs: { + params, + owner, + maxDuration, + registry, + bondingRegistry, + usdcToken, + }, blockNumber, address: enclaveAddress, }, diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts new file mode 100644 index 0000000000..22a433f3d5 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import { + EnclaveTicketToken, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveEnclaveTicketToken function + */ +export interface EnclaveTicketTokenArgs { + underlyingUSDC?: string; + registry?: string; + owner?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the EnclaveTicketToken contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed EnclaveTicketToken contract + */ +export const deployAndSaveEnclaveTicketToken = async ({ + underlyingUSDC, + registry, + owner, + hre, +}: EnclaveTicketTokenArgs): Promise<{ + enclaveTicketToken: EnclaveTicketToken; +}> => { + const { ignition, ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("EnclaveTicketToken", chain); + + if ( + !underlyingUSDC || + !registry || + !owner || + (preDeployedArgs?.constructorArgs?.underlyingUSDC === underlyingUSDC && + preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.owner === owner) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "EnclaveTicketToken address not found, it must be deployed first", + ); + } + const enclaveTicketTokenContract = EnclaveTicketTokenFactory.connect( + preDeployedArgs.address, + signer, + ); + return { enclaveTicketToken: enclaveTicketTokenContract }; + } + + const enclaveTicketToken = await ignition.deploy(EnclaveTicketTokenModule, { + parameters: { + EnclaveTicketToken: { + underlyingUSDC, + registry, + owner, + }, + }, + }); + + await enclaveTicketToken.enclaveTicketToken.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const enclaveTicketTokenAddress = + await enclaveTicketToken.enclaveTicketToken.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + underlyingUSDC, + registry, + owner, + }, + blockNumber, + address: enclaveTicketTokenAddress, + }, + "EnclaveTicketToken", + chain, + ); + + const enclaveTicketTokenContract = EnclaveTicketTokenFactory.connect( + enclaveTicketTokenAddress, + signer, + ); + + return { enclaveTicketToken: enclaveTicketTokenContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts new file mode 100644 index 0000000000..27e435b4e6 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import { + EnclaveToken, + EnclaveToken__factory as EnclaveTokenFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveEnclaveToken function + */ +export interface EnclaveTokenArgs { + owner?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the EnclaveToken contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed EnclaveToken contract + */ +export const deployAndSaveEnclaveToken = async ({ + owner, + hre, +}: EnclaveTokenArgs): Promise<{ + enclaveToken: EnclaveToken; +}> => { + const { ignition, ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("EnclaveToken", chain); + + if (!owner || preDeployedArgs?.constructorArgs?.owner === owner) { + if (!preDeployedArgs?.address) { + throw new Error( + "EnclaveToken address not found, it must be deployed first", + ); + } + const enclaveTokenContract = EnclaveTokenFactory.connect( + preDeployedArgs.address, + signer, + ); + return { enclaveToken: enclaveTokenContract }; + } + + const enclaveToken = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner, + }, + }, + }); + + await enclaveToken.enclaveToken.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const enclaveTokenAddress = await enclaveToken.enclaveToken.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + owner, + }, + blockNumber, + address: enclaveTokenAddress, + }, + "EnclaveToken", + chain, + ); + + const enclaveTokenContract = EnclaveTokenFactory.connect( + enclaveTokenAddress, + signer, + ); + + return { enclaveToken: enclaveTokenContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts b/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts new file mode 100644 index 0000000000..6a4db972b0 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import { MockUSDC, MockUSDC__factory as MockUSDCFactory } from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveMockStableToken function + */ +export interface MockStableTokenArgs { + initialSupply?: number; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the MockStableToken contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed MockStableToken contract + */ +export const deployAndSaveMockStableToken = async ({ + initialSupply, + hre, +}: MockStableTokenArgs): Promise<{ + mockStableToken: MockUSDC; +}> => { + const { ignition, ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("MockUSDC", chain); + + if ( + initialSupply === undefined || + preDeployedArgs?.constructorArgs?.initialSupply === + initialSupply?.toString() + ) { + if (!preDeployedArgs?.address) { + throw new Error("MockUSDC address not found, it must be deployed first"); + } + const mockStableTokenContract = MockUSDCFactory.connect( + preDeployedArgs.address, + signer, + ); + return { mockStableToken: mockStableTokenContract }; + } + + const mockStableToken = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply, + }, + }, + }); + + await mockStableToken.mockUSDC.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const mockStableTokenAddress = await mockStableToken.mockUSDC.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + initialSupply: initialSupply?.toString(), + }, + blockNumber, + address: mockStableTokenAddress, + }, + "MockUSDC", + chain, + ); + + const mockStableTokenContract = MockUSDCFactory.connect( + mockStableTokenAddress, + signer, + ); + + return { mockStableToken: mockStableTokenContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts new file mode 100644 index 0000000000..a294559c00 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { + SlashingManager, + SlashingManager__factory as SlashingManagerFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveSlashingManager function + */ +export interface SlashingManagerArgs { + admin?: string; + bondingRegistry?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the SlashingManager contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed SlashingManager contract + */ +export const deployAndSaveSlashingManager = async ({ + admin, + bondingRegistry, + hre, +}: SlashingManagerArgs): Promise<{ + slashingManager: SlashingManager; +}> => { + const { ignition, ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("SlashingManager", chain); + + if ( + !admin || + !bondingRegistry || + (preDeployedArgs?.constructorArgs?.admin === admin && + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "SlashingManager address not found, it must be deployed first", + ); + } + const slashingManagerContract = SlashingManagerFactory.connect( + preDeployedArgs.address, + signer, + ); + return { slashingManager: slashingManagerContract }; + } + + const slashingManager = await ignition.deploy(SlashingManagerModule, { + parameters: { + SlashingManager: { + admin, + bondingRegistry, + }, + }, + }); + + await slashingManager.slashingManager.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const slashingManagerAddress = + await slashingManager.slashingManager.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + admin, + bondingRegistry, + }, + blockNumber, + address: slashingManagerAddress, + }, + "SlashingManager", + chain, + ); + + const slashingManagerContract = SlashingManagerFactory.connect( + slashingManagerAddress, + signer, + ); + + return { slashingManager: slashingManagerContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index de9d52c166..bf0ac8f5cc 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -5,9 +5,14 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import hre from "hardhat"; +import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; import { deployAndSaveEnclave } from "./deployAndSave/enclave"; +import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; +import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; +import { deployAndSaveMockStableToken } from "./deployAndSave/mockStableToken"; import { deployAndSaveNaiveRegistryFilter } from "./deployAndSave/naiveRegistryFilter"; +import { deployAndSaveSlashingManager } from "./deployAndSave/slashingManager"; import { deployMocks } from "./deployMocks"; /** @@ -32,51 +37,123 @@ export const deployEnclave = async (withMocks?: boolean) => { const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; const addressOne = "0x0000000000000000000000000000000000000001"; - const { enclave } = await deployAndSaveEnclave({ - params: encoded, + const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; + let usdcTokenAddress: string; + + if (shouldDeployMocks) { + console.log("Deploying mock USDC token..."); + const { mockStableToken } = await deployAndSaveMockStableToken({ + initialSupply: 1000000, + hre, + }); + usdcTokenAddress = await mockStableToken.getAddress(); + console.log("MockUSDC deployed to:", usdcTokenAddress); + } else { + throw new Error( + "USDC token address must be provided for production deployment", + ); + } + + console.log("Deploying ENCL token..."); + const { enclaveToken } = await deployAndSaveEnclaveToken({ owner: ownerAddress, - maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), + hre, + }); + const enclaveTokenAddress = await enclaveToken.getAddress(); + console.log("EnclaveToken deployed to:", enclaveTokenAddress); + + console.log("Deploying EnclaveTicketToken..."); + const { enclaveTicketToken } = await deployAndSaveEnclaveTicketToken({ + underlyingUSDC: usdcTokenAddress, registry: addressOne, + owner: ownerAddress, hre, }); + const enclaveTicketTokenAddress = await enclaveTicketToken.getAddress(); + console.log("EnclaveTicketToken deployed to:", enclaveTicketTokenAddress); - const enclaveAddress = await enclave.getAddress(); + console.log("Deploying SlashingManager..."); + const { slashingManager } = await deployAndSaveSlashingManager({ + admin: ownerAddress, + bondingRegistry: addressOne, + hre, + }); + const slashingManagerAddress = await slashingManager.getAddress(); + console.log("SlashingManager deployed to:", slashingManagerAddress); - console.log("Enclave deployed to: ", enclaveAddress); + console.log("Deploying BondingRegistry..."); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ + owner: ownerAddress, + ticketToken: enclaveTicketTokenAddress, + licenseToken: enclaveTokenAddress, + registry: addressOne, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseUnits("10", 6).toString(), + licenseRequiredBond: ethers.parseEther("1000").toString(), + minTicketBalance: 1, + exitDelay: 7 * 24 * 60 * 60, + hre, + }); + const bondingRegistryAddress = await bondingRegistry.getAddress(); + console.log("BondingRegistry deployed to:", bondingRegistryAddress); + console.log("Deploying CiphernodeRegistry..."); const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ - enclaveAddress: enclaveAddress, + enclaveAddress: addressOne, owner: ownerAddress, hre, }); - const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); + console.log("CiphernodeRegistry deployed to:", ciphernodeRegistryAddress); - console.log("CiphernodeRegistry deployed to: ", ciphernodeRegistryAddress); + console.log("Deploying Enclave..."); + const { enclave } = await deployAndSaveEnclave({ + params: encoded, + owner: ownerAddress, + maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), + registry: ciphernodeRegistryAddress, + bondingRegistry: bondingRegistryAddress, + usdcToken: usdcTokenAddress, + hre, + }); + const enclaveAddress = await enclave.getAddress(); + console.log("Enclave deployed to:", enclaveAddress); + console.log("Deploying NaiveRegistryFilter..."); const { naiveRegistryFilter } = await deployAndSaveNaiveRegistryFilter({ ciphernodeRegistryAddress: ciphernodeRegistryAddress, owner: ownerAddress, hre, }); - const naiveRegistryFilterAddress = await naiveRegistryFilter.getAddress(); + console.log("NaiveRegistryFilter deployed to:", naiveRegistryFilterAddress); - console.log("NaiveRegistryFilter deployed to: ", naiveRegistryFilterAddress); + /////////////////////////////////////////// + // Configure cross-contract dependencies + /////////////////////////////////////////// - const registryAddress = await enclave.ciphernodeRegistry(); + console.log("Configuring cross-contract dependencies..."); - if (registryAddress === ciphernodeRegistryAddress) { - console.log(`Enclave contract already has registry`); - } else { - const tx = await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); - await tx.wait(); + console.log("Setting Enclave address in CiphernodeRegistry..."); + await ciphernodeRegistry.setEnclave(enclaveAddress); - console.log(`Enclave contract updated with registry`); - } + console.log("Setting BondingRegistry address in CiphernodeRegistry..."); + await ciphernodeRegistry.setBondingRegistry(bondingRegistryAddress); - // Deploy mocks only if specified - const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; + console.log("Setting BondingRegistry address in EnclaveTicketToken..."); + await enclaveTicketToken.setRegistry(bondingRegistryAddress); + + console.log("Setting CiphernodeRegistry address in BondingRegistry..."); + await bondingRegistry.setRegistry(ciphernodeRegistryAddress); + + console.log("Setting BondingRegistry address in SlashingManager..."); + await slashingManager.setBondingRegistry(bondingRegistryAddress); + + console.log("Setting SlashingManager address in BondingRegistry..."); + await bondingRegistry.setSlashingManager(slashingManagerAddress); + + console.log("Setting Enclave as reward distributor in BondingRegistry..."); + await bondingRegistry.setRewardDistributor(enclaveAddress); if (shouldDeployMocks) { const { decryptionVerifierAddress, e3ProgramAddress } = await deployMocks(); @@ -106,4 +183,19 @@ export const deployEnclave = async (withMocks?: boolean) => { await tx.wait(); console.log(`Successfully enabled E3 Program in Enclave contract`); } + + console.log(` + ============================================ + Deployment Complete! + ============================================ + MockUSDC: ${usdcTokenAddress} + EnclaveToken (ENCL): ${enclaveTokenAddress} + EnclaveTicketToken: ${enclaveTicketTokenAddress} + SlashingManager: ${slashingManagerAddress} + BondingRegistry: ${bondingRegistryAddress} + CiphernodeRegistry: ${ciphernodeRegistryAddress} + Enclave: ${enclaveAddress} + NaiveRegistryFilter: ${naiveRegistryFilterAddress} + ============================================ + `); }; diff --git a/packages/enclave-contracts/scripts/index.ts b/packages/enclave-contracts/scripts/index.ts index b080e50ba0..85f5499b89 100644 --- a/packages/enclave-contracts/scripts/index.ts +++ b/packages/enclave-contracts/scripts/index.ts @@ -7,9 +7,14 @@ export * from "./deployEnclave"; export * from "./deployMocks"; export * from "./utils"; +export * from "./deployAndSave/bondingRegistry"; export * from "./deployAndSave/ciphernodeRegistryOwnable"; export * from "./deployAndSave/enclave"; +export * from "./deployAndSave/enclaveTicketToken"; +export * from "./deployAndSave/enclaveToken"; +export * from "./deployAndSave/mockStableToken"; export * from "./deployAndSave/naiveRegistryFilter"; +export * from "./deployAndSave/slashingManager"; export * from "./deployAndSave/mockComputeProvider"; export * from "./deployAndSave/mockDecryptionVerifier"; export * from "./deployAndSave/mockInputValidator"; diff --git a/packages/enclave-contracts/test/BondingRegistry.spec.ts b/packages/enclave-contracts/test/BondingRegistry.spec.ts index a02081a989..4bded34ff1 100644 --- a/packages/enclave-contracts/test/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/BondingRegistry.spec.ts @@ -24,16 +24,12 @@ import { } from "../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; -const AddressTwo = "0x0000000000000000000000000000000000000002"; -const AddressThree = "0x0000000000000000000000000000000000000003"; const { ethers, networkHelpers, ignition } = await network.connect(); const { loadFixture, time } = networkHelpers; -// Hash function used to compute the tree nodes for ciphernode registry. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); -// Reason constants matching the contract const REASON_DEPOSIT = ethers.encodeBytes32String("DEPOSIT"); const REASON_WITHDRAW = ethers.encodeBytes32String("WITHDRAW"); const REASON_BOND = ethers.encodeBytes32String("BOND"); @@ -41,10 +37,9 @@ const REASON_UNBOND = ethers.encodeBytes32String("UNBOND"); describe("BondingRegistry", function () { const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60; - const TICKET_PRICE = ethers.parseUnits("10", 6); // 10 USDC per ticket (6 decimals) - const LICENSE_REQUIRED_BOND = ethers.parseEther("1000"); // 1000 ENCL required - const MIN_TICKET_BALANCE = 5; // minimum 5 tickets - + const TICKET_PRICE = ethers.parseUnits("10", 6); + const LICENSE_REQUIRED_BOND = ethers.parseEther("1000"); + const MIN_TICKET_BALANCE = 5; async function setup() { const [owner, operator1, operator2, treasury, notTheOwner] = await ethers.getSigners(); @@ -54,16 +49,14 @@ describe("BondingRegistry", function () { const operator2Address = await operator2.getAddress(); const treasuryAddress = await treasury.getAddress(); - // Deploy USDC mock const usdcContract = await ignition.deploy(MockStableTokenModule, { parameters: { MockUSDC: { - initialSupply: 1000000, // 1M USDC (with 6 decimals) + initialSupply: 1000000, }, }, }); - // Deploy ENCL token const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { parameters: { EnclaveToken: { @@ -72,7 +65,6 @@ describe("BondingRegistry", function () { }, }); - // Deploy CiphernodeRegistry for testing const ciphernodeRegistryContract = await ignition.deploy( MockCiphernodeRegistryModule, { @@ -85,34 +77,31 @@ describe("BondingRegistry", function () { }, ); - // Deploy EnclaveTicketToken const ticketTokenContract = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { EnclaveTicketToken: { underlyingUSDC: await usdcContract.mockUSDC.getAddress(), - registry: AddressOne, // temporary, will be updated + registry: AddressOne, owner: ownerAddress, }, }, }, ); - // Deploy SlashingManager const slashingManagerContract = await ignition.deploy( SlashingManagerModule, { parameters: { SlashingManager: { admin: ownerAddress, - bondingRegistry: AddressOne, // temporary, will be updated + bondingRegistry: AddressOne, }, }, }, ); - // Deploy BondingRegistry const bondingRegistryContract = await ignition.deploy( BondingRegistryModule, { @@ -134,7 +123,6 @@ describe("BondingRegistry", function () { }, ); - // Connect to deployed contracts const bondingRegistry = BondingRegistryFactory.connect( await bondingRegistryContract.bondingRegistry.getAddress(), owner, @@ -160,7 +148,6 @@ describe("BondingRegistry", function () { owner, ); - // Update contract references with actual addresses await ticketToken.setRegistry(await bondingRegistry.getAddress()); await slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), @@ -169,11 +156,9 @@ describe("BondingRegistry", function () { await slashingManager.getAddress(), ); - // Setup initial token balances and approvals - await usdcToken.mint(ownerAddress, ethers.parseUnits("100000", 6)); // 100k USDC - await usdcToken.mint(operator1Address, ethers.parseUnits("100000", 6)); // 100k USDC - await usdcToken.mint(operator2Address, ethers.parseUnits("100000", 6)); // 100k USDC - + await usdcToken.mint(ownerAddress, ethers.parseUnits("100000", 6)); + await usdcToken.mint(operator1Address, ethers.parseUnits("100000", 6)); + await usdcToken.mint(operator2Address, ethers.parseUnits("100000", 6)); await licenseToken.mintAllocation( ownerAddress, ethers.parseEther("100000"), @@ -190,10 +175,8 @@ describe("BondingRegistry", function () { "Test allocation", ); - // Enable transfers for testing await licenseToken.setTransferRestriction(false); - // Setup Merkle tree for ciphernode registry const tree = new LeanIMT(hash); return { @@ -238,7 +221,7 @@ describe("BondingRegistry", function () { MIN_TICKET_BALANCE, ); expect(await bondingRegistry.exitDelay()).to.equal(SEVEN_DAYS_IN_SECONDS); - expect(await bondingRegistry.licenseActiveBps()).to.equal(8000); // 80% + expect(await bondingRegistry.licenseActiveBps()).to.equal(8000); }); }); @@ -278,20 +261,16 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond initial license const bondAmount = ethers.parseEther("1000"); await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); await bondingRegistry.connect(operator1).bondLicense(bondAmount); - // Register operator await bondingRegistry.connect(operator1).registerOperator(); - // Start deregistration (which triggers exit) await bondingRegistry.connect(operator1).deregisterOperator([]); - // Try to bond more during exit await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); @@ -331,13 +310,11 @@ describe("BondingRegistry", function () { const bondAmount = ethers.parseEther("1000"); const unbondAmount = ethers.parseEther("200"); - // Bond first await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); await bondingRegistry.connect(operator1).bondLicense(bondAmount); - // Unbond await expect( bondingRegistry.connect(operator1).unbondLicense(unbondAmount), ) @@ -379,17 +356,16 @@ describe("BondingRegistry", function () { const bondAmount = ethers.parseEther("1000"); const unbondAmount = ethers.parseEther("200"); - // Bond first await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); await bondingRegistry.connect(operator1).bondLicense(bondAmount); - // Unbond await bondingRegistry.connect(operator1).unbondLicense(unbondAmount); - const [ticketPending, licensePending] = - await bondingRegistry.pendingExits(await operator1.getAddress()); + const [, licensePending] = await bondingRegistry.pendingExits( + await operator1.getAddress(), + ); expect(licensePending).to.equal(unbondAmount); }); }); @@ -399,7 +375,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond enough license tokens const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -411,7 +386,7 @@ describe("BondingRegistry", function () { expect(await bondingRegistry.isRegistered(await operator1.getAddress())) .to.be.true; expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be - .false; // no tickets yet + .false; }); it("reverts if not properly licensed", async function () { @@ -426,7 +401,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -434,7 +408,6 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Try to register again await expect( bondingRegistry.connect(operator1).registerOperator(), ).to.be.revertedWithCustomError(bondingRegistry, "AlreadyRegistered"); @@ -444,7 +417,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -452,13 +424,10 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Deregister await bondingRegistry.connect(operator1).deregisterOperator([]); - // Wait for exit delay to pass await time.increase(SEVEN_DAYS_IN_SECONDS + 1); - // Re-bond and register await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); @@ -476,7 +445,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -516,7 +484,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Bond license const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -524,14 +491,12 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Add tickets - const ticketAmount = ethers.parseUnits("100", 6); // 100 USDC + const ticketAmount = ethers.parseUnits("100", 6); await usdcToken .connect(operator1) .approve(await ticketToken.getAddress(), ticketAmount); await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); - // Deregister await bondingRegistry.connect(operator1).deregisterOperator([]); const [ticketPending, licensePending] = @@ -551,7 +516,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -559,8 +523,7 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Add tickets - const ticketAmount = ethers.parseUnits("100", 6); // 100 USDC + const ticketAmount = ethers.parseUnits("100", 6); await usdcToken .connect(operator1) .approve(await ticketToken.getAddress(), ticketAmount); @@ -590,7 +553,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -598,7 +560,6 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Add enough tickets to become active (5 tickets * 10 USDC each = 50 USDC) const ticketAmount = ethers.parseUnits("50", 6); await usdcToken .connect(operator1) @@ -628,7 +589,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -652,7 +612,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Setup operator with tickets const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -666,7 +625,6 @@ describe("BondingRegistry", function () { .approve(await ticketToken.getAddress(), ticketAmount); await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); - // Remove some tickets const removeAmount = ethers.parseUnits("30", 6); await expect( bondingRegistry.connect(operator1).removeTicketBalance(removeAmount), @@ -693,7 +651,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Setup operator with tickets const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -707,14 +664,14 @@ describe("BondingRegistry", function () { .approve(await ticketToken.getAddress(), ticketAmount); await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); - // Remove some tickets const removeAmount = ethers.parseUnits("30", 6); await bondingRegistry .connect(operator1) .removeTicketBalance(removeAmount); - const [ticketPending, licensePending] = - await bondingRegistry.pendingExits(await operator1.getAddress()); + const [ticketPending] = await bondingRegistry.pendingExits( + await operator1.getAddress(), + ); expect(ticketPending).to.equal(removeAmount); }); @@ -727,7 +684,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Setup active operator const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -735,13 +691,12 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - const ticketAmount = ethers.parseUnits("60", 6); // 6 tickets worth + const ticketAmount = ethers.parseUnits("60", 6); await usdcToken .connect(operator1) .approve(await ticketToken.getAddress(), ticketAmount); await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); - // Remove enough to go below minimum (remove 2 tickets worth, leaving 4 < 5 minimum) const removeAmount = ethers.parseUnits("20", 6); await expect( bondingRegistry.connect(operator1).removeTicketBalance(removeAmount), @@ -757,7 +712,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -783,7 +737,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Setup operator and bond/tickets const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -797,13 +750,10 @@ describe("BondingRegistry", function () { .approve(await ticketToken.getAddress(), ticketAmount); await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); - // Deregister to queue assets for exit await bondingRegistry.connect(operator1).deregisterOperator([]); - // Wait for exit delay await time.increase(SEVEN_DAYS_IN_SECONDS + 1); - // Claim exits const initialUSDCBalance = await usdcToken.balanceOf( await operator1.getAddress(), ); @@ -827,7 +777,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -835,7 +784,6 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Deregister but don't wait await bondingRegistry.connect(operator1).deregisterOperator([]); await expect( @@ -852,7 +800,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Setup and queue assets for exit const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -868,10 +815,8 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).deregisterOperator([]); - // Wait for exit delay await time.increase(SEVEN_DAYS_IN_SECONDS + 1); - // Claim partial amounts const partialTickets = ethers.parseUnits("50", 6); const partialLicense = ethers.parseEther("500"); @@ -893,7 +838,6 @@ describe("BondingRegistry", function () { await licenseToken.balanceOf(await operator1.getAddress()), ).to.equal(initialENCLBalance + partialLicense); - // Check remaining pending amounts const [remainingTickets, remainingLicense] = await bondingRegistry.pendingExits(await operator1.getAddress()); expect(remainingTickets).to.equal(ticketAmount - partialTickets); @@ -906,7 +850,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond exactly the minimum required (80% of LICENSE_REQUIRED_BOND) const minBond = (LICENSE_REQUIRED_BOND * 8000n) / 10000n; await licenseToken .connect(operator1) @@ -921,7 +864,6 @@ describe("BondingRegistry", function () { const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond less than minimum required const insufficientBond = (LICENSE_REQUIRED_BOND * 7999n) / 10000n; await licenseToken .connect(operator1) @@ -943,7 +885,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Bond and register const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -951,7 +892,6 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Add tickets worth exactly 10 tickets (10 * 10 USDC = 100 USDC) const ticketAmount = ethers.parseUnits("100", 6); await usdcToken .connect(operator1) @@ -966,9 +906,6 @@ describe("BondingRegistry", function () { it("returns 0 when ticket price is 0", async function () { const { bondingRegistry, operator1 } = await loadFixture(setup); - // This test should check the internal logic - if ticketPrice were 0, it would return 0 - // Since we can't set ticketPrice to 0 via setTicketPrice due to validation, - // we just verify that a fresh operator has 0 available tickets expect( await bondingRegistry.availableTickets(await operator1.getAddress()), ).to.equal(0); @@ -1021,7 +958,7 @@ describe("BondingRegistry", function () { it("allows owner to set license active basis points", async function () { const { bondingRegistry } = await loadFixture(setup); - const newBps = 9000; // 90% + const newBps = 9000; await expect(bondingRegistry.setLicenseActiveBps(newBps)) .to.emit(bondingRegistry, "ConfigurationUpdated") .withArgs( @@ -1060,11 +997,6 @@ describe("BondingRegistry", function () { it("allows owner to withdraw slashed funds", async function () { const { bondingRegistry, treasury } = await loadFixture(setup); - // Simulate some slashed funds (would normally come from slashing operations) - // For testing, we'll directly set the slashed balances by calling internal slashing functions - // This would normally be done by the slashing manager - - // Test that the function exists and can be called (even with 0 amounts) await expect(bondingRegistry.withdrawSlashedFunds(0, 0)) .to.emit(bondingRegistry, "SlashedFundsWithdrawn") .withArgs(await treasury.getAddress(), 0, 0); @@ -1093,7 +1025,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // Setup active operator const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -1110,10 +1041,8 @@ describe("BondingRegistry", function () { expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be .true; - // Unbond enough license to fall below 80% threshold - const unbondAmount = LICENSE_REQUIRED_BOND / 5n; // Remove 20%, leaving 80% exactly at threshold - await bondingRegistry.connect(operator1).unbondLicense(unbondAmount + 1n); // Remove just 1 wei more - + const unbondAmount = LICENSE_REQUIRED_BOND / 5n; + await bondingRegistry.connect(operator1).unbondLicense(unbondAmount + 1n); expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be .false; expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to @@ -1130,7 +1059,6 @@ describe("BondingRegistry", function () { operator2, } = await loadFixture(setup); - // Operator 1: Licensed but not active (no tickets) const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -1138,7 +1066,6 @@ describe("BondingRegistry", function () { await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).registerOperator(); - // Operator 2: Licensed and active await licenseToken .connect(operator2) .approve(await bondingRegistry.getAddress(), bondAmount); @@ -1151,7 +1078,6 @@ describe("BondingRegistry", function () { .approve(await ticketToken.getAddress(), ticketAmount); await bondingRegistry.connect(operator2).addTicketBalance(ticketAmount); - // Check states expect(await bondingRegistry.isRegistered(await operator1.getAddress())) .to.be.true; expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be @@ -1172,7 +1098,6 @@ describe("BondingRegistry", function () { operator1, } = await loadFixture(setup); - // 1. Bond license const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) @@ -1181,14 +1106,12 @@ describe("BondingRegistry", function () { expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to .be.true; - // 2. Register await bondingRegistry.connect(operator1).registerOperator(); expect(await bondingRegistry.isRegistered(await operator1.getAddress())) .to.be.true; expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be .false; - // 3. Add tickets to become active const ticketAmount = ethers.parseUnits("60", 6); await usdcToken .connect(operator1) @@ -1197,7 +1120,6 @@ describe("BondingRegistry", function () { expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be .true; - // 4. Deregister await bondingRegistry.connect(operator1).deregisterOperator([]); expect(await bondingRegistry.isRegistered(await operator1.getAddress())) .to.be.false; @@ -1205,7 +1127,6 @@ describe("BondingRegistry", function () { await bondingRegistry.hasExitInProgress(await operator1.getAddress()), ).to.be.true; - // 5. Wait and claim await time.increase(SEVEN_DAYS_IN_SECONDS + 1); const initialUSDCBalance = await usdcToken.balanceOf( @@ -1226,7 +1147,6 @@ describe("BondingRegistry", function () { await licenseToken.balanceOf(await operator1.getAddress()), ).to.equal(initialENCLBalance + bondAmount); - // 6. Re-register after claiming await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 5e8479f25b..80e7272ddc 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { LeanIMT } from "@zk-kit/lean-imt"; import { expect } from "chai"; +import type { Signer } from "ethers"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; @@ -24,8 +25,11 @@ import SlashingManagerModule from "../ignition/modules/slashingManager"; import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, Enclave__factory as EnclaveFactory, + MockUSDC__factory as MockUSDCFactory, NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, } from "../types"; +import type { Enclave } from "../types/contracts/Enclave"; +import type { MockUSDC } from "../types/contracts/test/MockStableToken.sol/MockUSDC"; const { ethers, ignition, networkHelpers } = await network.connect(); const { loadFixture, time, mine } = networkHelpers; @@ -60,10 +64,10 @@ describe("Enclave", function () { // Helper function to approve USDC and make request const makeRequest = async ( - enclave: any, - usdcToken: any, - requestParams: any, - signer?: any, + enclave: Enclave, + usdcToken: MockUSDC, + requestParams: Parameters[0], + signer?: Signer, ) => { const fee = await enclave.getE3Quote(requestParams); const tokenContract = signer ? usdcToken.connect(signer) : usdcToken; @@ -86,6 +90,11 @@ describe("Enclave", function () { }, }); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { parameters: { EnclaveToken: { @@ -99,7 +108,7 @@ describe("Enclave", function () { { parameters: { EnclaveTicketToken: { - underlyingUSDC: await usdcContract.mockUSDC.getAddress(), + underlyingUSDC: await usdcToken.getAddress(), registry: addressOne, owner: ownerAddress, }, @@ -148,7 +157,7 @@ describe("Enclave", function () { registry: addressOne, bondingRegistry: await bondingRegistryContract.bondingRegistry.getAddress(), - usdcToken: await usdcContract.mockUSDC.getAddress(), + usdcToken: await usdcToken.getAddress(), }, }, }); @@ -264,11 +273,8 @@ describe("Enclave", function () { ), }; - await usdcContract.mockUSDC.mint( - ownerAddress, - ethers.parseUnits("1000000", 6), - ); - await usdcContract.mockUSDC.mint( + await usdcToken.mint(ownerAddress, ethers.parseUnits("1000000", 6)); + await usdcToken.mint( await notTheOwner.getAddress(), ethers.parseUnits("1000000", 6), ); @@ -290,7 +296,7 @@ describe("Enclave", function () { bondingRegistry: bondingRegistryContract.bondingRegistry, ticketToken: ticketTokenContract.enclaveTicketToken, licenseToken: enclTokenContract.enclaveToken, - usdcToken: usdcContract.mockUSDC, + usdcToken, slashingManager: slashingManagerContract.slashingManager, tree, mocks: { @@ -739,7 +745,7 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(usdcToken, "ERC20InsufficientAllowance"); }); it("reverts if threshold is 0", async function () { - const { enclave, request, usdcToken, owner } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const fee = await enclave.getE3Quote({ filter: request.filter, threshold: [0, 2], @@ -1414,7 +1420,7 @@ describe("Enclave", function () { const { enclave, request, usdcToken, naiveRegistryFilterContract } = await loadFixture(setup); const currentTime = await time.latest(); - const tx = await makeRequest(enclave, usdcToken, { + await makeRequest(enclave, usdcToken, { ...request, startWindow: [currentTime, currentTime + 100], }); diff --git a/packages/enclave-contracts/test/SlashingManager.spec.ts b/packages/enclave-contracts/test/SlashingManager.spec.ts new file mode 100644 index 0000000000..4876583cf1 --- /dev/null +++ b/packages/enclave-contracts/test/SlashingManager.spec.ts @@ -0,0 +1,1193 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +import BondingRegistryModule from "../ignition/modules/bondingRegistry"; +import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../ignition/modules/enclaveToken"; +import MockSlashingVerifierModule from "../ignition/modules/mockSlashingVerifier"; +import MockStableTokenModule from "../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockSlashingVerifier__factory as MockSlashingVerifierFactory, + MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, +} from "../types"; +import type { SlashingManager } from "../types/contracts/slashing/SlashingManager"; +import type { MockSlashingVerifier } from "../types/contracts/test/MockSlashingVerifier"; + +const { ethers, networkHelpers, ignition } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +describe("SlashingManager", function () { + const REASON_MISBEHAVIOR = ethers.encodeBytes32String("misbehavior"); + const REASON_INACTIVITY = ethers.encodeBytes32String("inactivity"); + const REASON_DOUBLE_SIGN = ethers.encodeBytes32String("doubleSign"); + + const SLASHER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("SLASHER_ROLE")); + const VERIFIER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("VERIFIER_ROLE")); + const GOVERNANCE_ROLE = ethers.keccak256( + ethers.toUtf8Bytes("GOVERNANCE_ROLE"), + ); + const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; + + async function setupPolicies( + slashingManager: SlashingManager, + mockVerifier: MockSlashingVerifier, + ) { + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + + const evidencePolicy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + + const banPolicy = { + ticketPenalty: ethers.parseUnits("100", 6), + licensePenalty: ethers.parseEther("500"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: true, + appealWindow: 0, + enabled: true, + }; + + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + await slashingManager.setSlashPolicy(REASON_DOUBLE_SIGN, banPolicy); + } + + async function setup() { + const [owner, slasher, verifier, operator, notTheOwner] = + await ethers.getSigners(); + const ownerAddress = await owner.getAddress(); + const operatorAddress = await operator.getAddress(); + + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, + }, + }, + }); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + underlyingUSDC: await usdcContract.mockUSDC.getAddress(), + registry: ownerAddress, + owner: ownerAddress, + }, + }, + }, + ); + + const mockVerifierContract = await ignition.deploy( + MockSlashingVerifierModule, + ); + + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: ownerAddress, + }, + }, + }, + ); + + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: ethers.ZeroAddress, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: 7 * 24 * 60 * 60, + }, + }, + }, + ); + + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + const enclaveToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + const ticketToken = EnclaveTicketTokenFactory.connect( + await ticketTokenContract.enclaveTicketToken.getAddress(), + owner, + ); + const mockVerifier = MockSlashingVerifierFactory.connect( + await mockVerifierContract.mockSlashingVerifier.getAddress(), + owner, + ); + const slashingManager = SlashingManagerFactory.connect( + await slashingManagerContract.slashingManager.getAddress(), + owner, + ); + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), + ); + + await enclaveToken.setTransferRestriction(false); + + await enclaveToken.mintAllocation( + operatorAddress, + ethers.parseEther("2000"), + "Test allocation", + ); + + await slashingManager.addSlasher(await slasher.getAddress()); + await slashingManager.addVerifier(await verifier.getAddress()); + + return { + owner, + slasher, + verifier, + operator, + operatorAddress, + notTheOwner, + slashingManager, + bondingRegistry, + enclaveToken, + ticketToken, + usdcToken, + mockVerifier, + }; + } + + describe("constructor / initialization", function () { + it("should set the admin role correctly", async function () { + const { slashingManager, owner } = await loadFixture(setup); + + expect( + await slashingManager.hasRole( + DEFAULT_ADMIN_ROLE, + await owner.getAddress(), + ), + ).to.be.true; + expect( + await slashingManager.hasRole( + GOVERNANCE_ROLE, + await owner.getAddress(), + ), + ).to.be.true; + }); + + it("should set the bonding registry correctly", async function () { + const { slashingManager, bondingRegistry } = await loadFixture(setup); + + expect(await slashingManager.bondingRegistry()).to.equal( + await bondingRegistry.getAddress(), + ); + }); + + it("should revert if admin is zero address", async function () { + await expect( + ignition.deploy(SlashingManagerModule, { + parameters: { + SlashingManager: { + admin: ethers.ZeroAddress, + bondingRegistry: ethers.ZeroAddress, + }, + }, + }), + ).to.be.rejected; + }); + }); + + describe("setSlashPolicy()", function () { + it("should set a valid slash policy", async function () { + const { slashingManager, mockVerifier } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + + await expect(slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy)) + .to.emit(slashingManager, "SlashPolicyUpdated") + .withArgs(REASON_MISBEHAVIOR, Object.values(policy)); + + const storedPolicy = + await slashingManager.getSlashPolicy(REASON_MISBEHAVIOR); + expect(storedPolicy.ticketPenalty).to.equal(policy.ticketPenalty); + expect(storedPolicy.licensePenalty).to.equal(policy.licensePenalty); + expect(storedPolicy.requiresProof).to.equal(policy.requiresProof); + expect(storedPolicy.enabled).to.equal(policy.enabled); + }); + + it("should set a policy without proof requirement", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + + await expect(slashingManager.setSlashPolicy(REASON_INACTIVITY, policy)) + .to.emit(slashingManager, "SlashPolicyUpdated") + .withArgs(REASON_INACTIVITY, Object.values(policy)); + }); + + it("should revert if caller is not governance", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + + await expect( + slashingManager + .connect(notTheOwner) + .setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError( + slashingManager, + "AccessControlUnauthorizedAccount", + ); + }); + + it("should revert if reason is zero", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(ethers.ZeroHash, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if policy is disabled", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: false, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if no penalties are set", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: 0, + licensePenalty: 0, + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if proof required but no verifier set", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "VerifierNotSet"); + }); + + it("should revert if proof required but appeal window set", async function () { + const { slashingManager, mockVerifier } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if no proof required but no appeal window", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + }); + + describe("role management", function () { + it("should add and remove slasher role", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + await slashingManager.addSlasher(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + SLASHER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.true; + + await slashingManager.removeSlasher(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + SLASHER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.false; + }); + + it("should add and remove verifier role", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + await slashingManager.addVerifier(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + VERIFIER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.true; + + await slashingManager.removeVerifier(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + VERIFIER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.false; + }); + + it("should revert if non-admin tries to add slasher", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + await expect( + slashingManager + .connect(notTheOwner) + .addSlasher(await notTheOwner.getAddress()), + ).to.be.revert(ethers); + }); + + it("should revert if zero address is added as slasher", async function () { + const { slashingManager } = await loadFixture(setup); + + await expect( + slashingManager.addSlasher(ethers.ZeroAddress), + ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); + }); + }); + + describe("proposeSlash()", function () { + it("should propose slash with proof", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + const proof = ethers.toUtf8Bytes("Valid proof data"); + const currentTime = await time.latest(); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), + ) + .to.emit(slashingManager, "SlashProposed") + .withArgs( + 0, + operatorAddress, + REASON_MISBEHAVIOR, + ethers.parseUnits("50", 6), + ethers.parseEther("100"), + currentTime + 1, + await slasher.getAddress(), + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.operator).to.equal(operatorAddress); + expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); + expect(proposal.proofVerified).to.be.true; + expect(proposal.proposer).to.equal(await slasher.getAddress()); + }); + + it("should propose slash without proof (evidence-based)", async function () { + const { slashingManager, slasher, operatorAddress } = + await loadFixture(setup); + + const evidencePolicy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + + const proof = ethers.toUtf8Bytes(""); + const currentTime = await time.latest(); + const appealWindow = 7 * 24 * 60 * 60; + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_INACTIVITY, proof), + ) + .to.emit(slashingManager, "SlashProposed") + .withArgs( + 0, + operatorAddress, + REASON_INACTIVITY, + ethers.parseUnits("20", 6), + ethers.parseEther("50"), + currentTime + appealWindow + 1, + await slasher.getAddress(), + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.proofVerified).to.be.false; + expect(proposal.executableAt).to.be.greaterThan( + currentTime + appealWindow, + ); + }); + + it("should revert if caller is not slasher", async function () { + const { slashingManager, notTheOwner, operatorAddress } = + await loadFixture(setup); + + const proof = ethers.toUtf8Bytes("Some proof"); + + await expect( + slashingManager + .connect(notTheOwner) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + + it("should revert if operator is zero address", async function () { + const { slashingManager, slasher } = await loadFixture(setup); + + const proof = ethers.toUtf8Bytes("Some proof"); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(ethers.ZeroAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); + }); + + it("should revert if slash reason is disabled", async function () { + const { slashingManager, slasher, operatorAddress } = + await loadFixture(setup); + + const proof = ethers.toUtf8Bytes("Some proof"); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof), + ).to.be.revertedWithCustomError(slashingManager, "SlashReasonDisabled"); + }); + + it("should revert if proof required but not provided", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + const emptyProof = ethers.toUtf8Bytes(""); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, emptyProof), + ).to.be.revertedWithCustomError(slashingManager, "ProofRequired"); + }); + + it("should increment totalProposals", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + const evidencePolicy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 7 * 24 * 60 * 60, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + + expect(await slashingManager.totalProposals()).to.equal(0); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + expect(await slashingManager.totalProposals()).to.equal(1); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + expect(await slashingManager.totalProposals()).to.equal(2); + }); + }); + + describe("executeSlash()", function () { + it("should execute slash with proof immediately", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + await expect(slashingManager.connect(slasher).executeSlash(0)) + .to.emit(slashingManager, "SlashExecuted") + .withArgs( + 0, + operatorAddress, + REASON_MISBEHAVIOR, + ethers.parseUnits("50", 6), + ethers.parseEther("100"), + true, + true, + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.executedTicket).to.be.true; + expect(proposal.executedLicense).to.be.true; + }); + + it("should execute slash after appeal window expires", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealWindowActive"); + + await time.increase(7 * 24 * 60 * 60 + 1); + + await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + slashingManager, + "SlashExecuted", + ); + }); + + it("should ban node when policy requires it", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("Serious violation proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + + await expect(slashingManager.connect(slasher).executeSlash(0)) + .to.emit(slashingManager, "NodeBanned") + .withArgs( + operatorAddress, + REASON_DOUBLE_SIGN, + await slashingManager.getAddress(), + ); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + }); + + it("should revert if proposal doesn't exist", async function () { + const { slashingManager, slasher } = await loadFixture(setup); + + await expect( + slashingManager.connect(slasher).executeSlash(999), + ).to.be.revertedWithCustomError(slashingManager, "InvalidProposal"); + }); + + it("should revert if already executed", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + await slashingManager.connect(slasher).executeSlash(0); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); + }); + + it("should revert if caller is not slasher", async function () { + const { + slashingManager, + slasher, + notTheOwner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + await expect( + slashingManager.connect(notTheOwner).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + }); + + describe("appeal system", function () { + it("should allow operator to file appeal", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + const evidence = "I was not inactive, here's the proof..."; + + await expect(slashingManager.connect(operator).fileAppeal(0, evidence)) + .to.emit(slashingManager, "AppealFiled") + .withArgs(0, operatorAddress, REASON_INACTIVITY, evidence); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.appealed).to.be.true; + }); + + it("should revert if non-operator tries to appeal", async function () { + const { + slashingManager, + slasher, + notTheOwner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await expect( + slashingManager.connect(notTheOwner).fileAppeal(0, "Not my appeal"), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + + it("should revert if appeal window expired", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await time.increase(7 * 24 * 60 * 60 + 1); + + await expect( + slashingManager.connect(operator).fileAppeal(0, "Too late"), + ).to.be.revertedWithCustomError(slashingManager, "AppealWindowExpired"); + }); + + it("should revert if already appealed", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await slashingManager.connect(operator).fileAppeal(0, "First appeal"); + + await expect( + slashingManager.connect(operator).fileAppeal(0, "Second appeal"), + ).to.be.revertedWithCustomError(slashingManager, "AlreadyAppealed"); + }); + + it("should allow governance to resolve appeal (approve)", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + + const resolution = "Appeal approved after review"; + + await expect( + slashingManager.connect(owner).resolveAppeal(0, true, resolution), + ) + .to.emit(slashingManager, "AppealResolved") + .withArgs( + 0, + operatorAddress, + true, + await owner.getAddress(), + resolution, + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.resolved).to.be.true; + expect(proposal.approved).to.be.true; + }); + + it("should allow governance to resolve appeal (deny)", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + + await slashingManager + .connect(owner) + .resolveAppeal(0, false, "Appeal denied"); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.resolved).to.be.true; + expect(proposal.approved).to.be.false; + }); + + it("should block execution if appeal is pending", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + + await time.increase(7 * 24 * 60 * 60 + 1); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealPending"); + }); + + it("should block execution if appeal was approved", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + await slashingManager.connect(owner).resolveAppeal(0, true, "Approved"); + + await time.increase(7 * 24 * 60 * 60 + 1); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealUpheld"); + }); + + it("should allow execution if appeal was denied", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + await slashingManager.connect(owner).resolveAppeal(0, false, "Denied"); + + await time.increase(7 * 24 * 60 * 60 + 1); + + await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + slashingManager, + "SlashExecuted", + ); + }); + }); + + describe("ban management", function () { + it("should allow governance to ban node", async function () { + const { slashingManager, owner, operatorAddress } = + await loadFixture(setup); + + const reason = ethers.encodeBytes32String("manual_ban"); + + await expect( + slashingManager.connect(owner).banNode(operatorAddress, reason), + ) + .to.emit(slashingManager, "NodeBanned") + .withArgs(operatorAddress, reason, await owner.getAddress()); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + }); + + it("should allow governance to unban node", async function () { + const { slashingManager, owner, operatorAddress } = + await loadFixture(setup); + + await slashingManager + .connect(owner) + .banNode(operatorAddress, ethers.encodeBytes32String("test")); + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + + await expect(slashingManager.connect(owner).unbanNode(operatorAddress)) + .to.emit(slashingManager, "NodeUnbanned") + .withArgs(operatorAddress, await owner.getAddress()); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + }); + + it("should revert if non-governance tries to ban", async function () { + const { slashingManager, notTheOwner, operatorAddress } = + await loadFixture(setup); + + await expect( + slashingManager + .connect(notTheOwner) + .banNode(operatorAddress, ethers.encodeBytes32String("test")), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + + it("should prevent proposing slashes against banned nodes", async function () { + const { slashingManager, owner, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(owner) + .banNode(operatorAddress, ethers.encodeBytes32String("test")); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_MISBEHAVIOR, + ethers.toUtf8Bytes("proof"), + ), + ).to.be.revertedWithCustomError(slashingManager, "CiphernodeBanned"); + }); + }); + + describe("view functions", function () { + it("should return correct slash policy", async function () { + const { slashingManager, mockVerifier } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: true, + appealWindow: 0, + enabled: true, + }; + + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy); + + const retrieved = + await slashingManager.getSlashPolicy(REASON_MISBEHAVIOR); + expect(retrieved.ticketPenalty).to.equal(policy.ticketPenalty); + expect(retrieved.licensePenalty).to.equal(policy.licensePenalty); + expect(retrieved.requiresProof).to.equal(policy.requiresProof); + expect(retrieved.proofVerifier).to.equal(policy.proofVerifier); + expect(retrieved.banNode).to.equal(policy.banNode); + expect(retrieved.appealWindow).to.equal(policy.appealWindow); + expect(retrieved.enabled).to.equal(policy.enabled); + }); + + it("should return correct slash proposal", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("test proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.operator).to.equal(operatorAddress); + expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); + expect(proposal.ticketAmount).to.equal(ethers.parseUnits("50", 6)); + expect(proposal.licenseAmount).to.equal(ethers.parseEther("100")); + expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.proofHash).to.equal(ethers.keccak256(proof)); + expect(proposal.proofVerified).to.be.true; + }); + + it("should revert for invalid proposal ID", async function () { + const { slashingManager } = await loadFixture(setup); + + await expect( + slashingManager.getSlashProposal(999), + ).to.be.revertedWithCustomError(slashingManager, "InvalidProposal"); + }); + }); +}); From 10b36f5b1ebbe483102ea2c4aa128d7c551cffaa Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 17:42:52 +0500 Subject: [PATCH 11/88] feat: ciphernode add script --- package.json | 2 +- .../enclave-contracts/contracts/Enclave.sol | 16 +- .../registry/CiphernodeRegistryOwnable.sol | 5 +- .../contracts/test/MockCiphernodeRegistry.sol | 4 + packages/enclave-contracts/hardhat.config.ts | 4 + packages/enclave-contracts/package.json | 2 + .../scripts/deployAndSave/enclaveToken.ts | 24 + .../scripts/deployEnclave.ts | 2 +- .../enclave-contracts/tasks/ciphernode.ts | 519 +++++++++++++++++- .../test/SlashingManager.spec.ts | 5 + 10 files changed, 550 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index b5bbe1bcfe..7a708b3fed 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "prepare": "husky", "enclave": "cd crates && ./scripts/launch.sh", "ciphernode:lint": "cargo fmt -- --check", - "ciphernode:add": "cd packages/enclave-contracts && pnpm ciphernode:add", + "ciphernode:add": "cd packages/enclave-contracts && pnpm ciphernode:admin-add", "ciphernode:remove": "cd packages/enclave-contracts && pnpm ciphernode:remove", "ciphernode:test": "cd crates && ./scripts/test.sh", "ciphernode:build": "cargo build --locked --release", diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 13aa52df79..aa698ee5b9 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -93,6 +93,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { error PlaintextOutputAlreadyPublished(uint256 e3Id); error InsufficientBalance(); error InsufficientAllowance(); + error InvalidBondingRegistry(IBondingRegistry bondingRegistry); + error InvalidUsdcToken(IERC20 usdcToken); //////////////////////////////////////////////////////////// // // @@ -362,7 +364,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { function _distributeRewards(uint256 e3Id) internal { IRegistryFilter.Committee memory committee = ciphernodeRegistry .getCommittee(e3Id); - uint256[] memory amounts = new uint256[](committee.nodes.length); // We might need to pay different amounts to different nodes. @@ -374,10 +375,10 @@ contract Enclave is IEnclave, OwnableUpgradeable { // Approve the BondingRegistry to spend the USDC tokens usdcToken.approve(address(bondingRegistry), e3Payments[e3Id]); - // Distribute rewards to the committee - bondingRegistry.distributeRewards(usdcToken, committee.nodes, amounts); // Zero out the payment e3Payments[e3Id] = 0; + // Distribute rewards to the committee + bondingRegistry.distributeRewards(usdcToken, committee.nodes, amounts); // Where does dust go? Treasury maybe? usdcToken.approve(address(bondingRegistry), 0); } @@ -412,6 +413,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { function setBondingRegistry( IBondingRegistry _bondingRegistry ) public onlyOwner returns (bool success) { + require( + address(_bondingRegistry) != address(0) && + _bondingRegistry != bondingRegistry, + InvalidBondingRegistry(_bondingRegistry) + ); bondingRegistry = _bondingRegistry; success = true; emit BondingRegistrySet(address(_bondingRegistry)); @@ -420,6 +426,10 @@ contract Enclave is IEnclave, OwnableUpgradeable { function setUsdcToken( IERC20 _usdcToken ) public onlyOwner returns (bool success) { + require( + address(_usdcToken) != address(0) && _usdcToken != usdcToken, + InvalidUsdcToken(_usdcToken) + ); usdcToken = _usdcToken; success = true; emit UsdcTokenSet(address(_usdcToken)); diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 5a1bf85d73..a1181872df 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -224,8 +224,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { function getCommittee( uint256 e3Id - ) public view returns (IRegistryFilter.Committee memory) { - return registryFilters[e3Id].getCommittee(e3Id); + ) public view returns (IRegistryFilter.Committee memory committee) { + committee = registryFilters[e3Id].getCommittee(e3Id); + require(committee.nodes.length > 0, CommitteeNotPublished()); } function treeSize() public view returns (uint256) { diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index cbd6107a87..b07898c852 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -21,12 +21,14 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { } } + // solhint-disable-next-line no-empty-blocks function addCiphernode(address) external {} function isEnabled(address) external pure returns (bool) { return true; } + // solhint-disable-next-line no-empty-blocks function removeCiphernode(address, uint256[] calldata) external {} // solhint-disable no-empty-blocks @@ -77,12 +79,14 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { } } + // solhint-disable-next-line no-empty-blocks function addCiphernode(address) external {} function isEnabled(address) external pure returns (bool) { return true; } + // solhint-disable-next-line no-empty-blocks function removeCiphernode(address, uint256[] calldata) external {} // solhint-disable no-empty-blocks diff --git a/packages/enclave-contracts/hardhat.config.ts b/packages/enclave-contracts/hardhat.config.ts index 0aae4886ef..cebe7f2a14 100644 --- a/packages/enclave-contracts/hardhat.config.ts +++ b/packages/enclave-contracts/hardhat.config.ts @@ -13,6 +13,8 @@ import type { HardhatUserConfig } from "hardhat/config"; import { ciphernodeAdd, + ciphernodeAdminAdd, + ciphernodeMintTokens, ciphernodeRemove, ciphernodeSiblings, } from "./tasks/ciphernode"; @@ -97,6 +99,8 @@ const config: HardhatUserConfig = { ], tasks: [ ciphernodeAdd, + ciphernodeAdminAdd, + ciphernodeMintTokens, ciphernodeRemove, ciphernodeSiblings, requestCommittee, diff --git a/packages/enclave-contracts/package.json b/packages/enclave-contracts/package.json index 9fcbc286e6..d7e40b75ca 100644 --- a/packages/enclave-contracts/package.json +++ b/packages/enclave-contracts/package.json @@ -150,6 +150,8 @@ "e3:activate": "hardhat e3:activate", "e3:enable": "hardhat e3:enable", "ciphernode:add": "hardhat ciphernode:add", + "ciphernode:admin-add": "hardhat ciphernode:admin-add", + "ciphernode:mint-tokens": "hardhat ciphernode:mint-tokens", "ciphernode:remove": "hardhat ciphernode:remove", "ciphernode:siblings": "hardhat ciphernode:siblings", "committee:new": "hardhat committee:new", diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts index 27e435b4e6..891e053010 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts @@ -47,6 +47,20 @@ export const deployAndSaveEnclaveToken = async ({ preDeployedArgs.address, signer, ); + + if (chain === "localhost" || chain === "hardhat") { + try { + const isRestricted = await enclaveTokenContract.transfersRestricted(); + if (isRestricted) { + const tx = await enclaveTokenContract.setTransferRestriction(false); + await tx.wait(); + console.log("Transfer restrictions disabled for local development"); + } + } catch (error) { + console.warn("Failed to disable transfer restrictions:", error); + } + } + return { enclaveToken: enclaveTokenContract }; } @@ -81,5 +95,15 @@ export const deployAndSaveEnclaveToken = async ({ signer, ); + if (chain === "localhost" || chain === "hardhat") { + try { + const tx = await enclaveTokenContract.setTransferRestriction(false); + await tx.wait(); + console.log("Transfer restrictions disabled for local development"); + } catch (error) { + console.warn("Failed to disable transfer restrictions:", error); + } + } + return { enclaveToken: enclaveTokenContract }; }; diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index bf0ac8f5cc..477b331565 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -89,7 +89,7 @@ export const deployEnclave = async (withMocks?: boolean) => { registry: addressOne, slashedFundsTreasury: ownerAddress, ticketPrice: ethers.parseUnits("10", 6).toString(), - licenseRequiredBond: ethers.parseEther("1000").toString(), + licenseRequiredBond: ethers.parseEther("100").toString(), minTicketBalance: 1, exitDelay: 7 * 24 * 60 * 60, hre, diff --git a/packages/enclave-contracts/tasks/ciphernode.ts b/packages/enclave-contracts/tasks/ciphernode.ts index fede694761..c201fd92d2 100644 --- a/packages/enclave-contracts/tasks/ciphernode.ts +++ b/packages/enclave-contracts/tasks/ciphernode.ts @@ -8,38 +8,168 @@ import { ZeroAddress } from "ethers"; import { task } from "hardhat/config"; import { poseidon2 } from "poseidon-lite"; +import { + BondingRegistry__factory as BondingRegistryFactory, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockUSDC__factory as MockUSDCFactory, +} from "../types"; + export const ciphernodeAdd = task( "ciphernode:add", - "Register a ciphernode to the registry", + "Register a ciphernode to the bonding registry and ciphernode registry", ) .addOption({ - name: "ciphernodeAddress", - description: "address of ciphernode to register", - defaultValue: ZeroAddress, + name: "privateKey", + description: + "private key of the ciphernode to register (must have ENCL and USDC)", + defaultValue: "", + }) + .addOption({ + name: "licenseBondAmount", + description: + "amount of ENCL to bond (in wei, e.g., 1000000000000000000000 for 1000 ENCL)", + defaultValue: "1000000000000000000000", // 1000 ENCL + }) + .addOption({ + name: "ticketAmount", + description: + "amount of USDC to deposit for tickets (in wei, e.g., 100000000 for 100 USDC)", + defaultValue: "1000000000", // 1000 USDC }) .setAction(async () => ({ - default: async ({ ciphernodeAddress }, hre) => { - const { deployAndSaveCiphernodeRegistryOwnable } = await import( - "../scripts/deployAndSave/ciphernodeRegistryOwnable" + default: async ({ privateKey, licenseBondAmount, ticketAmount }, hre) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + if (!privateKey) { + throw new Error("Private key is required. Use --private-key option."); + } + + const wallet = new ethers.Wallet(privateKey, ethers.provider); + console.log(`Registering ciphernode: ${wallet.address}`); + + const { deployAndSaveBondingRegistry } = await import( + "../scripts/deployAndSave/bondingRegistry" ); - const { ciphernodeRegistry } = - await deployAndSaveCiphernodeRegistryOwnable({ hre }); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + + const licenseTokenAddress = await bondingRegistry.licenseToken(); + const licenseToken = EnclaveTokenFactory.connect( + licenseTokenAddress, + wallet, + ); + + const ticketTokenAddress = await bondingRegistry.ticketToken(); + const ticketToken = EnclaveTicketTokenFactory.connect( + ticketTokenAddress, + wallet, + ); + + const usdcAddress = await ticketToken.underlying(); + const usdcToken = MockUSDCFactory.connect(usdcAddress, wallet); + + const bondingRegistryConnected = BondingRegistryFactory.connect( + await bondingRegistry.getAddress(), + wallet, + ); + + try { + console.log("Step 1: Checking balances..."); + const enclBalance = await licenseToken.balanceOf(wallet.address); + const usdcBalance = await usdcToken.balanceOf(wallet.address); + + console.log(`ENCL balance: ${ethers.formatEther(enclBalance)}`); + console.log(`USDC balance: ${ethers.formatUnits(usdcBalance, 6)}`); + + const licenseBondAmountBigInt = BigInt(licenseBondAmount); + const ticketAmountBigInt = BigInt(ticketAmount); + + if (enclBalance < licenseBondAmountBigInt) { + throw new Error( + `Insufficient ENCL balance. Need: ${ethers.formatEther(licenseBondAmountBigInt)}, Have: ${ethers.formatEther(enclBalance)}`, + ); + } + + if (usdcBalance < ticketAmountBigInt) { + throw new Error( + `Insufficient USDC balance. Need: ${ethers.formatUnits(ticketAmountBigInt, 6)}, Have: ${ethers.formatUnits(usdcBalance, 6)}`, + ); + } + + console.log("Step 2: Approving ENCL for license bond..."); + const approveTx = await licenseToken.approve( + await bondingRegistry.getAddress(), + licenseBondAmountBigInt, + ); + await approveTx.wait(); + console.log("ENCL approved"); + + console.log("Step 3: Bonding license..."); + const bondTx = await bondingRegistryConnected.bondLicense( + licenseBondAmountBigInt, + ); + await bondTx.wait(); + console.log( + `Licensed bonded: ${ethers.formatEther(licenseBondAmountBigInt)} ENCL`, + ); + + console.log("Step 4: Registering as operator..."); + const registerTx = await bondingRegistryConnected.registerOperator(); + await registerTx.wait(); + console.log( + "Operator registered (automatically added to CiphernodeRegistry)", + ); + + console.log("Step 5: Approving USDC for ticket purchase..."); + const approveUsdcTx = await usdcToken.approve( + ticketTokenAddress, + ticketAmountBigInt, + ); + await approveUsdcTx.wait(); + console.log("USDC approved"); + + console.log("Step 6: Adding ticket balance..."); + const ticketTx = + await bondingRegistryConnected.addTicketBalance(ticketAmountBigInt); + await ticketTx.wait(); + console.log( + `Ticket balance added: ${ethers.formatUnits(ticketAmountBigInt, 6)} USDC worth`, + ); - const tx = await ciphernodeRegistry.addCiphernode(ciphernodeAddress); - await tx.wait(); - console.log(`Ciphernode ${ciphernodeAddress} registered`); + const isRegistered = await bondingRegistry.isRegistered(wallet.address); + const isActive = await bondingRegistry.isActive(wallet.address); + const licenseBond = await bondingRegistry.getLicenseBond( + wallet.address, + ); + const ticketBalance = await bondingRegistry.getTicketBalance( + wallet.address, + ); + + console.log("\n=== Registration Complete ==="); + console.log(`Ciphernode: ${wallet.address}`); + console.log(`Registered: ${isRegistered}`); + console.log(`Active: ${isActive}`); + console.log(`License Bond: ${ethers.formatEther(licenseBond)} ENCL`); + console.log( + `Ticket Balance: ${ethers.formatUnits(ticketBalance, 6)} USDC worth`, + ); + } catch (error) { + console.error("Registration failed:", error); + throw error; + } }, })) .build(); export const ciphernodeRemove = task( "ciphernode:remove", - "Remove a ciphernode from the registry", + "Deregister a ciphernode from the bonding registry", ) .addOption({ - name: "ciphernodeAddress", - description: "address of ciphernode to remove", - defaultValue: ZeroAddress, + name: "privateKey", + description: "private key of the ciphernode to deregister", + defaultValue: "", }) .addOption({ name: "siblings", @@ -47,22 +177,359 @@ export const ciphernodeRemove = task( defaultValue: "", }) .setAction(async () => ({ - default: async ({ ciphernodeAddress, siblings }, hre) => { - const { deployAndSaveCiphernodeRegistryOwnable } = await import( - "../scripts/deployAndSave/ciphernodeRegistryOwnable" + default: async ({ privateKey, siblings }, hre) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + if (!privateKey) { + throw new Error("Private key is required. Use --private-key option."); + } + + const wallet = new ethers.Wallet(privateKey, ethers.provider); + console.log(`Deregistering ciphernode: ${wallet.address}`); + + const { deployAndSaveBondingRegistry } = await import( + "../scripts/deployAndSave/bondingRegistry" ); - const { ciphernodeRegistry } = - await deployAndSaveCiphernodeRegistryOwnable({ hre }); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + + const bondingRegistryConnected = bondingRegistry.connect(wallet); const siblingsArray = siblings.split(",").map((s: string) => BigInt(s)); - const tx = await ciphernodeRegistry.removeCiphernode( - ciphernodeAddress, - siblingsArray, + try { + console.log( + "Deregistering operator (will also remove from CiphernodeRegistry)...", + ); + const tx = + await bondingRegistryConnected.deregisterOperator(siblingsArray); + await tx.wait(); + + console.log(`Ciphernode ${wallet.address} deregistered`); + console.log( + "Note: Funds are now in exit queue. Use claimExits() after the exit delay period.", + ); + } catch (error) { + console.error("Deregistration failed:", error); + throw error; + } + }, + })) + .build(); + +export const ciphernodeMintTokens = task( + "ciphernode:mint-tokens", + "Mint ENCL and USDC tokens for a ciphernode (for testing)", +) + .addOption({ + name: "ciphernodeAddress", + description: "address of ciphernode to mint tokens for", + defaultValue: ZeroAddress, + }) + .addOption({ + name: "enclAmount", + description: + "amount of ENCL to mint (in ether units, e.g., 2000 for 2000 ENCL)", + defaultValue: "2000", + }) + .addOption({ + name: "usdcAmount", + description: + "amount of USDC to mint (in USDC units, e.g., 1000 for 1000 USDC)", + defaultValue: "1000", + }) + .setAction(async () => ({ + default: async ({ ciphernodeAddress, enclAmount, usdcAmount }, hre) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + if (ciphernodeAddress === ZeroAddress) { + throw new Error( + "Ciphernode address is required. Use --ciphernode-address option.", + ); + } + + const { deployAndSaveEnclaveToken } = await import( + "../scripts/deployAndSave/enclaveToken" + ); + const { enclaveToken } = await deployAndSaveEnclaveToken({ hre }); + + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" ); - await tx.wait(); + const { mockStableToken: mockUSDC } = await deployAndSaveMockStableToken({ + hre, + }); - console.log(`Ciphernode ${ciphernodeAddress} removed`); + // Get properly typed contracts using factories + const [signer] = await ethers.getSigners(); + const enclaveTokenTyped = EnclaveTokenFactory.connect( + await enclaveToken.getAddress(), + signer, + ); + const mockUSDCTyped = MockUSDCFactory.connect( + await mockUSDC.getAddress(), + signer, + ); + + try { + console.log(`Minting tokens for: ${ciphernodeAddress}`); + + console.log(`Minting ${enclAmount} ENCL...`); + const enclTx = await enclaveTokenTyped.mintAllocation( + ciphernodeAddress, + ethers.parseEther(enclAmount), + "Ciphernode allocation", + ); + await enclTx.wait(); + console.log(`${enclAmount} ENCL minted`); + + console.log(`Minting ${usdcAmount} USDC...`); + const usdcTx = await mockUSDCTyped.mint( + ciphernodeAddress, + ethers.parseUnits(usdcAmount, 6), + ); + await usdcTx.wait(); + console.log(`${usdcAmount} USDC minted`); + + const enclBalance = + await enclaveTokenTyped.balanceOf(ciphernodeAddress); + const usdcBalance = await mockUSDCTyped.balanceOf(ciphernodeAddress); + + console.log("\n=== Token Balances ==="); + console.log(`ENCL: ${ethers.formatEther(enclBalance)}`); + console.log(`USDC: ${ethers.formatUnits(usdcBalance, 6)}`); + } catch (error) { + console.error("Token minting failed:", error); + throw error; + } + }, + })) + .build(); + +export const ciphernodeAdminAdd = task( + "ciphernode:admin-add", + "Register a ciphernode using admin privileges (for testing/setup)", +) + .addOption({ + name: "ciphernodeAddress", + description: "address of ciphernode to register", + defaultValue: ZeroAddress, + }) + .addOption({ + name: "adminPrivateKey", + description: + "private key of admin wallet (optional, uses anvil first key if not provided)", + defaultValue: "", + }) + .addOption({ + name: "licenseBondAmount", + description: + "amount of ENCL to bond (in ether units, e.g., 1000 for 1000 ENCL)", + defaultValue: "1000", + }) + .addOption({ + name: "ticketAmount", + description: + "amount of USDC for tickets (in USDC units, e.g., 1000 for 1000 USDC)", + defaultValue: "1000", + }) + .setAction(async () => ({ + default: async ( + { ciphernodeAddress, adminPrivateKey, licenseBondAmount, ticketAmount }, + hre, + ) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + if (ciphernodeAddress === ZeroAddress) { + throw new Error( + "Ciphernode address is required. Use --ciphernode-address option.", + ); + } + + let adminWallet; + if (adminPrivateKey) { + adminWallet = new ethers.Wallet(adminPrivateKey, ethers.provider); + } else { + const anvilFirstKey = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + adminWallet = new ethers.Wallet(anvilFirstKey, ethers.provider); + } + + console.log(`Admin wallet: ${adminWallet.address}`); + console.log(`Registering ciphernode: ${ciphernodeAddress}`); + + const { deployAndSaveBondingRegistry } = await import( + "../scripts/deployAndSave/bondingRegistry" + ); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + + const { deployAndSaveEnclaveToken } = await import( + "../scripts/deployAndSave/enclaveToken" + ); + const { enclaveToken } = await deployAndSaveEnclaveToken({ hre }); + + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" + ); + const { mockStableToken: mockUSDC } = await deployAndSaveMockStableToken({ + hre, + }); + + // Connect contracts to admin wallet + const enclaveTokenConnected = enclaveToken.connect(adminWallet); + const mockUSDCConnected = mockUSDC.connect(adminWallet); + + // Get ticket token address + const ticketTokenAddress = await bondingRegistry.ticketToken(); + + try { + const licenseBondWei = ethers.parseEther(licenseBondAmount); + const ticketAmountWei = ethers.parseUnits(ticketAmount, 6); + + console.log("Step 1: Minting and transferring ENCL to ciphernode..."); + + // Mint ENCL tokens to admin + const enclTx = await enclaveTokenConnected.mintAllocation( + adminWallet.address, + licenseBondWei, + "Admin allocation for ciphernode registration", + ); + await enclTx.wait(); + + // Transfer ENCL to ciphernode + const transferTx = await enclaveTokenConnected.transfer( + ciphernodeAddress, + licenseBondWei, + ); + await transferTx.wait(); + console.log(`${licenseBondAmount} ENCL transferred to ciphernode`); + + console.log("Step 2: Minting USDC to admin..."); + const usdcTx = await mockUSDCConnected.mint( + adminWallet.address, + ticketAmountWei, + ); + await usdcTx.wait(); + console.log(`${ticketAmount} USDC minted to admin`); + + // Impersonate the ciphernode for license bonding and registration + console.log( + "Step 3: Impersonating ciphernode for license operations...", + ); + await connection.provider.request({ + method: "hardhat_impersonateAccount", + params: [ciphernodeAddress], + }); + + // Fund the impersonated account with ETH for gas + await connection.provider.request({ + method: "hardhat_setBalance", + params: [ciphernodeAddress, "0x1000000000000000000000"], // 1 ETH + }); + + const ciphernodeSigner = await ethers.getSigner(ciphernodeAddress); + const enclaveTokenAsCiphernode = enclaveToken.connect(ciphernodeSigner); + const bondingRegistryAsCiphernode = + bondingRegistry.connect(ciphernodeSigner); + + // Approve and bond license as ciphernode + const approveTx = await enclaveTokenAsCiphernode.approve( + await bondingRegistry.getAddress(), + licenseBondWei, + ); + await approveTx.wait(); + + const bondTx = + await bondingRegistryAsCiphernode.bondLicense(licenseBondWei); + await bondTx.wait(); + console.log(`License bonded: ${licenseBondAmount} ENCL`); + + // Register as operator + const registerTx = await bondingRegistryAsCiphernode.registerOperator(); + await registerTx.wait(); + console.log( + "Operator registered (automatically added to CiphernodeRegistry)", + ); + + // Stop impersonating + await connection.provider.request({ + method: "hardhat_stopImpersonatingAccount", + params: [ciphernodeAddress], + }); + + console.log("Step 4: Adding ticket balance via admin..."); + // Now add ticket balance - admin approves USDC, then calls addTicketBalance as ciphernode + const approveUsdcTx = await mockUSDCConnected.approve( + ticketTokenAddress, + ticketAmountWei, + ); + await approveUsdcTx.wait(); + + // Impersonate again for ticket balance + await connection.provider.request({ + method: "hardhat_impersonateAccount", + params: [ciphernodeAddress], + }); + + // Fund the impersonated account with ETH for gas (again) + await connection.provider.request({ + method: "hardhat_setBalance", + params: [ciphernodeAddress, "0x1000000000000000000000"], // 1 ETH + }); + + const ciphernodeSigner2 = await ethers.getSigner(ciphernodeAddress); + const bondingRegistryAsCiphernode2 = + bondingRegistry.connect(ciphernodeSigner2); + + // Transfer USDC from admin to ciphernode first + const usdcTransferTx = await mockUSDCConnected.transfer( + ciphernodeAddress, + ticketAmountWei, + ); + await usdcTransferTx.wait(); + + // Now ciphernode can add ticket balance + const mockUSDCAsCiphernode = mockUSDC.connect(ciphernodeSigner2); + const approveUsdcAsCiphernodeTx = await mockUSDCAsCiphernode.approve( + ticketTokenAddress, + ticketAmountWei, + ); + await approveUsdcAsCiphernodeTx.wait(); + + const addTicketTx = + await bondingRegistryAsCiphernode2.addTicketBalance(ticketAmountWei); + await addTicketTx.wait(); + console.log(`Ticket balance added: ${ticketAmount} USDC worth`); + + // Stop impersonating + await connection.provider.request({ + method: "hardhat_stopImpersonatingAccount", + params: [ciphernodeAddress], + }); + + // Check final status + const isRegistered = + await bondingRegistry.isRegistered(ciphernodeAddress); + const isActive = await bondingRegistry.isActive(ciphernodeAddress); + const licenseBond = + await bondingRegistry.getLicenseBond(ciphernodeAddress); + const ticketBalance = + await bondingRegistry.getTicketBalance(ciphernodeAddress); + + console.log("\n=== Registration Complete ==="); + console.log(`Ciphernode: ${ciphernodeAddress}`); + console.log(`Registered: ${isRegistered}`); + console.log(`Active: ${isActive}`); + console.log(`License Bond: ${ethers.formatEther(licenseBond)} ENCL`); + console.log( + `Ticket Balance: ${ethers.formatUnits(ticketBalance, 6)} USDC worth`, + ); + } catch (error) { + console.error("Admin registration failed:", error); + throw error; + } }, })) .build(); diff --git a/packages/enclave-contracts/test/SlashingManager.spec.ts b/packages/enclave-contracts/test/SlashingManager.spec.ts index 4876583cf1..fc6d2b8c3d 100644 --- a/packages/enclave-contracts/test/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/SlashingManager.spec.ts @@ -1,3 +1,8 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. import { expect } from "chai"; import { network } from "hardhat"; From f19e8a880b0be261bf236bd810776728a006704f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 17:54:37 +0500 Subject: [PATCH 12/88] chore: fix linting errors --- .../enclave-contracts/tasks/ciphernode.ts | 90 +++++++------------ 1 file changed, 30 insertions(+), 60 deletions(-) diff --git a/packages/enclave-contracts/tasks/ciphernode.ts b/packages/enclave-contracts/tasks/ciphernode.ts index c201fd92d2..91c620d662 100644 --- a/packages/enclave-contracts/tasks/ciphernode.ts +++ b/packages/enclave-contracts/tasks/ciphernode.ts @@ -8,13 +8,6 @@ import { ZeroAddress } from "ethers"; import { task } from "hardhat/config"; import { poseidon2 } from "poseidon-lite"; -import { - BondingRegistry__factory as BondingRegistryFactory, - EnclaveTicketToken__factory as EnclaveTicketTokenFactory, - EnclaveToken__factory as EnclaveTokenFactory, - MockUSDC__factory as MockUSDCFactory, -} from "../types"; - export const ciphernodeAdd = task( "ciphernode:add", "Register a ciphernode to the bonding registry and ciphernode registry", @@ -29,13 +22,13 @@ export const ciphernodeAdd = task( name: "licenseBondAmount", description: "amount of ENCL to bond (in wei, e.g., 1000000000000000000000 for 1000 ENCL)", - defaultValue: "1000000000000000000000", // 1000 ENCL + defaultValue: "1000000000000000000000", }) .addOption({ name: "ticketAmount", description: "amount of USDC to deposit for tickets (in wei, e.g., 100000000 for 100 USDC)", - defaultValue: "1000000000", // 1000 USDC + defaultValue: "1000000000", }) .setAction(async () => ({ default: async ({ privateKey, licenseBondAmount, ticketAmount }, hre) => { @@ -52,27 +45,26 @@ export const ciphernodeAdd = task( const { deployAndSaveBondingRegistry } = await import( "../scripts/deployAndSave/bondingRegistry" ); - const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); - - const licenseTokenAddress = await bondingRegistry.licenseToken(); - const licenseToken = EnclaveTokenFactory.connect( - licenseTokenAddress, - wallet, + const { deployAndSaveEnclaveTicketToken } = await import( + "../scripts/deployAndSave/enclaveTicketToken" ); - - const ticketTokenAddress = await bondingRegistry.ticketToken(); - const ticketToken = EnclaveTicketTokenFactory.connect( - ticketTokenAddress, - wallet, + const { deployAndSaveEnclaveToken } = await import( + "../scripts/deployAndSave/enclaveToken" ); - - const usdcAddress = await ticketToken.underlying(); - const usdcToken = MockUSDCFactory.connect(usdcAddress, wallet); - - const bondingRegistryConnected = BondingRegistryFactory.connect( - await bondingRegistry.getAddress(), - wallet, + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" ); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + const { enclaveToken } = await deployAndSaveEnclaveToken({ hre }); + const { enclaveTicketToken } = await deployAndSaveEnclaveTicketToken({ + hre, + }); + const { mockStableToken } = await deployAndSaveMockStableToken({ hre }); + + const licenseToken = enclaveToken.connect(wallet); + const ticketToken = enclaveTicketToken.connect(wallet); + const usdcToken = mockStableToken.connect(wallet); + const bondingRegistryConnected = bondingRegistry.connect(wallet); try { console.log("Step 1: Checking balances..."); @@ -123,7 +115,7 @@ export const ciphernodeAdd = task( console.log("Step 5: Approving USDC for ticket purchase..."); const approveUsdcTx = await usdcToken.approve( - ticketTokenAddress, + ticketToken.getAddress(), ticketAmountBigInt, ); await approveUsdcTx.wait(); @@ -257,26 +249,19 @@ export const ciphernodeMintTokens = task( const { deployAndSaveMockStableToken } = await import( "../scripts/deployAndSave/mockStableToken" ); - const { mockStableToken: mockUSDC } = await deployAndSaveMockStableToken({ + const { mockStableToken } = await deployAndSaveMockStableToken({ hre, }); - // Get properly typed contracts using factories const [signer] = await ethers.getSigners(); - const enclaveTokenTyped = EnclaveTokenFactory.connect( - await enclaveToken.getAddress(), - signer, - ); - const mockUSDCTyped = MockUSDCFactory.connect( - await mockUSDC.getAddress(), - signer, - ); + const enclaveTokenContract = enclaveToken.connect(signer); + const mockUSDCContract = mockStableToken.connect(signer); try { console.log(`Minting tokens for: ${ciphernodeAddress}`); console.log(`Minting ${enclAmount} ENCL...`); - const enclTx = await enclaveTokenTyped.mintAllocation( + const enclTx = await enclaveTokenContract.mintAllocation( ciphernodeAddress, ethers.parseEther(enclAmount), "Ciphernode allocation", @@ -285,7 +270,7 @@ export const ciphernodeMintTokens = task( console.log(`${enclAmount} ENCL minted`); console.log(`Minting ${usdcAmount} USDC...`); - const usdcTx = await mockUSDCTyped.mint( + const usdcTx = await mockUSDCContract.mint( ciphernodeAddress, ethers.parseUnits(usdcAmount, 6), ); @@ -293,8 +278,8 @@ export const ciphernodeMintTokens = task( console.log(`${usdcAmount} USDC minted`); const enclBalance = - await enclaveTokenTyped.balanceOf(ciphernodeAddress); - const usdcBalance = await mockUSDCTyped.balanceOf(ciphernodeAddress); + await enclaveTokenContract.balanceOf(ciphernodeAddress); + const usdcBalance = await mockUSDCContract.balanceOf(ciphernodeAddress); console.log("\n=== Token Balances ==="); console.log(`ENCL: ${ethers.formatEther(enclBalance)}`); @@ -377,11 +362,9 @@ export const ciphernodeAdminAdd = task( hre, }); - // Connect contracts to admin wallet const enclaveTokenConnected = enclaveToken.connect(adminWallet); const mockUSDCConnected = mockUSDC.connect(adminWallet); - // Get ticket token address const ticketTokenAddress = await bondingRegistry.ticketToken(); try { @@ -390,7 +373,6 @@ export const ciphernodeAdminAdd = task( console.log("Step 1: Minting and transferring ENCL to ciphernode..."); - // Mint ENCL tokens to admin const enclTx = await enclaveTokenConnected.mintAllocation( adminWallet.address, licenseBondWei, @@ -398,7 +380,6 @@ export const ciphernodeAdminAdd = task( ); await enclTx.wait(); - // Transfer ENCL to ciphernode const transferTx = await enclaveTokenConnected.transfer( ciphernodeAddress, licenseBondWei, @@ -414,7 +395,6 @@ export const ciphernodeAdminAdd = task( await usdcTx.wait(); console.log(`${ticketAmount} USDC minted to admin`); - // Impersonate the ciphernode for license bonding and registration console.log( "Step 3: Impersonating ciphernode for license operations...", ); @@ -423,10 +403,9 @@ export const ciphernodeAdminAdd = task( params: [ciphernodeAddress], }); - // Fund the impersonated account with ETH for gas await connection.provider.request({ method: "hardhat_setBalance", - params: [ciphernodeAddress, "0x1000000000000000000000"], // 1 ETH + params: [ciphernodeAddress, "0x1000000000000000000000"], }); const ciphernodeSigner = await ethers.getSigner(ciphernodeAddress); @@ -434,7 +413,6 @@ export const ciphernodeAdminAdd = task( const bondingRegistryAsCiphernode = bondingRegistry.connect(ciphernodeSigner); - // Approve and bond license as ciphernode const approveTx = await enclaveTokenAsCiphernode.approve( await bondingRegistry.getAddress(), licenseBondWei, @@ -446,51 +424,45 @@ export const ciphernodeAdminAdd = task( await bondTx.wait(); console.log(`License bonded: ${licenseBondAmount} ENCL`); - // Register as operator const registerTx = await bondingRegistryAsCiphernode.registerOperator(); await registerTx.wait(); console.log( "Operator registered (automatically added to CiphernodeRegistry)", ); - // Stop impersonating await connection.provider.request({ method: "hardhat_stopImpersonatingAccount", params: [ciphernodeAddress], }); console.log("Step 4: Adding ticket balance via admin..."); - // Now add ticket balance - admin approves USDC, then calls addTicketBalance as ciphernode + const approveUsdcTx = await mockUSDCConnected.approve( ticketTokenAddress, ticketAmountWei, ); await approveUsdcTx.wait(); - // Impersonate again for ticket balance await connection.provider.request({ method: "hardhat_impersonateAccount", params: [ciphernodeAddress], }); - // Fund the impersonated account with ETH for gas (again) await connection.provider.request({ method: "hardhat_setBalance", - params: [ciphernodeAddress, "0x1000000000000000000000"], // 1 ETH + params: [ciphernodeAddress, "0x1000000000000000000000"], }); const ciphernodeSigner2 = await ethers.getSigner(ciphernodeAddress); const bondingRegistryAsCiphernode2 = bondingRegistry.connect(ciphernodeSigner2); - // Transfer USDC from admin to ciphernode first const usdcTransferTx = await mockUSDCConnected.transfer( ciphernodeAddress, ticketAmountWei, ); await usdcTransferTx.wait(); - // Now ciphernode can add ticket balance const mockUSDCAsCiphernode = mockUSDC.connect(ciphernodeSigner2); const approveUsdcAsCiphernodeTx = await mockUSDCAsCiphernode.approve( ticketTokenAddress, @@ -503,13 +475,11 @@ export const ciphernodeAdminAdd = task( await addTicketTx.wait(); console.log(`Ticket balance added: ${ticketAmount} USDC worth`); - // Stop impersonating await connection.provider.request({ method: "hardhat_stopImpersonatingAccount", params: [ciphernodeAddress], }); - // Check final status const isRegistered = await bondingRegistry.isRegistered(ciphernodeAddress); const isActive = await bondingRegistry.isActive(ciphernodeAddress); From 58cfe9fcde4517505cbbd3cda6a2035186ee290a Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 17:59:50 +0500 Subject: [PATCH 13/88] chore: refactor --- .../scripts/deployAndSave/enclaveToken.ts | 46 ++++++++++--------- .../{ => Registry}/BondingRegistry.spec.ts | 14 +++--- .../CiphernodeRegistryOwnable.spec.ts | 0 .../NaiveRegistryFilter.spec.ts | 0 .../{ => Slashing}/SlashingManager.spec.ts | 18 ++++---- 5 files changed, 41 insertions(+), 37 deletions(-) rename packages/enclave-contracts/test/{ => Registry}/BondingRegistry.spec.ts (98%) rename packages/enclave-contracts/test/{CiphernodeRegistry => Registry}/CiphernodeRegistryOwnable.spec.ts (100%) rename packages/enclave-contracts/test/{CiphernodeRegistry => Registry}/NaiveRegistryFilter.spec.ts (100%) rename packages/enclave-contracts/test/{ => Slashing}/SlashingManager.spec.ts (98%) diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts index 891e053010..70ced195e5 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts @@ -20,6 +20,29 @@ export interface EnclaveTokenArgs { hre: HardhatRuntimeEnvironment; } +/** + * Disables transfer restrictions for local development + */ +async function disableTransferRestrictionsForLocal( + contract: EnclaveToken, + chain: string, +): Promise { + if (chain !== "localhost" && chain !== "hardhat") { + return; + } + + try { + const isRestricted = await contract.transfersRestricted(); + if (isRestricted) { + const tx = await contract.setTransferRestriction(false); + await tx.wait(); + console.log("Transfer restrictions disabled for local development"); + } + } catch (error) { + console.warn("Failed to disable transfer restrictions:", error); + } +} + /** * Deploys the EnclaveToken contract and saves the deployment arguments * @param param0 - The deployment arguments @@ -48,18 +71,7 @@ export const deployAndSaveEnclaveToken = async ({ signer, ); - if (chain === "localhost" || chain === "hardhat") { - try { - const isRestricted = await enclaveTokenContract.transfersRestricted(); - if (isRestricted) { - const tx = await enclaveTokenContract.setTransferRestriction(false); - await tx.wait(); - console.log("Transfer restrictions disabled for local development"); - } - } catch (error) { - console.warn("Failed to disable transfer restrictions:", error); - } - } + await disableTransferRestrictionsForLocal(enclaveTokenContract, chain); return { enclaveToken: enclaveTokenContract }; } @@ -95,15 +107,7 @@ export const deployAndSaveEnclaveToken = async ({ signer, ); - if (chain === "localhost" || chain === "hardhat") { - try { - const tx = await enclaveTokenContract.setTransferRestriction(false); - await tx.wait(); - console.log("Transfer restrictions disabled for local development"); - } catch (error) { - console.warn("Failed to disable transfer restrictions:", error); - } - } + await disableTransferRestrictionsForLocal(enclaveTokenContract, chain); return { enclaveToken: enclaveTokenContract }; }; diff --git a/packages/enclave-contracts/test/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts similarity index 98% rename from packages/enclave-contracts/test/BondingRegistry.spec.ts rename to packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts index 4bded34ff1..587af24316 100644 --- a/packages/enclave-contracts/test/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -8,12 +8,12 @@ import { expect } from "chai"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; -import BondingRegistryModule from "../ignition/modules/bondingRegistry"; -import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; -import EnclaveTokenModule from "../ignition/modules/enclaveToken"; -import MockCiphernodeRegistryModule from "../ignition/modules/mockCiphernodeRegistry"; -import MockStableTokenModule from "../ignition/modules/mockStableToken"; -import SlashingManagerModule from "../ignition/modules/slashingManager"; +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockCiphernodeRegistryModule from "../../ignition/modules/mockCiphernodeRegistry"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { BondingRegistry__factory as BondingRegistryFactory, CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, @@ -21,7 +21,7 @@ import { EnclaveToken__factory as EnclaveTokenFactory, MockUSDC__factory as MockUSDCFactory, SlashingManager__factory as SlashingManagerFactory, -} from "../types"; +} from "../../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; diff --git a/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts similarity index 100% rename from packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts rename to packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts diff --git a/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts b/packages/enclave-contracts/test/Registry/NaiveRegistryFilter.spec.ts similarity index 100% rename from packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts rename to packages/enclave-contracts/test/Registry/NaiveRegistryFilter.spec.ts diff --git a/packages/enclave-contracts/test/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts similarity index 98% rename from packages/enclave-contracts/test/SlashingManager.spec.ts rename to packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index fc6d2b8c3d..0b6806dde2 100644 --- a/packages/enclave-contracts/test/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -6,12 +6,12 @@ import { expect } from "chai"; import { network } from "hardhat"; -import BondingRegistryModule from "../ignition/modules/bondingRegistry"; -import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; -import EnclaveTokenModule from "../ignition/modules/enclaveToken"; -import MockSlashingVerifierModule from "../ignition/modules/mockSlashingVerifier"; -import MockStableTokenModule from "../ignition/modules/mockStableToken"; -import SlashingManagerModule from "../ignition/modules/slashingManager"; +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockSlashingVerifierModule from "../../ignition/modules/mockSlashingVerifier"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { BondingRegistry__factory as BondingRegistryFactory, EnclaveTicketToken__factory as EnclaveTicketTokenFactory, @@ -19,9 +19,9 @@ import { MockSlashingVerifier__factory as MockSlashingVerifierFactory, MockUSDC__factory as MockUSDCFactory, SlashingManager__factory as SlashingManagerFactory, -} from "../types"; -import type { SlashingManager } from "../types/contracts/slashing/SlashingManager"; -import type { MockSlashingVerifier } from "../types/contracts/test/MockSlashingVerifier"; +} from "../../types"; +import type { SlashingManager } from "../../types/contracts/slashing/SlashingManager"; +import type { MockSlashingVerifier } from "../../types/contracts/test/MockSlashingVerifier"; const { ethers, networkHelpers, ignition } = await network.connect(); const { loadFixture, time } = networkHelpers; From af8d7ac8b4c952b5d7bf23bd8483252a80e7c002 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 19:40:32 +0500 Subject: [PATCH 14/88] fix: mint USDC when requesting committee: --- packages/enclave-contracts/tasks/enclave.ts | 58 +++++++++++++++------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index 960c396125..b5bd7f0c3a 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -83,11 +83,24 @@ export const requestCommittee = task( }, hre, ) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + const { deployAndSaveEnclave } = await import( "../scripts/deployAndSave/enclave" ); + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" + ); const { enclave } = await deployAndSaveEnclave({ hre }); + const { mockStableToken: mockUSDC } = await deployAndSaveMockStableToken({ + hre, + }); + + const [signer] = await ethers.getSigners(); + const enclaveContract = enclave.connect(signer); + const mockUSDCContract = mockUSDC.connect(signer); const enclaveArgs = readDeploymentArgs( "Enclave", @@ -151,29 +164,42 @@ export const requestCommittee = task( ); } - console.log({ + const requestParams = { filter: filter === ZeroAddress ? filterArgs.address : filter, - threshold: [thresholdQuorum, thresholdTotal], - startWindow: [windowStart, windowEnd], + threshold: [thresholdQuorum, thresholdTotal] as [number, number], + startWindow: [windowStart, windowEnd] as [number, number], duration: duration, e3Program: e3Address === ZeroAddress ? mockE3ProgramArgs!.address : e3Address, e3ProgramParams, computeProviderParams, - }); - const tx = await enclave.request( - { - filter: filter === ZeroAddress ? filterArgs.address : filter, - threshold: [thresholdQuorum, thresholdTotal], - startWindow: [windowStart, windowEnd], - duration: duration, - e3Program: - e3Address === ZeroAddress ? mockE3ProgramArgs!.address : e3Address, - e3ProgramParams, - computeProviderParams, - }, - { value: "1000000000000000000" }, + }; + + console.log("Request parameters:", requestParams); + + const fee = await enclaveContract.getE3Quote(requestParams); + console.log(`E3 fee: ${ethers.formatUnits(fee, 6)} USDC`); + + const usdcBalance = await mockUSDCContract.balanceOf(signer.address); + console.log(`USDC balance: ${ethers.formatUnits(usdcBalance, 6)} USDC`); + + if (usdcBalance < fee) { + const mintAmount = fee - usdcBalance + ethers.parseUnits("1000", 6); + console.log(`Minting ${ethers.formatUnits(mintAmount, 6)} USDC...`); + const mintTx = await mockUSDCContract.mint(signer.address, mintAmount); + await mintTx.wait(); + console.log("USDC minted"); + } + + console.log("Approving USDC spending..."); + const approveTx = await mockUSDCContract.approve( + await enclaveContract.getAddress(), + fee, ); + await approveTx.wait(); + console.log("USDC approved"); + + const tx = await enclaveContract.request(requestParams); console.log("Requesting committee... ", tx.hash); await tx.wait(); From 1c4cb167898bb959a4ab8aad1d7de48369f0895e Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 30 Sep 2025 21:11:23 +0500 Subject: [PATCH 15/88] fix: contract addresses --- examples/CRISP/Readme.md | 15 ++++++++------- examples/CRISP/client/.env.example | 4 ++-- examples/CRISP/deploy/config.toml | 2 +- examples/CRISP/enclave.config.yaml | 10 +++++----- examples/CRISP/server/.env.example | 8 ++++---- templates/default/enclave.config.yaml | 8 ++++---- tests/integration/enclave.config.yaml | 6 +++--- tests/integration/fns.sh | 8 ++++---- 8 files changed, 31 insertions(+), 30 deletions(-) diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index feb492f557..f0828a9f52 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -22,7 +22,7 @@ To start the development environment, run the following commands from inside the ```sh pnpm install # install dependencies pnpm dev:setup # build the project -pnpm dev:up # run the services +pnpm dev:up # run the services ``` The two commands above will start everything you need to run the CRISP protocol. You can then interact with it using the web application at `http://localhost:3000`. Please ensure you follow the prerequisites installation for running the protocol locally. @@ -102,6 +102,7 @@ To set up the CRISP dApp in your local environment, follow these steps: ```sh cd examples/CRISP/client ``` + 2. Start the development server: ```sh @@ -164,9 +165,9 @@ After deployment, you will see the addresses for the following contracts: Note down the first four addresses as they will be needed to configure `risc0`, `local_testnet` and the `server`. -### Step 3: Deploy the RISC Zero Contracts +### Step 3: Deploy the RISC Zero Contracts -> Please note that this step is optional for development only. You can run the program server in dev mode which does not use Risc0. +> Please note that this step is optional for development only. You can run the program server in dev mode which does not use Risc0. 1. Navigate to the `CRISP/lib/risc0-ethereum` directory. @@ -234,10 +235,10 @@ CHAIN_ID=31337 CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -CIPHERNODE_REGISTRY_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" -NAIVE_REGISTRY_FILTER_ADDRESS="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -E3_PROGRAM_ADDRESS="0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1" # CRISPProgram Contract Address +ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" +NAIVE_REGISTRY_FILTER_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" +E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address # E3 Config E3_WINDOW_SIZE=600 diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 6d6aac6760..ef9c17b74d 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,5 +1,5 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1 # Default E3 program address from anvil -VITE_SEMAPHORE_ADDRESS=0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE +VITE_E3_PROGRAM_ADDRESS=0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8 # Default E3 program address from anvil +VITE_SEMAPHORE_ADDRESS=0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E diff --git a/examples/CRISP/deploy/config.toml b/examples/CRISP/deploy/config.toml index 95d5f815b8..6173c80a83 100644 --- a/examples/CRISP/deploy/config.toml +++ b/examples/CRISP/deploy/config.toml @@ -14,4 +14,4 @@ enclaveAddress = "0xE3000000000000000000000000000000000000E3" [profile.custom] chainId = 31337 riscZeroVerifierAddress = "0x0000000000000000000000000000000000000000" # Deployed with the script. Don't set or it will be skipped. -enclaveAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # Based on default deployment address using local anvil node +enclaveAddress = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" # Based on default deployment address using local anvil node diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 611cd14fc1..f5509b38b8 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -2,17 +2,17 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - enclave: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" - ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" - + enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + filter_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + program: dev: true # risc0: # risc0_dev_mode: 1 # bonsai_api_key: xxxxxxxxxxxxxxxx # bonsai_api_url: xxxxxxxxxxxxxxxx - + nodes: cn1: address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index e25bfab57f..fd92d87342 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -10,10 +10,10 @@ CHAIN_ID=31337 CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -CIPHERNODE_REGISTRY_ADDRESS="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -NAIVE_REGISTRY_FILTER_ADDRESS="0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -E3_PROGRAM_ADDRESS="0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" # CRISPProgram Contract Address +ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" +NAIVE_REGISTRY_FILTER_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" +E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address # E3 Config E3_WINDOW_SIZE=40 diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 20a95d91b4..bc9fe505fc 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,10 +2,10 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x0B306BF915C4d645ff596e518fAf3F9669b97016" - enclave: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" - ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + e3_program: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" + enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + filter_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" program: dev: true diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 69d5603303..ffe410bcac 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,9 +2,9 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - enclave: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" - ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + filter_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" nodes: cn1: diff --git a/tests/integration/fns.sh b/tests/integration/fns.sh index 865bf7296a..b63d37b935 100644 --- a/tests/integration/fns.sh +++ b/tests/integration/fns.sh @@ -21,10 +21,10 @@ CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams # These contracts are based on the deterministic order of hardhat deploy # We _may_ wish to get these off the hardhat environment somehow? -ENCLAVE_CONTRACT="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -REGISTRY_CONTRACT="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -REGISTRY_FILTER_CONTRACT="0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -INPUT_VALIDATOR_CONTRACT="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" +ENCLAVE_CONTRACT="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +REGISTRY_CONTRACT="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" +REGISTRY_FILTER_CONTRACT="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" +INPUT_VALIDATOR_CONTRACT="0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" # These are random addresses for now CIPHERNODE_ADDRESS_1="0x2546BcD3c84621e976D8185a91A922aE77ECEc30" From 8b1f9ee208bbf00bc39ca1731810da35397089f6 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 1 Oct 2025 16:33:56 +0500 Subject: [PATCH 16/88] fix: review fixes --- packages/enclave-contracts/README.md | 48 ++++++++++- .../enclave-contracts/contracts/Enclave.sol | 47 +++++----- .../contracts/interfaces/IEnclave.sol | 16 +++- .../registry/NaiveRegistryFilter.sol | 4 +- .../contracts/token/EnclaveTicketToken.sol | 86 ++++++++++++------- .../contracts/token/EnclaveToken.sol | 56 +++++------- .../ignition/modules/enclave.ts | 4 +- .../ignition/modules/enclaveTicketToken.ts | 4 +- .../scripts/deployAndSave/enclave.ts | 12 +-- .../deployAndSave/enclaveTicketToken.ts | 12 +-- .../scripts/deployEnclave.ts | 16 ++-- .../enclave-contracts/tasks/ciphernode.ts | 57 ++++-------- .../enclave-contracts/test/Enclave.spec.ts | 4 +- .../test/Registry/BondingRegistry.spec.ts | 2 +- .../test/Slashing/SlashingManager.spec.ts | 2 +- 15 files changed, 207 insertions(+), 163 deletions(-) diff --git a/packages/enclave-contracts/README.md b/packages/enclave-contracts/README.md index 0963347a37..40b5c9b5ec 100644 --- a/packages/enclave-contracts/README.md +++ b/packages/enclave-contracts/README.md @@ -47,12 +47,56 @@ pnpm deploy:mocks --network localhost This will ensure that you are a local node running, as well as that there are no conflicting deployments stored in localhost. +## Configuration + +### Using Environment Variables (Development) + +For development, you can set your private key in a `.env` file: + +```sh +# .env +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### Using Hardhat Configuration Variables (Production) + +For production, it's recommended to use Hardhat's configuration variables +system: + +```sh +# Set your configuration variable (securely stored) +npx hardhat vars set PRIVATE_KEY + +``` + +Then update `hardhat.config.ts` to use configuration variables: + +```typescript +import { vars } from "hardhat/config"; + +const privateKey = vars.get("PRIVATE_KEY", ""); +``` + ## Registering a Ciphernode -To add a ciphernode to the registry, run +The tasks use the first signer configured in your Hardhat network configuration. + +To add a ciphernode to the registry: + +```sh +pnpm ciphernode:add --network [network] +``` + +Options: + +- `--license-bond-amount`: Amount of ENCL to bond (default: 1000 ENCL) +- `--ticket-amount`: Amount of USDC for tickets (default: 1000 USDC) + +For testing/development, you can also use the admin task to register any +ciphernode address: ```sh -pnpm ciphernode:add --network [network] --ciphernode-address [address] +pnpm ciphernode:admin-add --network localhost --ciphernode-address [address] ``` To request a new committee, run diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index aa698ee5b9..827a58ace1 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -36,7 +36,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { ICiphernodeRegistry public ciphernodeRegistry; // address of the Ciphernode registry. IBondingRegistry public bondingRegistry; // address of the Bonding registry. - IERC20 public usdcToken; // address of the USDC token. + IERC20 public feeToken; // address of the Fee token. uint256 public maxDuration; // maximum duration of a computation in seconds. uint256 public nexte3Id; // ID of the next E3. @@ -94,7 +94,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { error InsufficientBalance(); error InsufficientAllowance(); error InvalidBondingRegistry(IBondingRegistry bondingRegistry); - error InvalidUsdcToken(IERC20 usdcToken); + error InvalidFeeToken(IERC20 feeToken); //////////////////////////////////////////////////////////// // // @@ -109,7 +109,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { address _owner, ICiphernodeRegistry _ciphernodeRegistry, IBondingRegistry _bondingRegistry, - IERC20 _usdcToken, + IERC20 _feeToken, uint256 _maxDuration, bytes[] memory _e3ProgramsParams ) { @@ -117,7 +117,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { _owner, _ciphernodeRegistry, _bondingRegistry, - _usdcToken, + _feeToken, _maxDuration, _e3ProgramsParams ); @@ -131,7 +131,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { address _owner, ICiphernodeRegistry _ciphernodeRegistry, IBondingRegistry _bondingRegistry, - IERC20 _usdcToken, + IERC20 _feeToken, uint256 _maxDuration, bytes[] memory _e3ProgramsParams ) public initializer { @@ -139,7 +139,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { setMaxDuration(_maxDuration); setCiphernodeRegistry(_ciphernodeRegistry); setBondingRegistry(_bondingRegistry); - setUsdcToken(_usdcToken); + setFeeToken(_feeToken); setE3ProgramsParams(_e3ProgramsParams); if (_owner != owner()) transferOwnership(_owner); } @@ -222,7 +222,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { e3s[e3Id] = e3; e3Payments[e3Id] = e3Fee; - usdcToken.safeTransferFrom(msg.sender, address(this), e3Fee); + feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); require( ciphernodeRegistry.requestCommittee( @@ -366,21 +366,24 @@ contract Enclave is IEnclave, OwnableUpgradeable { .getCommittee(e3Id); uint256[] memory amounts = new uint256[](committee.nodes.length); - // We might need to pay different amounts to different nodes. + // TODO: do we need to pay different amounts to different nodes? // For now, we'll pay the same amount to all nodes. uint256 amount = e3Payments[e3Id] / committee.nodes.length; for (uint256 i = 0; i < committee.nodes.length; i++) { amounts[i] = amount; } - // Approve the BondingRegistry to spend the USDC tokens - usdcToken.approve(address(bondingRegistry), e3Payments[e3Id]); - // Zero out the payment + uint256 totalAmount = e3Payments[e3Id]; e3Payments[e3Id] = 0; - // Distribute rewards to the committee - bondingRegistry.distributeRewards(usdcToken, committee.nodes, amounts); - // Where does dust go? Treasury maybe? - usdcToken.approve(address(bondingRegistry), 0); + + feeToken.approve(address(bondingRegistry), totalAmount); + + bondingRegistry.distributeRewards(feeToken, committee.nodes, amounts); + + // TODO: decide where does dust go? Treasury maybe? + feeToken.approve(address(bondingRegistry), 0); + + emit RewardsDistributed(e3Id, committee.nodes, amounts); } //////////////////////////////////////////////////////////// @@ -423,16 +426,16 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit BondingRegistrySet(address(_bondingRegistry)); } - function setUsdcToken( - IERC20 _usdcToken + function setFeeToken( + IERC20 _feeToken ) public onlyOwner returns (bool success) { require( - address(_usdcToken) != address(0) && _usdcToken != usdcToken, - InvalidUsdcToken(_usdcToken) + address(_feeToken) != address(0) && _feeToken != feeToken, + InvalidFeeToken(_feeToken) ); - usdcToken = _usdcToken; + feeToken = _feeToken; success = true; - emit UsdcTokenSet(address(_usdcToken)); + emit FeeTokenSet(address(_feeToken)); } function enableE3Program( @@ -518,6 +521,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { return InternalLeanIMT._root(inputs[e3Id]); } + // TODO: this should be calculated based on the E3 program and the parameters + // This is just a placeholder for now function getE3Quote( E3RequestParams calldata ) public pure returns (uint256 fee) { diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 1982780e78..a8639e04c8 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -74,9 +74,19 @@ interface IEnclave { /// @param bondingRegistry The address of the BondingRegistry contract. event BondingRegistrySet(address bondingRegistry); - /// @notice This event MUST be emitted any time the USDC token is set. - /// @param usdcToken The address of the USDC token. - event UsdcTokenSet(address usdcToken); + /// @notice This event MUST be emitted any time the fee token is set. + /// @param feeToken The address of the fee token. + event FeeTokenSet(address feeToken); + + /// @notice This event MUST be emitted when rewards are distributed to committee members. + /// @param e3Id The ID of the E3 computation. + /// @param nodes The addresses of the committee members receiving rewards. + /// @param amounts The reward amounts for each committee member. + event RewardsDistributed( + uint256 indexed e3Id, + address[] nodes, + uint256[] amounts + ); /// @notice The event MUST be emitted any time an encryption scheme is enabled. /// @param encryptionSchemeId The ID of the encryption scheme that was enabled. diff --git a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol index a7774dc097..2e430da8f9 100644 --- a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol +++ b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol @@ -89,8 +89,8 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { function publishCommittee( uint256 e3Id, - address[] memory nodes, - bytes memory publicKey + address[] calldata nodes, + bytes calldata publicKey ) external onlyOwner { IRegistryFilter.Committee storage committee = committees[e3Id]; require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol index 50187fbcae..dd87411104 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol @@ -21,7 +21,7 @@ import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; /** * @title EnclaveTicketToken (ETK) - * @notice Non-transferable non-delegatable ERC20Votes wrapper over USDC for operator staking + * @notice Non-transferable non-delegatable ERC20Votes wrapper over a Stable token (USDC, DAI etc.) for operator staking */ contract EnclaveTicketToken is ERC20, @@ -30,42 +30,55 @@ contract EnclaveTicketToken is Ownable, ERC20Wrapper { - address public registry; - + // Custom errors error NotRegistry(); error TransferNotAllowed(); error ZeroAddress(); error DelegationLocked(); + /// @dev Address of the registry contract that manages deposits and withdrawals. + address public registry; + modifier onlyRegistry() { if (msg.sender != registry) revert NotRegistry(); _; } + /** + * @notice Deploy the Enclave Ticket Token. + * @param baseToken The underlying ERC20 token to wrap (e.g., USDC, DAI). + * @param registry_ The address of the registry contract. + * @param initialOwner_ The address that will own the contract. + */ constructor( - IERC20 underlyingUSDC, + IERC20 baseToken, address registry_, address initialOwner_ ) ERC20("Enclave Ticket Token", "ETK") ERC20Permit("Enclave Ticket Token") - ERC20Wrapper(underlyingUSDC) + ERC20Wrapper(baseToken) Ownable(initialOwner_) { - require(registry_ != address(0), ZeroAddress()); - registry = registry_; + setRegistry(registry_); } - function setRegistry(address newRegistry) external onlyOwner { + /** + * @notice Set a new registry contract address. + * @dev Only callable by the contract owner. + * @param newRegistry The address of the new registry contract. + */ + function setRegistry(address newRegistry) public onlyOwner { require(newRegistry != address(0), ZeroAddress()); registry = newRegistry; } /** - * @notice Deposit USDC and mint ticket tokens to operator - * @param operator Address to receive the ticket tokens - * @param amount Amount of USDC to wrap - * @return success True if successful + * @notice Deposit Base token and mint ticket tokens to operator. + * @dev Only callable by the registry contract. Auto-delegates on first deposit. + * @param operator Address to receive the ticket tokens. + * @param amount Amount of tokens to deposit. + * @return success True if successful. */ function depositFor( address operator, @@ -80,11 +93,12 @@ contract EnclaveTicketToken is } /** - * @notice Deposit USDC from an account and mint ticket tokens to an account - * @param from Address to deposit from - * @param to Address to mint to - * @param amount Amount of USDC to deposit - * @return success True if successful + * @notice Deposit Base token from an account and mint ticket tokens to an account. + * @dev Only callable by the registry contract. Auto-delegates on first deposit. + * @param from Address to deposit from. + * @param to Address to mint to. + * @param amount Amount of tokens to deposit. + * @return True if successful. */ function depositFrom( address from, @@ -98,11 +112,11 @@ contract EnclaveTicketToken is } /** - * @notice Burn ticket tokens and transfer USDC to receiver - * @dev Registry must have approval or use permit before calling - * @param receiver Address to receive the USDC - * @param amount Amount of ticket tokens to burn - * @return success True if successful + * @notice Burn ticket tokens and transfer Base token to receiver. + * @dev Only callable by the registry contract. + * @param receiver Address to receive the Underlying token. + * @param amount Amount of ticket tokens to burn. + * @return success True if successful. */ function withdrawTo( address receiver, @@ -112,9 +126,10 @@ contract EnclaveTicketToken is } /** - * @notice Burn ticket tokens - * @param operator Address to burn from - * @param amount Amount of ticket tokens to burn + * @notice Burn ticket tokens from an operator. + * @dev Only callable by the registry contract. + * @param operator Address to burn from. + * @param amount Amount of ticket tokens to burn. */ function burnTickets( address operator, @@ -124,16 +139,17 @@ contract EnclaveTicketToken is } /** - * @notice Payout ticket tokens to an address - * @param to Address to payout to - * @param amount Amount of ticket tokens to payout + * @notice Transfer underlying tokens to an address without burning ticket tokens. + * @dev Only callable by the registry contract. + * @param to Address to payout to. + * @param amount Amount of ticket tokens to payout. */ function payout(address to, uint256 amount) external onlyRegistry { IERC20(address(underlying())).transfer(to, amount); } /** - * @notice Prevent transfers between users (only mint/burn allowed) + * @dev Override ERC20Votes update hook to prevent transfers between users. */ function _update( address from, @@ -147,16 +163,14 @@ contract EnclaveTicketToken is } /** - * @notice Delegate voting power to an address. - * @dev This function is locked and cannot be used. + * @dev Prevent delegation of voting power. */ function delegate(address) public pure override { revert DelegationLocked(); } /** - * @notice Delegate voting power to an address using a signature. - * @dev This function is locked and cannot be used. + * @dev Prevent delegation of voting power via signature. */ function delegateBySig( address, @@ -169,6 +183,9 @@ contract EnclaveTicketToken is revert DelegationLocked(); } + /** + * @dev Expose decimals from the underlying token. + */ function decimals() public view @@ -178,6 +195,9 @@ contract EnclaveTicketToken is return super.decimals(); } + /** + * @dev Expose permit nonces via both ERC20Permit and OpenZeppelin Nonces. + */ function nonces( address owner ) public view override(ERC20Permit, Nonces) returns (uint256) { diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol index 597c6d56d1..aa75da9f95 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -114,32 +114,30 @@ contract EnclaveToken is * @param allocations Array of allocation descriptions. */ function batchMintAllocations( - address[] memory recipients, - uint256[] memory amounts, - string[] memory allocations + address[] calldata recipients, + uint256[] calldata amounts, + string[] calldata allocations ) external onlyRole(MINTER_ROLE) { - if ( - recipients.length != amounts.length || - amounts.length != allocations.length - ) revert ArrayLengthMismatch(); - - uint256 totalAmount = 0; - for (uint256 i = 0; i < amounts.length; i++) { - totalAmount += amounts[i]; - } - if (totalMinted + totalAmount > TOTAL_SUPPLY) - revert ExceedsTotalSupply(); + uint256 len = recipients.length; + if (amounts.length != len || allocations.length != len) + revert ArrayLengthMismatch(); + + uint256 minted = totalMinted; + + for (uint256 i = 0; i < len; i++) { + address recipient = recipients[i]; + uint256 amount = amounts[i]; + if (recipient == address(0)) revert ZeroAddress(); + if (amount == 0) revert ZeroAmount(); - for (uint256 i = 0; i < recipients.length; i++) { - address rec = recipients[i]; - uint256 amt = amounts[i]; - if (rec == address(0)) revert ZeroAddress(); - if (amt == 0) revert ZeroAmount(); + if (amount > TOTAL_SUPPLY - minted) revert ExceedsTotalSupply(); + minted += amount; - _mint(rec, amt); - emit AllocationMinted(rec, amt, allocations[i]); + _mint(recipient, amount); + emit AllocationMinted(recipient, amount, allocations[i]); } - totalMinted += totalAmount; + + totalMinted = minted; } /** @@ -152,20 +150,6 @@ contract EnclaveToken is emit TransferRestrictionUpdated(restricted); } - /** - * @notice Add or remove an address from the transfer whitelist. - * @dev Only the owner may call this. - * @param account Address whose whitelist status is to be updated. - * @param whitelisted Whether the address should be whitelisted. - */ - function setTransferWhitelist( - address account, - bool whitelisted - ) external onlyOwner { - transferWhitelisted[account] = whitelisted; - emit TransferWhitelistUpdated(account, whitelisted); - } - /** * @notice Toggle an account's whitelist status. * @dev Only the owner may call this. diff --git a/packages/enclave-contracts/ignition/modules/enclave.ts b/packages/enclave-contracts/ignition/modules/enclave.ts index 5ec3528c79..03aa4f9777 100644 --- a/packages/enclave-contracts/ignition/modules/enclave.ts +++ b/packages/enclave-contracts/ignition/modules/enclave.ts @@ -13,13 +13,13 @@ export default buildModule("Enclave", (m) => { const maxDuration = m.getParameter("maxDuration"); const registry = m.getParameter("registry"); const bondingRegistry = m.getParameter("bondingRegistry"); - const usdcToken = m.getParameter("usdcToken"); + const feeToken = m.getParameter("feeToken"); const poseidonT3 = m.library("PoseidonT3"); const enclave = m.contract( "Enclave", - [owner, registry, bondingRegistry, usdcToken, maxDuration, [params]], + [owner, registry, bondingRegistry, feeToken, maxDuration, [params]], { libraries: { PoseidonT3: poseidonT3, diff --git a/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts b/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts index 476903cf14..de98a2a1d8 100644 --- a/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts +++ b/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts @@ -8,12 +8,12 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("EnclaveTicketToken", (m) => { - const underlyingUSDC = m.getParameter("underlyingUSDC"); + const baseToken = m.getParameter("baseToken"); const registry = m.getParameter("registry"); const owner = m.getParameter("owner"); const enclaveTicketToken = m.contract("EnclaveTicketToken", [ - underlyingUSDC, + baseToken, registry, owner, ]); diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index f3e2df9a33..3ac14e0c96 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -18,7 +18,7 @@ export interface EnclaveArgs { maxDuration?: string; registry?: string; bondingRegistry?: string; - usdcToken?: string; + feeToken?: string; hre: HardhatRuntimeEnvironment; } @@ -33,7 +33,7 @@ export const deployAndSaveEnclave = async ({ maxDuration, registry, bondingRegistry, - usdcToken, + feeToken, hre, }: EnclaveArgs): Promise<{ enclave: Enclave }> => { const { ignition, ethers } = await hre.network.connect(); @@ -49,13 +49,13 @@ export const deployAndSaveEnclave = async ({ !maxDuration || !registry || !bondingRegistry || - !usdcToken || + !feeToken || (preDeployedArgs?.constructorArgs?.params === params && preDeployedArgs?.constructorArgs?.owner === owner && preDeployedArgs?.constructorArgs?.maxDuration === maxDuration && preDeployedArgs?.constructorArgs?.registry === registry && preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && - preDeployedArgs?.constructorArgs?.usdcToken === usdcToken) + preDeployedArgs?.constructorArgs?.feeToken === feeToken) ) { if (!preDeployedArgs?.address) { throw new Error("Enclave address not found, it must be deployed first"); @@ -75,7 +75,7 @@ export const deployAndSaveEnclave = async ({ maxDuration, registry, bondingRegistry, - usdcToken, + feeToken, }, }, }); @@ -93,7 +93,7 @@ export const deployAndSaveEnclave = async ({ maxDuration, registry, bondingRegistry, - usdcToken, + feeToken, }, blockNumber, address: enclaveAddress, diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts index 22a433f3d5..a5ee11c305 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts @@ -16,7 +16,7 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; * The arguments for the deployAndSaveEnclaveTicketToken function */ export interface EnclaveTicketTokenArgs { - underlyingUSDC?: string; + baseToken?: string; registry?: string; owner?: string; hre: HardhatRuntimeEnvironment; @@ -28,7 +28,7 @@ export interface EnclaveTicketTokenArgs { * @returns The deployed EnclaveTicketToken contract */ export const deployAndSaveEnclaveTicketToken = async ({ - underlyingUSDC, + baseToken, registry, owner, hre, @@ -42,10 +42,10 @@ export const deployAndSaveEnclaveTicketToken = async ({ const preDeployedArgs = readDeploymentArgs("EnclaveTicketToken", chain); if ( - !underlyingUSDC || + !baseToken || !registry || !owner || - (preDeployedArgs?.constructorArgs?.underlyingUSDC === underlyingUSDC && + (preDeployedArgs?.constructorArgs?.baseToken === baseToken && preDeployedArgs?.constructorArgs?.registry === registry && preDeployedArgs?.constructorArgs?.owner === owner) ) { @@ -64,7 +64,7 @@ export const deployAndSaveEnclaveTicketToken = async ({ const enclaveTicketToken = await ignition.deploy(EnclaveTicketTokenModule, { parameters: { EnclaveTicketToken: { - underlyingUSDC, + baseToken, registry, owner, }, @@ -81,7 +81,7 @@ export const deployAndSaveEnclaveTicketToken = async ({ storeDeploymentArgs( { constructorArgs: { - underlyingUSDC, + baseToken, registry, owner, }, diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 477b331565..2b93acc6f7 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -38,19 +38,19 @@ export const deployEnclave = async (withMocks?: boolean) => { const addressOne = "0x0000000000000000000000000000000000000001"; const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; - let usdcTokenAddress: string; + let feeTokenAddress: string; if (shouldDeployMocks) { - console.log("Deploying mock USDC token..."); + console.log("Deploying mock Fee token..."); const { mockStableToken } = await deployAndSaveMockStableToken({ initialSupply: 1000000, hre, }); - usdcTokenAddress = await mockStableToken.getAddress(); - console.log("MockUSDC deployed to:", usdcTokenAddress); + feeTokenAddress = await mockStableToken.getAddress(); + console.log("MockFeeToken deployed to:", feeTokenAddress); } else { throw new Error( - "USDC token address must be provided for production deployment", + "Fee token address must be provided for production deployment", ); } @@ -64,7 +64,7 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Deploying EnclaveTicketToken..."); const { enclaveTicketToken } = await deployAndSaveEnclaveTicketToken({ - underlyingUSDC: usdcTokenAddress, + baseToken: feeTokenAddress, registry: addressOne, owner: ownerAddress, hre, @@ -113,7 +113,7 @@ export const deployEnclave = async (withMocks?: boolean) => { maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), registry: ciphernodeRegistryAddress, bondingRegistry: bondingRegistryAddress, - usdcToken: usdcTokenAddress, + feeToken: feeTokenAddress, hre, }); const enclaveAddress = await enclave.getAddress(); @@ -188,7 +188,7 @@ export const deployEnclave = async (withMocks?: boolean) => { ============================================ Deployment Complete! ============================================ - MockUSDC: ${usdcTokenAddress} + MockFeeToken: ${feeTokenAddress} EnclaveToken (ENCL): ${enclaveTokenAddress} EnclaveTicketToken: ${enclaveTicketTokenAddress} SlashingManager: ${slashingManagerAddress} diff --git a/packages/enclave-contracts/tasks/ciphernode.ts b/packages/enclave-contracts/tasks/ciphernode.ts index 91c620d662..a966c14d07 100644 --- a/packages/enclave-contracts/tasks/ciphernode.ts +++ b/packages/enclave-contracts/tasks/ciphernode.ts @@ -12,12 +12,6 @@ export const ciphernodeAdd = task( "ciphernode:add", "Register a ciphernode to the bonding registry and ciphernode registry", ) - .addOption({ - name: "privateKey", - description: - "private key of the ciphernode to register (must have ENCL and USDC)", - defaultValue: "", - }) .addOption({ name: "licenseBondAmount", description: @@ -31,16 +25,12 @@ export const ciphernodeAdd = task( defaultValue: "1000000000", }) .setAction(async () => ({ - default: async ({ privateKey, licenseBondAmount, ticketAmount }, hre) => { + default: async ({ licenseBondAmount, ticketAmount }, hre) => { const connection = await hre.network.connect(); const { ethers } = connection; - if (!privateKey) { - throw new Error("Private key is required. Use --private-key option."); - } - - const wallet = new ethers.Wallet(privateKey, ethers.provider); - console.log(`Registering ciphernode: ${wallet.address}`); + const [signer] = await ethers.getSigners(); + console.log(`Registering ciphernode: ${signer.address}`); const { deployAndSaveBondingRegistry } = await import( "../scripts/deployAndSave/bondingRegistry" @@ -61,15 +51,15 @@ export const ciphernodeAdd = task( }); const { mockStableToken } = await deployAndSaveMockStableToken({ hre }); - const licenseToken = enclaveToken.connect(wallet); - const ticketToken = enclaveTicketToken.connect(wallet); - const usdcToken = mockStableToken.connect(wallet); - const bondingRegistryConnected = bondingRegistry.connect(wallet); + const licenseToken = enclaveToken.connect(signer); + const ticketToken = enclaveTicketToken.connect(signer); + const usdcToken = mockStableToken.connect(signer); + const bondingRegistryConnected = bondingRegistry.connect(signer); try { console.log("Step 1: Checking balances..."); - const enclBalance = await licenseToken.balanceOf(wallet.address); - const usdcBalance = await usdcToken.balanceOf(wallet.address); + const enclBalance = await licenseToken.balanceOf(signer.address); + const usdcBalance = await usdcToken.balanceOf(signer.address); console.log(`ENCL balance: ${ethers.formatEther(enclBalance)}`); console.log(`USDC balance: ${ethers.formatUnits(usdcBalance, 6)}`); @@ -129,17 +119,17 @@ export const ciphernodeAdd = task( `Ticket balance added: ${ethers.formatUnits(ticketAmountBigInt, 6)} USDC worth`, ); - const isRegistered = await bondingRegistry.isRegistered(wallet.address); - const isActive = await bondingRegistry.isActive(wallet.address); + const isRegistered = await bondingRegistry.isRegistered(signer.address); + const isActive = await bondingRegistry.isActive(signer.address); const licenseBond = await bondingRegistry.getLicenseBond( - wallet.address, + signer.address, ); const ticketBalance = await bondingRegistry.getTicketBalance( - wallet.address, + signer.address, ); console.log("\n=== Registration Complete ==="); - console.log(`Ciphernode: ${wallet.address}`); + console.log(`Ciphernode: ${signer.address}`); console.log(`Registered: ${isRegistered}`); console.log(`Active: ${isActive}`); console.log(`License Bond: ${ethers.formatEther(licenseBond)} ENCL`); @@ -158,34 +148,25 @@ export const ciphernodeRemove = task( "ciphernode:remove", "Deregister a ciphernode from the bonding registry", ) - .addOption({ - name: "privateKey", - description: "private key of the ciphernode to deregister", - defaultValue: "", - }) .addOption({ name: "siblings", description: "comma separated siblings from tree proof", defaultValue: "", }) .setAction(async () => ({ - default: async ({ privateKey, siblings }, hre) => { + default: async ({ siblings }, hre) => { const connection = await hre.network.connect(); const { ethers } = connection; - if (!privateKey) { - throw new Error("Private key is required. Use --private-key option."); - } - - const wallet = new ethers.Wallet(privateKey, ethers.provider); - console.log(`Deregistering ciphernode: ${wallet.address}`); + const [signer] = await ethers.getSigners(); + console.log(`Deregistering ciphernode: ${signer.address}`); const { deployAndSaveBondingRegistry } = await import( "../scripts/deployAndSave/bondingRegistry" ); const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); - const bondingRegistryConnected = bondingRegistry.connect(wallet); + const bondingRegistryConnected = bondingRegistry.connect(signer); const siblingsArray = siblings.split(",").map((s: string) => BigInt(s)); @@ -197,7 +178,7 @@ export const ciphernodeRemove = task( await bondingRegistryConnected.deregisterOperator(siblingsArray); await tx.wait(); - console.log(`Ciphernode ${wallet.address} deregistered`); + console.log(`Ciphernode ${signer.address} deregistered`); console.log( "Note: Funds are now in exit queue. Use claimExits() after the exit delay period.", ); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 80e7272ddc..7139ba636d 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -108,7 +108,7 @@ describe("Enclave", function () { { parameters: { EnclaveTicketToken: { - underlyingUSDC: await usdcToken.getAddress(), + baseToken: await usdcToken.getAddress(), registry: addressOne, owner: ownerAddress, }, @@ -157,7 +157,7 @@ describe("Enclave", function () { registry: addressOne, bondingRegistry: await bondingRegistryContract.bondingRegistry.getAddress(), - usdcToken: await usdcToken.getAddress(), + feeToken: await usdcToken.getAddress(), }, }, }); diff --git a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts index 587af24316..0b788537ab 100644 --- a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -82,7 +82,7 @@ describe("BondingRegistry", function () { { parameters: { EnclaveTicketToken: { - underlyingUSDC: await usdcContract.mockUSDC.getAddress(), + baseToken: await usdcContract.mockUSDC.getAddress(), registry: AddressOne, owner: ownerAddress, }, diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 0b6806dde2..be5c7e2338 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -104,7 +104,7 @@ describe("SlashingManager", function () { { parameters: { EnclaveTicketToken: { - underlyingUSDC: await usdcContract.mockUSDC.getAddress(), + baseToken: await usdcContract.mockUSDC.getAddress(), registry: ownerAddress, owner: ownerAddress, }, From 14711f3392288bd9dd2c6f3e0d9aa73f0c9403b7 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 1 Oct 2025 17:16:40 +0500 Subject: [PATCH 17/88] chore: updates --- packages/enclave-contracts/contracts/Enclave.sol | 2 +- .../contracts/interfaces/IEnclave.sol | 2 +- .../contracts/token/EnclaveTicketToken.sol | 13 +++++++++++-- packages/enclave-contracts/tasks/ciphernode.ts | 2 +- .../test/Registry/BondingRegistry.spec.ts | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 827a58ace1..25f3ba980d 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -152,7 +152,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { function request( E3RequestParams calldata requestParams - ) external payable returns (uint256 e3Id, E3 memory e3) { + ) external returns (uint256 e3Id, E3 memory e3) { uint256 e3Fee = getE3Quote(requestParams); require(e3Fee > 0, PaymentRequired(e3Fee)); require( diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index a8639e04c8..e4c85bcdb2 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -144,7 +144,7 @@ interface IEnclave { /// @return e3 The E3 struct. function request( E3RequestParams calldata requestParams - ) external payable returns (uint256 e3Id, E3 memory e3); + ) external returns (uint256 e3Id, E3 memory e3); /// @notice This function should be called to activate an Encrypted Execution Environment (E3) once it has been /// initialized and is ready for input. diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol index dd87411104..12ce059189 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol @@ -7,6 +7,9 @@ pragma solidity ^0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ERC20Wrapper } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; @@ -30,6 +33,7 @@ contract EnclaveTicketToken is Ownable, ERC20Wrapper { + using SafeERC20 for IERC20; // Custom errors error NotRegistry(); error TransferNotAllowed(); @@ -105,7 +109,12 @@ contract EnclaveTicketToken is address to, uint256 amount ) external onlyRegistry returns (bool) { - IERC20(address(underlying())).transferFrom(from, address(this), amount); + SafeERC20.safeTransferFrom( + IERC20(address(underlying())), + from, + address(this), + amount + ); _mint(to, amount); if (delegates(to) == address(0)) _delegate(to, to); return true; @@ -145,7 +154,7 @@ contract EnclaveTicketToken is * @param amount Amount of ticket tokens to payout. */ function payout(address to, uint256 amount) external onlyRegistry { - IERC20(address(underlying())).transfer(to, amount); + SafeERC20.safeTransfer(IERC20(address(underlying())), to, amount); } /** diff --git a/packages/enclave-contracts/tasks/ciphernode.ts b/packages/enclave-contracts/tasks/ciphernode.ts index a966c14d07..af0e10f207 100644 --- a/packages/enclave-contracts/tasks/ciphernode.ts +++ b/packages/enclave-contracts/tasks/ciphernode.ts @@ -21,7 +21,7 @@ export const ciphernodeAdd = task( .addOption({ name: "ticketAmount", description: - "amount of USDC to deposit for tickets (in wei, e.g., 100000000 for 100 USDC)", + "amount of USDC to deposit for tickets (in wei, e.g., 1000000000 for 1000 USDC)", defaultValue: "1000000000", }) .setAction(async () => ({ diff --git a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts index 0b788537ab..b53d785db1 100644 --- a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -903,7 +903,7 @@ describe("BondingRegistry", function () { ).to.equal(10); }); - it("returns 0 when ticket price is 0", async function () { + it("returns 0 when operator has zero ticket balance", async function () { const { bondingRegistry, operator1 } = await loadFixture(setup); expect( From 66b3ec4a6c0d2389a834b274002e42040e03feb0 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 2 Oct 2025 14:23:21 +0500 Subject: [PATCH 18/88] fix: more review fixes --- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 37 ++++- .../NaiveRegistryFilter.json | 6 +- .../contracts/interfaces/ISlashingManager.sol | 8 +- .../contracts/lib/ExitQueueLib.sol | 3 +- .../contracts/slashing/SlashingManager.sol | 36 +++-- .../contracts/token/EnclaveToken.sol | 12 +- .../enclave-contracts/test/Enclave.spec.ts | 140 +++++++++--------- .../test/Slashing/SlashingManager.spec.ts | 4 +- 9 files changed, 134 insertions(+), 114 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 2dbedad471..8ea1ee30a1 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -343,5 +343,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_27-57dc158663b619d7ef07ffec146d462efa6e9350" + "buildInfoId": "solc-0_8_27-6b881742dbc2999fe64dba44ba36cf997f70c079" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 59ab1bb790..3f9777bf63 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -241,6 +241,19 @@ "name": "EncryptionSchemeEnabled", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "feeToken", + "type": "address" + } + ], + "name": "FeeTokenSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -307,14 +320,26 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, { "indexed": false, - "internalType": "address", - "name": "usdcToken", - "type": "address" + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" } ], - "name": "UsdcTokenSet", + "name": "RewardsDistributed", "type": "event" }, { @@ -757,7 +782,7 @@ "type": "tuple" } ], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -786,5 +811,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_27-57dc158663b619d7ef07ffec146d462efa6e9350" + "buildInfoId": "solc-0_8_27-6b881742dbc2999fe64dba44ba36cf997f70c079" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json index d72e4fba9f..5d1ef4122a 100644 --- a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +++ b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json @@ -299,11 +299,11 @@ "type": "function" } ], - "bytecode": "0x608060405234801561000f575f5ffd5b5060405161104338038061104383398101604081905261002e916102f6565b610038828261003f565b5050610327565b5f5160206110235f395f51905f52805468010000000000000000810460ff1615906001600160401b03165f811580156100755750825b90505f826001600160401b031660011480156100905750303b155b90508115801561009e575080155b156100bc5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156100ea57845460ff60401b1916680100000000000000001785555b6100f333610175565b6100fc86610189565b5f5160206110035f395f51905f52546001600160a01b0388811691161461012657610126876101b2565b831561016c57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b61017d6101f1565b6101868161022e565b50565b610191610236565b5f80546001600160a01b0319166001600160a01b0392909216919091179055565b6101ba610236565b6001600160a01b0381166101e857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101868161027e565b5f5160206110235f395f51905f525468010000000000000000900460ff1661022c57604051631afcd79f60e31b815260040160405180910390fd5b565b6101ba6101f1565b336102555f5160206110035f395f51905f52546001600160a01b031690565b6001600160a01b03161461022c5760405163118cdaa760e01b81523360048201526024016101df565b5f5160206110035f395f51905f5280546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80516001600160a01b03811681146102f1575f5ffd5b919050565b5f5f60408385031215610307575f5ffd5b610310836102db565b915061031e602084016102db565b90509250929050565b610ccf806103345f395ff3fe608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610908565b6101e8565b6040516100dc919061091f565b60405180910390f35b6100f86100f3366004610a88565b6102f8565b005b61010d610108366004610b6b565b6103d9565b60405190151581526020016100dc565b6100f861012b366004610b9b565b610469565b6100f86105bd565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610bcc565b6105d0565b6100f86101b3366004610bcc565b610606565b6101da6101c6366004610908565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107a4565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610648565b5f8381526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b825161034290829060208601906107ca565b50815160208084019190912060028301555f546040516001600160a01b039091169163d9bbec9591879161037891889101610bec565b604051602081830303815290604052856040518463ffffffff1660e01b81526004016103a693929190610c65565b5f604051808303815f87803b1580156103bd575f5ffd5b505af11580156103cf573d5f5f3e3d5ffd5b5050505050505050565b5f80546001600160a01b03163314610404576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610442576040516334c2a65d60e11b815260040160405180910390fd5b5f83815260016020819052604090912061045f910183600261083a565b5060019392505050565b5f6104726106a3565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f8115801561049e5750825b90505f8267ffffffffffffffff1660011480156104ba5750303b155b9050811580156104c8575080155b156104e65760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051a57845468ff00000000000000001916680100000000000000001785555b610523336106cb565b61052c866105d0565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105695761056987610606565b83156105b457845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105c5610648565b6105ce5f6106dc565b565b6105d8610648565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b61060e610648565b6001600160a01b03811661063c57604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b610645816106dc565b50565b3361067a7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105ce5760405163118cdaa760e01b8152336004820152602401610633565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106d3610759565b6106458161077e565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b610761610786565b6105ce57604051631afcd79f60e31b815260040160405180910390fd5b61060e610759565b5f61078f6106a3565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107be6108d6565b81526020015f81525090565b828054828255905f5260205f2090810192821561082a579160200282015b8281111561082a578251825473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b039091161782556020909201916001909101906107e8565b506108369291506108f4565b5090565b60018301918390821561082a579160200282015f5b8382111561089957833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261084f565b80156108c95782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610899565b50506108369291506108f4565b60405180604001604052806002906020820280368337509192915050565b5b80821115610836575f81556001016108f5565b5f60208284031215610918575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610970576001600160a01b038451168252602082019150602084019350600183019250610944565b50602086015192506040850191505f5b60028110156109a557835163ffffffff16835260209384019390920191600101610980565b506040860151608086015280935050505092915050565b634e487b7160e01b5f52604160045260245ffd5b604051601f8201601f1916810167ffffffffffffffff811182821017156109f9576109f96109bc565b604052919050565b80356001600160a01b0381168114610a17575f5ffd5b919050565b5f82601f830112610a2b575f5ffd5b813567ffffffffffffffff811115610a4557610a456109bc565b610a58601f8201601f19166020016109d0565b818152846020838601011115610a6c575f5ffd5b816020850160208301375f918101602001919091529392505050565b5f5f5f60608486031215610a9a575f5ffd5b83359250602084013567ffffffffffffffff811115610ab7575f5ffd5b8401601f81018613610ac7575f5ffd5b803567ffffffffffffffff811115610ae157610ae16109bc565b8060051b610af1602082016109d0565b91825260208184018101929081019089841115610b0c575f5ffd5b6020850194505b83851015610b3557610b2485610a01565b825260209485019490910190610b13565b95505050506040850135905067ffffffffffffffff811115610b55575f5ffd5b610b6186828701610a1c565b9150509250925092565b5f5f60608385031215610b7c575f5ffd5b8235915060608301841015610b8f575f5ffd5b50926020919091019150565b5f5f60408385031215610bac575f5ffd5b610bb583610a01565b9150610bc360208401610a01565b90509250929050565b5f60208284031215610bdc575f5ffd5b610be582610a01565b9392505050565b602080825282518282018190525f918401906040840190835b81811015610c2c5783516001600160a01b0316835260209384019390920191600101610c05565b509095945050505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b838152606060208201525f610c7d6060830185610c37565b8281036040840152610c8f8185610c37565b969550505050505056fea264697066735822122002870213daa8b2b1ad455a064f9487408ab2f4be0fab8576883c54f9b425485164736f6c634300081b00339016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610908565b6101e8565b6040516100dc919061091f565b60405180910390f35b6100f86100f3366004610a88565b6102f8565b005b61010d610108366004610b6b565b6103d9565b60405190151581526020016100dc565b6100f861012b366004610b9b565b610469565b6100f86105bd565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610bcc565b6105d0565b6100f86101b3366004610bcc565b610606565b6101da6101c6366004610908565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107a4565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610648565b5f8381526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b825161034290829060208601906107ca565b50815160208084019190912060028301555f546040516001600160a01b039091169163d9bbec9591879161037891889101610bec565b604051602081830303815290604052856040518463ffffffff1660e01b81526004016103a693929190610c65565b5f604051808303815f87803b1580156103bd575f5ffd5b505af11580156103cf573d5f5f3e3d5ffd5b5050505050505050565b5f80546001600160a01b03163314610404576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610442576040516334c2a65d60e11b815260040160405180910390fd5b5f83815260016020819052604090912061045f910183600261083a565b5060019392505050565b5f6104726106a3565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f8115801561049e5750825b90505f8267ffffffffffffffff1660011480156104ba5750303b155b9050811580156104c8575080155b156104e65760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051a57845468ff00000000000000001916680100000000000000001785555b610523336106cb565b61052c866105d0565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105695761056987610606565b83156105b457845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105c5610648565b6105ce5f6106dc565b565b6105d8610648565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b61060e610648565b6001600160a01b03811661063c57604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b610645816106dc565b50565b3361067a7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105ce5760405163118cdaa760e01b8152336004820152602401610633565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106d3610759565b6106458161077e565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b610761610786565b6105ce57604051631afcd79f60e31b815260040160405180910390fd5b61060e610759565b5f61078f6106a3565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107be6108d6565b81526020015f81525090565b828054828255905f5260205f2090810192821561082a579160200282015b8281111561082a578251825473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b039091161782556020909201916001909101906107e8565b506108369291506108f4565b5090565b60018301918390821561082a579160200282015f5b8382111561089957833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261084f565b80156108c95782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610899565b50506108369291506108f4565b60405180604001604052806002906020820280368337509192915050565b5b80821115610836575f81556001016108f5565b5f60208284031215610918575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610970576001600160a01b038451168252602082019150602084019350600183019250610944565b50602086015192506040850191505f5b60028110156109a557835163ffffffff16835260209384019390920191600101610980565b506040860151608086015280935050505092915050565b634e487b7160e01b5f52604160045260245ffd5b604051601f8201601f1916810167ffffffffffffffff811182821017156109f9576109f96109bc565b604052919050565b80356001600160a01b0381168114610a17575f5ffd5b919050565b5f82601f830112610a2b575f5ffd5b813567ffffffffffffffff811115610a4557610a456109bc565b610a58601f8201601f19166020016109d0565b818152846020838601011115610a6c575f5ffd5b816020850160208301375f918101602001919091529392505050565b5f5f5f60608486031215610a9a575f5ffd5b83359250602084013567ffffffffffffffff811115610ab7575f5ffd5b8401601f81018613610ac7575f5ffd5b803567ffffffffffffffff811115610ae157610ae16109bc565b8060051b610af1602082016109d0565b91825260208184018101929081019089841115610b0c575f5ffd5b6020850194505b83851015610b3557610b2485610a01565b825260209485019490910190610b13565b95505050506040850135905067ffffffffffffffff811115610b55575f5ffd5b610b6186828701610a1c565b9150509250925092565b5f5f60608385031215610b7c575f5ffd5b8235915060608301841015610b8f575f5ffd5b50926020919091019150565b5f5f60408385031215610bac575f5ffd5b610bb583610a01565b9150610bc360208401610a01565b90509250929050565b5f60208284031215610bdc575f5ffd5b610be582610a01565b9392505050565b602080825282518282018190525f918401906040840190835b81811015610c2c5783516001600160a01b0316835260209384019390920191600101610c05565b509095945050505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b838152606060208201525f610c7d6060830185610c37565b8281036040840152610c8f8185610c37565b969550505050505056fea264697066735822122002870213daa8b2b1ad455a064f9487408ab2f4be0fab8576883c54f9b425485164736f6c634300081b0033", + "bytecode": "0x608060405234801561000f575f5ffd5b50604051610fbe380380610fbe83398101604081905261002e916102f6565b610038828261003f565b5050610327565b5f516020610f9e5f395f51905f52805468010000000000000000810460ff1615906001600160401b03165f811580156100755750825b90505f826001600160401b031660011480156100905750303b155b90508115801561009e575080155b156100bc5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156100ea57845460ff60401b1916680100000000000000001785555b6100f333610175565b6100fc86610189565b5f516020610f7e5f395f51905f52546001600160a01b0388811691161461012657610126876101b2565b831561016c57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b61017d6101f1565b6101868161022e565b50565b610191610236565b5f80546001600160a01b0319166001600160a01b0392909216919091179055565b6101ba610236565b6001600160a01b0381166101e857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101868161027e565b5f516020610f9e5f395f51905f525468010000000000000000900460ff1661022c57604051631afcd79f60e31b815260040160405180910390fd5b565b6101ba6101f1565b336102555f516020610f7e5f395f51905f52546001600160a01b031690565b6001600160a01b03161461022c5760405163118cdaa760e01b81523360048201526024016101df565b5f516020610f7e5f395f51905f5280546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80516001600160a01b03811681146102f1575f5ffd5b919050565b5f5f60408385031215610307575f5ffd5b610310836102db565b915061031e602084016102db565b90509250929050565b610c4a806103345f395ff3fe608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610910565b6101e8565b6040516100dc9190610927565b60405180910390f35b6100f86100f3366004610a09565b6102f8565b005b61010d610108366004610ab4565b6103e3565b60405190151581526020016100dc565b6100f861012b366004610aff565b610473565b6100f86105c7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b30565b6105da565b6100f86101b3366004610b30565b610610565b6101da6101c6366004610910565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107ae565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610652565b5f8581526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b61033b8186866107d4565b50828260405161034c929190610b50565b60405190819003812060028301555f546001600160a01b03169063d9bbec9590889061037e9089908990602001610b5f565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103ae9493929190610ba9565b5f604051808303815f87803b1580156103c5575f5ffd5b505af11580156103d7573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b0316331461040e576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff161561044c576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104699101836002610842565b5060019392505050565b5f61047c6106ad565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f811580156104a85750825b90505f8267ffffffffffffffff1660011480156104c45750303b155b9050811580156104d2575080155b156104f05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561052457845468ff00000000000000001916680100000000000000001785555b61052d336106d5565b610536866105da565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105735761057387610610565b83156105be57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105cf610652565b6105d85f6106e6565b565b6105e2610652565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610618610652565b6001600160a01b03811661064657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61064f816106e6565b50565b336106847f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105d85760405163118cdaa760e01b815233600482015260240161063d565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106dd610763565b61064f81610788565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61076b610790565b6105d857604051631afcd79f60e31b815260040160405180910390fd5b610618610763565b5f6107996106ad565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107c86108de565b81526020015f81525090565b828054828255905f5260205f20908101928215610832579160200282015b8281111561083257815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107f2565b5061083e9291506108fc565b5090565b600183019183908215610832579160200282015f5b838211156108a157833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff1602179055509260200192600401602081600301049283019260010302610857565b80156108d15782816101000a81549063ffffffff02191690556004016020816003010492830192600103026108a1565b505061083e9291506108fc565b60405180604001604052806002906020820280368337509192915050565b5b8082111561083e575f81556001016108fd565b5f60208284031215610920575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610978576001600160a01b03845116825260208201915060208401935060018301925061094c565b50602086015192506040850191505f5b60028110156109ad57835163ffffffff16835260209384019390920191600101610988565b506040860151608086015280935050505092915050565b5f5f83601f8401126109d4575f5ffd5b50813567ffffffffffffffff8111156109eb575f5ffd5b602083019150836020828501011115610a02575f5ffd5b9250929050565b5f5f5f5f5f60608688031215610a1d575f5ffd5b85359450602086013567ffffffffffffffff811115610a3a575f5ffd5b8601601f81018813610a4a575f5ffd5b803567ffffffffffffffff811115610a60575f5ffd5b8860208260051b8401011115610a74575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a97575f5ffd5b610aa3888289016109c4565b969995985093965092949392505050565b5f5f60608385031215610ac5575f5ffd5b8235915060608301841015610ad8575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610afa575f5ffd5b919050565b5f5f60408385031215610b10575f5ffd5b610b1983610ae4565b9150610b2760208401610ae4565b90509250929050565b5f60208284031215610b40575f5ffd5b610b4982610ae4565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b9f576001600160a01b03610b8a84610ae4565b16825260209283019290910190600101610b71565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea26469706673582212200e87877fdd60512702be369e26862a97a943161952854c92bf82b5c8f0c8a46064736f6c634300081b00339016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610910565b6101e8565b6040516100dc9190610927565b60405180910390f35b6100f86100f3366004610a09565b6102f8565b005b61010d610108366004610ab4565b6103e3565b60405190151581526020016100dc565b6100f861012b366004610aff565b610473565b6100f86105c7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b30565b6105da565b6100f86101b3366004610b30565b610610565b6101da6101c6366004610910565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107ae565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610652565b5f8581526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b61033b8186866107d4565b50828260405161034c929190610b50565b60405190819003812060028301555f546001600160a01b03169063d9bbec9590889061037e9089908990602001610b5f565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103ae9493929190610ba9565b5f604051808303815f87803b1580156103c5575f5ffd5b505af11580156103d7573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b0316331461040e576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff161561044c576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104699101836002610842565b5060019392505050565b5f61047c6106ad565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f811580156104a85750825b90505f8267ffffffffffffffff1660011480156104c45750303b155b9050811580156104d2575080155b156104f05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561052457845468ff00000000000000001916680100000000000000001785555b61052d336106d5565b610536866105da565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105735761057387610610565b83156105be57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105cf610652565b6105d85f6106e6565b565b6105e2610652565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610618610652565b6001600160a01b03811661064657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61064f816106e6565b50565b336106847f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105d85760405163118cdaa760e01b815233600482015260240161063d565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106dd610763565b61064f81610788565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61076b610790565b6105d857604051631afcd79f60e31b815260040160405180910390fd5b610618610763565b5f6107996106ad565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107c86108de565b81526020015f81525090565b828054828255905f5260205f20908101928215610832579160200282015b8281111561083257815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107f2565b5061083e9291506108fc565b5090565b600183019183908215610832579160200282015f5b838211156108a157833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff1602179055509260200192600401602081600301049283019260010302610857565b80156108d15782816101000a81549063ffffffff02191690556004016020816003010492830192600103026108a1565b505061083e9291506108fc565b60405180604001604052806002906020820280368337509192915050565b5b8082111561083e575f81556001016108fd565b5f60208284031215610920575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610978576001600160a01b03845116825260208201915060208401935060018301925061094c565b50602086015192506040850191505f5b60028110156109ad57835163ffffffff16835260209384019390920191600101610988565b506040860151608086015280935050505092915050565b5f5f83601f8401126109d4575f5ffd5b50813567ffffffffffffffff8111156109eb575f5ffd5b602083019150836020828501011115610a02575f5ffd5b9250929050565b5f5f5f5f5f60608688031215610a1d575f5ffd5b85359450602086013567ffffffffffffffff811115610a3a575f5ffd5b8601601f81018813610a4a575f5ffd5b803567ffffffffffffffff811115610a60575f5ffd5b8860208260051b8401011115610a74575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a97575f5ffd5b610aa3888289016109c4565b969995985093965092949392505050565b5f5f60608385031215610ac5575f5ffd5b8235915060608301841015610ad8575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610afa575f5ffd5b919050565b5f5f60408385031215610b10575f5ffd5b610b1983610ae4565b9150610b2760208401610ae4565b90509250929050565b5f60208284031215610b40575f5ffd5b610b4982610ae4565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b9f576001600160a01b03610b8a84610ae4565b16825260209283019290910190600101610b71565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea26469706673582212200e87877fdd60512702be369e26862a97a943161952854c92bf82b5c8f0c8a46064736f6c634300081b0033", "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/registry/NaiveRegistryFilter.sol", - "buildInfoId": "solc-0_8_27-57dc158663b619d7ef07ffec146d462efa6e9350" + "buildInfoId": "solc-0_8_27-6b881742dbc2999fe64dba44ba36cf997f70c079" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index 69b9592926..7901ca2423 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -43,7 +43,7 @@ interface ISlashingManager { bool executedLicense; // True if license penalty executed bool appealed; // True if operator filed appeal bool resolved; // True if appeal was resolved - bool approved; // True if appeal was approved (penalty cancelled) + bool appealUpheld; // True if appeal was approved (penalty cancelled) uint256 proposedAt; // Timestamp when proposed uint256 executableAt; // Timestamp when execution is allowed address proposer; // Address that proposed the slash @@ -124,7 +124,7 @@ interface ISlashingManager { event AppealResolved( uint256 indexed proposalId, address indexed operator, - bool approved, + bool appealUpheld, address resolver, string resolution ); @@ -257,12 +257,12 @@ interface ISlashingManager { /** * @notice Resolve an appeal (governance only) * @param proposalId ID of the proposal with appeal - * @param approved True to approve appeal (cancel slash), false to deny + * @param appealUpheld True to approve appeal (cancel slash), false to deny * @param resolution Resolution explanation string */ function resolveAppeal( uint256 proposalId, - bool approved, + bool appealUpheld, string calldata resolution ) external; diff --git a/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol index 3e59cc6ad3..d9a3cc18d7 100644 --- a/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol +++ b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol @@ -86,8 +86,7 @@ library ExitQueueLib { } if (!merged) { - operatorQueue.push(); - ExitTranche storage t = operatorQueue[len]; + ExitTranche storage t = operatorQueue.push(); t.unlockTimestamp = unlockTimestamp; t.ticketAmount = ticketAmount; t.licenseAmount = licenseAmount; diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 51bf2de170..ce9ee11ab1 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -173,21 +173,10 @@ contract SlashingManager is ISlashingManager, AccessControl { ) external onlySlasher notBanned(operator) returns (uint256 proposalId) { require(operator != address(0), ZeroAddress()); - SlashPolicy storage policy = slashPolicies[reason]; + SlashPolicy memory policy = slashPolicies[reason]; require(policy.enabled, SlashReasonDisabled()); proposalId = totalProposals; - bool proofVerified = false; - - if (policy.requiresProof) { - require(proof.length != 0, ProofRequired()); - proofVerified = ISlashVerifier(policy.proofVerifier).verify( - proposalId, - proof - ); - require(proofVerified, InvalidProof()); - } - uint256 executableAt = block.timestamp + policy.appealWindow; SlashProposal storage p = _proposals[proposalId]; @@ -199,12 +188,21 @@ contract SlashingManager is ISlashingManager, AccessControl { p.executedLicense = false; p.appealed = false; p.resolved = false; - p.approved = false; + p.appealUpheld = false; p.proposedAt = block.timestamp; p.executableAt = executableAt; p.proposer = msg.sender; p.proofHash = keccak256(proof); - p.proofVerified = proofVerified; + + if (policy.requiresProof) { + require(proof.length != 0, ProofRequired()); + bool ok = ISlashVerifier(policy.proofVerifier).verify( + proposalId, + proof + ); + require(ok, InvalidProof()); + p.proofVerified = true; + } emit SlashProposed( proposalId, @@ -226,7 +224,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // Has already been executed? require(!(p.executedTicket && p.executedLicense), AlreadyExecuted()); - SlashPolicy storage policy = slashPolicies[p.reason]; + SlashPolicy memory policy = slashPolicies[p.reason]; if (policy.requiresProof) { // Appeal window is 0 by policy validation, so we dont check for appeal gating @@ -236,7 +234,7 @@ contract SlashingManager is ISlashingManager, AccessControl { require(block.timestamp >= p.executableAt, AppealWindowActive()); if (p.appealed) { require(p.resolved, AppealPending()); - require(!p.approved, AppealUpheld()); // approved = appeal upheld => cancel slash, return? + require(!p.appealUpheld, AppealUpheld()); // approved = appeal upheld => cancel slash, return? } } @@ -301,7 +299,7 @@ contract SlashingManager is ISlashingManager, AccessControl { function resolveAppeal( uint256 proposalId, - bool approved, + bool appealUpheld, string calldata resolution ) external onlyGovernance { require(proposalId < totalProposals, InvalidProposal()); @@ -311,12 +309,12 @@ contract SlashingManager is ISlashingManager, AccessControl { require(!p.resolved, AlreadyResolved()); p.resolved = true; - p.approved = approved; // true => cancel slash, false => slash stands + p.appealUpheld = appealUpheld; // true => cancel slash, false => slash stands emit AppealResolved( proposalId, p.operator, - approved, + appealUpheld, msg.sender, resolution ); diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol index aa75da9f95..7be76a80f3 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -37,7 +37,7 @@ contract EnclaveToken is error TransferNotAllowed(); /// @dev Maximum supply of the token (18 decimals). - uint256 public constant TOTAL_SUPPLY = 1_200_000_000e18; + uint256 public constant MAX_SUPPLY = 1_200_000_000e18; /// @dev Role allowing accounts to mint new tokens. bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); @@ -76,7 +76,6 @@ contract EnclaveToken is _grantRole(MINTER_ROLE, _owner); // Initialise state variables. - totalMinted = 0; transfersRestricted = true; transferWhitelisted[_owner] = true; @@ -99,7 +98,7 @@ contract EnclaveToken is if (recipient == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); // Ensure we do not exceed the total supply. - if (totalMinted + amount > TOTAL_SUPPLY) revert ExceedsTotalSupply(); + if (totalMinted + amount > MAX_SUPPLY) revert ExceedsTotalSupply(); _mint(recipient, amount); totalMinted += amount; @@ -122,22 +121,19 @@ contract EnclaveToken is if (amounts.length != len || allocations.length != len) revert ArrayLengthMismatch(); - uint256 minted = totalMinted; + uint256 minted = totalSupply(); for (uint256 i = 0; i < len; i++) { address recipient = recipients[i]; uint256 amount = amounts[i]; - if (recipient == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); - if (amount > TOTAL_SUPPLY - minted) revert ExceedsTotalSupply(); + if (amount > MAX_SUPPLY - minted) revert ExceedsTotalSupply(); minted += amount; _mint(recipient, amount); emit AllocationMinted(recipient, amount, allocations[i]); } - - totalMinted = minted; } /** diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 7139ba636d..d8b7b991f9 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -766,94 +766,96 @@ describe("Enclave", function () { e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, }), - ).to.be.revertedWithCustomError(enclave, "InvalidThreshold"); + ) + .to.be.revertedWithCustomError(enclave, "InvalidThreshold") + .withArgs([0, 2]); }); it("reverts if threshold is greater than number", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, naiveRegistryFilterContract, usdcToken } = + await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: [3, 2], - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidThreshold"); + makeRequest(enclave, usdcToken, { + filter: await naiveRegistryFilterContract.getAddress(), + threshold: [3, 2], + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidThreshold") + .withArgs([3, 2]); }); it("reverts if duration is 0", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, naiveRegistryFilterContract, usdcToken } = + await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: 0, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidDuration"); + makeRequest(enclave, usdcToken, { + filter: await naiveRegistryFilterContract.getAddress(), + threshold: request.threshold, + startWindow: request.startWindow, + duration: 0, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidDuration") + .withArgs(0); }); it("reverts if duration is greater than maxDuration", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, naiveRegistryFilterContract, usdcToken } = + await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: time.duration.days(31), - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidDuration"); + makeRequest(enclave, usdcToken, { + filter: await naiveRegistryFilterContract.getAddress(), + threshold: request.threshold, + startWindow: request.startWindow, + duration: time.duration.days(31), + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidDuration") + .withArgs(time.duration.days(31)); }); it("reverts if E3 Program is not enabled", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, naiveRegistryFilterContract, usdcToken } = + await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: ethers.ZeroAddress, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), + makeRequest(enclave, usdcToken, { + filter: await naiveRegistryFilterContract.getAddress(), + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: ethers.ZeroAddress, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }), ) .to.be.revertedWithCustomError(enclave, "E3ProgramNotAllowed") .withArgs(ethers.ZeroAddress); }); it("reverts if given encryption scheme is not enabled", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, naiveRegistryFilterContract, usdcToken } = + await loadFixture(setup); await enclave.disableEncryptionScheme(encryptionSchemeId); await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - }, - { value: 10 }, - ), + makeRequest(enclave, usdcToken, { + filter: await naiveRegistryFilterContract.getAddress(), + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + }), ) .to.be.revertedWithCustomError(enclave, "InvalidEncryptionScheme") .withArgs(encryptionSchemeId); diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index be5c7e2338..2c13700a99 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -954,7 +954,7 @@ describe("SlashingManager", function () { const proposal = await slashingManager.getSlashProposal(0); expect(proposal.resolved).to.be.true; - expect(proposal.approved).to.be.true; + expect(proposal.appealUpheld).to.be.true; }); it("should allow governance to resolve appeal (deny)", async function () { @@ -984,7 +984,7 @@ describe("SlashingManager", function () { const proposal = await slashingManager.getSlashProposal(0); expect(proposal.resolved).to.be.true; - expect(proposal.approved).to.be.false; + expect(proposal.appealUpheld).to.be.false; }); it("should block execution if appeal is pending", async function () { From 0a3958d3f44214c03d35e943c752c6b4588c324f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 2 Oct 2025 15:57:01 +0500 Subject: [PATCH 19/88] fix: contract updates and TODO comments --- .../contracts/interfaces/ISlashingManager.sol | 6 +- .../contracts/registry/BondingRegistry.sol | 4 ++ .../contracts/slashing/SlashingManager.sol | 36 ++++------ .../test/Slashing/SlashingManager.spec.ts | 25 +------ packages/enclave-react/README.md | 31 ++++----- packages/enclave-sdk/README.md | 17 +++-- packages/enclave-sdk/src/contract-client.ts | 69 ++++++++++--------- packages/enclave-sdk/src/enclave-sdk.ts | 68 +++++++++--------- .../default/client/src/pages/WizardSDK.tsx | 1 - templates/default/tests/integration.spec.ts | 41 ++++++----- 10 files changed, 136 insertions(+), 162 deletions(-) diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index 7901ca2423..49f0d5aa95 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -39,8 +39,7 @@ interface ISlashingManager { bytes32 reason; // Reason hash (maps to SlashPolicy) uint256 ticketAmount; // Calculated ticket penalty amount uint256 licenseAmount; // Calculated license penalty amount - bool executedTicket; // True if ticket penalty executed - bool executedLicense; // True if license penalty executed + bool executed; // True if penalty executed bool appealed; // True if operator filed appeal bool resolved; // True if appeal was resolved bool appealUpheld; // True if appeal was approved (penalty cancelled) @@ -104,8 +103,7 @@ interface ISlashingManager { bytes32 indexed reason, uint256 ticketAmount, uint256 licenseAmount, - bool ticketExecuted, - bool licenseExecuted + bool executed ); /** diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 81296dd57a..bb51f6f6ca 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -65,6 +65,10 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { uint256 public licenseRequiredBond; uint256 public minTicketBalance; uint64 public exitDelay; + + // TODO: There's a scenario where a node can bond the required license bond, + // then register and immediately withdraw 20% of the license bond. And still be a part of + // the protocol. Is this correct? uint256 public licenseActiveBps = 8_000; // 80% // Operator data structure diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index ce9ee11ab1..b68558cc12 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -65,11 +65,6 @@ contract SlashingManager is ISlashingManager, AccessControl { _; } - modifier notBanned(address node) { - if (banned[node]) revert CiphernodeBanned(); - _; - } - // ====================== // Constructor // ====================== @@ -122,6 +117,7 @@ contract SlashingManager is ISlashingManager, AccessControl { if (policy.requiresProof) { require(policy.proofVerifier != address(0), VerifierNotSet()); + // TODO: Should we allow appeal window for proof required? require(policy.appealWindow == 0, InvalidPolicy()); } else { require(policy.appealWindow > 0, InvalidPolicy()); @@ -170,7 +166,13 @@ contract SlashingManager is ISlashingManager, AccessControl { address operator, bytes32 reason, bytes calldata proof - ) external onlySlasher notBanned(operator) returns (uint256 proposalId) { + ) + external + // TODO: Do we need an onlySlasher modifier? + // Can anyone propose a slash? + onlySlasher + returns (uint256 proposalId) + { require(operator != address(0), ZeroAddress()); SlashPolicy memory policy = slashPolicies[reason]; @@ -184,11 +186,6 @@ contract SlashingManager is ISlashingManager, AccessControl { p.reason = reason; p.ticketAmount = policy.ticketPenalty; p.licenseAmount = policy.licensePenalty; - p.executedTicket = false; - p.executedLicense = false; - p.appealed = false; - p.resolved = false; - p.appealUpheld = false; p.proposedAt = block.timestamp; p.executableAt = executableAt; p.proposer = msg.sender; @@ -222,7 +219,8 @@ contract SlashingManager is ISlashingManager, AccessControl { SlashProposal storage p = _proposals[proposalId]; // Has already been executed? - require(!(p.executedTicket && p.executedLicense), AlreadyExecuted()); + require(!p.executed, AlreadyExecuted()); + p.executed = true; SlashPolicy memory policy = slashPolicies[p.reason]; @@ -238,27 +236,20 @@ contract SlashingManager is ISlashingManager, AccessControl { } } - bool ticketExecuted = p.executedTicket; - bool licenseExecuted = p.executedLicense; - - if (!p.executedTicket && p.ticketAmount > 0) { + if (p.ticketAmount > 0) { bondingRegistry.slashTicketBalance( p.operator, p.ticketAmount, p.reason ); - p.executedTicket = true; - ticketExecuted = true; } - if (!p.executedLicense && p.licenseAmount > 0) { + if (p.licenseAmount > 0) { bondingRegistry.slashLicenseBond( p.operator, p.licenseAmount, p.reason ); - p.executedLicense = true; - licenseExecuted = true; } if (policy.banNode) { @@ -272,8 +263,7 @@ contract SlashingManager is ISlashingManager, AccessControl { p.reason, p.ticketAmount, p.licenseAmount, - ticketExecuted, - licenseExecuted + p.executed ); } diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 2c13700a99..4b966373fe 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -706,12 +706,10 @@ describe("SlashingManager", function () { ethers.parseUnits("50", 6), ethers.parseEther("100"), true, - true, ); const proposal = await slashingManager.getSlashProposal(0); - expect(proposal.executedTicket).to.be.true; - expect(proposal.executedLicense).to.be.true; + expect(proposal.executed).to.be.true; }); it("should execute slash after appeal window expires", async function () { @@ -1116,27 +1114,6 @@ describe("SlashingManager", function () { .banNode(operatorAddress, ethers.encodeBytes32String("test")), ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); }); - - it("should prevent proposing slashes against banned nodes", async function () { - const { slashingManager, owner, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); - - await setupPolicies(slashingManager, mockVerifier); - - await slashingManager - .connect(owner) - .banNode(operatorAddress, ethers.encodeBytes32String("test")); - - await expect( - slashingManager - .connect(slasher) - .proposeSlash( - operatorAddress, - REASON_MISBEHAVIOR, - ethers.toUtf8Bytes("proof"), - ), - ).to.be.revertedWithCustomError(slashingManager, "CiphernodeBanned"); - }); }); describe("view functions", function () { diff --git a/packages/enclave-react/README.md b/packages/enclave-react/README.md index 255ffe7562..c2d3398c88 100644 --- a/packages/enclave-react/README.md +++ b/packages/enclave-react/README.md @@ -19,7 +19,7 @@ pnpm add @enclave-e3/react @enclave-e3/contracts A React hook for interacting with the Enclave SDK. This hook provides a clean interface for managing SDK state, handling contract interactions, and listening to events. ```tsx -import { useEnclaveSDK } from '@enclave-e3/react'; +import { useEnclaveSDK } from "@enclave-e3/react"; function MyComponent() { const { @@ -32,14 +32,14 @@ function MyComponent() { onEnclaveEvent, off, EnclaveEventType, - RegistryEventType + RegistryEventType, } = useEnclaveSDK({ autoConnect: true, contracts: { - enclave: '0x...', - ciphernodeRegistry: '0x...' + enclave: "0x...", + ciphernodeRegistry: "0x...", }, - chainId: 1 + chainId: 1, }); // Listen to events @@ -47,7 +47,7 @@ function MyComponent() { if (!isInitialized) return; const handleE3Requested = (event) => { - console.log('E3 requested:', event.data); + console.log("E3 requested:", event.data); }; onEnclaveEvent(EnclaveEventType.E3_REQUESTED, handleE3Requested); @@ -61,18 +61,17 @@ function MyComponent() { const handleRequest = async () => { try { const hash = await requestE3({ - filter: '0x...', + filter: "0x...", threshold: [2, 3], startWindow: [BigInt(Date.now()), BigInt(Date.now() + 300000)], duration: BigInt(1800), - e3Program: '0x...', - e3ProgramParams: '0x...', - computeProviderParams: '0x...', - value: BigInt('1000000000000000') // 0.001 ETH + e3Program: "0x...", + e3ProgramParams: "0x...", + computeProviderParams: "0x...", }); - console.log('E3 requested with hash:', hash); + console.log("E3 requested with hash:", hash); } catch (error) { - console.error('Failed to request E3:', error); + console.error("Failed to request E3:", error); } }; @@ -86,9 +85,7 @@ function MyComponent() { return (
- +
); } @@ -133,4 +130,4 @@ function MyComponent() { ## License -MIT \ No newline at end of file +MIT diff --git a/packages/enclave-sdk/README.md b/packages/enclave-sdk/README.md index 5e193b7b3f..0e6cd8ce2d 100644 --- a/packages/enclave-sdk/README.md +++ b/packages/enclave-sdk/README.md @@ -160,7 +160,7 @@ interface EnclaveEvent { The SDK includes a React hook for easy integration: ```typescript -import { useEnclaveSDK } from '@enclave-e3/contracts/sdk'; +import { useEnclaveSDK } from "@enclave-e3/contracts/sdk"; function MyComponent() { const { @@ -171,20 +171,20 @@ function MyComponent() { connectWallet, requestE3, onEnclaveEvent, - EnclaveEventType + EnclaveEventType, } = useEnclaveSDK({ contracts: { - enclave: '0x...', - ciphernodeRegistry: '0x...' + enclave: "0x...", + ciphernodeRegistry: "0x...", }, - rpcUrl: 'YOUR_RPC_URL', - autoConnect: true + rpcUrl: "YOUR_RPC_URL", + autoConnect: true, }); useEffect(() => { if (isInitialized) { onEnclaveEvent(EnclaveEventType.E3_REQUESTED, (event) => { - console.log('New E3 request:', event); + console.log("New E3 request:", event); }); } }, [isInitialized]); @@ -193,7 +193,7 @@ function MyComponent() {
{!isInitialized && ( )} {/* Your UI */} @@ -218,7 +218,6 @@ await sdk.requestE3({ e3Program: `0x${string}`, e3ProgramParams: `0x${string}`, computeProviderParams: `0x${string}`, - value?: bigint, gasLimit?: bigint }); diff --git a/packages/enclave-sdk/src/contract-client.ts b/packages/enclave-sdk/src/contract-client.ts index 105cea07e1..b5e99a1c4a 100644 --- a/packages/enclave-sdk/src/contract-client.ts +++ b/packages/enclave-sdk/src/contract-client.ts @@ -32,9 +32,9 @@ export class ContractClient { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; } = { - enclave: "0x0000000000000000000000000000000000000000", - ciphernodeRegistry: "0x0000000000000000000000000000000000000000", - }, + enclave: "0x0000000000000000000000000000000000000000", + ciphernodeRegistry: "0x0000000000000000000000000000000000000000", + } ) { if (!isValidAddress(addresses.enclave)) { throw new SDKError("Invalid Enclave contract address", "INVALID_ADDRESS"); @@ -42,7 +42,7 @@ export class ContractClient { if (!isValidAddress(addresses.ciphernodeRegistry)) { throw new SDKError( "Invalid CiphernodeRegistry contract address", - "INVALID_ADDRESS", + "INVALID_ADDRESS" ); } } @@ -65,7 +65,7 @@ export class ContractClient { } catch (error) { throw new SDKError( `Failed to initialize contracts: ${error}`, - "INITIALIZATION_FAILED", + "INITIALIZATION_FAILED" ); } } @@ -82,13 +82,12 @@ export class ContractClient { e3Program: `0x${string}`, e3ProgramParams: `0x${string}`, computeProviderParams: `0x${string}`, - value?: bigint, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.walletClient) { throw new SDKError( "Wallet client required for write operations", - "NO_WALLET", + "NO_WALLET" ); } @@ -107,17 +106,18 @@ export class ContractClient { address: this.addresses.enclave, abi: Enclave__factory.abi, functionName: "request", - args: [{ - filter, - threshold, - startWindow, - duration, - e3Program, - e3ProgramParams, - computeProviderParams, - }], + args: [ + { + filter, + threshold, + startWindow, + duration, + e3Program, + e3ProgramParams, + computeProviderParams, + }, + ], account, - value: value || BigInt(0), gas: gasLimit, }); @@ -137,12 +137,12 @@ export class ContractClient { public async activateE3( e3Id: bigint, publicKey: `0x${string}`, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.walletClient) { throw new SDKError( "Wallet client required for write operations", - "NO_WALLET", + "NO_WALLET" ); } @@ -171,7 +171,7 @@ export class ContractClient { } catch (error) { throw new SDKError( `Failed to activate E3: ${error}`, - "ACTIVATE_E3_FAILED", + "ACTIVATE_E3_FAILED" ); } } @@ -183,12 +183,12 @@ export class ContractClient { public async publishInput( e3Id: bigint, data: `0x${string}`, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.walletClient) { throw new SDKError( "Wallet client required for write operations", - "NO_WALLET", + "NO_WALLET" ); } @@ -217,7 +217,7 @@ export class ContractClient { } catch (error) { throw new SDKError( `Failed to publish input: ${error}`, - "PUBLISH_INPUT_FAILED", + "PUBLISH_INPUT_FAILED" ); } } @@ -230,12 +230,12 @@ export class ContractClient { e3Id: bigint, ciphertextOutput: `0x${string}`, proof: `0x${string}`, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.walletClient) { throw new SDKError( "Wallet client required for write operations", - "NO_WALLET", + "NO_WALLET" ); } @@ -266,7 +266,7 @@ export class ContractClient { } catch (error) { throw new SDKError( `Failed to publish ciphertext output: ${error}`, - "PUBLISH_CIPHERTEXT_OUTPUT_FAILED", + "PUBLISH_CIPHERTEXT_OUTPUT_FAILED" ); } } @@ -297,7 +297,7 @@ export class ContractClient { /** * Get the public key for an E3 computation * Based on the contract: committeePublicKey(uint256 e3Id) returns (bytes32 publicKeyHash) - * @param e3Id + * @param e3Id * @returns The public key */ public async getE3PublicKey(e3Id: bigint): Promise<`0x${string}`> { @@ -315,7 +315,10 @@ export class ContractClient { return result; } catch (error) { - throw new SDKError(`Failed to get E3 public key: ${error}`, "GET_E3_PUBLIC_KEY_FAILED"); + throw new SDKError( + `Failed to get E3 public key: ${error}`, + "GET_E3_PUBLIC_KEY_FAILED" + ); } } @@ -327,12 +330,12 @@ export class ContractClient { args: readonly unknown[], contractAddress: `0x${string}`, abi: Abi, - value?: bigint, + value?: bigint ): Promise { if (!this.walletClient) { throw new SDKError( "Wallet client required for gas estimation", - "NO_WALLET", + "NO_WALLET" ); } @@ -357,7 +360,7 @@ export class ContractClient { } catch (error) { throw new SDKError( `Failed to estimate gas: ${error}`, - "GAS_ESTIMATION_FAILED", + "GAS_ESTIMATION_FAILED" ); } } @@ -376,7 +379,7 @@ export class ContractClient { } catch (error) { throw new SDKError( `Failed to wait for transaction: ${error}`, - "TRANSACTION_WAIT_FAILED", + "TRANSACTION_WAIT_FAILED" ); } } diff --git a/packages/enclave-sdk/src/enclave-sdk.ts b/packages/enclave-sdk/src/enclave-sdk.ts index 8e0af6b226..66e03d03b5 100644 --- a/packages/enclave-sdk/src/enclave-sdk.ts +++ b/packages/enclave-sdk/src/enclave-sdk.ts @@ -35,7 +35,10 @@ import type { ProtocolParams, VerifiableEncryptionResult, } from "./types"; -import { bfv_encrypt_number, bfv_verifiable_encrypt_number } from "@enclave-e3/wasm"; +import { + bfv_encrypt_number, + bfv_verifiable_encrypt_number, +} from "@enclave-e3/wasm"; import { CircuitInputs, generateProof } from "./greco"; import { CompiledCircuit } from "@noir-lang/noir_js"; @@ -65,7 +68,7 @@ export class EnclaveSDK { if (!isValidAddress(config.contracts.ciphernodeRegistry)) { throw new SDKError( "Invalid CiphernodeRegistry contract address", - "INVALID_ADDRESS", + "INVALID_ADDRESS" ); } @@ -73,7 +76,7 @@ export class EnclaveSDK { this.contractClient = new ContractClient( config.publicClient, config.walletClient, - config.contracts, + config.contracts ); this.protocol = config.protocol; @@ -86,7 +89,7 @@ export class EnclaveSDK { this.protocolParams = BfvProtocolParams.BFV_NORMAL; break; default: - throw new Error("Protocol not supported") + throw new Error("Protocol not supported"); } } } @@ -104,7 +107,7 @@ export class EnclaveSDK { } catch (error) { throw new SDKError( `Failed to initialize SDK: ${error}`, - "SDK_INITIALIZATION_FAILED", + "SDK_INITIALIZATION_FAILED" ); } } @@ -115,19 +118,22 @@ export class EnclaveSDK { * @param publicKey - The public key to use for encryption * @returns The encrypted number */ - public async encryptNumber(data: bigint, publicKey: Uint8Array): Promise { + public async encryptNumber( + data: bigint, + publicKey: Uint8Array + ): Promise { await initializeWasm(); switch (this.protocol) { case FheProtocol.BFV: return bfv_encrypt_number( - data, + data, publicKey, this.protocolParams.degree, this.protocolParams.plaintextModulus, - this.protocolParams.moduli, + this.protocolParams.moduli ); default: - throw new Error("Protocol not supported") + throw new Error("Protocol not supported"); } } @@ -139,19 +145,19 @@ export class EnclaveSDK { * @returns The encrypted number and the proof */ public async encryptNumberAndGenProof( - data: bigint, + data: bigint, publicKey: Uint8Array, - circuit: CompiledCircuit, + circuit: CompiledCircuit ): Promise { await initializeWasm(); switch (this.protocol) { case FheProtocol.BFV: const [encryptedVote, circuitInputs] = bfv_verifiable_encrypt_number( - data, + data, publicKey, this.protocolParams.degree, this.protocolParams.plaintextModulus, - this.protocolParams.moduli, + this.protocolParams.moduli ); const inputs = JSON.parse(circuitInputs) as CircuitInputs; @@ -163,7 +169,7 @@ export class EnclaveSDK { proof, }; default: - throw new Error("Protocol not supported") + throw new Error("Protocol not supported"); } } @@ -178,7 +184,6 @@ export class EnclaveSDK { e3Program: `0x${string}`; e3ProgramParams: `0x${string}`; computeProviderParams: `0x${string}`; - value?: bigint; gasLimit?: bigint; }): Promise { console.log(">>> REQUEST"); @@ -195,8 +200,7 @@ export class EnclaveSDK { params.e3Program, params.e3ProgramParams, params.computeProviderParams, - params.value, - params.gasLimit, + params.gasLimit ); } @@ -219,7 +223,7 @@ export class EnclaveSDK { public async activateE3( e3Id: bigint, publicKey: `0x${string}`, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.initialized) { await this.initialize(); @@ -234,7 +238,7 @@ export class EnclaveSDK { public async publishInput( e3Id: bigint, data: `0x${string}`, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.initialized) { await this.initialize(); @@ -250,7 +254,7 @@ export class EnclaveSDK { e3Id: bigint, ciphertextOutput: `0x${string}`, proof: `0x${string}`, - gasLimit?: bigint, + gasLimit?: bigint ): Promise { if (!this.initialized) { await this.initialize(); @@ -260,7 +264,7 @@ export class EnclaveSDK { e3Id, ciphertextOutput, proof, - gasLimit, + gasLimit ); } @@ -280,11 +284,11 @@ export class EnclaveSDK { */ public onEnclaveEvent( eventType: T, - callback: EventCallback, + callback: EventCallback ): void { // Determine which contract to listen to based on event type const isEnclaveEvent = Object.values(EnclaveEventType).includes( - eventType as EnclaveEventType, + eventType as EnclaveEventType ); const contractAddress = isEnclaveEvent ? this.config.contracts.enclave @@ -297,7 +301,7 @@ export class EnclaveSDK { contractAddress, eventType, abi, - callback, + callback ); } @@ -306,7 +310,7 @@ export class EnclaveSDK { */ public off( eventType: T, - callback: EventCallback, + callback: EventCallback ): void { this.eventListener.off(eventType, callback); } @@ -316,7 +320,7 @@ export class EnclaveSDK { */ public once( type: T, - callback: EventCallback, + callback: EventCallback ): void { const handler: EventCallback = (event) => { this.off(type, handler); @@ -334,10 +338,10 @@ export class EnclaveSDK { public async getHistoricalEvents( eventType: AllEventTypes, fromBlock?: bigint, - toBlock?: bigint, + toBlock?: bigint ): Promise { const isEnclaveEvent = Object.values(EnclaveEventType).includes( - eventType as EnclaveEventType, + eventType as EnclaveEventType ); const contractAddress = isEnclaveEvent ? this.config.contracts.enclave @@ -351,7 +355,7 @@ export class EnclaveSDK { eventType, abi, fromBlock, - toBlock, + toBlock ); } @@ -381,14 +385,14 @@ export class EnclaveSDK { args: readonly unknown[], contractAddress: `0x${string}`, abi: Abi, - value?: bigint, + value?: bigint ): Promise { return this.contractClient.estimateGas( functionName, args, contractAddress, abi, - value, + value ); } @@ -434,7 +438,7 @@ export class EnclaveSDK { this.contractClient = new ContractClient( this.config.publicClient, this.config.walletClient, - this.config.contracts, + this.config.contracts ); this.initialized = false; diff --git a/templates/default/client/src/pages/WizardSDK.tsx b/templates/default/client/src/pages/WizardSDK.tsx index 06d2914a26..2b62b65d64 100644 --- a/templates/default/client/src/pages/WizardSDK.tsx +++ b/templates/default/client/src/pages/WizardSDK.tsx @@ -732,7 +732,6 @@ const WizardSDK: React.FC = () => { e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, - value: BigInt('1000000000000000'), // 0.001 ETH }) setLastTransactionHash(hash) diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index 7da9ff660a..23a08b55be 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -177,7 +177,7 @@ describe("Integration", () => { "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", protocol: FheProtocol.BFV, }); - + it("should run an integration test", async () => { const { waitForEvent } = await setupEventListeners(sdk, store); @@ -191,10 +191,10 @@ describe("Integration", () => { const computeProviderParams = encodeComputeProviderParams( DEFAULT_COMPUTE_PROVIDER_PARAMS ); - + let state; let event; - + // REQUEST phase await waitForEvent(EnclaveEventType.E3_REQUESTED, async () => { console.log("Requested E3..."); @@ -206,61 +206,64 @@ describe("Integration", () => { e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, - value: BigInt("1000000000000000"), // 0.001 ETH }); }); - + state = store.get(0n); assert(state); assert.strictEqual(state.e3Id, 0n); assert.strictEqual(state.filter, contracts.filterRegistry); assert.strictEqual(state.type, "requested"); - + // Ciphernodes will publish a public key within the COMMITTEE_PUBLISHED event event = await waitForEvent(RegistryEventType.COMMITTEE_PUBLISHED); - + state = store.get(0n); assert(state); assert.strictEqual(state.type, "committee_published"); assert.strictEqual(state.publicKey, event.data.publicKey); - + let { e3Id, publicKey } = state; - + // ACTIVATION phase event = await waitForEvent(EnclaveEventType.E3_ACTIVATED, async () => { await sdk.activateE3(e3Id, publicKey); }); - + state = store.get(0n); assert(state); assert.strictEqual(state.type, "activated"); - + // INPUT PUBLISHING phase const num1 = 12n; const num2 = 21n; const publicKeyBytes = hexToBytes(state.publicKey); const enc1 = await sdk.encryptNumber(num1, publicKeyBytes); const enc2 = await sdk.encryptNumber(num2, publicKeyBytes); - + await waitForEvent(EnclaveEventType.INPUT_PUBLISHED, async () => { await sdk.publishInput( e3Id, - `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`, + `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, "0")).join( + "" + )}` as `0x${string}` ); }); await waitForEvent(EnclaveEventType.INPUT_PUBLISHED, async () => { await sdk.publishInput( e3Id, - `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`, + `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, "0")).join( + "" + )}` as `0x${string}` ); }); - + const plaintextEvent = await waitForEvent( EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED ); - + const parsed = hexToUint8Array(plaintextEvent.data.plaintextOutput); - + expect(BigInt(parsed[0])).toBe(num1 + num2); - }, 9999999) -}) + }, 9999999); +}); From f4f73f0d8a28c6d918690b68e25a05ace0ca752b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 4 Oct 2025 03:00:04 +0500 Subject: [PATCH 20/88] chore: wip --- crates/evm-helpers/src/contracts.rs | 40 ++- deploy/local/contracts.sh | 2 +- deploy/local/nodes.sh | 2 +- docs/pages/building-with-enclave.mdx | 5 +- docs/pages/computation-flow.mdx | 8 +- examples/CRISP/client/.env.example | 4 +- examples/CRISP/server/.env.example | 2 + examples/CRISP/server/src/cli/approve.rs | 65 ++++ examples/CRISP/server/src/cli/commands.rs | 29 +- examples/CRISP/server/src/cli/main.rs | 1 + examples/CRISP/server/src/config.rs | 1 + .../ICiphernodeRegistry.json | 86 ++++- .../interfaces/IEnclave.sol/IEnclave.json | 140 ++++++++- .../NaiveRegistryFilter.json | 6 +- .../enclave-contracts/contracts/Enclave.sol | 172 ++++++++-- .../contracts/interfaces/IBondingRegistry.sol | 38 +++ .../interfaces/ICiphernodeRegistry.sol | 37 +++ .../contracts/interfaces/IComputeProvider.sol | 14 +- .../interfaces/IDecryptionVerifier.sol | 17 +- .../contracts/interfaces/IE3.sol | 35 ++- .../contracts/interfaces/IE3Program.sol | 31 +- .../contracts/interfaces/IEnclave.sol | 59 ++++ .../contracts/interfaces/IInputValidator.sol | 15 +- .../contracts/interfaces/IRegistryFilter.sol | 20 ++ .../contracts/interfaces/ISlashVerifier.sol | 15 +- .../contracts/interfaces/ISlashingManager.sol | 295 +++++++++++++----- .../contracts/lib/ExitQueueLib.sol | 146 +++++++++ .../contracts/registry/BondingRegistry.sol | 125 +++++++- .../registry/CiphernodeRegistryOwnable.sol | 78 +++++ .../registry/NaiveRegistryFilter.sol | 38 +++ .../contracts/slashing/SlashingManager.sol | 54 +++- .../contracts/test/MockCiphernodeRegistry.sol | 106 ++++--- .../contracts/token/EnclaveTicketToken.sol | 86 +++-- .../contracts/token/EnclaveToken.sol | 116 +++++-- 34 files changed, 1613 insertions(+), 275 deletions(-) create mode 100644 examples/CRISP/server/src/cli/approve.rs diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index 91d2768280..0d161cb4b6 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -74,7 +74,7 @@ sol! { mapping(uint256 e3Id => uint256 inputCount) public inputCounts; mapping(uint256 e3Id => bytes params) public e3Params; mapping(address e3Program => bool allowed) public e3Programs; - function request(E3RequestParams memory request) external payable returns (uint256 e3Id, E3 memory e3); + function request(E3RequestParams memory request) external returns (uint256 e3Id, E3 memory e3); function activate(uint256 e3Id,bytes memory publicKey) external returns (bool success); function enableE3Program(address e3Program) public onlyOwner returns (bool success); function publishInput(uint256 e3Id, bytes memory data) external returns (bool success); @@ -82,6 +82,7 @@ sol! { function publishPlaintextOutput(uint256 e3Id, bytes memory data) external returns (bool success); function getE3(uint256 e3Id) external view returns (E3 memory e3); function getRoot(uint256 id) public view returns (uint256); + function getE3Quote(E3RequestParams memory request) external view returns (uint256 fee); } } @@ -108,6 +109,18 @@ pub trait EnclaveRead { /// Check if an E3 program is enabled async fn is_e3_program_enabled(&self, e3_program: Address) -> Result; + + /// Get the fee quote for an E3 request + async fn get_e3_quote( + &self, + filter: Address, + threshold: [u32; 2], + start_window: [U256; 2], + duration: U256, + e3_program: Address, + e3_params: Bytes, + compute_provider_params: Bytes, + ) -> Result; } /// Trait for write operations on the Enclave contract @@ -330,6 +343,31 @@ where let enabled = contract.e3Programs(e3_program).call().await?; Ok(enabled) } + + async fn get_e3_quote( + &self, + filter: Address, + threshold: [u32; 2], + start_window: [U256; 2], + duration: U256, + e3_program: Address, + e3_params: Bytes, + compute_provider_params: Bytes, + ) -> Result { + let e3_request = E3RequestParams { + filter, + threshold, + startWindow: start_window, + duration, + e3Program: e3_program, + e3ProgramParams: e3_params, + computeProviderParams: compute_provider_params, + }; + + let contract = Enclave::new(self.contract_address, &self.provider); + let fee = contract.getE3Quote(e3_request).call().await?; + Ok(fee) + } } // Implement EnclaveWrite only for contracts with ReadWrite marker diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index 95337162e8..257792c8a6 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -1,7 +1,7 @@ # !/bin/bash # Install the enclave binary -cargo install --locked --path ./crates/cli --bin enclave -f +# cargo install --locked --path ./crates/cli --bin enclave -f # Deploy Contacts (cd packages/enclave-contracts && rm -rf deployments/localhost && pnpm deploy:mocks --network localhost) diff --git a/deploy/local/nodes.sh b/deploy/local/nodes.sh index 15033ab2b7..32789b87b0 100755 --- a/deploy/local/nodes.sh +++ b/deploy/local/nodes.sh @@ -1,7 +1,7 @@ # !/bin/bash # Install the enclave binary -cargo install --locked --path ./crates/cli --bin enclave -f +# cargo install --locked --path ./crates/cli --bin enclave -f concurrently \ --names "ANVIL,NODES" \ diff --git a/docs/pages/building-with-enclave.mdx b/docs/pages/building-with-enclave.mdx index e68fbb532c..c0f2113244 100644 --- a/docs/pages/building-with-enclave.mdx +++ b/docs/pages/building-with-enclave.mdx @@ -52,7 +52,7 @@ function request( IE3Program e3Program, bytes memory e3ProgramParams, bytes memory computeProviderParams -) external payable +) external ``` 2. Contract validates request parameters @@ -238,18 +238,15 @@ the finality of the outcome and to appropriately handle re-orgs. ## Best Practices 1. **Request Management** - - Set appropriate thresholds - Choose realistic time windows 2. **Input Handling** - - Encrypt inputs properly - Include necessary ZKPs - Submit within time windows 3. **Result Processing** - - Listen for all relevant events - Implement timeout handling - Verify result integrity diff --git a/docs/pages/computation-flow.mdx b/docs/pages/computation-flow.mdx index cd4ade37b7..d2ee08a111 100644 --- a/docs/pages/computation-flow.mdx +++ b/docs/pages/computation-flow.mdx @@ -35,7 +35,7 @@ Providers, or other network participant. IE3Program e3Program, bytes memory e3ProgramParams, bytes memory computeProviderParams - ) external payable returns (uint256 e3Id, E3 memory e3) + ) external returns (uint256 e3Id, E3 memory e3) ``` ### Phase 2: Node Selection @@ -57,9 +57,9 @@ During this phase, Data Providers — who may include individual users, applicat ensure they are valid for the requested E3. Some of these proofs are generic (e.g., proof of valid encryption) while others will be specific to your application. 3. **Submit Inputs**: Both encrypted data and ZKPs are submitted to the Enclave contract, which will - call the `validate` function on your E3P InputValidator smart contract. The input hash is then added to a Merkle - tree, the root of which can later be used to anchor proofs of correct execution of your E3 - Program. + call the `validate` function on your E3P InputValidator smart contract. The input hash is then + added to a Merkle tree, the root of which can later be used to anchor proofs of correct execution + of your E3 Program. ```solidity function validate( diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index ef9c17b74d..4a4a824c5b 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,5 +1,5 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8 # Default E3 program address from anvil -VITE_SEMAPHORE_ADDRESS=0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E +VITE_E3_PROGRAM_ADDRESS=0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9 # Default E3 program address from anvil +VITE_SEMAPHORE_ADDRESS=0x67d269191c92Caf3cD7723F116c85e6E9bf55933 \ No newline at end of file diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index fd92d87342..2950c555d6 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -14,6 +14,8 @@ ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" NAIVE_REGISTRY_FILTER_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address +FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" + # E3 Config E3_WINDOW_SIZE=40 diff --git a/examples/CRISP/server/src/cli/approve.rs b/examples/CRISP/server/src/cli/approve.rs new file mode 100644 index 0000000000..c248fa3df3 --- /dev/null +++ b/examples/CRISP/server/src/cli/approve.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use alloy::primitives::{Address, U256}; +use alloy::sol; +use eyre::Result; + +sol! { + #[derive(Debug)] + #[sol(rpc)] + contract ERC20 { + function approve(address spender, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + } +} + +pub async fn approve_token( + http_rpc_url: &str, + private_key: &str, + token_address: &str, + spender_address: &str, + amount: U256, +) -> Result<()> { + use alloy::network::EthereumWallet; + use alloy::providers::{Provider, ProviderBuilder}; + use alloy::signers::local::PrivateKeySigner; + + let token_address: Address = token_address.parse()?; + let spender_address: Address = spender_address.parse()?; + let signer: PrivateKeySigner = private_key.parse()?; + let wallet = EthereumWallet::from(signer); + + let provider = ProviderBuilder::new() + .wallet(wallet) + .connect(http_rpc_url) + .await?; + + let contract = ERC20::new(token_address, &provider); + + let owner = provider.get_accounts().await?[0]; + let current_allowance = contract.allowance(owner, spender_address).call().await?; + + log::info!("Current allowance: {}", current_allowance); + + if current_allowance < amount { + log::info!( + "Approving {} tokens for spender {}", + amount, + spender_address + ); + let builder = contract.approve(spender_address, amount); + let receipt = builder.send().await?.get_receipt().await?; + log::info!( + "Approval successful. TxHash: {:?}", + receipt.transaction_hash + ); + } else { + log::info!("Sufficient allowance already exists"); + } + + Ok(()) +} diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index 2abbf7fd64..df80c4ae2f 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -10,9 +10,10 @@ use log::info; use reqwest::Client; use serde::{Deserialize, Serialize}; -use super::{CLI_DB}; +use super::approve; +use super::CLI_DB; use alloy::primitives::{Address, Bytes, U256}; -use crisp::config::CONFIG; +use crisp::config::CONFIG; use e3_sdk::bfv_helpers::{build_bfv_params_arc, encode_bfv_params, params::SET_2048_1032193_1}; use e3_sdk::evm_helpers::contracts::{EnclaveContract, EnclaveRead, EnclaveWrite}; use fhe_rs::bfv::{BfvParameters, Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; @@ -89,6 +90,30 @@ pub async fn initialize_crisp_round() -> Result<(), Box bool allowed) public e3Programs; - // Mapping of E3s. + /// @notice Mapping storing all E3 instances by their ID. + /// @dev Contains the full state and configuration of each E3. mapping(uint256 e3Id => E3 e3) public e3s; - // Mapping of input merkle trees. + /// @notice Mapping of input merkle trees for each E3. + /// @dev Uses Lean IMT for efficient incremental merkle tree operations. mapping(uint256 e3Id => LeanIMTData imt) public inputs; - // Mapping counting the number of inputs for each E3. + /// @notice Counter tracking the number of inputs published for each E3. + /// @dev Used as the index when inserting new inputs into the merkle tree. mapping(uint256 e3Id => uint256 inputCount) public inputCounts; - // Mapping of enabled encryption schemes. + /// @notice Mapping of enabled encryption schemes to their decryption verifiers. + /// @dev Each encryption scheme ID maps to a contract that can verify decrypted outputs. mapping(bytes32 encryptionSchemeId => IDecryptionVerifier decryptionVerifier) public decryptionVerifiers; - /// Mapping that stores the valid E3 program ABI encoded parameter sets (e.g., BFV). + /// @notice Mapping storing valid E3 program ABI encoded parameter sets. + /// @dev Stores allowed encryption scheme parameters (e.g., BFV parameters). mapping(bytes e3ProgramParams => bool allowed) public e3ProgramsParams; - // Mapping of E3 payments. + /// @notice Mapping tracking fee payments for each E3. + /// @dev Stores the amount paid for an E3, distributed to committee upon completion. mapping(uint256 e3Id => uint256 e3Payment) public e3Payments; //////////////////////////////////////////////////////////// @@ -68,32 +94,107 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Thrown when committee selection fails during E3 request or activation. error CommitteeSelectionFailed(); + + /// @notice Thrown when an E3 request uses a program that is not enabled. + /// @param e3Program The E3 program address that is not allowed. error E3ProgramNotAllowed(IE3Program e3Program); + + /// @notice Thrown when attempting to activate an E3 that is already activated. + /// @param e3Id The ID of the E3 that is already activated. error E3AlreadyActivated(uint256 e3Id); + + /// @notice Thrown when the E3 start window or computation period has expired. error E3Expired(); + + /// @notice Thrown when attempting operations on an E3 that has not been activated yet. + /// @param e3Id The ID of the E3 that is not activated. error E3NotActivated(uint256 e3Id); + + /// @notice Thrown when attempting to activate an E3 before its start window begins. error E3NotReady(); + + /// @notice Thrown when attempting to access an E3 that does not exist. + /// @param e3Id The ID of the non-existent E3. error E3DoesNotExist(uint256 e3Id); + + /// @notice Thrown when attempting to enable a module or program that is already enabled. + /// @param module The address of the module that is already enabled. error ModuleAlreadyEnabled(address module); + + /// @notice Thrown when attempting to disable a module or program that is not enabled. + /// @param module The address of the module that is not enabled. error ModuleNotEnabled(address module); + + /// @notice Thrown when an invalid or disabled encryption scheme is used. + /// @param encryptionSchemeId The ID of the invalid encryption scheme. error InvalidEncryptionScheme(bytes32 encryptionSchemeId); + + /// @notice Thrown when attempting to publish input after the computation deadline has passed. + /// @param e3Id The ID of the E3. + /// @param expiration The expiration timestamp that has passed. error InputDeadlinePassed(uint256 e3Id, uint256 expiration); + + /// @notice Thrown when attempting to publish output before the input deadline has passed. + /// @param e3Id The ID of the E3. + /// @param expiration The expiration timestamp that has not yet passed. error InputDeadlineNotPassed(uint256 e3Id, uint256 expiration); + + /// @notice Thrown when the input validator in the computation request is invalid. + /// @param inputValidator The address of the invalid input validator. error InvalidComputationRequest(IInputValidator inputValidator); + + /// @notice Thrown when attempting to set an invalid ciphernode registry address. + /// @param ciphernodeRegistry The invalid ciphernode registry address. error InvalidCiphernodeRegistry(ICiphernodeRegistry ciphernodeRegistry); + + /// @notice Thrown when the requested duration exceeds maxDuration or is zero. + /// @param duration The invalid duration value. error InvalidDuration(uint256 duration); + + /// @notice Thrown when output verification fails. + /// @param output The invalid output data. error InvalidOutput(bytes output); + + /// @notice Thrown when input data is invalid. error InvalidInput(); + + /// @notice Thrown when the start window parameters are invalid. error InvalidStartWindow(); + + /// @notice Thrown when the threshold parameters are invalid (e.g., M > N or M = 0). + /// @param threshold The invalid threshold array [M, N]. error InvalidThreshold(uint32[2] threshold); + + /// @notice Thrown when attempting to publish ciphertext output that has already been published. + /// @param e3Id The ID of the E3. error CiphertextOutputAlreadyPublished(uint256 e3Id); + + /// @notice Thrown when attempting to publish plaintext output before ciphertext output. + /// @param e3Id The ID of the E3. error CiphertextOutputNotPublished(uint256 e3Id); + + /// @notice Thrown when payment is required but not provided or insufficient. + /// @param value The required payment amount. error PaymentRequired(uint256 value); + + /// @notice Thrown when attempting to publish plaintext output that has already been published. + /// @param e3Id The ID of the E3. error PlaintextOutputAlreadyPublished(uint256 e3Id); + + /// @notice Thrown when the caller has insufficient token balance. error InsufficientBalance(); + + /// @notice Thrown when the contract has insufficient token allowance. error InsufficientAllowance(); + + /// @notice Thrown when attempting to set an invalid bonding registry address. + /// @param bondingRegistry The invalid bonding registry address. error InvalidBondingRegistry(IBondingRegistry bondingRegistry); + + /// @notice Thrown when attempting to set an invalid fee token address. + /// @param feeToken The invalid fee token address. error InvalidFeeToken(IERC20 feeToken); //////////////////////////////////////////////////////////// @@ -102,9 +203,14 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// - /// @param _owner The owner of this contract - /// @param _maxDuration The maximum duration of a computation in seconds - /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV) + /// @notice Constructs the Enclave contract. + /// @dev Calls initialize() to set up the contract state. Can be used for non-proxy deployments. + /// @param _owner The owner address of this contract. + /// @param _ciphernodeRegistry The address of the Ciphernode Registry contract. + /// @param _bondingRegistry The address of the Bonding Registry contract. + /// @param _feeToken The address of the ERC20 token used for E3 fees. + /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). constructor( address _owner, ICiphernodeRegistry _ciphernodeRegistry, @@ -123,10 +229,14 @@ contract Enclave is IEnclave, OwnableUpgradeable { ); } - /// @param _owner The owner of this contract - /// @param _ciphernodeRegistry The address of the ciphernode registry - /// @param _maxDuration The maximum duration of a computation in seconds - /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV) + /// @notice Initializes the Enclave contract with initial configuration. + /// @dev This function can only be called once due to the initializer modifier. Sets up core dependencies. + /// @param _owner The owner address of this contract. + /// @param _ciphernodeRegistry The address of the Ciphernode Registry contract. + /// @param _bondingRegistry The address of the Bonding Registry contract. + /// @param _feeToken The address of the ERC20 token used for E3 fees. + /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). function initialize( address _owner, ICiphernodeRegistry _ciphernodeRegistry, @@ -150,6 +260,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc IEnclave function request( E3RequestParams calldata requestParams ) external returns (uint256 e3Id, E3 memory e3) { @@ -241,6 +352,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { ); } + /// @inheritdoc IEnclave function activate( uint256 e3Id, bytes calldata publicKey @@ -266,6 +378,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { return true; } + /// @inheritdoc IEnclave function publishInput( uint256 e3Id, bytes calldata data @@ -294,6 +407,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit InputPublished(e3Id, input, inputHash, inputIndex); } + /// @inheritdoc IEnclave function publishCiphertextOutput( uint256 e3Id, bytes calldata ciphertextOutput, @@ -323,6 +437,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit CiphertextOutputPublished(e3Id, ciphertextOutput); } + /// @inheritdoc IEnclave function publishPlaintextOutput( uint256 e3Id, bytes calldata plaintextOutput, @@ -361,6 +476,10 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Distributes rewards to committee members after successful E3 completion. + /// @dev Divides the E3 payment equally among all committee members and transfers via bonding registry. + /// @dev Emits RewardsDistributed event upon successful distribution. + /// @param e3Id The ID of the E3 for which to distribute rewards. function _distributeRewards(uint256 e3Id) internal { IRegistryFilter.Committee memory committee = ciphernodeRegistry .getCommittee(e3Id); @@ -392,6 +511,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc IEnclave function setMaxDuration( uint256 _maxDuration ) public onlyOwner returns (bool success) { @@ -400,6 +520,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit MaxDurationSet(_maxDuration); } + /// @inheritdoc IEnclave function setCiphernodeRegistry( ICiphernodeRegistry _ciphernodeRegistry ) public onlyOwner returns (bool success) { @@ -413,6 +534,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit CiphernodeRegistrySet(address(_ciphernodeRegistry)); } + /// @inheritdoc IEnclave function setBondingRegistry( IBondingRegistry _bondingRegistry ) public onlyOwner returns (bool success) { @@ -426,6 +548,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit BondingRegistrySet(address(_bondingRegistry)); } + /// @inheritdoc IEnclave function setFeeToken( IERC20 _feeToken ) public onlyOwner returns (bool success) { @@ -438,6 +561,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit FeeTokenSet(address(_feeToken)); } + /// @inheritdoc IEnclave function enableE3Program( IE3Program e3Program ) public onlyOwner returns (bool success) { @@ -450,6 +574,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3ProgramEnabled(e3Program); } + /// @inheritdoc IEnclave function disableE3Program( IE3Program e3Program ) public onlyOwner returns (bool success) { @@ -459,6 +584,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3ProgramDisabled(e3Program); } + /// @inheritdoc IEnclave function setDecryptionVerifier( bytes32 encryptionSchemeId, IDecryptionVerifier decryptionVerifier @@ -473,6 +599,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit EncryptionSchemeEnabled(encryptionSchemeId); } + /// @inheritdoc IEnclave function disableEncryptionScheme( bytes32 encryptionSchemeId ) public onlyOwner returns (bool success) { @@ -488,6 +615,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit EncryptionSchemeDisabled(encryptionSchemeId); } + /// @inheritdoc IEnclave function setE3ProgramsParams( bytes[] memory _e3ProgramsParams ) public onlyOwner returns (bool success) { @@ -508,11 +636,13 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc IEnclave function getE3(uint256 e3Id) public view returns (E3 memory e3) { e3 = e3s[e3Id]; require(e3.e3Program != IE3Program(address(0)), E3DoesNotExist(e3Id)); } + /// @inheritdoc IEnclave function getInputRoot(uint256 e3Id) public view returns (uint256) { require( e3s[e3Id].e3Program != IE3Program(address(0)), @@ -521,14 +651,14 @@ contract Enclave is IEnclave, OwnableUpgradeable { return InternalLeanIMT._root(inputs[e3Id]); } - // TODO: this should be calculated based on the E3 program and the parameters - // This is just a placeholder for now + /// @inheritdoc IEnclave function getE3Quote( E3RequestParams calldata ) public pure returns (uint256 fee) { fee = 1 * 10 ** 6; } + /// @inheritdoc IEnclave function getDecryptionVerifier( bytes32 encryptionSchemeId ) public view returns (IDecryptionVerifier) { diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index c0816b87fa..5c805ebc24 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -186,6 +186,37 @@ interface IBondingRegistry { */ function exitDelay() external view returns (uint64); + /** + * @notice Get operator's ticket balance at a specific block + * @param operator Address of the operator + * @param blockNumber Block number to query + * @return Ticket balance at the specified block + */ + function getTicketBalanceAtBlock( + address operator, + uint256 blockNumber + ) external view returns (uint256); + + /** + * @notice Get operator's total pending exit amounts + * @param operator Address of the operator + * @return ticket Total pending ticket balance in exit queue + * @return license Total pending license bond in exit queue + */ + function pendingExits( + address operator + ) external view returns (uint256 ticket, uint256 license); + + /** + * @notice Preview how much an operator can currently claim + * @param operator Address of the operator + * @return ticket Claimable ticket balance + * @return license Claimable license bond + */ + function previewClaimable( + address operator + ) external view returns (uint256 ticket, uint256 license); + /** * @notice Get slashed funds treasury address * @return Address where slashed funds are sent @@ -383,6 +414,13 @@ interface IBondingRegistry { */ function setSlashingManager(address newSlashingManager) external; + /** + * @notice Set reward distributor address + * @param newRewardDistributor New reward distributor address + * @dev Only callable by contract owner + */ + function setRewardDistributor(address newRewardDistributor) external; + /** * @notice Withdraw slashed funds to treasury * @param ticketAmount Amount of slashed ticket balance to withdraw diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index f44f45abf8..60f619fc1d 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -7,6 +7,12 @@ pragma solidity >=0.8.27; import { IRegistryFilter } from "./IRegistryFilter.sol"; +/** + * @title ICiphernodeRegistry + * @notice Interface for managing ciphernode registration and committee selection + * @dev This registry maintains an Incremental Merkle Tree (IMT) of registered ciphernodes + * and coordinates committee selection for E3 computations through registry filters + */ interface ICiphernodeRegistry { /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. @@ -56,6 +62,10 @@ interface ICiphernodeRegistry { uint256 size ); + /// @notice Check if a ciphernode is eligible for committee selection + /// @dev A ciphernode is eligible if it is enabled in the registry and meets bonding requirements + /// @param ciphernode Address of the ciphernode to check + /// @return eligible Whether the ciphernode is eligible for committee selection function isCiphernodeEligible(address ciphernode) external returns (bool); /// @notice Check if a ciphernode is enabled in the registry @@ -119,4 +129,31 @@ interface ICiphernodeRegistry { function getCommittee( uint256 e3Id ) external view returns (IRegistryFilter.Committee memory committee); + + /// @notice Returns the current root of the ciphernode IMT + /// @return Current IMT root + function root() external view returns (uint256); + + /// @notice Returns the IMT root at the time a committee was requested + /// @param e3Id ID of the E3 + /// @return IMT root at time of committee request + function rootAt(uint256 e3Id) external view returns (uint256); + + /// @notice Returns the current size of the ciphernode IMT + /// @return Size of the IMT + function treeSize() external view returns (uint256); + + /// @notice Returns the address of the bonding registry + /// @return Address of the bonding registry contract + function getBondingRegistry() external view returns (address); + + /// @notice Sets the Enclave contract address + /// @dev Only callable by owner + /// @param _enclave Address of the Enclave contract + function setEnclave(address _enclave) external; + + /// @notice Sets the bonding registry contract address + /// @dev Only callable by owner + /// @param _bondingRegistry Address of the bonding registry contract + function setBondingRegistry(address _bondingRegistry) external; } diff --git a/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol b/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol index 74f8d0f231..2f2a625600 100644 --- a/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol +++ b/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol @@ -7,9 +7,19 @@ pragma solidity >=0.8.27; import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; +/** + * @title IComputeProvider + * @notice Interface for compute provider validation and configuration + * @dev Compute providers define how computations are executed and verified in the E3 system + */ interface IComputeProvider { - /// @notice This function should be called by the Enclave contract to validate the compute provider parameters. - /// @param params ABI encoded compute provider parameters. + /// @notice Validate compute provider parameters and return the appropriate decryption verifier + /// @dev This function is called by the Enclave contract during E3 request to validate + /// compute provider configuration + /// @param e3Id ID of the E3 computation + /// @param seed Random seed for the computation + /// @param params ABI encoded compute provider parameters + /// @return decryptionVerifier The decryption verifier contract to use for this computation function validate( uint256 e3Id, uint256 seed, diff --git a/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol b/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol index bb3025c775..e7b727914e 100644 --- a/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol +++ b/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol @@ -5,13 +5,18 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title IDecryptionVerifier + * @notice Interface for verifying decrypted computation outputs + * @dev Implements cryptographic verification of plaintext outputs from encrypted computations + */ interface IDecryptionVerifier { - /// @notice This function should be called by the Enclave contract to verify the - /// decryption of output of a computation. - /// @param e3Id ID of the E3. - /// @param plaintextOutputHash The keccak256 hash of the plaintext output to be verified. - /// @param proof ABI encoded proof of the given output hash. - /// @return success Whether or not the plaintextOutputHash was successfully verified. + /// @notice Verify the decryption of a computation output + /// @dev This function is called by the Enclave contract when plaintext output is published + /// @param e3Id ID of the E3 computation + /// @param plaintextOutputHash The keccak256 hash of the plaintext output to be verified + /// @param proof ABI encoded proof of the decryption validity + /// @return success Whether the plaintextOutputHash was successfully verified function verify( uint256 e3Id, bytes32 plaintextOutputHash, diff --git a/packages/enclave-contracts/contracts/interfaces/IE3.sol b/packages/enclave-contracts/contracts/interfaces/IE3.sol index 40450579fe..a41883e642 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3.sol @@ -9,21 +9,26 @@ import { IInputValidator } from "./IInputValidator.sol"; import { IE3Program } from "./IE3Program.sol"; import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; -/// @title E3 struct -/// @notice This struct represents an E3 computation. -/// @param threshold M/N threshold for the committee. -/// @param requestBlock Block number when the E3 was requested. -/// @param startWindow Start window for the computation: index zero is minimum, index 1 is the maxium. -/// @param duration Duration of the E3. -/// @param expiration Timestamp when committee duties expire. -/// @param e3Program Address of the E3 Program contract. -/// @param e3ProgramParams ABI encoded computation parameters. -/// @param computeProvider Address of the compute provider contract. -/// @param inputValidator Address of the input validator contract. -/// @param decryptionVerifier Address of the output verifier contract. -/// @param committeeId ID of the selected committee. -/// @param ciphertextOutput Encrypted output data. -/// @param plaintextOutput Decrypted output data. +/** + * @title E3 + * @notice Represents a complete E3 (Encrypted Execution Environment) computation request and its lifecycle + * @dev This struct tracks all parameters, state, and results of an encrypted computation + * from request through completion + * @param seed Random seed for committee selection and computation initialization + * @param threshold M/N threshold for the committee (M required out of N total members) + * @param requestBlock Block number when the E3 computation was requested + * @param startWindow Start window for the computation: index 0 is minimum block, index 1 is the maximum block + * @param duration Duration of the E3 computation in blocks or time units + * @param expiration Timestamp when committee duties expire and computation is considered failed + * @param encryptionSchemeId Identifier for the encryption scheme used in this computation + * @param e3Program Address of the E3 Program contract that validates and verifies the computation + * @param e3ProgramParams ABI encoded computation parameters specific to the E3 program + * @param inputValidator Address of the input validator contract for input verification + * @param decryptionVerifier Address of the output verifier contract for decryption verification + * @param committeePublicKey The public key of the selected committee for this computation + * @param ciphertextOutput Hash of the encrypted output data produced by the computation + * @param plaintextOutput Decrypted output data after committee decryption + */ struct E3 { uint256 seed; uint32[2] threshold; diff --git a/packages/enclave-contracts/contracts/interfaces/IE3Program.sol b/packages/enclave-contracts/contracts/interfaces/IE3Program.sol index 1ac8df976a..0dd1f642c1 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3Program.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3Program.sol @@ -7,14 +7,20 @@ pragma solidity >=0.8.27; import { IInputValidator } from "./IInputValidator.sol"; +/** + * @title IE3Program + * @notice Interface for E3 program validation and verification + * @dev E3 programs define the computation logic and validation rules for encrypted execution environments + */ interface IE3Program { - /// @notice This function should be called by the Enclave contract to validate the computation parameters. - /// @param e3Id ID of the E3. - /// @param seed Seed for the computation. - /// @param e3ProgramParams ABI encoded computation parameters. - /// @param computeProviderParams ABI encoded compute provider parameters. - /// @return encryptionSchemeId ID of the encryption scheme to be used for the computation. - /// @return inputValidator The input validator to be used for the computation. + /// @notice Validate E3 computation parameters and return encryption scheme and input validator + /// @dev This function is called by the Enclave contract during E3 request to configure the computation + /// @param e3Id ID of the E3 computation + /// @param seed Random seed for the computation + /// @param e3ProgramParams ABI encoded E3 program parameters + /// @param computeProviderParams ABI encoded compute provider parameters + /// @return encryptionSchemeId ID of the encryption scheme to be used for the computation + /// @return inputValidator The input validator to be used for the computation function validate( uint256 e3Id, uint256 seed, @@ -24,11 +30,12 @@ interface IE3Program { external returns (bytes32 encryptionSchemeId, IInputValidator inputValidator); - /// @notice This function should be called by the Enclave contract to verify the decrypted output of an E3. - /// @param e3Id ID of the E3. - /// @param ciphertextOutputHash The keccak256 hash of output data to be verified. - /// @param proof ABI encoded data to verify the ciphertextOutputHash. - /// @return success Whether the output data is valid. + /// @notice Verify the ciphertext output of an E3 computation + /// @dev This function is called by the Enclave contract when ciphertext output is published + /// @param e3Id ID of the E3 computation + /// @param ciphertextOutputHash The keccak256 hash of output data to be verified + /// @param proof ABI encoded data to verify the ciphertextOutputHash + /// @return success Whether the output data is valid function verify( uint256 e3Id, bytes32 ciphertextOutputHash, diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index e4c85bcdb2..4b7d9a3674 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -6,6 +6,10 @@ pragma solidity >=0.8.27; import { E3, IE3Program } from "./IE3.sol"; +import { ICiphernodeRegistry } from "./ICiphernodeRegistry.sol"; +import { IBondingRegistry } from "./IBondingRegistry.sol"; +import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IEnclave { //////////////////////////////////////////////////////////// @@ -207,6 +211,28 @@ interface IEnclave { uint256 _maxDuration ) external returns (bool success); + /// @notice Sets the Ciphernode Registry contract address. + /// @dev This function MUST revert if the address is zero or the same as the current registry. + /// @param _ciphernodeRegistry The address of the new Ciphernode Registry contract. + /// @return success True if the registry was successfully set. + function setCiphernodeRegistry( + ICiphernodeRegistry _ciphernodeRegistry + ) external returns (bool success); + + /// @notice Sets the Bonding Registry contract address. + /// @dev This function MUST revert if the address is zero or the same as the current registry. + /// @param _bondingRegistry The address of the new Bonding Registry contract. + /// @return success True if the registry was successfully set. + function setBondingRegistry( + IBondingRegistry _bondingRegistry + ) external returns (bool success); + + /// @notice Sets the fee token used for E3 payments. + /// @dev This function MUST revert if the address is zero or the same as the current fee token. + /// @param _feeToken The address of the new fee token. + /// @return success True if the fee token was successfully set. + function setFeeToken(IERC20 _feeToken) external returns (bool success); + /// @notice This function should be called to enable an E3 Program. /// @param e3Program The address of the E3 Program. /// @return success True if the E3 Program was successfully enabled. @@ -221,6 +247,32 @@ interface IEnclave { IE3Program e3Program ) external returns (bool success); + /// @notice Sets or enables a decryption verifier for a specific encryption scheme. + /// @dev This function MUST revert if the verifier address is zero or already set to the same value. + /// @param encryptionSchemeId The unique identifier for the encryption scheme. + /// @param decryptionVerifier The address of the decryption verifier contract. + /// @return success True if the verifier was successfully set. + function setDecryptionVerifier( + bytes32 encryptionSchemeId, + IDecryptionVerifier decryptionVerifier + ) external returns (bool success); + + /// @notice Disables a previously enabled encryption scheme. + /// @dev This function MUST revert if the encryption scheme is not currently enabled. + /// @param encryptionSchemeId The unique identifier for the encryption scheme to disable. + /// @return success True if the encryption scheme was successfully disabled. + function disableEncryptionScheme( + bytes32 encryptionSchemeId + ) external returns (bool success); + + /// @notice Sets the allowed E3 program parameters. + /// @dev This function enables specific parameter sets for E3 programs (e.g., BFV encryption parameters). + /// @param _e3ProgramsParams Array of ABI encoded parameter sets to allow. + /// @return success True if the parameters were successfully set. + function setE3ProgramsParams( + bytes[] memory _e3ProgramsParams + ) external returns (bool success); + //////////////////////////////////////////////////////////// // // // Get Functions // @@ -246,4 +298,11 @@ interface IEnclave { function getE3Quote( E3RequestParams calldata e3Params ) external view returns (uint256 fee); + + /// @notice Returns the decryption verifier for a given encryption scheme. + /// @param encryptionSchemeId The unique identifier for the encryption scheme. + /// @return The decryption verifier contract for the specified encryption scheme. + function getDecryptionVerifier( + bytes32 encryptionSchemeId + ) external view returns (IDecryptionVerifier); } diff --git a/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol b/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol index cc1b9c7336..2c87a2ba05 100644 --- a/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol +++ b/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol @@ -5,12 +5,17 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title IInputValidator + * @notice Interface for validating computation inputs + * @dev Input validators enforce access control and validation rules for E3 computation inputs + */ interface IInputValidator { - /// @notice This function should be called by the Enclave contract to validate the - /// input of a computation. - /// @param sender The account that is submitting the input. - /// @param data The input to be verified. - /// @return input The decoded, policy-approved application payload. + /// @notice Validate and process input data for a computation + /// @dev This function is called by the Enclave contract when input is published + /// @param sender The account that is submitting the input + /// @param data The input data to be validated + /// @return input The decoded, policy-approved application payload function validate( address sender, bytes memory data diff --git a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol b/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol index 1dce174abb..71a9b926c1 100644 --- a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol +++ b/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol @@ -5,18 +5,38 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title IRegistryFilter + * @notice Interface for filtering and selecting committee members from the registry + * @dev Registry filters implement committee selection algorithms for E3 computations + */ interface IRegistryFilter { + /** + * @notice Committee data structure + * @param nodes Array of selected ciphernode addresses + * @param threshold M/N threshold for the committee (M required signatures out of N members) + * @param publicKey Hash of the committee's aggregated public key + */ struct Committee { address[] nodes; uint32[2] threshold; bytes32 publicKey; } + /// @notice Request a committee for an E3 computation + /// @dev This function is called by the CiphernodeRegistry to initiate committee selection + /// @param e3Id ID of the E3 computation + /// @param threshold M/N threshold for the committee + /// @return success Whether the committee request was successful function requestCommittee( uint256 e3Id, uint32[2] calldata threshold ) external returns (bool success); + /// @notice Get the committee for an E3 computation + /// @dev This function returns the selected committee after it has been published + /// @param e3Id ID of the E3 computation + /// @return committee The selected committee data function getCommittee( uint256 e3Id ) external view returns (Committee memory); diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol index 328723acbb..a2ac4b860f 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol @@ -5,12 +5,17 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title ISlashVerifier + * @notice Interface for verifying slash proofs + * @dev Slash verifiers implement cryptographic or logical verification of slash proposals + */ interface ISlashVerifier { - /// @notice This function should be called by the SlashingManager contract to verify the - /// proof of a slash. - /// @param proposalId ID of the proposal. - /// @param proof ABI encoded proof of the given proposal. - /// @return success Whether or not the proof was successfully verified. + /// @notice Verify a slash proof + /// @dev This function is called by the SlashingManager contract during slash proposal to verify proof validity + /// @param proposalId ID of the slash proposal + /// @param proof ABI encoded proof data supporting the slash + /// @return success Whether the proof was successfully verified function verify( uint256 proposalId, bytes memory proof diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index 49f0d5aa95..3d3e100195 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -20,56 +20,111 @@ interface ISlashingManager { /** * @notice Slashing policy configuration for different slash reasons + * @dev Defines penalties, proof requirements, and appeal mechanisms for each slash type + * @param ticketPenalty Amount of ticket collateral to slash (in wei) + * @param licensePenalty Amount of license bond to slash (in wei) + * @param requiresProof Whether this slash type requires cryptographic proof verification + * @param proofVerifier Address of the ISlashVerifier contract for proof validation + * @param banNode Whether executing this slash will permanently ban the node + * @param appealWindow Time window in seconds for operators to appeal (0 = immediate execution, no appeals) + * @param enabled Whether this slash type is currently active and can be proposed */ struct SlashPolicy { - uint256 ticketPenalty; // Amount for ticket collateral penalty - uint256 licensePenalty; // Amount for license stake penalty - bool requiresProof; // True if slash requires verifier proof - address proofVerifier; // Address of the verifier contract for proof verification - bool banNode; // True if this slash type would result in banning - uint256 appealWindow; // Seconds operators have to appeal (0 = immediate execution) - bool enabled; // True if this slash type is currently enabled + uint256 ticketPenalty; + uint256 licensePenalty; + bool requiresProof; + address proofVerifier; + bool banNode; + uint256 appealWindow; + bool enabled; } /** - * @notice Slash proposal details + * @notice Slash proposal details tracking the full lifecycle of a slash + * @dev Stores all state needed for proposal, appeal, and execution workflows + * @param operator Address of the ciphernode operator being slashed + * @param reason Hash of the slash reason (maps to SlashPolicy configuration) + * @param ticketAmount Amount of ticket collateral to slash (copied from policy at proposal time) + * @param licenseAmount Amount of license bond to slash (copied from policy at proposal time) + * @param executed Whether the slashing penalties have been executed + * @param appealed Whether the operator has filed an appeal + * @param resolved Whether the appeal has been resolved by governance + * @param appealUpheld Whether the appeal was approved (true = cancel slash, false = slash proceeds) + * @param proposedAt Block timestamp when the slash was proposed + * @param executableAt Block timestamp when execution becomes possible (proposedAt + appealWindow) + * @param proposer Address that created this slash proposal + * @param proofHash Keccak256 hash of the proof data submitted with the proposal + * @param proofVerified Whether the proof was successfully verified by the proof verifier contract */ struct SlashProposal { - address operator; // Address being slashed - bytes32 reason; // Reason hash (maps to SlashPolicy) - uint256 ticketAmount; // Calculated ticket penalty amount - uint256 licenseAmount; // Calculated license penalty amount - bool executed; // True if penalty executed - bool appealed; // True if operator filed appeal - bool resolved; // True if appeal was resolved - bool appealUpheld; // True if appeal was approved (penalty cancelled) - uint256 proposedAt; // Timestamp when proposed - uint256 executableAt; // Timestamp when execution is allowed - address proposer; // Address that proposed the slash - bytes32 proofHash; // Hash of the proof data - bool proofVerified; // True if proof was verified + address operator; + bytes32 reason; + uint256 ticketAmount; + uint256 licenseAmount; + bool executed; + bool appealed; + bool resolved; + bool appealUpheld; + uint256 proposedAt; + uint256 executableAt; + address proposer; + bytes32 proofHash; + bool proofVerified; } // ====================== // Errors // ====================== + /// @notice Thrown when a zero address is provided where a valid address is required error ZeroAddress(); + + /// @notice Thrown when caller lacks required role permissions for the operation error Unauthorized(); + + /// @notice Thrown when a slash policy configuration is invalid error InvalidPolicy(); + + /// @notice Thrown when referencing a proposal ID that doesn't exist or is in invalid state error InvalidProposal(); + + /// @notice Thrown when proof is required by policy but not provided error ProofRequired(); + + /// @notice Thrown when provided proof fails verification error InvalidProof(); + + /// @notice Thrown when attempting to execute a slash whose appeal was upheld error AppealUpheld(); + + /// @notice Thrown when attempting to execute a slash with an unresolved appeal error AppealPending(); + + /// @notice Thrown when attempting to file an appeal after the appeal window has closed error AppealWindowExpired(); + + /// @notice Thrown when attempting to execute a slash before the appeal window has closed error AppealWindowActive(); + + /// @notice Thrown when attempting to file a second appeal for the same proposal error AlreadyAppealed(); + + /// @notice Thrown when attempting to execute a slash that has already been executed error AlreadyExecuted(); + + /// @notice Thrown when attempting to resolve an appeal that has already been resolved error AlreadyResolved(); + + /// @notice Thrown when referencing a slash reason that doesn't exist error SlashReasonNotFound(); + + /// @notice Thrown when attempting to propose a slash for a disabled reason error SlashReasonDisabled(); + + /// @notice Thrown when a banned ciphernode attempts a restricted operation error CiphernodeBanned(); + + /// @notice Thrown when a policy requires proof but no verifier contract is configured error VerifierNotSet(); // ====================== @@ -77,12 +132,21 @@ interface ISlashingManager { // ====================== /** - * @notice Emitted when a slash policy is updated + * @notice Emitted when a slash policy is created or updated + * @param reason Hash of the slash reason being configured + * @param policy The complete policy configuration including penalties and appeal settings */ event SlashPolicyUpdated(bytes32 indexed reason, SlashPolicy policy); /** - * @notice Emitted when a slash is proposed + * @notice Emitted when a new slash proposal is created + * @param proposalId Unique ID of the created proposal + * @param operator Address of the ciphernode operator being slashed + * @param reason Hash of the slash reason + * @param ticketAmount Amount of ticket collateral to be slashed + * @param licenseAmount Amount of license bond to be slashed + * @param executableAt Timestamp when the slash can be executed (after appeal window) + * @param proposer Address that created the proposal */ event SlashProposed( uint256 indexed proposalId, @@ -95,7 +159,13 @@ interface ISlashingManager { ); /** - * @notice Emitted when a slash is executed + * @notice Emitted when a slash proposal is executed and penalties are applied + * @param proposalId ID of the executed proposal + * @param operator Address of the slashed operator + * @param reason Hash of the slash reason + * @param ticketAmount Amount of ticket collateral slashed + * @param licenseAmount Amount of license bond slashed + * @param executed Execution status (should always be true) */ event SlashExecuted( uint256 indexed proposalId, @@ -107,7 +177,11 @@ interface ISlashingManager { ); /** - * @notice Emitted when an appeal is filed + * @notice Emitted when an operator files an appeal against a slash proposal + * @param proposalId ID of the proposal being appealed + * @param operator Address of the operator filing the appeal + * @param reason Hash of the slash reason being appealed + * @param evidence Evidence string provided by the operator supporting their appeal */ event AppealFiled( uint256 indexed proposalId, @@ -117,7 +191,12 @@ interface ISlashingManager { ); /** - * @notice Emitted when an appeal is resolved + * @notice Emitted when governance resolves an appeal + * @param proposalId ID of the proposal with the resolved appeal + * @param operator Address of the operator who appealed + * @param appealUpheld Whether the appeal was approved (true = slash cancelled, false = slash proceeds) + * @param resolver Address of the governance account that resolved the appeal + * @param resolution Explanation string for the resolution decision */ event AppealResolved( uint256 indexed proposalId, @@ -128,7 +207,10 @@ interface ISlashingManager { ); /** - * @notice Emitted when a node is banned + * @notice Emitted when a node is banned from the network + * @param node Address of the banned node + * @param reason Hash of the reason for banning + * @param banner Address that executed the ban (governance or contract) */ event NodeBanned( address indexed node, @@ -137,7 +219,9 @@ interface ISlashingManager { ); /** - * @notice Emitted when a node is unbanned + * @notice Emitted when a previously banned node is unbanned + * @param node Address of the unbanned node + * @param unbanner Address of the governance account that unbanned the node */ event NodeUnbanned(address indexed node, address unbanner); @@ -146,42 +230,61 @@ interface ISlashingManager { // ====================== /** - * @notice Get slash policy for a reason + * @notice Retrieves the slash policy configuration for a given reason + * @param reason Hash of the slash reason to query + * @return policy The complete SlashPolicy struct (returns default empty struct if not configured) */ function getSlashPolicy( bytes32 reason - ) external view returns (SlashPolicy memory); + ) external view returns (SlashPolicy memory policy); /** - * @notice Get slash proposal details + * @notice Retrieves the details of a slash proposal + * @param proposalId ID of the proposal to query + * @return proposal The complete SlashProposal struct + * @dev Reverts with InvalidProposal if proposalId >= totalProposals */ function getSlashProposal( uint256 proposalId - ) external view returns (SlashProposal memory); + ) external view returns (SlashProposal memory proposal); /** - * @notice Get total number of proposals + * @notice Returns the total number of slash proposals ever created + * @return count The total count of proposals (next proposalId will be this value) */ - function totalProposals() external view returns (uint256); + function totalProposals() external view returns (uint256 count); /** - * @notice Check if a node is banned + * @notice Checks whether a node is currently banned + * @param node Address of the node to check + * @return isBanned True if the node is banned, false otherwise */ - function isBanned(address node) external view returns (bool); + function isBanned(address node) external view returns (bool isBanned); /** - * @notice Get bonding vault contract + * @notice Returns the bonding registry contract used for executing slashes + * @return registry The IBondingRegistry contract instance */ - function bondingRegistry() external view returns (IBondingRegistry); + function bondingRegistry() + external + view + returns (IBondingRegistry registry); // ====================== // Admin Functions // ====================== /** - * @notice Set slash policy for a reason - * @param reason Reason hash to set policy for - * @param policy Policy configuration + * @notice Creates or updates the slash policy for a specific reason + * @dev Only callable by GOVERNANCE_ROLE. Validates policy constraints before setting + * @param reason Hash of the slash reason to configure (must be non-zero) + * @param policy Complete policy configuration including penalties, proof requirements, and appeal settings + * Requirements: + * - reason must not be bytes32(0) + * - policy.enabled must be true + * - At least one of ticketPenalty or licensePenalty must be non-zero + * - If requiresProof is true, proofVerifier must be set and appealWindow must be 0 + * - If requiresProof is false, appealWindow must be greater than 0 */ function setSlashPolicy( bytes32 reason, @@ -189,32 +292,37 @@ interface ISlashingManager { ) external; /** - * @notice Set bonding vault address - * @param newBondingRegistry New bonding vault contract address + * @notice Updates the bonding registry contract address + * @dev Only callable by DEFAULT_ADMIN_ROLE. Used to execute actual slashing of funds + * @param newBondingRegistry Address of the new IBondingRegistry contract (must be non-zero) */ function setBondingRegistry(address newBondingRegistry) external; /** - * @notice Add authorized slasher - * @param slasher Address to authorize for slashing + * @notice Grants SLASHER_ROLE to an address + * @dev Only callable by DEFAULT_ADMIN_ROLE. Slashers can propose and execute slashes + * @param slasher Address to grant slashing permissions (must be non-zero) */ function addSlasher(address slasher) external; /** - * @notice Remove authorized slasher - * @param slasher Address to remove from slashing authorization + * @notice Revokes SLASHER_ROLE from an address + * @dev Only callable by DEFAULT_ADMIN_ROLE + * @param slasher Address to revoke slashing permissions from */ function removeSlasher(address slasher) external; /** - * @notice Add authorized verifier - * @param verifier Address to authorize for proof verification + * @notice Grants VERIFIER_ROLE to an address + * @dev Only callable by DEFAULT_ADMIN_ROLE. Verifiers can validate proof-based slashes + * @param verifier Address to grant verification permissions (must be non-zero) */ function addVerifier(address verifier) external; /** - * @notice Remove authorized verifier - * @param verifier Address to remove from verification authorization + * @notice Revokes VERIFIER_ROLE from an address + * @dev Only callable by DEFAULT_ADMIN_ROLE + * @param verifier Address to revoke verification permissions from */ function removeVerifier(address verifier) external; @@ -223,11 +331,17 @@ interface ISlashingManager { // ====================== /** - * @notice Propose a slash with proof - * @param operator Address to slash - * @param reason Slash reason (must have configured policy) - * @param proof Proof data (if required by policy) - * @return proposalId ID of the created proposal + * @notice Creates a new slash proposal against an operator + * @dev Only callable by SLASHER_ROLE. Validates policy and proof if required + * @param operator Address of the ciphernode operator to slash (must be non-zero) + * @param reason Hash of the slash reason (must have an enabled policy configured) + * @param proof Proof data to be verified (required if policy.requiresProof is true, can be empty otherwise) + * @return proposalId Sequential ID of the created proposal + * Requirements: + * - operator must not be zero address + * - reason must have an enabled policy configured + * - If policy requires proof, proof must be non-empty and pass verification + * - Caller must have SLASHER_ROLE */ function proposeSlash( address operator, @@ -236,8 +350,20 @@ interface ISlashingManager { ) external returns (uint256 proposalId); /** - * @notice Execute a slash proposal - * @param proposalId ID of the proposal to execute + * @notice Executes a slash proposal and applies penalties to the operator + * @dev Only callable by SLASHER_ROLE. Validates execution conditions and applies slashing + * @param proposalId ID of the proposal to execute (must exist and not be already executed) + * Requirements: + * - Proposal must exist and not be already executed + * - For proof-required slashes: proof must be verified + * - For evidence-based slashes: appeal window must have expired + * - If appeal was filed and resolved, appeal must not have been upheld + * - Caller must have SLASHER_ROLE + * Effects: + * - Marks proposal as executed + * - Slashes ticket balance if ticketAmount > 0 + * - Slashes license bond if licenseAmount > 0 + * - Bans node if policy.banNode is true */ function executeSlash(uint256 proposalId) external; @@ -246,17 +372,32 @@ interface ISlashingManager { // ====================== /** - * @notice File an appeal for a slash proposal - * @param proposalId ID of the proposal to appeal - * @param evidence Evidence string supporting the appeal + * @notice Allows an operator to file an appeal against a slash proposal + * @dev Only the operator being slashed can file an appeal, and only within the appeal window + * @param proposalId ID of the proposal to appeal (must exist) + * @param evidence String containing evidence and arguments supporting the appeal + * Requirements: + * - Proposal must exist + * - Caller must be the operator being slashed + * - Current timestamp must be before proposal.executableAt (within appeal window) + * - Proposal must not already have an appeal filed */ function fileAppeal(uint256 proposalId, string calldata evidence) external; /** - * @notice Resolve an appeal (governance only) - * @param proposalId ID of the proposal with appeal - * @param appealUpheld True to approve appeal (cancel slash), false to deny - * @param resolution Resolution explanation string + * @notice Resolves an appeal by accepting or rejecting it + * @dev Only callable by GOVERNANCE_ROLE. If appeal is upheld, the slash cannot be executed + * @param proposalId ID of the proposal with the appeal to resolve (must exist and have an appeal) + * @param appealUpheld True to uphold the appeal (cancel the slash), false to deny the appeal + * (allow slash to proceed) + * @param resolution String explaining the governance decision + * Requirements: + * - Proposal must exist and have an appeal filed + * - Appeal must not already be resolved + * - Caller must have GOVERNANCE_ROLE + * Effects: + * - Marks appeal as resolved + * - Sets appealUpheld flag (true = slash cancelled, false = slash can proceed) */ function resolveAppeal( uint256 proposalId, @@ -269,15 +410,29 @@ interface ISlashingManager { // ====================== /** - * @notice Ban a node (governance only) - * @param node Address to ban - * @param reason Reason for banning + * @notice Bans a node from the network + * @dev Only callable by GOVERNANCE_ROLE. Bans can also occur automatically via executeSlash + * @param node Address of the node to ban (must be non-zero) + * @param reason Hash of the reason for banning + * Requirements: + * - node must not be zero address + * - Caller must have GOVERNANCE_ROLE + * Effects: + * - Sets banned[node] to true + * - Emits NodeBanned event */ function banNode(address node, bytes32 reason) external; /** - * @notice Unban a node (governance only) - * @param node Address to unban + * @notice Removes a ban from a previously banned node + * @dev Only callable by GOVERNANCE_ROLE + * @param node Address of the node to unban (must be non-zero) + * Requirements: + * - node must not be zero address + * - Caller must have GOVERNANCE_ROLE + * Effects: + * - Sets banned[node] to false + * - Emits NodeUnbanned event */ function unbanNode(address node) external; } diff --git a/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol index d9a3cc18d7..12d250dcc1 100644 --- a/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol +++ b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol @@ -6,29 +6,65 @@ pragma solidity >=0.8.27; +/** + * @title ExitQueueLib + * @notice Library for managing time-locked exit queues for tickets and licenses + * @dev Implements a queue system where assets are locked for a delay period before they can be claimed or slashed. + * Assets are organized into tranches based on unlock timestamps, allowing efficient batch operations. + */ library ExitQueueLib { + /** + * @notice Represents a single tranche of assets with a specific unlock timestamp + * @dev Multiple assets queued at the same time are merged into the same tranche for efficiency + * @param unlockTimestamp The timestamp when assets in this tranche become claimable + * @param ticketAmount The amount of tickets in this tranche + * @param licenseAmount The amount of licenses in this tranche + */ struct ExitTranche { uint64 unlockTimestamp; uint256 ticketAmount; uint256 licenseAmount; } + /** + * @notice Tracks total pending amounts for an operator across all tranches + * @param ticketAmount Total pending tickets waiting in the exit queue + * @param licenseAmount Total pending licenses waiting in the exit queue + */ struct PendingAmounts { uint256 ticketAmount; uint256 licenseAmount; } + /** + * @notice Main state structure for the exit queue system + * @dev Contains all per-operator queue data and pending totals + * @param operatorQueues Maps operator addresses to their arrays of exit tranches + * @param queueHeadIndex Maps operator addresses to the current head index (for efficient cleanup) + * @param pendingTotals Maps operator addresses to their total pending amounts + */ struct ExitQueueState { mapping(address operator => ExitTranche[] operatorQueues) operatorQueues; mapping(address operator => uint256 queueHeadIndex) queueHeadIndex; mapping(address operator => PendingAmounts operatorPendings) pendingTotals; } + /** + * @notice Types of assets that can be queued for exit + * @dev Used internally to differentiate between ticket and license operations + */ enum AssetType { Ticket, License } + /** + * @notice Emitted when assets are queued for exit + * @param operator The operator whose assets were queued + * @param ticketAmount The amount of tickets queued + * @param licenseAmount The amount of licenses queued + * @param unlockTimestamp The timestamp when these assets will become claimable + */ event AssetsQueuedForExit( address indexed operator, uint256 ticketAmount, @@ -36,12 +72,25 @@ library ExitQueueLib { uint64 unlockTimestamp ); + /** + * @notice Emitted when assets are claimed from the exit queue + * @param operator The operator who claimed the assets + * @param ticketAmount The amount of tickets claimed + * @param licenseAmount The amount of licenses claimed + */ event AssetsClaimed( address indexed operator, uint256 ticketAmount, uint256 licenseAmount ); + /** + * @notice Emitted when pending assets are slashed + * @param operator The operator whose assets were slashed + * @param ticketAmount The amount of tickets slashed + * @param licenseAmount The amount of licenses slashed + * @param includedLockedAssets Whether locked (not yet unlocked) assets were included in the slash + */ event PendingAssetsSlashed( address indexed operator, uint256 ticketAmount, @@ -49,10 +98,25 @@ library ExitQueueLib { bool includedLockedAssets ); + /// @notice Thrown when attempting to queue zero amount of both asset types error ZeroAmountNotAllowed(); + + /// @notice Thrown when timestamp calculation would overflow uint64 error TimestampOverflow(); + + /// @notice Thrown when accessing an invalid queue index error IndexOutOfBounds(); + /** + * @notice Queues both tickets and licenses for exit with a time delay + * @dev Assets are added to the operator's queue and will be claimable after exitDelaySeconds. + * If a tranche with the same unlock timestamp already exists, amounts are merged into it. + * @param state The exit queue state storage + * @param operator The operator whose assets are being queued + * @param exitDelaySeconds The number of seconds until assets become claimable + * @param ticketAmount The amount of tickets to queue (can be 0) + * @param licenseAmount The amount of licenses to queue (can be 0) + */ function queueAssetsForExit( ExitQueueState storage state, address operator, @@ -108,6 +172,14 @@ library ExitQueueLib { ); } + /** + * @notice Queues only tickets for exit with a time delay + * @dev Convenience function that calls queueAssetsForExit with licenseAmount = 0 + * @param state The exit queue state storage + * @param operator The operator whose tickets are being queued + * @param exitDelaySeconds The number of seconds until tickets become claimable + * @param ticketAmount The amount of tickets to queue + */ function queueTicketsForExit( ExitQueueState storage state, address operator, @@ -117,6 +189,14 @@ library ExitQueueLib { queueAssetsForExit(state, operator, exitDelaySeconds, ticketAmount, 0); } + /** + * @notice Queues only licenses for exit with a time delay + * @dev Convenience function that calls queueAssetsForExit with ticketAmount = 0 + * @param state The exit queue state storage + * @param operator The operator whose licenses are being queued + * @param exitDelaySeconds The number of seconds until licenses become claimable + * @param licenseAmount The amount of licenses to queue + */ function queueLicensesForExit( ExitQueueState storage state, address operator, @@ -126,6 +206,14 @@ library ExitQueueLib { queueAssetsForExit(state, operator, exitDelaySeconds, 0, licenseAmount); } + /** + * @notice Gets the total pending amounts for an operator across all tranches + * @dev Returns both locked (not yet claimable) and unlocked (claimable) amounts + * @param state The exit queue state storage + * @param operator The operator to query + * @return ticketAmount Total pending tickets in the exit queue + * @return licenseAmount Total pending licenses in the exit queue + */ function getPendingAmounts( ExitQueueState storage state, address operator @@ -134,6 +222,14 @@ library ExitQueueLib { return (pending.ticketAmount, pending.licenseAmount); } + /** + * @notice Previews the amounts that can be claimed at the current block timestamp + * @dev Iterates through tranches and sums up amounts where unlock timestamp has passed + * @param state The exit queue state storage + * @param operator The operator to query + * @return ticketAmount Total claimable tickets at current timestamp + * @return licenseAmount Total claimable licenses at current timestamp + */ function previewClaimableAmounts( ExitQueueState storage state, address operator @@ -153,6 +249,17 @@ library ExitQueueLib { } } + /** + * @notice Claims unlocked assets from the exit queue + * @dev Only processes tranches where unlock timestamp has passed. Updates pending totals + * and cleans up empty tranches. + * @param state The exit queue state storage + * @param operator The operator claiming assets + * @param maxTicketAmount Maximum tickets to claim (actual claimed may be less if queue has fewer) + * @param maxLicenseAmount Maximum licenses to claim (actual claimed may be less if queue has fewer) + * @return ticketsClaimed Actual amount of tickets claimed + * @return licensesClaimed Actual amount of licenses claimed + */ function claimAssets( ExitQueueState storage state, address operator, @@ -191,6 +298,18 @@ library ExitQueueLib { } } + /** + * @notice Slashes pending assets from the exit queue + * @dev Can optionally include locked (not yet unlocked) assets. Updates pending totals + * and cleans up empty tranches. + * @param state The exit queue state storage + * @param operator The operator whose assets are being slashed + * @param ticketAmountToSlash Maximum tickets to slash + * @param licenseAmountToSlash Maximum licenses to slash + * @param includeLockedAssets If true, slashes locked assets; if false, only slashes unlocked assets + * @return ticketsSlashed Actual amount of tickets slashed + * @return licensesSlashed Actual amount of licenses slashed + */ function slashPendingAssets( ExitQueueState storage state, address operator, @@ -235,6 +354,15 @@ library ExitQueueLib { } } + /** + * @notice Updates the pending totals for an operator + * @dev Internal helper to increase or decrease pending amounts. Uses bitwise OR for efficient zero check. + * @param state The exit queue state storage + * @param operator The operator whose pending totals are being updated + * @param ticketAmountDelta The change in ticket amount + * @param licenseAmountDelta The change in license amount + * @param isIncrease If true, increases totals; if false, decreases totals + */ function _updatePendingTotals( ExitQueueState storage state, address operator, @@ -259,6 +387,13 @@ library ExitQueueLib { } } + /** + * @notice Cleans up empty tranches from the head of the queue + * @dev Advances the queue head index past all tranches with zero tickets and licenses. + * This prevents the queue from growing unbounded and reduces gas costs for future operations. + * @param state The exit queue state storage + * @param operator The operator whose queue is being cleaned up + */ function _cleanupEmptyTranches( ExitQueueState storage state, address operator @@ -278,6 +413,17 @@ library ExitQueueLib { state.queueHeadIndex[operator] = currentIndex; } + /** + * @notice Takes assets from the queue, either for claiming or slashing + * @dev Iterates through tranches from head to tail, taking assets up to wantedAmount. + * Respects unlock timestamps unless includeLockedAssets is true. + * @param state The exit queue state storage + * @param operator The operator whose assets are being taken + * @param wantedAmount The maximum amount to take + * @param assetType Whether to take tickets or licenses + * @param includeLockedAssets If true, takes locked assets; if false, only takes unlocked assets + * @return takenAmount The actual amount taken (may be less than wantedAmount if queue has fewer assets) + */ function _takeAssetsFromQueue( ExitQueueState storage state, address operator, diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index bb51f6f6ca..f40d7839ae 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -23,7 +23,8 @@ import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; /** * @title BondingRegistry - * @notice Main registry for operator balance and license bonds + * @notice Implementation of the bonding registry managing operator ticket balances and license bonds + * @dev Handles deposits, withdrawals, slashing, exits, and integrates with registry and slashing manager */ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { using SafeERC20 for IERC20; @@ -33,45 +34,62 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Constants // ====================== + /// @dev Reason code for ticket balance deposits bytes32 private constant REASON_DEPOSIT = bytes32("DEPOSIT"); + + /// @dev Reason code for ticket balance withdrawals bytes32 private constant REASON_WITHDRAW = bytes32("WITHDRAW"); + + /// @dev Reason code for license bond operations bytes32 private constant REASON_BOND = bytes32("BOND"); + + /// @dev Reason code for license unbond operations bytes32 private constant REASON_UNBOND = bytes32("UNBOND"); // ====================== // Storage // ====================== - /// @notice ticket token (ETK (Underlying USDC)) + /// @notice Ticket token (ETK with underlying USDC) used for collateral EnclaveTicketToken public ticketToken; - /// @notice License token (ENCL) + /// @notice License token (ENCL) required for operator registration IERC20 public licenseToken; - /// @notice Registry contract for committee membership checks + /// @notice Registry contract for managing committee membership ICiphernodeRegistry public registry; - /// @notice Authorized slashing manager + /// @notice Address authorized to perform slashing operations address public slashingManager; - /// @notice Authorized reward distributor + /// @notice Address authorized to distribute rewards to operators address public rewardDistributor; - /// @notice Treasury address for slashed funds + /// @notice Treasury address that receives slashed funds address public slashedFundsTreasury; - // Configuration + /// @notice Price per ticket in ticket token units uint256 public ticketPrice; + + /// @notice Minimum license bond required for initial registration uint256 public licenseRequiredBond; + + /// @notice Minimum number of tickets required to maintain active status uint256 public minTicketBalance; + + /// @notice Time delay in seconds before exits can be claimed uint64 public exitDelay; - // TODO: There's a scenario where a node can bond the required license bond, - // then register and immediately withdraw 20% of the license bond. And still be a part of - // the protocol. Is this correct? - uint256 public licenseActiveBps = 8_000; // 80% + /// @notice Percentage (in basis points) of license bond that must remain bonded to stay active + /// @dev Default 8000 = 80%. Allows operators to unbond up to 20% while remaining active + uint256 public licenseActiveBps = 8_000; - // Operator data structure + /// @notice Operator state data structure + /// @param licenseBond Amount of license tokens currently bonded + /// @param exitUnlocksAt Timestamp when pending exit can be claimed + /// @param registered Whether operator is registered in the protocol + /// @param exitRequested Whether operator has requested to exit + /// @param active Whether operator meets all requirements for active status struct Operator { uint256 licenseBond; uint64 exitUnlocksAt; @@ -80,27 +98,34 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { bool active; } - // Operator data + /// @notice Maps operator address to their state data mapping(address operator => Operator data) internal operators; - // Total slashed funds available for treasury withdrawal + /// @notice Total slashed ticket balance available for treasury withdrawal uint256 public slashedTicketBalance; + + /// @notice Total slashed license bond available for treasury withdrawal uint256 public slashedLicenseBond; // ====================== // Exit Queue library state // ====================== + + /// @dev Internal state for managing exit queue of tickets and licenses ExitQueueLib.ExitQueueState private _exits; // ====================== // Modifiers // ====================== + /// @dev Restricts function access to only the slashing manager modifier onlySlashingManager() { if (msg.sender != slashingManager) revert Unauthorized(); _; } + /// @dev Reverts if operator has an exit in progress that hasn't unlocked yet + /// @param operator Address of the operator to check modifier noExitInProgress(address operator) { Operator storage op = operators[operator]; if (op.exitRequested && block.timestamp < op.exitUnlocksAt) @@ -114,6 +139,16 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Constructor that initializes the bonding registry + /// @param _owner Address that will own the contract + /// @param _ticketToken Ticket token contract for collateral + /// @param _licenseToken License token contract for bonding + /// @param _registry Ciphernode registry contract + /// @param _slashedFundsTreasury Address to receive slashed funds + /// @param _ticketPrice Initial price per ticket + /// @param _licenseRequiredBond Initial required license bond for registration + /// @param _minTicketBalance Initial minimum ticket balance for activation + /// @param _exitDelay Initial exit delay period in seconds constructor( address _owner, EnclaveTicketToken _ticketToken, @@ -138,6 +173,17 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { ); } + /// @notice Initializes the bonding registry contract + /// @dev Can only be called once due to initializer modifier + /// @param _owner Address that will own the contract + /// @param _ticketToken Ticket token contract for collateral + /// @param _licenseToken License token contract for bonding + /// @param _registry Ciphernode registry contract + /// @param _slashedFundsTreasury Address to receive slashed funds + /// @param _ticketPrice Initial price per ticket + /// @param _licenseRequiredBond Initial required license bond for registration + /// @param _minTicketBalance Initial minimum ticket balance for activation + /// @param _exitDelay Initial exit delay period in seconds function initialize( address _owner, EnclaveTicketToken _ticketToken, @@ -165,16 +211,19 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // View Functions // ====================== + /// @inheritdoc IBondingRegistry function getTicketBalance( address operator ) external view returns (uint256) { return ticketToken.balanceOf(operator); } + /// @inheritdoc IBondingRegistry function getLicenseBond(address operator) external view returns (uint256) { return operators[operator].licenseBond; } + /// @inheritdoc IBondingRegistry function availableTickets( address operator ) external view returns (uint256) { @@ -182,6 +231,11 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { return ticketToken.balanceOf(operator) / ticketPrice; } + /// @notice Get operator's ticket balance at a specific block + /// @dev Uses checkpoint mechanism from ticket token + /// @param operator Address of the operator + /// @param blockNumber Block number to query + /// @return Ticket balance at the specified block function getTicketBalanceAtBlock( address operator, uint256 blockNumber @@ -189,30 +243,42 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { return ticketToken.getPastVotes(operator, blockNumber); } + /// @notice Get operator's total pending exit amounts + /// @param operator Address of the operator + /// @return ticket Total pending ticket balance in exit queue + /// @return license Total pending license bond in exit queue function pendingExits( address operator ) external view returns (uint256 ticket, uint256 license) { return _exits.getPendingAmounts(operator); } + /// @notice Preview how much an operator can currently claim + /// @param operator Address of the operator + /// @return ticket Claimable ticket balance + /// @return license Claimable license bond function previewClaimable( address operator ) external view returns (uint256 ticket, uint256 license) { return _exits.previewClaimableAmounts(operator); } + /// @inheritdoc IBondingRegistry function isLicensed(address operator) external view returns (bool) { return operators[operator].licenseBond >= _minLicenseBond(); } + /// @inheritdoc IBondingRegistry function isRegistered(address operator) external view returns (bool) { return operators[operator].registered; } + /// @inheritdoc IBondingRegistry function isActive(address operator) external view returns (bool) { return operators[operator].active; } + /// @inheritdoc IBondingRegistry function hasExitInProgress(address operator) external view returns (bool) { Operator storage op = operators[operator]; return op.exitRequested && block.timestamp < op.exitUnlocksAt; @@ -222,6 +288,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Operator Functions // ====================== + /// @inheritdoc IBondingRegistry function registerOperator() external noExitInProgress(msg.sender) { // Clear previous exit request if (operators[msg.sender].exitRequested) { @@ -249,6 +316,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(msg.sender); } + /// @inheritdoc IBondingRegistry function deregisterOperator( uint256[] calldata siblingNodes ) external noExitInProgress(msg.sender) { @@ -298,6 +366,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(msg.sender); } + /// @inheritdoc IBondingRegistry function addTicketBalance( uint256 amount ) external noExitInProgress(msg.sender) { @@ -316,6 +385,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(msg.sender); } + /// @inheritdoc IBondingRegistry function removeTicketBalance( uint256 amount ) external noExitInProgress(msg.sender) { @@ -339,6 +409,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(msg.sender); } + /// @inheritdoc IBondingRegistry function bondLicense(uint256 amount) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); @@ -359,6 +430,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(msg.sender); } + /// @inheritdoc IBondingRegistry function unbondLicense( uint256 amount ) external noExitInProgress(msg.sender) { @@ -385,6 +457,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Claim Functions // ====================== + /// @inheritdoc IBondingRegistry function claimExits( uint256 maxTicketAmount, uint256 maxLicenseAmount @@ -405,6 +478,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Slashing Functions // ====================== + /// @inheritdoc IBondingRegistry function slashTicketBalance( address operator, uint256 requestedSlashAmount, @@ -456,6 +530,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(operator); } + /// @inheritdoc IBondingRegistry function slashLicenseBond( address operator, uint256 requestedSlashAmount, @@ -509,6 +584,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Reward Distribution Functions // ====================== + /// @inheritdoc IBondingRegistry function distributeRewards( IERC20 rewardToken, address[] calldata recipients, @@ -532,6 +608,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Admin Functions // ====================== + /// @inheritdoc IBondingRegistry function setTicketPrice(uint256 newTicketPrice) public onlyOwner { require(newTicketPrice != 0, InvalidConfiguration()); @@ -541,6 +618,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { emit ConfigurationUpdated("ticketPrice", oldValue, newTicketPrice); } + /// @inheritdoc IBondingRegistry function setLicenseRequiredBond( uint256 newLicenseRequiredBond ) public onlyOwner { @@ -556,6 +634,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { ); } + /// @inheritdoc IBondingRegistry function setLicenseActiveBps(uint256 newBps) public onlyOwner { require(newBps > 0 && newBps <= 10_000, InvalidConfiguration()); @@ -565,6 +644,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { emit ConfigurationUpdated("licenseActiveBps", oldValue, newBps); } + /// @inheritdoc IBondingRegistry function setMinTicketBalance(uint256 newMinTicketBalance) public onlyOwner { uint256 oldValue = minTicketBalance; minTicketBalance = newMinTicketBalance; @@ -576,6 +656,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { ); } + /// @inheritdoc IBondingRegistry function setExitDelay(uint64 newExitDelay) public onlyOwner { uint256 oldValue = uint256(exitDelay); exitDelay = newExitDelay; @@ -583,6 +664,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { emit ConfigurationUpdated("exitDelay", oldValue, uint256(newExitDelay)); } + /// @inheritdoc IBondingRegistry function setSlashedFundsTreasury( address newSlashedFundsTreasury ) public onlyOwner { @@ -590,30 +672,38 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { slashedFundsTreasury = newSlashedFundsTreasury; } + /// @inheritdoc IBondingRegistry function setTicketToken( EnclaveTicketToken newTicketToken ) public onlyOwner { ticketToken = newTicketToken; } + /// @inheritdoc IBondingRegistry function setLicenseToken(IERC20 newLicenseToken) public onlyOwner { licenseToken = newLicenseToken; } + /// @inheritdoc IBondingRegistry function setRegistry(ICiphernodeRegistry newRegistry) public onlyOwner { registry = newRegistry; } + /// @inheritdoc IBondingRegistry function setSlashingManager(address newSlashingManager) public onlyOwner { slashingManager = newSlashingManager; } + /// @notice Sets the reward distributor address + /// @dev Only callable by owner + /// @param newRewardDistributor Address of the reward distributor function setRewardDistributor( address newRewardDistributor ) public onlyOwner { rewardDistributor = newRewardDistributor; } + /// @inheritdoc IBondingRegistry function withdrawSlashedFunds( uint256 ticketAmount, uint256 licenseAmount @@ -642,6 +732,9 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { // Internal Functions // ====================== + /// @dev Updates operator's active status based on current conditions + /// @dev Operator is active if: registered, has minimum license bond, and has minimum tickets + /// @param operator Address of the operator to update function _updateOperatorStatus(address operator) internal { Operator storage op = operators[operator]; bool newActiveStatus = op.registered && @@ -656,6 +749,8 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { } } + /// @dev Calculates the minimum license bond required to maintain active status + /// @return Minimum license bond (licenseRequiredBond * licenseActiveBps / 10000) function _minLicenseBond() internal view returns (uint256) { return (licenseRequiredBond * licenseActiveBps) / 10_000; } diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index a1181872df..575e5e8e3d 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -16,6 +16,11 @@ import { LeanIMTData } from "@zk-kit/lean-imt.sol/InternalLeanIMT.sol"; +/** + * @title CiphernodeRegistryOwnable + * @notice Ownable implementation of the ciphernode registry with IMT-based membership tracking + * @dev Manages ciphernode registration, committee selection, and integrates with bonding registry + */ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { using InternalLeanIMT for LeanIMTData; @@ -25,6 +30,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Emitted when the bonding registry address is set + /// @param bondingRegistry Address of the bonding registry contract event BondingRegistrySet(address indexed bondingRegistry); //////////////////////////////////////////////////////////// @@ -33,13 +40,25 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Address of the Enclave contract authorized to request committees address public enclave; + + /// @notice Address of the bonding registry for checking node eligibility address public bondingRegistry; + + /// @notice Current number of registered ciphernodes uint256 public numCiphernodes; + + /// @notice Incremental Merkle Tree (IMT) containing all registered ciphernodes LeanIMTData public ciphernodes; + /// @notice Maps E3 ID to its associated registry filter contract mapping(uint256 e3Id => IRegistryFilter filter) public registryFilters; + + /// @notice Maps E3 ID to the IMT root at the time of committee request mapping(uint256 e3Id => uint256 root) public roots; + + /// @notice Maps E3 ID to the hash of the committee's public key mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; //////////////////////////////////////////////////////////// @@ -48,17 +67,42 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Committee has already been requested for this E3 error CommitteeAlreadyRequested(); + + /// @notice Committee has already been published for this E3 error CommitteeAlreadyPublished(); + + /// @notice Caller is not the authorized filter for this E3 error OnlyFilter(); + + /// @notice Committee has not been published yet for this E3 error CommitteeNotPublished(); + + /// @notice Ciphernode is not enabled in the registry + /// @param node Address of the ciphernode error CiphernodeNotEnabled(address node); + + /// @notice Caller is not the Enclave contract error OnlyEnclave(); + + /// @notice Caller is not the bonding registry error OnlyBondingRegistry(); + + /// @notice Caller is neither owner nor bonding registry error NotOwnerOrBondingRegistry(); + + /// @notice Node is not bonded + /// @param node Address of the node error NodeNotBonded(address node); + + /// @notice Address cannot be zero error ZeroAddress(); + + /// @notice Bonding registry has not been set error BondingRegistryNotSet(); + + /// @notice Caller is not authorized error Unauthorized(); //////////////////////////////////////////////////////////// @@ -67,16 +111,19 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @dev Restricts function access to only the Enclave contract modifier onlyEnclave() { require(msg.sender == enclave, OnlyEnclave()); _; } + /// @dev Restricts function access to only the bonding registry modifier onlyBondingRegistry() { require(msg.sender == bondingRegistry, OnlyBondingRegistry()); _; } + /// @dev Restricts function access to owner or bonding registry modifier onlyOwnerOrBondingVault() { require( msg.sender == owner() || msg.sender == bondingRegistry, @@ -91,10 +138,17 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Constructor that initializes the registry with owner and enclave + /// @param _owner Address that will own the contract + /// @param _enclave Address of the Enclave contract constructor(address _owner, address _enclave) { initialize(_owner, _enclave); } + /// @notice Initializes the registry contract + /// @dev Can only be called once due to initializer modifier + /// @param _owner Address that will own the contract + /// @param _enclave Address of the Enclave contract function initialize(address _owner, address _enclave) public initializer { require(_owner != address(0), ZeroAddress()); require(_enclave != address(0), ZeroAddress()); @@ -110,6 +164,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc ICiphernodeRegistry function requestCommittee( uint256 e3Id, address filter, @@ -127,6 +182,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { success = true; } + /// @inheritdoc ICiphernodeRegistry function publishCommittee( uint256 e3Id, bytes calldata, @@ -139,6 +195,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit CommitteePublished(e3Id, publicKey); } + /// @inheritdoc ICiphernodeRegistry function addCiphernode(address node) external onlyOwnerOrBondingVault { if (isEnabled(node)) { return; @@ -155,6 +212,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { ); } + /// @inheritdoc ICiphernodeRegistry function removeCiphernode( address node, uint256[] calldata siblingNodes @@ -174,12 +232,18 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Sets the Enclave contract address + /// @dev Only callable by owner + /// @param _enclave Address of the Enclave contract function setEnclave(address _enclave) public onlyOwner { require(_enclave != address(0), ZeroAddress()); enclave = _enclave; emit EnclaveSet(_enclave); } + /// @notice Sets the bonding registry contract address + /// @dev Only callable by owner + /// @param _bondingRegistry Address of the bonding registry contract function setBondingRegistry(address _bondingRegistry) public onlyOwner { require(_bondingRegistry != address(0), ZeroAddress()); bondingRegistry = _bondingRegistry; @@ -192,6 +256,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc ICiphernodeRegistry function committeePublicKey( uint256 e3Id ) external view returns (bytes32 publicKeyHash) { @@ -199,6 +264,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(publicKeyHash != bytes32(0), CommitteeNotPublished()); } + /// @inheritdoc ICiphernodeRegistry function isCiphernodeEligible(address node) external view returns (bool) { if (!isEnabled(node)) return false; @@ -206,22 +272,30 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { return IBondingRegistry(bondingRegistry).isActive(node); } + /// @inheritdoc ICiphernodeRegistry function isEnabled(address node) public view returns (bool) { return ciphernodes._has(uint160(node)); } + /// @notice Returns the current root of the ciphernode IMT + /// @return Current IMT root function root() public view returns (uint256) { return (ciphernodes._root()); } + /// @notice Returns the IMT root at the time a committee was requested + /// @param e3Id ID of the E3 + /// @return IMT root at time of committee request function rootAt(uint256 e3Id) public view returns (uint256) { return roots[e3Id]; } + /// @inheritdoc ICiphernodeRegistry function getFilter(uint256 e3Id) public view returns (address filter) { return address(registryFilters[e3Id]); } + /// @inheritdoc ICiphernodeRegistry function getCommittee( uint256 e3Id ) public view returns (IRegistryFilter.Committee memory committee) { @@ -229,10 +303,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(committee.nodes.length > 0, CommitteeNotPublished()); } + /// @notice Returns the current size of the ciphernode IMT + /// @return Size of the IMT function treeSize() public view returns (uint256) { return ciphernodes.size; } + /// @notice Returns the address of the bonding registry + /// @return Address of the bonding registry contract function getBondingRegistry() external view returns (address) { return bondingRegistry; } diff --git a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol index 2e430da8f9..c8d6997bca 100644 --- a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol +++ b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol @@ -11,6 +11,11 @@ import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +/** + * @title NaiveRegistryFilter + * @notice Simple registry filter implementation for committee selection + * @dev Allows owner-controlled committee publication for E3 computations + */ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { //////////////////////////////////////////////////////////// // // @@ -18,8 +23,10 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Address of the ciphernode registry contract address public registry; + /// @notice Maps E3 ID to its committee data mapping(uint256 e3 => IRegistryFilter.Committee committee) public committees; @@ -29,11 +36,23 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Committee already exists for this E3 error CommitteeAlreadyExists(); + + /// @notice Committee has already been published for this E3 error CommitteeAlreadyPublished(); + + /// @notice Committee does not exist for this E3 error CommitteeDoesNotExist(); + + /// @notice Committee has not been published yet error CommitteeNotPublished(); + + /// @notice Ciphernode is not enabled in the registry + /// @param ciphernode Address of the ciphernode error CiphernodeNotEnabled(address ciphernode); + + /// @notice Caller is not the registry contract error OnlyRegistry(); //////////////////////////////////////////////////////////// @@ -42,11 +61,13 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @dev Restricts function access to only the registry contract modifier onlyRegistry() { require(msg.sender == registry, OnlyRegistry()); _; } + /// @dev Restricts function access to owner or eligible ciphernode modifier onlyOwnerOrCiphernode() { require( msg.sender == owner() || @@ -62,10 +83,17 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Constructor that initializes the filter with owner and registry + /// @param _owner Address that will own the contract + /// @param _registry Address of the ciphernode registry constructor(address _owner, address _registry) { initialize(_owner, _registry); } + /// @notice Initializes the filter contract + /// @dev Can only be called once due to initializer modifier + /// @param _owner Address that will own the contract + /// @param _registry Address of the ciphernode registry function initialize(address _owner, address _registry) public initializer { __Ownable_init(msg.sender); setRegistry(_registry); @@ -78,6 +106,7 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc IRegistryFilter function requestCommittee( uint256 e3Id, uint32[2] calldata threshold @@ -87,6 +116,11 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { success = true; } + /// @notice Publishes a committee for an E3 computation + /// @dev Only callable by owner. Stores committee data and notifies the registry + /// @param e3Id ID of the E3 computation + /// @param nodes Array of ciphernode addresses selected for the committee + /// @param publicKey Aggregated public key of the committee function publishCommittee( uint256 e3Id, address[] calldata nodes, @@ -109,6 +143,9 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @notice Sets the registry contract address + /// @dev Only callable by owner + /// @param _registry Address of the ciphernode registry contract function setRegistry(address _registry) public onlyOwner { registry = _registry; } @@ -119,6 +156,7 @@ contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc IRegistryFilter function getCommittee( uint256 e3Id ) external view returns (IRegistryFilter.Committee memory) { diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index b68558cc12..4aa70b3a3b 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -15,51 +15,67 @@ import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; /** * @title SlashingManager - * @notice Manages slashing proposals, appeals, and execution for the bonding system - * @dev UUPS upgradeable contract with role-based access control + * @notice Implementation of slashing management with proposal, appeal, and execution workflows + * @dev Role-based access control for slashers, verifiers, and governance with configurable slash policies */ contract SlashingManager is ISlashingManager, AccessControl { // ====================== // Constants & Roles // ====================== + /// @notice Role identifier for accounts authorized to propose and execute slashes bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); + + /// @notice Role identifier for accounts authorized to verify cryptographic proofs in slash proposals bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE"); + + /// @notice Role identifier for governance accounts that can configure policies, resolve appeals, and manage bans bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); // ====================== // Storage // ====================== - /// @notice Bonding registry contract + /// @notice Reference to the bonding registry contract where slash penalties are executed + /// @dev Used to call slashTicketBalance() and slashLicenseBond() when executing slashes IBondingRegistry public bondingRegistry; - /// @notice Slash policies by reason hash + /// @notice Mapping from slash reason hash to its configured policy + /// @dev Stores penalty amounts, proof requirements, and appeal settings for each slash type mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; - /// @notice All slash proposals + /// @notice Internal storage for all slash proposals indexed by proposal ID + /// @dev Sequentially indexed starting from 0, accessed via getSlashProposal() mapping(uint256 proposalId => SlashProposal proposal) internal _proposals; - /// @notice Total number of proposals created + /// @notice Counter for total number of slash proposals ever created + /// @dev Also serves as the next proposal ID to be assigned uint256 public totalProposals; - /// @notice Banned nodes + /// @notice Mapping tracking which nodes are currently banned from the network + /// @dev Set to true when a node is banned (either via executeSlash or banNode), false when unbanned mapping(address node => bool banned) public banned; // ====================== // Modifiers // ====================== + /// @notice Restricts function access to accounts with SLASHER_ROLE + /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlySlasher() { if (!hasRole(SLASHER_ROLE, msg.sender)) revert Unauthorized(); _; } + /// @notice Restricts function access to accounts with VERIFIER_ROLE + /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlyVerifier() { if (!hasRole(VERIFIER_ROLE, msg.sender)) revert Unauthorized(); _; } + /// @notice Restricts function access to accounts with GOVERNANCE_ROLE + /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlyGovernance() { if (!hasRole(GOVERNANCE_ROLE, msg.sender)) revert Unauthorized(); _; @@ -69,6 +85,15 @@ contract SlashingManager is ISlashingManager, AccessControl { // Constructor // ====================== + /** + * @notice Initializes the SlashingManager contract with admin and bonding registry + * @dev Sets up initial role assignments and bonding registry reference + * @param admin Address to receive DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE + * @param _bondingRegistry Address of the bonding registry contract for executing slashes + * Requirements: + * - admin must not be zero address + * - _bondingRegistry must not be zero address + */ constructor(address admin, address _bondingRegistry) { require(admin != address(0), ZeroAddress()); require(_bondingRegistry != address(0), ZeroAddress()); @@ -83,12 +108,14 @@ contract SlashingManager is ISlashingManager, AccessControl { // View Functions // ====================== + /// @inheritdoc ISlashingManager function getSlashPolicy( bytes32 reason ) external view returns (SlashPolicy memory) { return slashPolicies[reason]; } + /// @inheritdoc ISlashingManager function getSlashProposal( uint256 proposalId ) external view returns (SlashProposal memory) { @@ -96,6 +123,7 @@ contract SlashingManager is ISlashingManager, AccessControl { return _proposals[proposalId]; } + /// @inheritdoc ISlashingManager function isBanned(address node) external view returns (bool) { return banned[node]; } @@ -104,6 +132,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // Admin Functions // ====================== + /// @inheritdoc ISlashingManager function setSlashPolicy( bytes32 reason, SlashPolicy calldata policy @@ -127,6 +156,7 @@ contract SlashingManager is ISlashingManager, AccessControl { emit SlashPolicyUpdated(reason, policy); } + /// @inheritdoc ISlashingManager function setBondingRegistry( address newBondingRegistry ) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -134,17 +164,20 @@ contract SlashingManager is ISlashingManager, AccessControl { bondingRegistry = IBondingRegistry(newBondingRegistry); } + /// @inheritdoc ISlashingManager function addSlasher(address slasher) external onlyRole(DEFAULT_ADMIN_ROLE) { require(slasher != address(0), ZeroAddress()); _grantRole(SLASHER_ROLE, slasher); } + /// @inheritdoc ISlashingManager function removeSlasher( address slasher ) external onlyRole(DEFAULT_ADMIN_ROLE) { _revokeRole(SLASHER_ROLE, slasher); } + /// @inheritdoc ISlashingManager function addVerifier( address verifier ) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -152,6 +185,7 @@ contract SlashingManager is ISlashingManager, AccessControl { _grantRole(VERIFIER_ROLE, verifier); } + /// @inheritdoc ISlashingManager function removeVerifier( address verifier ) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -162,6 +196,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // Slashing Functions // ====================== + /// @inheritdoc ISlashingManager function proposeSlash( address operator, bytes32 reason, @@ -214,6 +249,7 @@ contract SlashingManager is ISlashingManager, AccessControl { totalProposals = proposalId + 1; } + /// @inheritdoc ISlashingManager function executeSlash(uint256 proposalId) external onlySlasher { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = _proposals[proposalId]; @@ -271,6 +307,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // Appeal Functions // ====================== + /// @inheritdoc ISlashingManager function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = _proposals[proposalId]; @@ -287,6 +324,7 @@ contract SlashingManager is ISlashingManager, AccessControl { emit AppealFiled(proposalId, p.operator, p.reason, evidence); } + /// @inheritdoc ISlashingManager function resolveAppeal( uint256 proposalId, bool appealUpheld, @@ -314,6 +352,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // Ban Management // ====================== + /// @inheritdoc ISlashingManager function banNode(address node, bytes32 reason) external onlyGovernance { require(node != address(0), ZeroAddress()); @@ -321,6 +360,7 @@ contract SlashingManager is ISlashingManager, AccessControl { emit NodeBanned(node, reason, msg.sender); } + /// @inheritdoc ISlashingManager function unbanNode(address node) external onlyGovernance { require(node != address(0), ZeroAddress()); diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index b07898c852..c31d567d85 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -21,23 +21,10 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { } } - // solhint-disable-next-line no-empty-blocks - function addCiphernode(address) external {} - function isEnabled(address) external pure returns (bool) { return true; } - // solhint-disable-next-line no-empty-blocks - function removeCiphernode(address, uint256[] calldata) external {} - - // solhint-disable no-empty-blocks - function publishCommittee( - uint256, - bytes calldata, - bytes calldata - ) external {} // solhint-disable-line no-empty-blocks - function committeePublicKey(uint256 e3Id) external pure returns (bytes32) { if (e3Id == type(uint256).max) { return bytes32(0); @@ -50,6 +37,18 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { return false; } + // solhint-disable-next-line no-empty-blocks + function addCiphernode(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function removeCiphernode(address, uint256[] calldata) external pure {} + + function publishCommittee( + uint256, + bytes calldata, + bytes calldata + ) external pure {} // solhint-disable-line no-empty-blocks + function getFilter(uint256) external pure returns (address) { return address(0); } @@ -57,13 +56,32 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { function getCommittee( uint256 ) external pure returns (IRegistryFilter.Committee memory) { - return - IRegistryFilter.Committee( - new address[](0), - [uint32(0), uint32(0)], - bytes32(0) - ); + address[] memory nodes = new address[](0); + uint32[2] memory threshold = [uint32(0), uint32(0)]; + return IRegistryFilter.Committee(nodes, threshold, bytes32(0)); + } + + function root() external pure returns (uint256) { + return 0; + } + + function rootAt(uint256) external pure returns (uint256) { + return 0; + } + + function treeSize() external pure returns (uint256) { + return 0; } + + function getBondingRegistry() external pure returns (address) { + return address(0); + } + + // solhint-disable-next-line no-empty-blocks + function setEnclave(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setBondingRegistry(address) external pure {} } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { @@ -79,22 +97,29 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { } } - // solhint-disable-next-line no-empty-blocks - function addCiphernode(address) external {} - function isEnabled(address) external pure returns (bool) { return true; } + function committeePublicKey(uint256) external pure returns (bytes32) { + return bytes32(0); + } + + function isCiphernodeEligible(address) external pure returns (bool) { + return false; + } + + // solhint-disable-next-line no-empty-blocks + function addCiphernode(address) external pure {} + // solhint-disable-next-line no-empty-blocks - function removeCiphernode(address, uint256[] calldata) external {} + function removeCiphernode(address, uint256[] calldata) external pure {} - // solhint-disable no-empty-blocks function publishCommittee( uint256, bytes calldata, bytes calldata - ) external {} // solhint-disable-line no-empty-blocks + ) external pure {} // solhint-disable-line no-empty-blocks function getFilter(uint256) external pure returns (address) { return address(0); @@ -103,19 +128,30 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function getCommittee( uint256 ) external pure returns (IRegistryFilter.Committee memory) { - return - IRegistryFilter.Committee( - new address[](0), - [uint32(0), uint32(0)], - bytes32(0) - ); + address[] memory nodes = new address[](0); + uint32[2] memory threshold = [uint32(0), uint32(0)]; + return IRegistryFilter.Committee(nodes, threshold, bytes32(0)); } - function committeePublicKey(uint256) external pure returns (bytes32) { - return bytes32(0); + function root() external pure returns (uint256) { + return 0; } - function isCiphernodeEligible(address) external pure returns (bool) { - return false; + function rootAt(uint256) external pure returns (uint256) { + return 0; + } + + function treeSize() external pure returns (uint256) { + return 0; } + + function getBondingRegistry() external pure returns (address) { + return address(0); + } + + // solhint-disable-next-line no-empty-blocks + function setEnclave(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setBondingRegistry(address) external pure {} } diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol index 12ce059189..9d932d6a66 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol @@ -24,7 +24,12 @@ import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; /** * @title EnclaveTicketToken (ETK) - * @notice Non-transferable non-delegatable ERC20Votes wrapper over a Stable token (USDC, DAI etc.) for operator staking + * @notice Non-transferable, non-delegatable ERC20Votes wrapper over a stablecoin for operator staking + * @dev ERC20 wrapper token that represents staked stablecoins (e.g., USDC, DAI) used for operator + * bonding in the Enclave protocol. Implements voting power tracking through ERC20Votes but + * prevents transfers between users and manual delegation. Deposits automatically delegate to + * self to enable voting power tracking. Only the designated registry contract can mint + * (deposit) and burn (withdraw) tokens. */ contract EnclaveTicketToken is ERC20, @@ -35,24 +40,37 @@ contract EnclaveTicketToken is { using SafeERC20 for IERC20; // Custom errors + + /// @notice Thrown when a function is called by an address other than the registry error NotRegistry(); + + /// @notice Thrown when attempting to transfer tokens between non-zero addresses error TransferNotAllowed(); + + /// @notice Thrown when a zero address is provided where a valid address is required error ZeroAddress(); + + /// @notice Thrown when attempting to manually delegate voting power error DelegationLocked(); - /// @dev Address of the registry contract that manages deposits and withdrawals. + /// @notice Address of the registry contract authorized to mint, burn, and manage ticket tokens + /// @dev Only this contract can call restricted functions like depositFor, withdrawTo, burnTickets, and payout address public registry; + /// @notice Restricts function access to only the registry contract + /// @dev Reverts with NotRegistry if caller is not the registry address modifier onlyRegistry() { if (msg.sender != registry) revert NotRegistry(); _; } /** - * @notice Deploy the Enclave Ticket Token. - * @param baseToken The underlying ERC20 token to wrap (e.g., USDC, DAI). - * @param registry_ The address of the registry contract. - * @param initialOwner_ The address that will own the contract. + * @notice Initializes the Enclave Ticket Token with name "Enclave Ticket Token" and symbol "ETK" + * @dev Sets up the token as an ERC20 wrapper around the provided base token (stablecoin). + * Initializes voting and permit functionality. The decimals will match the base token. + * @param baseToken The underlying ERC20 stablecoin to wrap (e.g., USDC, DAI) + * @param registry_ Address of the registry contract that will manage deposits and withdrawals + * @param initialOwner_ Address that will own the contract and can update the registry */ constructor( IERC20 baseToken, @@ -68,9 +86,10 @@ contract EnclaveTicketToken is } /** - * @notice Set a new registry contract address. - * @dev Only callable by the contract owner. - * @param newRegistry The address of the new registry contract. + * @notice Updates the registry contract address + * @dev Only callable by the contract owner. The new registry address cannot be zero. + * This function grants the new registry exclusive rights to mint, burn, and manage tokens. + * @param newRegistry Address of the new registry contract (must not be zero address) */ function setRegistry(address newRegistry) public onlyOwner { require(newRegistry != address(0), ZeroAddress()); @@ -78,11 +97,13 @@ contract EnclaveTicketToken is } /** - * @notice Deposit Base token and mint ticket tokens to operator. - * @dev Only callable by the registry contract. Auto-delegates on first deposit. - * @param operator Address to receive the ticket tokens. - * @param amount Amount of tokens to deposit. - * @return success True if successful. + * @notice Deposits underlying tokens from the registry and mints ticket tokens to an operator + * @dev Only callable by the registry contract. Transfers underlying tokens from the registry to + * this contract and mints an equivalent amount of ticket tokens. Automatically delegates + * voting power to the operator on their first deposit to enable voting power tracking. + * @param operator Address to receive the minted ticket tokens + * @param amount Number of underlying tokens to deposit and ticket tokens to mint + * @return success True if the deposit and minting succeeded */ function depositFor( address operator, @@ -97,12 +118,15 @@ contract EnclaveTicketToken is } /** - * @notice Deposit Base token from an account and mint ticket tokens to an account. - * @dev Only callable by the registry contract. Auto-delegates on first deposit. - * @param from Address to deposit from. - * @param to Address to mint to. - * @param amount Amount of tokens to deposit. - * @return True if successful. + * @notice Deposits underlying tokens from a specified account and mints ticket tokens to another account + * @dev Only callable by the registry contract. Transfers underlying tokens from the 'from' address + * to this contract and mints ticket tokens to the 'to' address. Useful for scenarios where + * the source and destination differ. Automatically delegates voting power to recipient on + * their first deposit. + * @param from Address to transfer underlying tokens from (must have approved this contract) + * @param to Address to receive the minted ticket tokens + * @param amount Number of underlying tokens to deposit and ticket tokens to mint + * @return bool True if the deposit and minting succeeded */ function depositFrom( address from, @@ -121,11 +145,13 @@ contract EnclaveTicketToken is } /** - * @notice Burn ticket tokens and transfer Base token to receiver. - * @dev Only callable by the registry contract. - * @param receiver Address to receive the Underlying token. - * @param amount Amount of ticket tokens to burn. - * @return success True if successful. + * @notice Burns ticket tokens from the registry and transfers underlying tokens to a receiver + * @dev Only callable by the registry contract. Burns ticket tokens from the registry's balance + * and transfers an equivalent amount of underlying tokens to the receiver address. Used + * when operators unstake their tokens. + * @param receiver Address to receive the underlying tokens + * @param amount Number of ticket tokens to burn and underlying tokens to transfer + * @return success True if the burn and transfer succeeded */ function withdrawTo( address receiver, @@ -135,10 +161,12 @@ contract EnclaveTicketToken is } /** - * @notice Burn ticket tokens from an operator. - * @dev Only callable by the registry contract. - * @param operator Address to burn from. - * @param amount Amount of ticket tokens to burn. + * @notice Burns ticket tokens from an operator's balance without transferring underlying tokens + * @dev Only callable by the registry contract. Used for slashing or penalizing operators where + * the underlying tokens should remain in the contract or be handled separately. Does not + * return underlying tokens to the operator. + * @param operator Address whose ticket tokens will be burned + * @param amount Number of ticket tokens to burn from the operator's balance */ function burnTickets( address operator, diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol index 7be76a80f3..2d600bcedd 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -21,6 +21,11 @@ import { /** * @title EnclaveToken + * @notice The governance and utility token for the Enclave protocol + * @dev ERC20 token with voting capabilities, permit functionality, and controlled minting. + * Implements transfer restrictions that can be toggled by the owner to control token + * transferability during early phases. Supports a maximum supply cap and role-based + * minting through the MINTER_ROLE. */ contract EnclaveToken is ERC20, @@ -30,43 +35,66 @@ contract EnclaveToken is AccessControl { // Custom errors + + /// @notice Thrown when a zero address is provided where a valid address is required error ZeroAddress(); + + /// @notice Thrown when attempting to mint zero tokens error ZeroAmount(); + + /// @notice Thrown when minting would exceed the maximum token supply error ExceedsTotalSupply(); + + /// @notice Thrown when array parameters have mismatched lengths error ArrayLengthMismatch(); + + /// @notice Thrown when a transfer is attempted while restrictions are active and neither party is whitelisted error TransferNotAllowed(); - /// @dev Maximum supply of the token (18 decimals). + /// @notice Maximum supply of the token: 1.2 billion tokens with 18 decimals + /// @dev Hard cap on total token supply that cannot be exceeded through minting uint256 public constant MAX_SUPPLY = 1_200_000_000e18; - /// @dev Role allowing accounts to mint new tokens. + /// @notice Role identifier for accounts authorized to mint new tokens + /// @dev Keccak256 hash of "MINTER_ROLE" used in AccessControl bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - /// @dev Tracks the amount of tokens minted so far. + /// @notice Tracks the cumulative amount of tokens minted since deployment + /// @dev Incremented with each mint operation to enforce MAX_SUPPLY cap uint256 public totalMinted; - /// @dev Mapping of addresses allowed to transfer when restrictions are active. + /// @notice Mapping of addresses permitted to transfer tokens when restrictions are active + /// @dev When transfersRestricted is true, only whitelisted addresses can send or receive tokens mapping(address account => bool allowed) public transferWhitelisted; - /// @dev Whether transfers are currently restricted. + /// @notice Indicates whether token transfers are currently restricted + /// @dev When true, only whitelisted addresses can transfer tokens bool public transfersRestricted; - /// @dev Emitted when tokens are minted as part of an allocation. + /// @notice Emitted when tokens are minted as part of a named allocation + /// @param recipient Address receiving the minted tokens + /// @param amount Number of tokens minted (18 decimals) + /// @param allocation Description of the allocation for tracking purposes event AllocationMinted( address indexed recipient, uint256 amount, string allocation ); - /// @dev Emitted whenever the transfer restriction flag is updated. + /// @notice Emitted when the transfer restriction setting is changed + /// @param restricted New state of transfer restrictions (true = restricted, false = unrestricted) event TransferRestrictionUpdated(bool restricted); - /// @dev Emitted when an address is added to or removed from the whitelist. + /// @notice Emitted when an address is added to or removed from the transfer whitelist + /// @param account Address whose whitelist status changed + /// @param whitelisted New whitelist status (true = whitelisted, false = not whitelisted) event TransferWhitelistUpdated(address indexed account, bool whitelisted); /** - * @notice Deploy the Enclave token. - * @param _owner Address that will initially own the contract and have admin rights. + * @notice Initializes the Enclave token with name "Enclave" and symbol "ENCL" + * @dev Sets up the token with voting and permit functionality. Grants admin and minter + * roles to the owner, enables transfer restrictions, and whitelists the owner. + * @param _owner Address that will own the contract and receive DEFAULT_ADMIN_ROLE and MINTER_ROLE */ constructor( address _owner @@ -84,11 +112,12 @@ contract EnclaveToken is } /** - * @notice Mint an allocation of tokens to a recipient. - * @dev Only accounts with the MINTER_ROLE may call this function. - * @param recipient Address to receive the minted tokens. - * @param amount Amount of tokens to mint (18 decimals). - * @param allocation Description of the allocation for off-chain bookkeeping. + * @notice Mints a named allocation of tokens to a specified recipient + * @dev Only callable by accounts with MINTER_ROLE. Reverts if recipient is zero address, + * amount is zero, or minting would exceed MAX_SUPPLY. Updates totalMinted tracker. + * @param recipient Address to receive the minted tokens (cannot be zero address) + * @param amount Number of tokens to mint in wei (18 decimals, must be greater than zero) + * @param allocation Human-readable description of this allocation for tracking and auditing purposes */ function mintAllocation( address recipient, @@ -106,11 +135,13 @@ contract EnclaveToken is } /** - * @notice Mint multiple allocations in a batch. - * @dev Only accounts with the MINTER_ROLE may call this function. - * @param recipients Array of addresses to receive tokens. - * @param amounts Corresponding array of amounts to mint. - * @param allocations Array of allocation descriptions. + * @notice Mints multiple named allocations to different recipients in a single transaction + * @dev Only callable by accounts with MINTER_ROLE. All arrays must have the same length. + * Reverts if any amount is zero or if the cumulative minting would exceed MAX_SUPPLY. + * Gas-efficient for distributing tokens to multiple addresses. + * @param recipients Array of addresses to receive minted tokens + * @param amounts Array of token amounts to mint (18 decimals, must match recipients length) + * @param allocations Array of allocation descriptions (must match recipients length) */ function batchMintAllocations( address[] calldata recipients, @@ -137,9 +168,11 @@ contract EnclaveToken is } /** - * @notice Enable or disable transfer restrictions. - * @dev Only the owner can toggle this flag. - * @param restricted Whether transfers should be restricted. + * @notice Enables or disables transfer restrictions for the token + * @dev Only callable by the contract owner. When restrictions are enabled, only whitelisted + * addresses can send or receive tokens. Useful for controlling token circulation during + * early phases before public trading. + * @param restricted True to enable restrictions, false to allow unrestricted transfers */ function setTransferRestriction(bool restricted) external onlyOwner { transfersRestricted = restricted; @@ -147,9 +180,11 @@ contract EnclaveToken is } /** - * @notice Toggle an account's whitelist status. - * @dev Only the owner may call this. - * @param account Address whose whitelist status should be toggled. + * @notice Toggles an account's transfer whitelist status between enabled and disabled + * @dev Only callable by the contract owner. Flips the current whitelist state for the given + * account. Whitelisted accounts can send and receive tokens even when transfer restrictions + * are active. + * @param account Address whose whitelist status will be toggled */ function toggleTransferWhitelist(address account) external onlyOwner { bool newStatus = !transferWhitelisted[account]; @@ -158,10 +193,12 @@ contract EnclaveToken is } /** - * @notice Whitelist contracts that are allowed to transfer while restricted. - * @dev Convenience function for whitelisting middleware contracts. - * @param bondingManager BondingManager contract to whitelist. - * @param vestingEscrow VestingEscrow contract to whitelist. + * @notice Whitelists key protocol contracts to allow them to transfer tokens during restricted periods + * @dev Only callable by the contract owner. Convenience function for whitelisting multiple protocol + * contracts in a single transaction. Zero addresses are safely ignored. Typically used to whitelist + * contracts like bonding managers and vesting escrows that need to handle tokens on behalf of users. + * @param bondingManager Address of the BondingManager contract (zero address skipped) + * @param vestingEscrow Address of the VestingEscrow contract (zero address skipped) */ function whitelistContracts( address bondingManager, @@ -178,7 +215,13 @@ contract EnclaveToken is } /** - * @dev Override ERC20Votes update hook to enforce transfer restrictions. + * @notice Internal hook that enforces transfer restrictions and updates voting power + * @dev Overrides ERC20 and ERC20Votes to add transfer restriction logic. Reverts if transfers + * are restricted and neither sender nor receiver is whitelisted. Minting (from == 0) and + * burning (to == 0) are always allowed regardless of restrictions. + * @param from Address sending tokens (zero address for minting) + * @param to Address receiving tokens (zero address for burning) + * @param value Amount of tokens being transferred */ function _update( address from, @@ -194,7 +237,10 @@ contract EnclaveToken is } /** - * @dev Expose ERC165 interface support via AccessControl. + * @notice Checks if this contract implements a given interface + * @dev Implements ERC165 interface detection via AccessControl + * @param interfaceId The interface identifier to check, as specified in ERC-165 + * @return bool True if the contract implements the interface, false otherwise */ function supportsInterface( bytes4 interfaceId @@ -203,7 +249,11 @@ contract EnclaveToken is } /** - * @dev Expose permit nonces via both ERC20Permit and OpenZeppelin Nonces. + * @notice Returns the current nonce for an address, used for permit signatures + * @dev Resolves the override conflict between ERC20Permit and Nonces by calling the parent + * implementation. Nonces are incremented with each permit to prevent replay attacks. + * @param owner Address to query the nonce for + * @return uint256 The current nonce value for the given address */ function nonces( address owner From 005dcf9c3b33d2642b1ca42662f9174227f52b80 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sun, 12 Oct 2025 01:09:17 +0500 Subject: [PATCH 21/88] chore: review fixes --- .../ICiphernodeRegistry.json | 6 +- .../interfaces/IEnclave.sol/IEnclave.json | 11 +- .../NaiveRegistryFilter.json | 6 +- .../enclave-contracts/contracts/Enclave.sol | 8 +- .../contracts/interfaces/ISlashingManager.sol | 48 +++----- .../contracts/registry/BondingRegistry.sol | 24 ++-- .../contracts/slashing/SlashingManager.sol | 27 ++--- .../contracts/token/EnclaveToken.sol | 4 +- packages/enclave-contracts/tasks/enclave.ts | 1 + .../enclave-contracts/test/Enclave.spec.ts | 6 + .../test/Slashing/SlashingManager.spec.ts | 105 +++++++++--------- 11 files changed, 110 insertions(+), 136 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 8fc5cac3df..2092c677b5 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -427,9 +427,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", -<<<<<<< HEAD - "buildInfoId": "solc-0_8_27-88ef88e3429853e02fdd29a56c19d354fb1c2647" -======= - "buildInfoId": "solc-0_8_27-5798467012d959a2cc08a145d9fd1c6bf5d530bb" ->>>>>>> dev + "buildInfoId": "solc-0_8_27-905ad4d81abe02c455b9927aa66f20cea780e518" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 9f8d4c2e76..28feb2005a 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -581,6 +581,11 @@ "internalType": "bytes", "name": "computeProviderParams", "type": "bytes" + }, + { + "internalType": "bytes", + "name": "customParams", + "type": "bytes" } ], "internalType": "struct IEnclave.E3RequestParams", @@ -969,9 +974,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", -<<<<<<< HEAD - "buildInfoId": "solc-0_8_27-5dc5e83b3f2c44cf8644ab0f2ffb318134b8c5b4" -======= - "buildInfoId": "solc-0_8_27-fceb9ac806c7c7b60cbad1945c3a7e17a11c588b" ->>>>>>> dev + "buildInfoId": "solc-0_8_27-905ad4d81abe02c455b9927aa66f20cea780e518" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json index 86f8ccdff0..64ca19bb6d 100644 --- a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +++ b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json @@ -305,9 +305,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/registry/NaiveRegistryFilter.sol", -<<<<<<< HEAD - "buildInfoId": "solc-0_8_27-88ef88e3429853e02fdd29a56c19d354fb1c2647" -======= - "buildInfoId": "solc-0_8_27-5798467012d959a2cc08a145d9fd1c6bf5d530bb" ->>>>>>> dev + "buildInfoId": "solc-0_8_27-905ad4d81abe02c455b9927aa66f20cea780e518" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index c30ce7c3fe..9e74b404fa 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -183,12 +183,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @param e3Id The ID of the E3. error PlaintextOutputAlreadyPublished(uint256 e3Id); - /// @notice Thrown when the caller has insufficient token balance. - error InsufficientBalance(); - - /// @notice Thrown when the contract has insufficient token allowance. - error InsufficientAllowance(); - /// @notice Thrown when attempting to set an invalid bonding registry address. /// @param bondingRegistry The invalid bonding registry address. error InvalidBondingRegistry(IBondingRegistry bondingRegistry); @@ -265,7 +259,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { E3RequestParams calldata requestParams ) external returns (uint256 e3Id, E3 memory e3) { uint256 e3Fee = getE3Quote(requestParams); - require(e3Fee > 0, PaymentRequired(e3Fee)); require( requestParams.threshold[1] >= requestParams.threshold[0] && requestParams.threshold[0] > 0, @@ -657,6 +650,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { E3RequestParams calldata ) public pure returns (uint256 fee) { fee = 1 * 10 ** 6; + require(fee > 0, PaymentRequired(fee)); } /// @inheritdoc IEnclave diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index 3d3e100195..d5711fc77b 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -207,24 +207,19 @@ interface ISlashingManager { ); /** - * @notice Emitted when a node is banned from the network - * @param node Address of the banned node - * @param reason Hash of the reason for banning - * @param banner Address that executed the ban (governance or contract) + * @notice Emitted when a node is banned or unbanned from the network + * @param node Address of the node + * @param status Whether the node is banned + * @param reason Hash of the reason for banning or unbanning + * @param updater Address that executed the ban (governance or contract) */ - event NodeBanned( + event NodeBanUpdated( address indexed node, + bool status, bytes32 indexed reason, - address banner + address updater ); - /** - * @notice Emitted when a previously banned node is unbanned - * @param node Address of the unbanned node - * @param unbanner Address of the governance account that unbanned the node - */ - event NodeUnbanned(address indexed node, address unbanner); - // ====================== // View Functions // ====================== @@ -410,29 +405,22 @@ interface ISlashingManager { // ====================== /** - * @notice Bans a node from the network + * @notice Bans or unbans a node from the network * @dev Only callable by GOVERNANCE_ROLE. Bans can also occur automatically via executeSlash * @param node Address of the node to ban (must be non-zero) + * @param status Whether to ban the node * @param reason Hash of the reason for banning * Requirements: * - node must not be zero address * - Caller must have GOVERNANCE_ROLE * Effects: - * - Sets banned[node] to true - * - Emits NodeBanned event + * - Sets banned[node] to status + * - Emits NodeBanned event if status is true + * - Emits NodeUnbanned event if status is false */ - function banNode(address node, bytes32 reason) external; - - /** - * @notice Removes a ban from a previously banned node - * @dev Only callable by GOVERNANCE_ROLE - * @param node Address of the node to unban (must be non-zero) - * Requirements: - * - node must not be zero address - * - Caller must have GOVERNANCE_ROLE - * Effects: - * - Sets banned[node] to false - * - Emits NodeUnbanned event - */ - function unbanNode(address node) external; + function updateBanStatus( + address node, + bool status, + bytes32 reason + ) external; } diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index f40d7839ae..8dee1952ea 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -127,7 +127,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @dev Reverts if operator has an exit in progress that hasn't unlocked yet /// @param operator Address of the operator to check modifier noExitInProgress(address operator) { - Operator storage op = operators[operator]; + Operator memory op = operators[operator]; if (op.exitRequested && block.timestamp < op.exitUnlocksAt) revert ExitInProgress(); _; @@ -227,7 +227,6 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { function availableTickets( address operator ) external view returns (uint256) { - if (ticketPrice == 0) return 0; return ticketToken.balanceOf(operator) / ticketPrice; } @@ -280,7 +279,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function hasExitInProgress(address operator) external view returns (bool) { - Operator storage op = operators[operator]; + Operator memory op = operators[operator]; return op.exitRequested && block.timestamp < op.exitUnlocksAt; } @@ -308,10 +307,8 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { operators[msg.sender].registered = true; - if (address(registry) != address(0)) { - // CiphernodeRegistry already emits an event when a ciphernode is added - registry.addCiphernode(msg.sender); - } + // CiphernodeRegistry already emits an event when a ciphernode is added + registry.addCiphernode(msg.sender); _updateOperatorStatus(msg.sender); } @@ -357,10 +354,8 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { ); } - if (address(registry) != address(0)) { - // CiphernodeRegistry already emits an event when a ciphernode is removed - registry.removeCiphernode(msg.sender, siblingNodes); - } + // CiphernodeRegistry already emits an event when a ciphernode is removed + registry.removeCiphernode(msg.sender, siblingNodes); emit CiphernodeDeregistrationRequested(msg.sender, op.exitUnlocksAt); _updateOperatorStatus(msg.sender); @@ -593,7 +588,8 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { require(msg.sender == rewardDistributor, OnlyRewardDistributor()); require(recipients.length == amounts.length, ArrayLengthMismatch()); - for (uint256 i = 0; i < recipients.length; i++) { + uint256 len = recipients.length; + for (uint256 i = 0; i < len; i++) { if (amounts[i] > 0 && operators[recipients[i]].registered) { rewardToken.safeTransferFrom( rewardDistributor, @@ -739,9 +735,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { Operator storage op = operators[operator]; bool newActiveStatus = op.registered && op.licenseBond >= _minLicenseBond() && - (ticketPrice == 0 || - ticketToken.balanceOf(operator) / ticketPrice >= - minTicketBalance); + (ticketToken.balanceOf(operator) / ticketPrice >= minTicketBalance); if (op.active != newActiveStatus) { op.active = newActiveStatus; diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 4aa70b3a3b..7001ef127d 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -214,6 +214,8 @@ contract SlashingManager is ISlashingManager, AccessControl { require(policy.enabled, SlashReasonDisabled()); proposalId = totalProposals; + totalProposals = proposalId + 1; + uint256 executableAt = block.timestamp + policy.appealWindow; SlashProposal storage p = _proposals[proposalId]; @@ -245,12 +247,10 @@ contract SlashingManager is ISlashingManager, AccessControl { executableAt, msg.sender ); - - totalProposals = proposalId + 1; } /// @inheritdoc ISlashingManager - function executeSlash(uint256 proposalId) external onlySlasher { + function executeSlash(uint256 proposalId) external { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = _proposals[proposalId]; @@ -290,7 +290,7 @@ contract SlashingManager is ISlashingManager, AccessControl { if (policy.banNode) { banned[p.operator] = true; - emit NodeBanned(p.operator, p.reason, address(this)); + emit NodeBanUpdated(p.operator, true, p.reason, msg.sender); } emit SlashExecuted( @@ -310,6 +310,7 @@ contract SlashingManager is ISlashingManager, AccessControl { /// @inheritdoc ISlashingManager function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); + // TODO: Should we reject the appeal if the proposal has a cryptographic proof? SlashProposal storage p = _proposals[proposalId]; // Only the accused can appeal @@ -353,18 +354,14 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @inheritdoc ISlashingManager - function banNode(address node, bytes32 reason) external onlyGovernance { - require(node != address(0), ZeroAddress()); - - banned[node] = true; - emit NodeBanned(node, reason, msg.sender); - } - - /// @inheritdoc ISlashingManager - function unbanNode(address node) external onlyGovernance { + function updateBanStatus( + address node, + bool status, + bytes32 reason + ) external onlyGovernance { require(node != address(0), ZeroAddress()); - banned[node] = false; - emit NodeUnbanned(node, msg.sender); + banned[node] = status; + emit NodeBanUpdated(node, status, reason, msg.sender); } } diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol index 2d600bcedd..70699d23ac 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -152,7 +152,7 @@ contract EnclaveToken is if (amounts.length != len || allocations.length != len) revert ArrayLengthMismatch(); - uint256 minted = totalSupply(); + uint256 minted = totalMinted; for (uint256 i = 0; i < len; i++) { address recipient = recipients[i]; @@ -165,6 +165,8 @@ contract EnclaveToken is _mint(recipient, amount); emit AllocationMinted(recipient, amount, allocations[i]); } + + totalMinted = minted; } /** diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index 7ca9b1c599..f70b970f15 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -180,6 +180,7 @@ export const requestCommittee = task( e3Address === ZeroAddress ? mockE3ProgramArgs!.address : e3Address, e3ProgramParams, computeProviderParams, + customParams, }; console.log("Request parameters:", requestParams); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 6fb5d50d71..a0975fb773 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -271,6 +271,10 @@ describe("Enclave", function () { ["address"], [await decryptionVerifier.mockDecryptionVerifier.getAddress()], ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], // arbitrary address. + ), }; await usdcToken.mint(ownerAddress, ethers.parseUnits("1000000", 6)); @@ -756,6 +760,7 @@ describe("Enclave", function () { e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, + customParams: request.customParams, }); await usdcToken.approve(await enclave.getAddress(), fee); await expect( @@ -1112,6 +1117,7 @@ describe("Enclave", function () { e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, + customParams: request.customParams, }); const e3Id = 0; diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 4b966373fe..20b29055e1 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -38,6 +38,8 @@ describe("SlashingManager", function () { ); const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; + const APPEAL_WINDOW = 7 * 24 * 60 * 60; + async function setupPolicies( slashingManager: SlashingManager, mockVerifier: MockSlashingVerifier, @@ -58,7 +60,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; @@ -142,7 +144,7 @@ describe("SlashingManager", function () { ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), minTicketBalance: 5, - exitDelay: 7 * 24 * 60 * 60, + exitDelay: APPEAL_WINDOW, }, }, }, @@ -283,7 +285,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; @@ -301,7 +303,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; @@ -324,7 +326,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; @@ -342,7 +344,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: false, }; @@ -360,7 +362,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; @@ -396,7 +398,7 @@ describe("SlashingManager", function () { requiresProof: true, proofVerifier: await mockVerifier.getAddress(), banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; @@ -536,14 +538,13 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); const proof = ethers.toUtf8Bytes(""); const currentTime = await time.latest(); - const appealWindow = 7 * 24 * 60 * 60; await expect( slashingManager @@ -557,14 +558,14 @@ describe("SlashingManager", function () { REASON_INACTIVITY, ethers.parseUnits("20", 6), ethers.parseEther("50"), - currentTime + appealWindow + 1, + currentTime + APPEAL_WINDOW + 1, await slasher.getAddress(), ); const proposal = await slashingManager.getSlashProposal(0); expect(proposal.proofVerified).to.be.false; expect(proposal.executableAt).to.be.greaterThan( - currentTime + appealWindow, + currentTime + APPEAL_WINDOW, ); }); @@ -649,7 +650,7 @@ describe("SlashingManager", function () { requiresProof: false, proofVerifier: ethers.ZeroAddress, banNode: false, - appealWindow: 7 * 24 * 60 * 60, + appealWindow: APPEAL_WINDOW, enabled: true, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); @@ -730,7 +731,7 @@ describe("SlashingManager", function () { slashingManager.connect(slasher).executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealWindowActive"); - await time.increase(7 * 24 * 60 * 60 + 1); + await time.increase(APPEAL_WINDOW + 1); await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( slashingManager, @@ -752,12 +753,8 @@ describe("SlashingManager", function () { expect(await slashingManager.isBanned(operatorAddress)).to.be.false; await expect(slashingManager.connect(slasher).executeSlash(0)) - .to.emit(slashingManager, "NodeBanned") - .withArgs( - operatorAddress, - REASON_DOUBLE_SIGN, - await slashingManager.getAddress(), - ); + .to.emit(slashingManager, "NodeBanUpdated") + .withArgs(operatorAddress, true, REASON_DOUBLE_SIGN, slasher); expect(await slashingManager.isBanned(operatorAddress)).to.be.true; }); @@ -786,27 +783,6 @@ describe("SlashingManager", function () { slashingManager.connect(slasher).executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); }); - - it("should revert if caller is not slasher", async function () { - const { - slashingManager, - slasher, - notTheOwner, - operatorAddress, - mockVerifier, - } = await loadFixture(setup); - - await setupPolicies(slashingManager, mockVerifier); - - const proof = ethers.toUtf8Bytes("Valid proof"); - await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); - - await expect( - slashingManager.connect(notTheOwner).executeSlash(0), - ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); - }); }); describe("appeal system", function () { @@ -882,7 +858,7 @@ describe("SlashingManager", function () { ethers.toUtf8Bytes(""), ); - await time.increase(7 * 24 * 60 * 60 + 1); + await time.increase(APPEAL_WINDOW + 1); await expect( slashingManager.connect(operator).fileAppeal(0, "Too late"), @@ -1005,7 +981,7 @@ describe("SlashingManager", function () { ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); - await time.increase(7 * 24 * 60 * 60 + 1); + await time.increase(APPEAL_WINDOW + 1); await expect( slashingManager.connect(slasher).executeSlash(0), @@ -1034,7 +1010,7 @@ describe("SlashingManager", function () { await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await slashingManager.connect(owner).resolveAppeal(0, true, "Approved"); - await time.increase(7 * 24 * 60 * 60 + 1); + await time.increase(APPEAL_WINDOW + 1); await expect( slashingManager.connect(slasher).executeSlash(0), @@ -1063,7 +1039,7 @@ describe("SlashingManager", function () { await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await slashingManager.connect(owner).resolveAppeal(0, false, "Denied"); - await time.increase(7 * 24 * 60 * 60 + 1); + await time.increase(APPEAL_WINDOW + 1); await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( slashingManager, @@ -1080,10 +1056,12 @@ describe("SlashingManager", function () { const reason = ethers.encodeBytes32String("manual_ban"); await expect( - slashingManager.connect(owner).banNode(operatorAddress, reason), + slashingManager + .connect(owner) + .updateBanStatus(operatorAddress, true, reason), ) - .to.emit(slashingManager, "NodeBanned") - .withArgs(operatorAddress, reason, await owner.getAddress()); + .to.emit(slashingManager, "NodeBanUpdated") + .withArgs(operatorAddress, true, reason, await owner.getAddress()); expect(await slashingManager.isBanned(operatorAddress)).to.be.true; }); @@ -1094,12 +1072,29 @@ describe("SlashingManager", function () { await slashingManager .connect(owner) - .banNode(operatorAddress, ethers.encodeBytes32String("test")); + .updateBanStatus( + operatorAddress, + true, + ethers.encodeBytes32String("test"), + ); expect(await slashingManager.isBanned(operatorAddress)).to.be.true; - await expect(slashingManager.connect(owner).unbanNode(operatorAddress)) - .to.emit(slashingManager, "NodeUnbanned") - .withArgs(operatorAddress, await owner.getAddress()); + await expect( + slashingManager + .connect(owner) + .updateBanStatus( + operatorAddress, + false, + ethers.encodeBytes32String("test"), + ), + ) + .to.emit(slashingManager, "NodeBanUpdated") + .withArgs( + operatorAddress, + false, + ethers.encodeBytes32String("test"), + await owner.getAddress(), + ); expect(await slashingManager.isBanned(operatorAddress)).to.be.false; }); @@ -1111,7 +1106,11 @@ describe("SlashingManager", function () { await expect( slashingManager .connect(notTheOwner) - .banNode(operatorAddress, ethers.encodeBytes32String("test")), + .updateBanStatus( + operatorAddress, + false, + ethers.encodeBytes32String("test"), + ), ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); }); }); From 498c9fd9a437261a2e1b00505f95daf4000950d2 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 14 Oct 2025 17:34:43 +0500 Subject: [PATCH 22/88] chore: merge economics ciphernode integration --- README.md | 25 +- crates/cli/src/print_env.rs | 10 +- crates/config/src/app_config.rs | 10 +- crates/config/src/contract.rs | 2 +- crates/config/src/store_keys.rs | 8 + crates/docs/user_guide.md | 2 +- crates/entrypoint/src/config_set/mod.rs | 6 +- .../entrypoint/src/start/aggregator_start.rs | 24 +- .../src/enclave_event/committee_published.rs | 30 ++ crates/events/src/enclave_event/mod.rs | 29 ++ .../plaintext_output_published.rs | 28 ++ .../enclave_event/ticket_balance_updated.rs | 30 ++ crates/evm-helpers/src/contracts.rs | 1 + crates/evm/src/bonding_registry_sol.rs | 127 +++++++ crates/evm/src/ciphernode_registry_sol.rs | 153 ++++++++- crates/evm/src/lib.rs | 8 +- crates/evm/src/registry_filter_sol.rs | 163 --------- crates/evm/src/repo.rs | 13 + crates/sortition/src/lib.rs | 5 + crates/sortition/src/node_state.rs | 270 +++++++++++++++ crates/sortition/src/repo.rs | 12 +- crates/sortition/src/sortition.rs | 144 +++++++- .../sortition/src/ticket_bonding_sortition.rs | 277 ++++++++++++++++ deploy/agg.yaml | 2 +- deploy/cn1.yaml | 2 +- deploy/cn2.yaml | 2 +- deploy/cn3.yaml | 2 +- examples/CRISP/enclave.config.yaml | 2 +- .../ICiphernodeRegistry.json | 46 +-- .../interfaces/IEnclave.sol/IEnclave.json | 18 +- .../NaiveRegistryFilter.json | 309 ------------------ .../enclave-contracts/contracts/Enclave.sol | 16 +- .../interfaces/ICiphernodeRegistry.sol | 43 ++- .../contracts/interfaces/IEnclave.sol | 10 +- .../contracts/interfaces/IRegistryFilter.sol | 43 --- .../registry/CiphernodeRegistryOwnable.sol | 52 ++- .../registry/NaiveRegistryFilter.sol | 167 ---------- .../contracts/test/MockCiphernodeRegistry.sol | 35 +- .../contracts/test/MockRegistryFilter.sol | 123 ------- .../ignition/modules/naiveRegistryFilter.ts | 20 -- .../deployAndSave/naiveRegistryFilter.ts | 87 ----- .../scripts/deployEnclave.ts | 11 - packages/enclave-contracts/scripts/index.ts | 1 - .../enclave-contracts/test/Enclave.spec.ts | 198 ++++------- .../CiphernodeRegistryOwnable.spec.ts | 157 ++------- .../test/Registry/NaiveRegistryFilter.spec.ts | 278 ---------------- templates/default/enclave.config.yaml | 2 +- tests/integration/enclave.config.yaml | 2 +- 48 files changed, 1337 insertions(+), 1668 deletions(-) create mode 100644 crates/events/src/enclave_event/committee_published.rs create mode 100644 crates/events/src/enclave_event/plaintext_output_published.rs create mode 100644 crates/events/src/enclave_event/ticket_balance_updated.rs create mode 100644 crates/evm/src/bonding_registry_sol.rs delete mode 100644 crates/evm/src/registry_filter_sol.rs create mode 100644 crates/sortition/src/node_state.rs create mode 100644 crates/sortition/src/ticket_bonding_sortition.rs delete mode 100644 packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json delete mode 100644 packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol delete mode 100644 packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol delete mode 100644 packages/enclave-contracts/contracts/test/MockRegistryFilter.sol delete mode 100644 packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts delete mode 100644 packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts delete mode 100644 packages/enclave-contracts/test/Registry/NaiveRegistryFilter.spec.ts diff --git a/README.md b/README.md index cb68369159..977e545edd 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ sequenceDiagram E3Program-->>Enclave: inputValidator Enclave->>ComputeProvider: validate(computeProviderParams) ComputeProvider-->>Enclave: decryptionVerifier - Enclave->>CiphernodeRegistry: requestCommittee(e3Id, filter, threshold) + Enclave->>CiphernodeRegistry: requestCommittee(e3Id, threshold) CiphernodeRegistry-->>Enclave: success Enclave-->>Users: e3Id, E3 struct @@ -223,6 +223,7 @@ pnpm bump:versions 1.0.0-beta.1 ``` This command automatically: + - ✅ Validates working directory is clean - ✅ Updates version in `Cargo.toml` (workspace version) - ✅ Updates version in all npm `package.json` files @@ -261,12 +262,12 @@ Once the tag is pushed, GitHub Actions automatically: - macOS (x86_64, aarch64) 3. **Runs** all tests 4. **Publishes** packages: - * All versions (stable and pre-release): - * ✅ Publishes to crates.io - * ✅ Publishes to npm - * Tag differences: - * Stable (`v1.0.0`): npm `latest` tag, updates `stable` git tag - * Pre-release (`v1.0.0-beta.1`): npm `next` tag, no `stable` tag update + - All versions (stable and pre-release): + - ✅ Publishes to crates.io + - ✅ Publishes to npm + - Tag differences: + - Stable (`v1.0.0`): npm `latest` tag, updates `stable` git tag + - Pre-release (`v1.0.0-beta.1`): npm `next` tag, no `stable` tag update 5. **Creates** GitHub Release with: - Binary downloads for all platforms - Release notes from CHANGELOG.md @@ -288,21 +289,27 @@ Enclave follows [Semantic Versioning](https://semver.org/): ### Which Version Should I Use? #### For Production (Mainnet) + Use stable versions only: + ```bash enclaveup install # Latest stable enclaveup install v1.0.0 # Specific stable version ``` #### For Testing (Testnet) + You can use pre-release versions: + ```bash enclaveup install --pre-release # Latest pre-release enclaveup install v1.0.0-beta.1 # Specific pre-release ``` #### For Development + Build from source: + ```bash git clone https://github.com/gnosisguild/enclave.git cd enclave @@ -313,7 +320,7 @@ cargo build --release ### Current Setup -- **`main`** - Stable branch +- **`main`** - Stable branch - **`v*.*.*`** - Version tags for releases - **`stable`** - Always points to the latest stable release - **`dev`** - Branch for ongoing development @@ -375,11 +382,13 @@ pnpm bump:versions --help If a release has issues: 1. **Mark as deprecated on npm**: + ```bash npm deprecate @enclave/sdk@1.0.0 "Critical bug, use 1.0.1" ``` 2. **Yank from crates.io** (if critical): + ```bash cargo yank --version 1.0.0 enclave ``` diff --git a/crates/cli/src/print_env.rs b/crates/cli/src/print_env.rs index 4cdcb8dd32..6da77762eb 100644 --- a/crates/cli/src/print_env.rs +++ b/crates/cli/src/print_env.rs @@ -14,13 +14,13 @@ pub fn extract_env_vars_vite(config: &AppConfig, chain: &str) -> String { if let Some(chain) = config.chains().iter().find(|c| c.name == chain.to_string()) { let enclave_addr = &chain.contracts.enclave; let registry_addr = &chain.contracts.ciphernode_registry; - let filter_addr = &chain.contracts.filter_registry; + let bonding_registry_addr = &chain.contracts.bonding_registry; env_vars.push(format!("VITE_ENCLAVE_ADDRESS={}", enclave_addr.address())); env_vars.push(format!("VITE_REGISTRY_ADDRESS={}", registry_addr.address())); env_vars.push(format!("VITE_RPC_URL={}", chain.rpc_url)); env_vars.push(format!( - "VITE_FILTER_REGISTRY_ADDRESS={}", - filter_addr.address() + "VITE_BONDING_REGISTRY_ADDRESS={}", + bonding_registry_addr.address() )); if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("VITE_E3_PROGRAM_ADDRESS={}", e3_program.address())); @@ -37,11 +37,11 @@ pub fn extract_env_vars(config: &AppConfig, chain: &str) -> String { if let Some(chain) = config.chains().iter().find(|c| c.name == chain.to_string()) { let enclave_addr = &chain.contracts.enclave; let registry_addr = &chain.contracts.ciphernode_registry; - let filter_addr = &chain.contracts.filter_registry; + let bonding_registry_addr = &chain.contracts.bonding_registry; env_vars.push(format!("ENCLAVE_ADDRESS={}", enclave_addr.address())); env_vars.push(format!("RPC_URL={}", chain.rpc_url)); env_vars.push(format!("REGISTRY_ADDRESS={}", registry_addr.address())); - env_vars.push(format!("FILTER_REGISTRY_ADDRESS={}", filter_addr.address())); + env_vars.push(format!("BONDING_REGISTRY_ADDRESS={}", bonding_registry_addr.address())); if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("E3_PROGRAM_ADDRESS={}", e3_program.address())); } diff --git a/crates/config/src/app_config.rs b/crates/config/src/app_config.rs index 8322954b05..a12e66c6a1 100644 --- a/crates/config/src/app_config.rs +++ b/crates/config/src/app_config.rs @@ -482,7 +482,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" node: config_dir: "/myconfig/override" @@ -651,7 +651,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; @@ -693,7 +693,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; config = load_config("_default", None, None).map_err(|err| err.to_string())?; @@ -715,7 +715,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; @@ -760,7 +760,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; diff --git a/crates/config/src/contract.rs b/crates/config/src/contract.rs index e44a40a5c0..17ee69f4d7 100644 --- a/crates/config/src/contract.rs +++ b/crates/config/src/contract.rs @@ -38,6 +38,6 @@ impl Contract { pub struct ContractAddresses { pub enclave: Contract, pub ciphernode_registry: Contract, - pub filter_registry: Contract, + pub bonding_registry: Contract, pub e3_program: Option, } diff --git a/crates/config/src/store_keys.rs b/crates/config/src/store_keys.rs index 9705135ff4..007961a91a 100644 --- a/crates/config/src/store_keys.rs +++ b/crates/config/src/store_keys.rs @@ -56,4 +56,12 @@ impl StoreKeys { pub fn ciphernode_registry_reader(chain_id: u64) -> String { format!("//evm_readers/ciphernode_registry/{chain_id}") } + + pub fn bonding_registry_reader(chain_id: u64) -> String { + format!("//evm_readers/bonding_registry/{chain_id}") + } + + pub fn node_state() -> String { + String::from("//node_state") + } } diff --git a/crates/docs/user_guide.md b/crates/docs/user_guide.md index e36765a4f6..dc9d451fb9 100644 --- a/crates/docs/user_guide.md +++ b/crates/docs/user_guide.md @@ -35,7 +35,7 @@ chains: ciphernode_registry: address: "0x0952388f6028a9Eda93a5041a3B216Ea331d97Ab" deploy_block: 7073318 - filter_registry: + bonding_registry: address: "0xcBaCE7C360b606bb554345b20884A28e41436934" deploy_block: 7073319 ``` diff --git a/crates/entrypoint/src/config_set/mod.rs b/crates/entrypoint/src/config_set/mod.rs index d083d543ae..a2ba685379 100644 --- a/crates/entrypoint/src/config_set/mod.rs +++ b/crates/entrypoint/src/config_set/mod.rs @@ -60,7 +60,7 @@ chains: ciphernode_registry: address: "{}" deploy_block: {} - filter_registry: + bonding_registry: address: "{}" deploy_block: {} "#, @@ -73,8 +73,8 @@ chains: get_contract_info("Enclave")?.deploy_block, get_contract_info("CiphernodeRegistryOwnable")?.address, get_contract_info("CiphernodeRegistryOwnable")?.deploy_block, - get_contract_info("NaiveRegistryFilter")?.address, - get_contract_info("NaiveRegistryFilter")?.deploy_block, + get_contract_info("BondingRegistry")?.address, + get_contract_info("BondingRegistry")?.deploy_block, ); fs::write(config_path.clone(), config_content)?; diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index af9ce02568..6e2c4f7879 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -13,8 +13,9 @@ use e3_data::RepositoriesFactory; use e3_events::{get_enclave_event_bus, EnclaveEvent, EventBus}; use e3_evm::{ helpers::{load_signer_from_repository, ProviderConfig}, + BondingRegistryReaderRepositoryFactory, BondingRegistrySolReader, CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, EnclaveSol, - EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, RegistryFilterSol, + EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, }; use e3_fhe::ext::FheExtension; use e3_net::{NetEventTranslator, NetRepositoryFactory}; @@ -65,12 +66,6 @@ pub async fn execute( chain.rpc_url.clone(), ) .await?; - RegistryFilterSol::attach( - &bus, - write_provider.clone(), - &chain.contracts.filter_registry.address(), - ) - .await?; CiphernodeRegistrySol::attach( &bus, read_provider.clone(), @@ -80,6 +75,21 @@ pub async fn execute( chain.rpc_url.clone(), ) .await?; + CiphernodeRegistrySol::attach_writer( + &bus, + write_provider.clone(), + &chain.contracts.ciphernode_registry.address(), + ) + .await?; + BondingRegistrySolReader::attach( + &bus, + read_provider.clone(), + &chain.contracts.bonding_registry.address(), + &repositories.bonding_registry_reader(read_provider.chain_id()), + chain.contracts.bonding_registry.deploy_block(), + chain.rpc_url.clone(), + ) + .await?; } E3Router::builder(&bus, store) diff --git a/crates/events/src/enclave_event/committee_published.rs b/crates/events/src/enclave_event/committee_published.rs new file mode 100644 index 0000000000..abb2e79dfa --- /dev/null +++ b/crates/events/src/enclave_event/committee_published.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteePublished { + pub e3_id: E3id, + pub nodes: Vec, + pub public_key: Vec, +} + +impl Display for CommitteePublished { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, nodes: {:?}, public_key_len: {}", + self.e3_id, + self.nodes, + self.public_key.len() + ) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index d21f71f29a..f0d199e73c 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -8,6 +8,7 @@ mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; mod ciphertext_output_published; +mod committee_published; mod compute_request; mod decryptionshare_created; mod die; @@ -16,15 +17,18 @@ mod e3_requested; mod enclave_error; mod keyshare_created; mod plaintext_aggregated; +mod plaintext_output_published; mod publickey_aggregated; mod publish_document; mod shutdown; mod test_event; +mod ticket_balance_updated; pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; pub use ciphertext_output_published::*; +pub use committee_published::*; pub use compute_request::*; pub use decryptionshare_created::*; pub use die::*; @@ -33,10 +37,12 @@ pub use e3_requested::*; pub use enclave_error::*; pub use keyshare_created::*; pub use plaintext_aggregated::*; +pub use plaintext_output_published::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use shutdown::*; pub use test_event::*; +pub use ticket_balance_updated::*; use crate::{E3id, ErrorEvent, Event, EventId}; use actix::Message; @@ -105,6 +111,18 @@ pub enum EnclaveEvent { id: EventId, data: CiphernodeRemoved, }, + TicketBalanceUpdated { + id: EventId, + data: TicketBalanceUpdated, + }, + CommitteePublished { + id: EventId, + data: CommitteePublished, + }, + PlaintextOutputPublished { + id: EventId, + data: PlaintextOutputPublished, + }, EnclaveError { id: EventId, data: EnclaveError, @@ -183,6 +201,9 @@ impl From for EventId { EnclaveEvent::CiphernodeSelected { id, .. } => id, EnclaveEvent::CiphernodeAdded { id, .. } => id, EnclaveEvent::CiphernodeRemoved { id, .. } => id, + EnclaveEvent::TicketBalanceUpdated { id, .. } => id, + EnclaveEvent::CommitteePublished { id, .. } => id, + EnclaveEvent::PlaintextOutputPublished { id, .. } => id, EnclaveEvent::EnclaveError { id, .. } => id, EnclaveEvent::E3RequestComplete { id, .. } => id, EnclaveEvent::Shutdown { id, .. } => id, @@ -201,6 +222,8 @@ impl EnclaveEvent { EnclaveEvent::DecryptionshareCreated { data, .. } => Some(data.e3_id), EnclaveEvent::PlaintextAggregated { data, .. } => Some(data.e3_id), EnclaveEvent::CiphernodeSelected { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteePublished { data, .. } => Some(data.e3_id), + EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), _ => None, } } @@ -216,6 +239,9 @@ impl EnclaveEvent { EnclaveEvent::CiphernodeSelected { data, .. } => format!("{}", data), EnclaveEvent::CiphernodeAdded { data, .. } => format!("{}", data), EnclaveEvent::CiphernodeRemoved { data, .. } => format!("{}", data), + EnclaveEvent::TicketBalanceUpdated { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteePublished { data, .. } => format!("{:?}", data), + EnclaveEvent::PlaintextOutputPublished { data, .. } => format!("{:?}", data), EnclaveEvent::E3RequestComplete { data, .. } => format!("{}", data), EnclaveEvent::EnclaveError { data, .. } => format!("{:?}", data), EnclaveEvent::Shutdown { data, .. } => format!("{:?}", data), @@ -237,6 +263,9 @@ impl_from_event!( CiphernodeSelected, CiphernodeAdded, CiphernodeRemoved, + TicketBalanceUpdated, + CommitteePublished, + PlaintextOutputPublished, EnclaveError, Shutdown, TestEvent diff --git a/crates/events/src/enclave_event/plaintext_output_published.rs b/crates/events/src/enclave_event/plaintext_output_published.rs new file mode 100644 index 0000000000..aa0c715afc --- /dev/null +++ b/crates/events/src/enclave_event/plaintext_output_published.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct PlaintextOutputPublished { + pub e3_id: E3id, + pub plaintext_output: Vec, +} + +impl Display for PlaintextOutputPublished { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, plaintext_output_len: {}", + self.e3_id, + self.plaintext_output.len() + ) + } +} diff --git a/crates/events/src/enclave_event/ticket_balance_updated.rs b/crates/events/src/enclave_event/ticket_balance_updated.rs new file mode 100644 index 0000000000..43133f19b7 --- /dev/null +++ b/crates/events/src/enclave_event/ticket_balance_updated.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use actix::Message; +use alloy::primitives::{FixedBytes, I256, U256}; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct TicketBalanceUpdated { + pub operator: String, + pub delta: I256, + pub new_balance: U256, + pub reason: FixedBytes<32>, + pub chain_id: u64, +} + +impl Display for TicketBalanceUpdated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "operator: {}, delta: {}, new_balance: {}, chain_id: {}", + self.operator, self.delta, self.new_balance, self.chain_id + ) + } +} diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index 8208158e40..21ba7ae652 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -365,6 +365,7 @@ where e3Program: e3_program, e3ProgramParams: e3_params, computeProviderParams: compute_provider_params, + customParams: Bytes::new(), }; let contract = Enclave::new(self.contract_address, &self.provider); diff --git a/crates/evm/src/bonding_registry_sol.rs b/crates/evm/src/bonding_registry_sol.rs new file mode 100644 index 0000000000..794cad99dd --- /dev/null +++ b/crates/evm/src/bonding_registry_sol.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::{event_reader::EvmEventReaderState, helpers::EthProvider, EvmEventReader}; +use actix::Addr; +use alloy::{ + primitives::{LogData, B256}, + providers::Provider, + sol, + sol_types::SolEvent, +}; +use anyhow::Result; +use e3_data::Repository; +use e3_events::{EnclaveEvent, EventBus}; +use tracing::{error, info, trace}; + +sol!( + #[sol(rpc)] + #[derive(Debug)] + IBondingRegistry, + "../../packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json" +); + +struct TicketBalanceUpdatedWithChainId(pub IBondingRegistry::TicketBalanceUpdated, pub u64); + +impl From for e3_events::TicketBalanceUpdated { + fn from(value: TicketBalanceUpdatedWithChainId) -> Self { + e3_events::TicketBalanceUpdated { + operator: value.0.operator.to_string(), + delta: value.0.delta, + new_balance: value.0.newBalance, + reason: value.0.reason, + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: TicketBalanceUpdatedWithChainId) -> Self { + let payload: e3_events::TicketBalanceUpdated = value.into(); + EnclaveEvent::from(payload) + } +} + +pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { + match topic { + Some(&IBondingRegistry::TicketBalanceUpdated::SIGNATURE_HASH) => { + let Ok(event) = IBondingRegistry::TicketBalanceUpdated::decode_log_data(data) else { + error!("Error parsing event TicketBalanceUpdated after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(TicketBalanceUpdatedWithChainId( + event, chain_id, + ))) + } + _topic => { + trace!( + topic=?_topic, + "Unknown event was received by BondingRegistry.sol parser but was ignored" + ); + None + } + } +} + +/// Connects to BondingRegistry.sol converting EVM events to EnclaveEvents +pub struct BondingRegistrySolReader; + +impl BondingRegistrySolReader { + pub async fn attach

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + ) -> Result>> + where + P: Provider + Clone + 'static, + { + let addr = EvmEventReader::attach( + provider, + extractor, + contract_address, + start_block, + &bus.clone().into(), + repository, + rpc_url, + ) + .await?; + + info!(address=%contract_address, "BondingRegistrySolReader is listening to address"); + + Ok(addr) + } +} + +/// Wrapper for a reader +pub struct BondingRegistrySol; + +impl BondingRegistrySol { + pub async fn attach

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + ) -> Result<()> + where + P: Provider + Clone + 'static, + { + BondingRegistrySolReader::attach( + bus, + provider, + contract_address, + repository, + start_block, + rpc_url, + ) + .await?; + Ok(()) + } +} diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index b2f1353bb1..831dc0a393 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -5,16 +5,20 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::{event_reader::EvmEventReaderState, helpers::EthProvider, EvmEventReader}; -use actix::Addr; +use actix::prelude::*; use alloy::{ - primitives::{LogData, B256}, - providers::Provider, + primitives::{Address, Bytes, LogData, B256, U256}, + providers::{Provider, WalletProvider}, + rpc::types::TransactionReceipt, sol, sol_types::SolEvent, }; use anyhow::Result; use e3_data::Repository; -use e3_events::{EnclaveEvent, EventBus}; +use e3_events::{ + BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, PublicKeyAggregated, + Shutdown, Subscribe, +}; use tracing::{error, info, trace}; sol!( @@ -143,7 +147,133 @@ impl CiphernodeRegistrySolReader { } } -/// Wrapper for a reader and a future writer +/// Writer for publishing committees to CiphernodeRegistry +pub struct CiphernodeRegistrySolWriter

{ + provider: EthProvider

, + contract_address: Address, + bus: Addr>, +} + +impl CiphernodeRegistrySolWriter

{ + pub async fn new( + bus: &Addr>, + provider: EthProvider

, + contract_address: Address, + ) -> Result { + Ok(Self { + provider, + contract_address, + bus: bus.clone(), + }) + } + + pub async fn attach( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + ) -> Result>> { + let addr = CiphernodeRegistrySolWriter::new(bus, provider, contract_address.parse()?) + .await? + .start(); + + let _ = bus + .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) + .await; + + Ok(addr) + } +} + +impl Actor for CiphernodeRegistrySolWriter

{ + type Context = actix::Context; +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::PublicKeyAggregated { data, .. } => { + // Only publish if the src and destination chains match + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } + EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), + _ => (), + } + } +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: PublicKeyAggregated, _: &mut Self::Context) -> Self::Result { + Box::pin({ + let e3_id = msg.e3_id.clone(); + let pubkey = msg.pubkey.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + let nodes = msg.nodes.clone(); + + async move { + let result = + publish_committee_to_registry(provider, contract_address, e3_id, nodes, pubkey) + .await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Committee published to registry"); + } + Err(err) => bus.err(EnclaveErrorType::Evm, err), + } + } + }) + } +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = (); + + fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { + ctx.stop(); + } +} + +pub async fn publish_committee_to_registry( + provider: EthProvider

, + contract_address: Address, + e3_id: E3id, + nodes: OrderedSet, + public_key: Vec, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let public_key = Bytes::from(public_key); + let nodes_vec: Vec

= nodes + .into_iter() + .filter_map(|node| node.parse().ok()) + .collect(); + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + let contract = ICiphernodeRegistry::new(contract_address, provider.provider()); + let builder = contract + .publishCommittee(e3_id, nodes_vec, public_key) + .nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + +/// Wrapper for a reader and writer pub struct CiphernodeRegistrySol; impl CiphernodeRegistrySol { @@ -167,7 +297,18 @@ impl CiphernodeRegistrySol { rpc_url, ) .await?; - // TODO: Writer if needed + Ok(()) + } + + pub async fn attach_writer

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + ) -> Result<()> + where + P: Provider + WalletProvider + Clone + 'static, + { + CiphernodeRegistrySolWriter::attach(bus, provider, contract_address).await?; Ok(()) } } diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 2156d2825d..477e20798c 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -4,19 +4,21 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod bonding_registry_sol; mod ciphernode_registry_sol; mod enclave_sol; mod enclave_sol_reader; mod enclave_sol_writer; mod event_reader; pub mod helpers; -mod registry_filter_sol; mod repo; -pub use ciphernode_registry_sol::{CiphernodeRegistrySol, CiphernodeRegistrySolReader}; +pub use bonding_registry_sol::{BondingRegistrySol, BondingRegistrySolReader}; +pub use ciphernode_registry_sol::{ + CiphernodeRegistrySol, CiphernodeRegistrySolReader, CiphernodeRegistrySolWriter, +}; pub use enclave_sol::EnclaveSol; pub use enclave_sol_reader::EnclaveSolReader; pub use enclave_sol_writer::EnclaveSolWriter; pub use event_reader::{EnclaveEvmEvent, EvmEventReader, EvmEventReaderState, ExtractorFn}; -pub use registry_filter_sol::{RegistryFilterSol, RegistryFilterSolWriter}; pub use repo::*; diff --git a/crates/evm/src/registry_filter_sol.rs b/crates/evm/src/registry_filter_sol.rs deleted file mode 100644 index bde572ea9c..0000000000 --- a/crates/evm/src/registry_filter_sol.rs +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use crate::helpers::EthProvider; -use actix::prelude::*; -use alloy::{ - primitives::{Address, Bytes, U256}, - providers::{Provider, WalletProvider}, - rpc::types::TransactionReceipt, - sol, -}; -use anyhow::Result; -use e3_events::{ - BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, PublicKeyAggregated, - Shutdown, Subscribe, -}; -use tracing::info; - -sol!( - #[sol(rpc)] - NaiveRegistryFilter, - "../../packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json" -); - -pub struct RegistryFilterSolWriter

{ - provider: EthProvider

, - contract_address: Address, - bus: Addr>, -} - -impl RegistryFilterSolWriter

{ - pub async fn new( - bus: &Addr>, - provider: EthProvider

, - contract_address: Address, - ) -> Result { - Ok(Self { - provider, - contract_address, - bus: bus.clone(), - }) - } - - pub async fn attach( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - ) -> Result>> { - let addr = RegistryFilterSolWriter::new(bus, provider, contract_address.parse()?) - .await? - .start(); - - let _ = bus - .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) - .await; - - Ok(addr) - } -} - -impl Actor for RegistryFilterSolWriter

{ - type Context = actix::Context; -} - -impl Handler - for RegistryFilterSolWriter

-{ - type Result = (); - - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { - match msg { - EnclaveEvent::PublicKeyAggregated { data, .. } => { - // Only publish if the src and destination chains match - if self.provider.chain_id() == data.e3_id.chain_id() { - ctx.notify(data); - } - } - EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), - _ => (), - } - } -} - -impl Handler - for RegistryFilterSolWriter

-{ - type Result = ResponseFuture<()>; - - fn handle(&mut self, msg: PublicKeyAggregated, _: &mut Self::Context) -> Self::Result { - Box::pin({ - let e3_id = msg.e3_id.clone(); - let pubkey = msg.pubkey.clone(); - let contract_address = self.contract_address; - let provider = self.provider.clone(); - let bus = self.bus.clone(); - let nodes = msg.nodes.clone(); - - async move { - let result = - publish_committee(provider, contract_address, e3_id, nodes, pubkey).await; - match result { - Ok(receipt) => { - info!(tx=%receipt.transaction_hash, "Transaction published"); - } - Err(err) => bus.err(EnclaveErrorType::Evm, err), - } - } - }) - } -} - -impl Handler - for RegistryFilterSolWriter

-{ - type Result = (); - - fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { - ctx.stop(); - } -} - -pub async fn publish_committee( - provider: EthProvider

, - contract_address: Address, - e3_id: E3id, - nodes: OrderedSet, - public_key: Vec, -) -> Result { - let e3_id: U256 = e3_id.try_into()?; - let public_key = Bytes::from(public_key); - let nodes: Vec

= nodes - .into_iter() - .filter_map(|node| node.parse().ok()) - .collect(); - let from_address = provider.provider().default_signer_address(); - let current_nonce = provider - .provider() - .get_transaction_count(from_address) - .pending() - .await?; - let contract = NaiveRegistryFilter::new(contract_address, provider.provider()); - let builder = contract - .publishCommittee(e3_id, nodes, public_key) - .nonce(current_nonce); - let receipt = builder.send().await?.get_receipt().await?; - Ok(receipt) -} - -pub struct RegistryFilterSol; - -impl RegistryFilterSol { - pub async fn attach( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - ) -> Result<()> { - RegistryFilterSolWriter::attach(bus, provider, contract_address).await?; - Ok(()) - } -} diff --git a/crates/evm/src/repo.rs b/crates/evm/src/repo.rs index 0bcf60c373..0455c09f04 100644 --- a/crates/evm/src/repo.rs +++ b/crates/evm/src/repo.rs @@ -41,3 +41,16 @@ impl CiphernodeRegistryReaderRepositoryFactory for Repositories { ) } } + +pub trait BondingRegistryReaderRepositoryFactory { + fn bonding_registry_reader(&self, chain_id: u64) -> Repository; +} + +impl BondingRegistryReaderRepositoryFactory for Repositories { + fn bonding_registry_reader(&self, chain_id: u64) -> Repository { + Repository::new( + self.store + .scope(StoreKeys::bonding_registry_reader(chain_id)), + ) + } +} diff --git a/crates/sortition/src/lib.rs b/crates/sortition/src/lib.rs index 6149472bd7..7b3b6b5a36 100644 --- a/crates/sortition/src/lib.rs +++ b/crates/sortition/src/lib.rs @@ -6,11 +6,16 @@ mod ciphernode_selector; mod distance; +mod node_state; mod repo; mod sortition; mod ticket; +mod ticket_bonding_sortition; mod ticket_sortition; pub use ciphernode_selector::*; +pub use node_state::*; pub use repo::*; pub use sortition::*; +pub use ticket_bonding_sortition::*; +pub use ticket_sortition::*; diff --git a/crates/sortition/src/node_state.rs b/crates/sortition/src/node_state.rs new file mode 100644 index 0000000000..1f585b579a --- /dev/null +++ b/crates/sortition/src/node_state.rs @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use actix::prelude::*; +use alloy::primitives::U256; +use anyhow::Result; +use e3_data::{AutoPersist, Persistable, Repository}; +use e3_events::{ + BusError, CommitteePublished, EnclaveErrorType, EnclaveEvent, EventBus, + PlaintextOutputPublished, Subscribe, TicketBalanceUpdated, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tracing::{info, trace}; + +/// State for a single ciphernode +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct NodeState { + /// Current ticket balance for this node + pub ticket_balance: U256, + /// Number of active E3 jobs this node is currently participating in + pub active_jobs: u64, +} + +/// State for all nodes across all chains +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct NodeStateStore { + /// Map of (chain_id, node_address) to node state + pub nodes: HashMap<(u64, String), NodeState>, + /// Current ticket price per chain + pub ticket_prices: HashMap, + /// Map of E3 ID to the committee nodes for that E3 + /// This is used to track which nodes are in which E3 jobs + pub e3_committees: HashMap>, +} + +impl NodeStateStore { + /// Get available tickets for a node, accounting for active jobs + pub fn available_tickets(&self, chain_id: u64, address: &str) -> u64 { + let ticket_price = self + .ticket_prices + .get(&chain_id) + .copied() + .unwrap_or(U256::from(1)); + if ticket_price.is_zero() { + return 0; + } + + let key = (chain_id, address.to_string()); + let node = self.nodes.get(&key); + + if let Some(node) = node { + let total_tickets = (node.ticket_balance / ticket_price) + .try_into() + .unwrap_or(0u64); + // Subtract active jobs from available tickets + total_tickets.saturating_sub(node.active_jobs) + } else { + 0 + } + } + + /// Get all nodes for a chain with their available tickets + pub fn get_nodes_with_tickets(&self, chain_id: u64) -> Vec<(String, u64)> { + self.nodes + .iter() + .filter(|((cid, _), _)| *cid == chain_id) + .map(|((_, addr), _)| (addr.clone(), self.available_tickets(chain_id, addr))) + .filter(|(_, tickets)| *tickets > 0) + .collect() + } +} + +/// Actor that manages node state +pub struct NodeStateManager { + state: Persistable, + bus: Addr>, +} + +impl NodeStateManager { + pub fn new(state: Persistable, bus: Addr>) -> Self { + Self { state, bus } + } + + pub async fn attach( + bus: &Addr>, + repository: &Repository, + ) -> Result> { + let state = repository + .clone() + .load_or_default(NodeStateStore::default()) + .await?; + + let addr = NodeStateManager::new(state, bus.clone()).start(); + + bus.send(Subscribe::new("TicketBalanceUpdated", addr.clone().into())) + .await?; + bus.send(Subscribe::new("CommitteePublished", addr.clone().into())) + .await?; + bus.send(Subscribe::new( + "PlaintextOutputPublished", + addr.clone().into(), + )) + .await?; + + info!("NodeStateManager actor started"); + Ok(addr) + } +} + +impl Actor for NodeStateManager { + type Context = Context; +} + +/// Message to set ticket price for a chain +#[derive(Message)] +#[rtype(result = "()")] +pub struct SetTicketPrice { + pub chain_id: u64, + pub price: U256, +} + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: SetTicketPrice, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + state.ticket_prices.insert(msg.chain_id, msg.price); + info!( + chain_id = msg.chain_id, + price = ?msg.price, + "Updated ticket price" + ); + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::TicketBalanceUpdated { data, .. } => { + ctx.notify(data); + } + EnclaveEvent::CommitteePublished { data, .. } => { + ctx.notify(data); + } + EnclaveEvent::PlaintextOutputPublished { data, .. } => { + ctx.notify(data); + } + _ => (), + } + } +} + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: TicketBalanceUpdated, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + let key = (msg.chain_id, msg.operator.clone()); + let node = state.nodes.entry(key).or_insert_with(NodeState::default); + + // Update ticket balance + node.ticket_balance = msg.new_balance; + + info!( + operator = %msg.operator, + chain_id = msg.chain_id, + new_balance = ?msg.new_balance, + "Updated ticket balance" + ); + + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: CommitteePublished, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + let chain_id = msg.e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); + + // Store the committee mapping for this E3 + state + .e3_committees + .insert(e3_id_str.clone(), msg.nodes.clone()); + + // Increment active jobs for each node in the committee + for node_addr in &msg.nodes { + let key = (chain_id, node_addr.clone()); + let node = state.nodes.entry(key).or_insert_with(NodeState::default); + node.active_jobs += 1; + + info!( + node = %node_addr, + chain_id = chain_id, + e3_id = ?msg.e3_id, + active_jobs = node.active_jobs, + "Incremented active jobs for node in committee" + ); + } + + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: PlaintextOutputPublished, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + let chain_id = msg.e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); + + // Get the committee nodes for this E3 + if let Some(committee_nodes) = state.e3_committees.remove(&e3_id_str) { + // Decrement active jobs for each node in the committee + for node_addr in &committee_nodes { + let key = (chain_id, node_addr.clone()); + if let Some(node) = state.nodes.get_mut(&key) { + node.active_jobs = node.active_jobs.saturating_sub(1); + + info!( + node = %node_addr, + chain_id = chain_id, + e3_id = ?msg.e3_id, + active_jobs = node.active_jobs, + "Decremented active jobs for node after E3 completion" + ); + } + } + + info!( + e3_id = ?msg.e3_id, + committee_size = committee_nodes.len(), + "PlaintextOutputPublished - job completed, decremented active jobs" + ); + } else { + info!( + e3_id = ?msg.e3_id, + "PlaintextOutputPublished - no committee found (might have been completed already)" + ); + } + + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} diff --git a/crates/sortition/src/repo.rs b/crates/sortition/src/repo.rs index 3c97133348..3f48aea8a2 100644 --- a/crates/sortition/src/repo.rs +++ b/crates/sortition/src/repo.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::SortitionBackend; +use crate::{NodeStateStore, SortitionBackend}; use e3_config::StoreKeys; use e3_data::{Repositories, Repository}; use std::collections::HashMap; @@ -18,3 +18,13 @@ impl SortitionRepositoryFactory for Repositories { Repository::new(self.store.scope(StoreKeys::sortition())) } } + +pub trait NodeStateRepositoryFactory { + fn node_state(&self) -> Repository; +} + +impl NodeStateRepositoryFactory for Repositories { + fn node_state(&self) -> Repository { + Repository::new(self.store.scope(StoreKeys::node_state())) + } +} diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index 7475b1e5bb..0a37dc4840 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -5,7 +5,9 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::distance::DistanceSortition; +use crate::node_state::NodeStateStore; use crate::ticket::{RegisteredNode, Ticket}; +use crate::ticket_bonding_sortition::{NodeWithTickets, TicketBondingSortition}; use crate::ticket_sortition::ScoreSortition; use actix::prelude::*; use alloy::primitives::Address; @@ -222,17 +224,66 @@ impl SortitionList for ScoreBackend { } } -/// Enum wrapper around the two supported backends. +/// Bonding-based sortition backend. +/// +/// Stores a set of hex-encoded addresses and delegates committee selection +/// to `TicketBondingSortition`. Ticket availability is calculated from +/// NodeStateManager: `floor(ticket_balance / ticket_price) - active_jobs` +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BondingBackend { + /// Set of registered node addresses (hex strings). + nodes: HashSet, +} + +impl Default for BondingBackend { + fn default() -> Self { + Self { + nodes: HashSet::new(), + } + } +} + +impl BondingBackend { + /// Get nodes with their available tickets from NodeStateStore + pub fn get_nodes_with_tickets( + &self, + chain_id: u64, + node_state: &NodeStateStore, + ) -> Vec { + self.nodes + .iter() + .filter_map(|addr_str| { + let addr = addr_str.parse::

().ok()?; + let available_tickets = node_state.available_tickets(chain_id, addr_str); + + // Only include nodes with available tickets + if available_tickets > 0 { + Some(NodeWithTickets { + address: addr, + available_tickets, + }) + } else { + None + } + }) + .collect() + } +} + +/// Enum wrapper around the supported backends. /// /// New chains should default to `Distance`. If a chain is intended to /// use score selection, construct it as `SortitionBackend::Score(ScoreBackend::default())` -/// and then populate tickets explicitly. +/// and then populate tickets explicitly. For bonding-based sortition with +/// dynamic ticket calculation, use `SortitionBackend::Bonding(BondingBackend::default())`. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum SortitionBackend { /// Distance-based selection (stores a simple set of addresses). Distance(DistanceBackend), /// Score-based selection (stores `RegisteredNode`s with tickets). Score(ScoreBackend), + /// Bonding-based selection (uses NodeStateManager for dynamic tickets). + Bonding(BondingBackend), } impl SortitionBackend { @@ -244,7 +295,7 @@ impl SortitionBackend { /// Helper for Score backends: assign local ticket IDs `1..=count` for `address`. /// /// # Errors - /// Returns an error if called on a `Distance` backend. + /// Returns an error if called on a `Distance` or `Bonding` backend. pub fn set_ticket_count_addr(&mut self, address: Address, count: u64) -> Result<()> { match self { SortitionBackend::Score(b) => { @@ -254,15 +305,44 @@ impl SortitionBackend { SortitionBackend::Distance(_) => { anyhow::bail!("set_ticket_count_addr is only valid for Score backend") } + SortitionBackend::Bonding(_) => { + anyhow::bail!("set_ticket_count_addr is not applicable to Bonding backend (uses NodeStateStore)") + } } } } +impl SortitionList for BondingBackend { + /// Check membership using bonding-based sortition. + /// + /// Note: This implementation cannot access NodeStateStore directly, + /// so it returns false. For proper bonding sortition, use the + /// `Sortition` actor's `GetHasNode` message which has access to state. + fn contains(&self, _seed: Seed, _size: usize, _address: String) -> Result { + // BondingBackend requires NodeStateStore which isn't available here + // The Sortition actor will handle this by querying the node state + Ok(false) + } + + fn add(&mut self, address: String) { + self.nodes.insert(address); + } + + fn remove(&mut self, address: String) { + self.nodes.remove(&address); + } + + fn nodes(&self) -> Vec { + self.nodes.iter().cloned().collect() + } +} + impl SortitionList for SortitionBackend { fn contains(&self, seed: Seed, size: usize, address: String) -> Result { match self { SortitionBackend::Distance(backend) => backend.contains(seed, size, address), SortitionBackend::Score(backend) => backend.contains(seed, size, address), + SortitionBackend::Bonding(backend) => backend.contains(seed, size, address), } } @@ -270,6 +350,7 @@ impl SortitionList for SortitionBackend { match self { SortitionBackend::Distance(backend) => backend.add(address), SortitionBackend::Score(backend) => backend.add(address), + SortitionBackend::Bonding(backend) => backend.add(address), } } @@ -277,6 +358,7 @@ impl SortitionList for SortitionBackend { match self { SortitionBackend::Distance(backend) => backend.remove(address), SortitionBackend::Score(backend) => backend.remove(address), + SortitionBackend::Bonding(backend) => backend.remove(address), } } @@ -284,6 +366,7 @@ impl SortitionList for SortitionBackend { match self { SortitionBackend::Distance(backend) => backend.nodes(), SortitionBackend::Score(backend) => backend.nodes(), + SortitionBackend::Bonding(backend) => backend.nodes(), } } } @@ -301,6 +384,8 @@ pub struct Sortition { list: Persistable>, /// Event bus for error reporting and enclave event subscription. bus: Addr>, + /// Optional reference to node state for bonding-based sortition + node_state: Option>, } /// Parameters for constructing a `Sortition` actor. @@ -310,6 +395,8 @@ pub struct SortitionParams { pub bus: Addr>, /// Persisted per-chain backend map. pub list: Persistable>, + /// Optional node state for bonding-based sortition + pub node_state: Option>, } impl Sortition { @@ -318,6 +405,7 @@ impl Sortition { Self { list: params.list, bus: params.bus, + node_state: params.node_state, } } @@ -333,6 +421,31 @@ impl Sortition { let addr = Sortition::new(SortitionParams { bus: bus.clone(), list, + node_state: None, // Legacy attach without node state + }) + .start(); + bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); + bus.do_send(Subscribe::new("CiphernodeRemoved", addr.clone().into())); + Ok(addr) + } + + /// Load persisted state with node state support for bonding-based sortition. + /// + /// This version allows bonding-based backends to query ticket availability. + #[instrument(name = "sortition_attach_with_node_state", skip_all)] + pub async fn attach_with_node_state( + bus: &Addr>, + store: Repository>, + node_state_store: Repository, + ) -> Result> { + let list = store.load_or_default(HashMap::new()).await?; + let node_state = node_state_store + .load_or_default(NodeStateStore::default()) + .await?; + let addr = Sortition::new(SortitionParams { + bus: bus.clone(), + list, + node_state: Some(node_state), }) .start(); bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); @@ -435,6 +548,31 @@ impl Handler for Sortition { self.list .try_with(|map| { if let Some(backend) = map.get(&msg.chain_id) { + // For bonding backends, we need to use the node state + if let SortitionBackend::Bonding(bonding_backend) = backend { + if let Some(node_state_ref) = &self.node_state { + return node_state_ref.try_with(|node_state| { + // Get nodes with their available tickets + let nodes_with_tickets = bonding_backend + .get_nodes_with_tickets(msg.chain_id, node_state); + + // Use ticket bonding sortition + let sortition = TicketBondingSortition::new(msg.size); + let target_addr: Address = msg.address.parse()?; + + // Get committee and check if address is included + let committee = sortition.get_committee( + &nodes_with_tickets, + msg.chain_id, + msg.seed.into(), + )?; + + Ok(committee.contains(&target_addr)) + }); + } + } + + // For other backends, use their native contains method backend.contains(msg.seed, msg.size, msg.address.clone()) } else { Ok(false) diff --git a/crates/sortition/src/ticket_bonding_sortition.rs b/crates/sortition/src/ticket_bonding_sortition.rs new file mode 100644 index 0000000000..9b507aaae8 --- /dev/null +++ b/crates/sortition/src/ticket_bonding_sortition.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use alloy::primitives::{keccak256, Address}; +use anyhow::{anyhow, Result}; +use num_bigint::BigUint; +use serde::{Deserialize, Serialize}; + +/// A node with its available tickets (after subtracting active jobs) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NodeWithTickets { + pub address: Address, + pub available_tickets: u64, +} + +/// A winning ticket with its score +#[derive(Clone, Debug)] +pub struct WinningTicket { + pub address: Address, + pub ticket_number: u64, + pub score: BigUint, +} + +/// Ticket-based sortition using bonding registry state +/// +/// Algorithm: +/// 1. Each node has N available tickets (ticket_balance / ticket_price - active_jobs) +/// 2. For each ticket of each node, compute hash(node_address, ticket_number, e3_id, seed) +/// 3. Find the lowest score ticket for each node +/// 4. Sort all nodes by their lowest ticket score +/// 5. Select the top M nodes +pub struct TicketBondingSortition { + /// Desired committee size + pub size: usize, +} + +impl TicketBondingSortition { + pub fn new(size: usize) -> Self { + Self { size } + } + + /// Compute the score for a specific ticket + /// + /// Score = keccak256(node_address || ticket_number || e3_id || seed) + fn compute_ticket_score( + node_address: Address, + ticket_number: u64, + e3_id: u64, + seed: u64, + ) -> BigUint { + let mut message = Vec::with_capacity(20 + 8 + 8 + 8); + message.extend_from_slice(node_address.as_slice()); + message.extend_from_slice(&ticket_number.to_be_bytes()); + message.extend_from_slice(&e3_id.to_be_bytes()); + message.extend_from_slice(&seed.to_be_bytes()); + + let hash = keccak256(&message); + BigUint::from_bytes_be(&hash.0) + } + + /// Find the lowest scoring ticket for a given node + fn find_best_ticket_for_node( + node: &NodeWithTickets, + e3_id: u64, + seed: u64, + ) -> Option { + if node.available_tickets == 0 { + return None; + } + + let mut best: Option = None; + + for ticket_num in 1..=node.available_tickets { + let score = Self::compute_ticket_score(node.address, ticket_num, e3_id, seed); + + match &best { + None => { + best = Some(WinningTicket { + address: node.address, + ticket_number: ticket_num, + score, + }); + } + Some(current_best) => { + if score < current_best.score + || (score == current_best.score && ticket_num < current_best.ticket_number) + { + best = Some(WinningTicket { + address: node.address, + ticket_number: ticket_num, + score, + }); + } + } + } + } + + best + } + + /// Determine the committee from a list of nodes with their available tickets + /// + /// Returns the sorted list of winning nodes (top M by lowest ticket score) + pub fn get_committee( + &self, + nodes: &[NodeWithTickets], + e3_id: u64, + seed: u64, + ) -> Result> { + if nodes.is_empty() || self.size == 0 { + return Ok(Vec::new()); + } + + // Find the best ticket for each node + let mut winning_tickets: Vec = nodes + .iter() + .filter_map(|node| Self::find_best_ticket_for_node(node, e3_id, seed)) + .collect(); + + if winning_tickets.is_empty() { + return Err(anyhow!("No nodes with available tickets")); + } + + // Sort by score (ascending), then by ticket number if scores are equal + winning_tickets.sort_unstable_by(|a, b| { + a.score + .cmp(&b.score) + .then(a.ticket_number.cmp(&b.ticket_number)) + }); + + // Select top M nodes + let selected_size = self.size.min(winning_tickets.len()); + Ok(winning_tickets + .into_iter() + .take(selected_size) + .map(|w| w.address) + .collect()) + } + + /// Check if a specific node is in the committee + pub fn is_node_in_committee( + &self, + nodes: &[NodeWithTickets], + e3_id: u64, + seed: u64, + target_address: Address, + ) -> Result { + let committee = self.get_committee(nodes, e3_id, seed)?; + Ok(committee.contains(&target_address)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::keccak256; + + fn address(i: u64) -> Address { + let h = keccak256([b"addr".as_slice(), &i.to_be_bytes()].concat()); + let mut bytes20 = [0u8; 20]; + bytes20.copy_from_slice(&h.0[12..32]); + Address::from(bytes20) + } + + #[test] + fn test_ticket_bonding_sortition() { + let nodes = vec![ + NodeWithTickets { + address: address(1), + available_tickets: 5, + }, + NodeWithTickets { + address: address(2), + available_tickets: 3, + }, + NodeWithTickets { + address: address(3), + available_tickets: 7, + }, + NodeWithTickets { + address: address(4), + available_tickets: 2, + }, + NodeWithTickets { + address: address(5), + available_tickets: 0, // No available tickets + }, + ]; + + let sortition = TicketBondingSortition::new(3); + let e3_id = 12345; + let seed = 0xABCDEF; + + let committee = sortition + .get_committee(&nodes, e3_id, seed) + .expect("Should get committee"); + + assert_eq!(committee.len(), 3); + println!("Committee: {:?}", committee); + + // Verify the committee is deterministic + let committee2 = sortition + .get_committee(&nodes, e3_id, seed) + .expect("Should get committee"); + assert_eq!(committee, committee2); + + // Verify node with 0 tickets is not selected + assert!(!committee.contains(&address(5))); + } + + #[test] + fn test_is_node_in_committee() { + let nodes = vec![ + NodeWithTickets { + address: address(1), + available_tickets: 5, + }, + NodeWithTickets { + address: address(2), + available_tickets: 3, + }, + NodeWithTickets { + address: address(3), + available_tickets: 7, + }, + ]; + + let sortition = TicketBondingSortition::new(2); + let e3_id = 12345; + let seed = 0xABCDEF; + + let committee = sortition + .get_committee(&nodes, e3_id, seed) + .expect("Should get committee"); + + for node in &nodes { + let is_in = sortition + .is_node_in_committee(&nodes, e3_id, seed, node.address) + .expect("Should check membership"); + assert_eq!( + is_in, + committee.contains(&node.address), + "Membership check should match committee" + ); + } + } + + #[test] + fn test_active_jobs_penalty() { + // Node 1 has more tickets but they're reduced by active jobs + let nodes = vec![ + NodeWithTickets { + address: address(1), + available_tickets: 10, // e.g., had 15 tickets but 5 active jobs + }, + NodeWithTickets { + address: address(2), + available_tickets: 10, // e.g., had 10 tickets but 0 active jobs + }, + ]; + + let sortition = TicketBondingSortition::new(1); + let e3_id = 12345; + let seed = 0xABCDEF; + + let committee = sortition + .get_committee(&nodes, e3_id, seed) + .expect("Should get committee"); + + assert_eq!(committee.len(), 1); + // Both have same available tickets, result is deterministic based on scores + } +} + diff --git a/deploy/agg.yaml b/deploy/agg.yaml index 6635a3b09a..806a2cc7bd 100644 --- a/deploy/agg.yaml +++ b/deploy/agg.yaml @@ -13,4 +13,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" \ No newline at end of file + bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" \ No newline at end of file diff --git a/deploy/cn1.yaml b/deploy/cn1.yaml index 689649b85f..3bfc6f97c6 100644 --- a/deploy/cn1.yaml +++ b/deploy/cn1.yaml @@ -7,4 +7,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" diff --git a/deploy/cn2.yaml b/deploy/cn2.yaml index b7d897435d..821bfa09e3 100644 --- a/deploy/cn2.yaml +++ b/deploy/cn2.yaml @@ -11,4 +11,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" diff --git a/deploy/cn3.yaml b/deploy/cn3.yaml index c892ab7f27..7112f0c2d6 100644 --- a/deploy/cn3.yaml +++ b/deploy/cn3.yaml @@ -11,4 +11,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index c3a5b77345..8e34ceb846 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -4,7 +4,7 @@ chains: contracts: enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - filter_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + bonding_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" program: dev: true diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 2092c677b5..72389614a7 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -93,6 +93,12 @@ "name": "e3Id", "type": "uint256" }, + { + "indexed": false, + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + }, { "indexed": false, "internalType": "bytes", @@ -112,12 +118,6 @@ "name": "e3Id", "type": "uint256" }, - { - "indexed": false, - "internalType": "address", - "name": "filter", - "type": "address" - }, { "indexed": false, "internalType": "uint32[2]", @@ -214,7 +214,7 @@ "type": "bytes32" } ], - "internalType": "struct IRegistryFilter.Committee", + "internalType": "struct ICiphernodeRegistry.Committee", "name": "committee", "type": "tuple" } @@ -222,25 +222,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "getFilter", - "outputs": [ - { - "internalType": "address", - "name": "filter", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -287,9 +268,9 @@ "type": "uint256" }, { - "internalType": "bytes", - "name": "proof", - "type": "bytes" + "internalType": "address[]", + "name": "nodes", + "type": "address[]" }, { "internalType": "bytes", @@ -327,11 +308,6 @@ "name": "e3Id", "type": "uint256" }, - { - "internalType": "address", - "name": "filter", - "type": "address" - }, { "internalType": "uint32[2]", "name": "threshold", @@ -427,5 +403,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_27-905ad4d81abe02c455b9927aa66f20cea780e518" + "buildInfoId": "solc-0_8_27-944178351e7c6a2ade6cf3019319b93f86f65a3f" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 28feb2005a..25f61f8588 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -204,12 +204,6 @@ "name": "e3", "type": "tuple" }, - { - "indexed": false, - "internalType": "address", - "name": "filter", - "type": "address" - }, { "indexed": true, "internalType": "contract IE3Program", @@ -547,11 +541,6 @@ "inputs": [ { "components": [ - { - "internalType": "address", - "name": "filter", - "type": "address" - }, { "internalType": "uint32[2]", "name": "threshold", @@ -709,11 +698,6 @@ "inputs": [ { "components": [ - { - "internalType": "address", - "name": "filter", - "type": "address" - }, { "internalType": "uint32[2]", "name": "threshold", @@ -974,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_27-905ad4d81abe02c455b9927aa66f20cea780e518" + "buildInfoId": "solc-0_8_27-944178351e7c6a2ade6cf3019319b93f86f65a3f" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json deleted file mode 100644 index 64ca19bb6d..0000000000 --- a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +++ /dev/null @@ -1,309 +0,0 @@ -{ - "_format": "hh3-artifact-1", - "contractName": "NaiveRegistryFilter", - "sourceName": "contracts/registry/NaiveRegistryFilter.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "address", - "name": "_registry", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "ciphernode", - "type": "address" - } - ], - "name": "CiphernodeNotEnabled", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeAlreadyExists", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeAlreadyPublished", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeDoesNotExist", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeNotPublished", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidInitialization", - "type": "error" - }, - { - "inputs": [], - "name": "NotInitializing", - "type": "error" - }, - { - "inputs": [], - "name": "OnlyRegistry", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "OwnableInvalidOwner", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "OwnableUnauthorizedAccount", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint64", - "name": "version", - "type": "uint64" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3", - "type": "uint256" - } - ], - "name": "committees", - "outputs": [ - { - "internalType": "bytes32", - "name": "publicKey", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "getCommittee", - "outputs": [ - { - "components": [ - { - "internalType": "address[]", - "name": "nodes", - "type": "address[]" - }, - { - "internalType": "uint32[2]", - "name": "threshold", - "type": "uint32[2]" - }, - { - "internalType": "bytes32", - "name": "publicKey", - "type": "bytes32" - } - ], - "internalType": "struct IRegistryFilter.Committee", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "address", - "name": "_registry", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "address[]", - "name": "nodes", - "type": "address[]" - }, - { - "internalType": "bytes", - "name": "publicKey", - "type": "bytes" - } - ], - "name": "publishCommittee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "registry", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint32[2]", - "name": "threshold", - "type": "uint32[2]" - } - ], - "name": "requestCommittee", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_registry", - "type": "address" - } - ], - "name": "setRegistry", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x608060405234801561000f575f5ffd5b50604051610fbe380380610fbe83398101604081905261002e916102f6565b610038828261003f565b5050610327565b5f516020610f9e5f395f51905f52805468010000000000000000810460ff1615906001600160401b03165f811580156100755750825b90505f826001600160401b031660011480156100905750303b155b90508115801561009e575080155b156100bc5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156100ea57845460ff60401b1916680100000000000000001785555b6100f333610175565b6100fc86610189565b5f516020610f7e5f395f51905f52546001600160a01b0388811691161461012657610126876101b2565b831561016c57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b61017d6101f1565b6101868161022e565b50565b610191610236565b5f80546001600160a01b0319166001600160a01b0392909216919091179055565b6101ba610236565b6001600160a01b0381166101e857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101868161027e565b5f516020610f9e5f395f51905f525468010000000000000000900460ff1661022c57604051631afcd79f60e31b815260040160405180910390fd5b565b6101ba6101f1565b336102555f516020610f7e5f395f51905f52546001600160a01b031690565b6001600160a01b03161461022c5760405163118cdaa760e01b81523360048201526024016101df565b5f516020610f7e5f395f51905f5280546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80516001600160a01b03811681146102f1575f5ffd5b919050565b5f5f60408385031215610307575f5ffd5b610310836102db565b915061031e602084016102db565b90509250929050565b610c4a806103345f395ff3fe608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610910565b6101e8565b6040516100dc9190610927565b60405180910390f35b6100f86100f3366004610a09565b6102f8565b005b61010d610108366004610ab4565b6103e3565b60405190151581526020016100dc565b6100f861012b366004610aff565b610473565b6100f86105c7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b30565b6105da565b6100f86101b3366004610b30565b610610565b6101da6101c6366004610910565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107ae565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610652565b5f8581526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b61033b8186866107d4565b50828260405161034c929190610b50565b60405190819003812060028301555f546001600160a01b03169063d9bbec9590889061037e9089908990602001610b5f565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103ae9493929190610ba9565b5f604051808303815f87803b1580156103c5575f5ffd5b505af11580156103d7573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b0316331461040e576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff161561044c576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104699101836002610842565b5060019392505050565b5f61047c6106ad565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f811580156104a85750825b90505f8267ffffffffffffffff1660011480156104c45750303b155b9050811580156104d2575080155b156104f05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561052457845468ff00000000000000001916680100000000000000001785555b61052d336106d5565b610536866105da565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105735761057387610610565b83156105be57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105cf610652565b6105d85f6106e6565b565b6105e2610652565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610618610652565b6001600160a01b03811661064657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61064f816106e6565b50565b336106847f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105d85760405163118cdaa760e01b815233600482015260240161063d565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106dd610763565b61064f81610788565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61076b610790565b6105d857604051631afcd79f60e31b815260040160405180910390fd5b610618610763565b5f6107996106ad565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107c86108de565b81526020015f81525090565b828054828255905f5260205f20908101928215610832579160200282015b8281111561083257815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107f2565b5061083e9291506108fc565b5090565b600183019183908215610832579160200282015f5b838211156108a157833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff1602179055509260200192600401602081600301049283019260010302610857565b80156108d15782816101000a81549063ffffffff02191690556004016020816003010492830192600103026108a1565b505061083e9291506108fc565b60405180604001604052806002906020820280368337509192915050565b5b8082111561083e575f81556001016108fd565b5f60208284031215610920575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610978576001600160a01b03845116825260208201915060208401935060018301925061094c565b50602086015192506040850191505f5b60028110156109ad57835163ffffffff16835260209384019390920191600101610988565b506040860151608086015280935050505092915050565b5f5f83601f8401126109d4575f5ffd5b50813567ffffffffffffffff8111156109eb575f5ffd5b602083019150836020828501011115610a02575f5ffd5b9250929050565b5f5f5f5f5f60608688031215610a1d575f5ffd5b85359450602086013567ffffffffffffffff811115610a3a575f5ffd5b8601601f81018813610a4a575f5ffd5b803567ffffffffffffffff811115610a60575f5ffd5b8860208260051b8401011115610a74575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a97575f5ffd5b610aa3888289016109c4565b969995985093965092949392505050565b5f5f60608385031215610ac5575f5ffd5b8235915060608301841015610ad8575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610afa575f5ffd5b919050565b5f5f60408385031215610b10575f5ffd5b610b1983610ae4565b9150610b2760208401610ae4565b90509250929050565b5f60208284031215610b40575f5ffd5b610b4982610ae4565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b9f576001600160a01b03610b8a84610ae4565b16825260209283019290910190600101610b71565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea264697066735822122020e93fd437e34a91439aa2b1a8f59d1bfa2eea1fbe17ad08c788d4cbda70bf5664736f6c634300081b00339016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca366004610910565b6101e8565b6040516100dc9190610927565b60405180910390f35b6100f86100f3366004610a09565b6102f8565b005b61010d610108366004610ab4565b6103e3565b60405190151581526020016100dc565b6100f861012b366004610aff565b610473565b6100f86105c7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b30565b6105da565b6100f86101b3366004610b30565b610610565b6101da6101c6366004610910565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f06107ae565b5f828152600160209081526040808320815181546080948102820185019093526060810183815290939192849284919084018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d575050509284525050506002919091015460209091015260408101519091506102f2576040516322e679e360e11b815260040160405180910390fd5b92915050565b610300610652565b5f8581526001602052604090206002810154156103305760405163632a22bb60e01b815260040160405180910390fd5b61033b8186866107d4565b50828260405161034c929190610b50565b60405190819003812060028301555f546001600160a01b03169063d9bbec9590889061037e9089908990602001610b5f565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103ae9493929190610ba9565b5f604051808303815f87803b1580156103c5575f5ffd5b505af11580156103d7573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b0316331461040e576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff161561044c576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104699101836002610842565b5060019392505050565b5f61047c6106ad565b805490915060ff68010000000000000000820416159067ffffffffffffffff165f811580156104a85750825b90505f8267ffffffffffffffff1660011480156104c45750303b155b9050811580156104d2575080155b156104f05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561052457845468ff00000000000000001916680100000000000000001785555b61052d336106d5565b610536866105da565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105735761057387610610565b83156105be57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105cf610652565b6105d85f6106e6565b565b6105e2610652565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610618610652565b6001600160a01b03811661064657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61064f816106e6565b50565b336106847f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105d85760405163118cdaa760e01b815233600482015260240161063d565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006102f2565b6106dd610763565b61064f81610788565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61076b610790565b6105d857604051631afcd79f60e31b815260040160405180910390fd5b610618610763565b5f6107996106ad565b5468010000000000000000900460ff16919050565b6040518060600160405280606081526020016107c86108de565b81526020015f81525090565b828054828255905f5260205f20908101928215610832579160200282015b8281111561083257815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107f2565b5061083e9291506108fc565b5090565b600183019183908215610832579160200282015f5b838211156108a157833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff1602179055509260200192600401602081600301049283019260010302610857565b80156108d15782816101000a81549063ffffffff02191690556004016020816003010492830192600103026108a1565b505061083e9291506108fc565b60405180604001604052806002906020820280368337509192915050565b5b8082111561083e575f81556001016108fd565b5f60208284031215610920575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b80831015610978576001600160a01b03845116825260208201915060208401935060018301925061094c565b50602086015192506040850191505f5b60028110156109ad57835163ffffffff16835260209384019390920191600101610988565b506040860151608086015280935050505092915050565b5f5f83601f8401126109d4575f5ffd5b50813567ffffffffffffffff8111156109eb575f5ffd5b602083019150836020828501011115610a02575f5ffd5b9250929050565b5f5f5f5f5f60608688031215610a1d575f5ffd5b85359450602086013567ffffffffffffffff811115610a3a575f5ffd5b8601601f81018813610a4a575f5ffd5b803567ffffffffffffffff811115610a60575f5ffd5b8860208260051b8401011115610a74575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a97575f5ffd5b610aa3888289016109c4565b969995985093965092949392505050565b5f5f60608385031215610ac5575f5ffd5b8235915060608301841015610ad8575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610afa575f5ffd5b919050565b5f5f60408385031215610b10575f5ffd5b610b1983610ae4565b9150610b2760208401610ae4565b90509250929050565b5f60208284031215610b40575f5ffd5b610b4982610ae4565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b9f576001600160a01b03610b8a84610ae4565b16825260209283019290910190600101610b71565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea264697066735822122020e93fd437e34a91439aa2b1a8f59d1bfa2eea1fbe17ad08c788d4cbda70bf5664736f6c634300081b0033", - "linkReferences": {}, - "deployedLinkReferences": {}, - "immutableReferences": {}, - "inputSourceName": "project/contracts/registry/NaiveRegistryFilter.sol", - "buildInfoId": "solc-0_8_27-905ad4d81abe02c455b9927aa66f20cea780e518" -} \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 9e74b404fa..a5b1512ed6 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -9,7 +9,6 @@ import { IEnclave, E3, IE3Program } from "./interfaces/IEnclave.sol"; import { IInputValidator } from "./interfaces/IInputValidator.sol"; import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; -import { IRegistryFilter } from "./interfaces/IRegistryFilter.sol"; import { IDecryptionVerifier } from "./interfaces/IDecryptionVerifier.sol"; import { OwnableUpgradeable @@ -330,20 +329,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); require( - ciphernodeRegistry.requestCommittee( - e3Id, - requestParams.filter, - requestParams.threshold - ), + ciphernodeRegistry.requestCommittee(e3Id, requestParams.threshold), CommitteeSelectionFailed() ); - emit E3Requested( - e3Id, - e3, - requestParams.filter, - requestParams.e3Program - ); + emit E3Requested(e3Id, e3, requestParams.e3Program); } /// @inheritdoc IEnclave @@ -475,7 +465,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Emits RewardsDistributed event upon successful distribution. /// @param e3Id The ID of the E3 for which to distribute rewards. function _distributeRewards(uint256 e3Id) internal { - IRegistryFilter.Committee memory committee = ciphernodeRegistry + ICiphernodeRegistry.Committee memory committee = ciphernodeRegistry .getCommittee(e3Id); uint256[] memory amounts = new uint256[](committee.nodes.length); diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 60f619fc1d..f9d3a131a6 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -5,29 +5,35 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; -import { IRegistryFilter } from "./IRegistryFilter.sol"; - /** * @title ICiphernodeRegistry * @notice Interface for managing ciphernode registration and committee selection * @dev This registry maintains an Incremental Merkle Tree (IMT) of registered ciphernodes - * and coordinates committee selection for E3 computations through registry filters + * and coordinates committee selection for E3 computations */ interface ICiphernodeRegistry { + /// @notice Struct representing a committee for an E3. + /// @param nodes Array of ciphernode addresses in the committee. + /// @param threshold The M/N threshold for the committee ([M, N]). + /// @param publicKey Hash of the committee's public key. + struct Committee { + address[] nodes; + uint32[2] threshold; + bytes32 publicKey; + } /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. - /// @param filter Address of the contract that will coordinate committee selection. /// @param threshold The M/N threshold for the committee. - event CommitteeRequested( - uint256 indexed e3Id, - address filter, - uint32[2] threshold - ); + event CommitteeRequested(uint256 indexed e3Id, uint32[2] threshold); /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. /// @param publicKey Public key of the committee. - event CommitteePublished(uint256 indexed e3Id, bytes publicKey); + event CommitteePublished( + uint256 indexed e3Id, + address[] nodes, + bytes publicKey + ); /// @notice This event MUST be emitted when a committee's active status changes. /// @param e3Id ID of the E3 for which the committee status changed. @@ -88,22 +94,21 @@ interface ICiphernodeRegistry { /// @notice Initiates the committee selection process for a specified E3. /// @dev This function MUST revert when not called by the Enclave contract. /// @param e3Id ID of the E3 for which to select the committee. - /// @param filter The address of the filter responsible for the committee selection process. /// @param threshold The M/N threshold for the committee. /// @return success True if committee selection was successfully initiated. function requestCommittee( uint256 e3Id, - address filter, uint32[2] calldata threshold ) external returns (bool success); /// @notice Publishes the public key resulting from the committee selection process. - /// @dev This function MUST revert if not called by the previously selected filter. + /// @dev This function MUST revert if not called by the owner. /// @param e3Id ID of the E3 for which to select the committee. - /// @param publicKey The hash of the public key generated by the given committee. + /// @param nodes Array of ciphernode addresses selected for the committee. + /// @param publicKey The public key generated by the given committee. function publishCommittee( uint256 e3Id, - bytes calldata proof, + address[] calldata nodes, bytes calldata publicKey ) external; @@ -116,19 +121,13 @@ interface ICiphernodeRegistry { uint256 e3Id ) external view returns (bytes32 publicKeyHash); - /// @notice This function should be called by the Enclave contract to get the filter for a given E3. - /// @dev This function MUST revert if no filter has been requested for the given E3. - /// @param e3Id ID of the E3 for which to get the filter. - /// @return filter The filter for the given E3. - function getFilter(uint256 e3Id) external view returns (address filter); - /// @notice This function should be called by the Enclave contract to get the committee for a given E3. /// @dev This function MUST revert if no committee has been requested for the given E3. /// @param e3Id ID of the E3 for which to get the committee. /// @return committee The committee for the given E3. function getCommittee( uint256 e3Id - ) external view returns (IRegistryFilter.Committee memory committee); + ) external view returns (Committee memory committee); /// @notice Returns the current root of the ciphernode IMT /// @return Current IMT root diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index a1030d4b08..e6f403b509 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -21,14 +21,8 @@ interface IEnclave { /// @notice This event MUST be emitted when an Encrypted Execution Environment (E3) is successfully requested. /// @param e3Id ID of the E3. /// @param e3 Details of the E3. - /// @param filter Address of the pool of nodes from which the Cipher Node committee was selected. /// @param e3Program Address of the Computation module selected. - event E3Requested( - uint256 e3Id, - E3 e3, - address filter, - IE3Program indexed e3Program - ); + event E3Requested(uint256 e3Id, E3 e3, IE3Program indexed e3Program); /// @notice This event MUST be emitted when an Encrypted Execution Environment (E3) is successfully activated. /// @param e3Id ID of the E3. @@ -119,7 +113,6 @@ interface IEnclave { //////////////////////////////////////////////////////////// /// @notice This struct contains the parameters to submit a request to Enclave. - /// @param filter The address of the pool of nodes from which to select the committee. /// @param threshold The M/N threshold for the committee. /// @param startWindow The start window for the computation. /// @param duration The duration of the computation in seconds. @@ -128,7 +121,6 @@ interface IEnclave { /// @param computeProviderParams The ABI encoded compute provider parameters. /// @param customParams Arbitrary ABI-encoded application-defined parameters. struct E3RequestParams { - address filter; uint32[2] threshold; uint256[2] startWindow; uint256 duration; diff --git a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol b/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol deleted file mode 100644 index 71a9b926c1..0000000000 --- a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -pragma solidity >=0.8.27; - -/** - * @title IRegistryFilter - * @notice Interface for filtering and selecting committee members from the registry - * @dev Registry filters implement committee selection algorithms for E3 computations - */ -interface IRegistryFilter { - /** - * @notice Committee data structure - * @param nodes Array of selected ciphernode addresses - * @param threshold M/N threshold for the committee (M required signatures out of N members) - * @param publicKey Hash of the committee's aggregated public key - */ - struct Committee { - address[] nodes; - uint32[2] threshold; - bytes32 publicKey; - } - - /// @notice Request a committee for an E3 computation - /// @dev This function is called by the CiphernodeRegistry to initiate committee selection - /// @param e3Id ID of the E3 computation - /// @param threshold M/N threshold for the committee - /// @return success Whether the committee request was successful - function requestCommittee( - uint256 e3Id, - uint32[2] calldata threshold - ) external returns (bool success); - - /// @notice Get the committee for an E3 computation - /// @dev This function returns the selected committee after it has been published - /// @param e3Id ID of the E3 computation - /// @return committee The selected committee data - function getCommittee( - uint256 e3Id - ) external view returns (Committee memory); -} diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 575e5e8e3d..0471cac748 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -6,7 +6,6 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { OwnableUpgradeable @@ -52,15 +51,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Incremental Merkle Tree (IMT) containing all registered ciphernodes LeanIMTData public ciphernodes; - /// @notice Maps E3 ID to its associated registry filter contract - mapping(uint256 e3Id => IRegistryFilter filter) public registryFilters; - /// @notice Maps E3 ID to the IMT root at the time of committee request mapping(uint256 e3Id => uint256 root) public roots; /// @notice Maps E3 ID to the hash of the committee's public key mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; + /// @notice Maps E3 ID to its committee data + mapping(uint256 e3Id => ICiphernodeRegistry.Committee committee) + public committees; + //////////////////////////////////////////////////////////// // // // Errors // @@ -73,9 +73,6 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Committee has already been published for this E3 error CommitteeAlreadyPublished(); - /// @notice Caller is not the authorized filter for this E3 - error OnlyFilter(); - /// @notice Committee has not been published yet for this E3 error CommitteeNotPublished(); @@ -167,32 +164,36 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @inheritdoc ICiphernodeRegistry function requestCommittee( uint256 e3Id, - address filter, uint32[2] calldata threshold ) external onlyEnclave returns (bool success) { require( - registryFilters[e3Id] == IRegistryFilter(address(0)), + committees[e3Id].threshold[1] == 0, CommitteeAlreadyRequested() ); - registryFilters[e3Id] = IRegistryFilter(filter); + committees[e3Id].threshold = threshold; roots[e3Id] = root(); - IRegistryFilter(filter).requestCommittee(e3Id, threshold); - emit CommitteeRequested(e3Id, filter, threshold); + emit CommitteeRequested(e3Id, threshold); success = true; } - /// @inheritdoc ICiphernodeRegistry + /// @notice Publishes a committee for an E3 computation + /// @dev Only callable by owner. Stores committee data and emits event + /// @param e3Id ID of the E3 computation + /// @param nodes Array of ciphernode addresses selected for the committee + /// @param publicKey Aggregated public key of the committee function publishCommittee( uint256 e3Id, - bytes calldata, + address[] calldata nodes, bytes calldata publicKey - ) external { - // only to be published by the filter - require(address(registryFilters[e3Id]) == msg.sender, OnlyFilter()); - - publicKeyHashes[e3Id] = keccak256(publicKey); - emit CommitteePublished(e3Id, publicKey); + ) external onlyOwner { + ICiphernodeRegistry.Committee storage committee = committees[e3Id]; + require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); + committee.nodes = nodes; + bytes32 publicKeyHash = keccak256(publicKey); + committee.publicKey = publicKeyHash; + publicKeyHashes[e3Id] = publicKeyHash; + emit CommitteePublished(e3Id, nodes, publicKey); } /// @inheritdoc ICiphernodeRegistry @@ -290,17 +291,12 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { return roots[e3Id]; } - /// @inheritdoc ICiphernodeRegistry - function getFilter(uint256 e3Id) public view returns (address filter) { - return address(registryFilters[e3Id]); - } - /// @inheritdoc ICiphernodeRegistry function getCommittee( uint256 e3Id - ) public view returns (IRegistryFilter.Committee memory committee) { - committee = registryFilters[e3Id].getCommittee(e3Id); - require(committee.nodes.length > 0, CommitteeNotPublished()); + ) public view returns (ICiphernodeRegistry.Committee memory committee) { + committee = committees[e3Id]; + require(committee.publicKey != bytes32(0), CommitteeNotPublished()); } /// @notice Returns the current size of the ciphernode IMT diff --git a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol deleted file mode 100644 index c8d6997bca..0000000000 --- a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -pragma solidity >=0.8.27; - -import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -/** - * @title NaiveRegistryFilter - * @notice Simple registry filter implementation for committee selection - * @dev Allows owner-controlled committee publication for E3 computations - */ -contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { - //////////////////////////////////////////////////////////// - // // - // Storage Variables // - // // - //////////////////////////////////////////////////////////// - - /// @notice Address of the ciphernode registry contract - address public registry; - - /// @notice Maps E3 ID to its committee data - mapping(uint256 e3 => IRegistryFilter.Committee committee) - public committees; - - //////////////////////////////////////////////////////////// - // // - // Errors // - // // - //////////////////////////////////////////////////////////// - - /// @notice Committee already exists for this E3 - error CommitteeAlreadyExists(); - - /// @notice Committee has already been published for this E3 - error CommitteeAlreadyPublished(); - - /// @notice Committee does not exist for this E3 - error CommitteeDoesNotExist(); - - /// @notice Committee has not been published yet - error CommitteeNotPublished(); - - /// @notice Ciphernode is not enabled in the registry - /// @param ciphernode Address of the ciphernode - error CiphernodeNotEnabled(address ciphernode); - - /// @notice Caller is not the registry contract - error OnlyRegistry(); - - //////////////////////////////////////////////////////////// - // // - // Modifiers // - // // - //////////////////////////////////////////////////////////// - - /// @dev Restricts function access to only the registry contract - modifier onlyRegistry() { - require(msg.sender == registry, OnlyRegistry()); - _; - } - - /// @dev Restricts function access to owner or eligible ciphernode - modifier onlyOwnerOrCiphernode() { - require( - msg.sender == owner() || - ICiphernodeRegistry(registry).isCiphernodeEligible(msg.sender), - CiphernodeNotEnabled(msg.sender) - ); - _; - } - - //////////////////////////////////////////////////////////// - // // - // Initialization // - // // - //////////////////////////////////////////////////////////// - - /// @notice Constructor that initializes the filter with owner and registry - /// @param _owner Address that will own the contract - /// @param _registry Address of the ciphernode registry - constructor(address _owner, address _registry) { - initialize(_owner, _registry); - } - - /// @notice Initializes the filter contract - /// @dev Can only be called once due to initializer modifier - /// @param _owner Address that will own the contract - /// @param _registry Address of the ciphernode registry - function initialize(address _owner, address _registry) public initializer { - __Ownable_init(msg.sender); - setRegistry(_registry); - if (_owner != owner()) transferOwnership(_owner); - } - - //////////////////////////////////////////////////////////// - // // - // Core Entrypoints // - // // - //////////////////////////////////////////////////////////// - - /// @inheritdoc IRegistryFilter - function requestCommittee( - uint256 e3Id, - uint32[2] calldata threshold - ) external onlyRegistry returns (bool success) { - require(committees[e3Id].threshold[1] == 0, CommitteeAlreadyExists()); - committees[e3Id].threshold = threshold; - success = true; - } - - /// @notice Publishes a committee for an E3 computation - /// @dev Only callable by owner. Stores committee data and notifies the registry - /// @param e3Id ID of the E3 computation - /// @param nodes Array of ciphernode addresses selected for the committee - /// @param publicKey Aggregated public key of the committee - function publishCommittee( - uint256 e3Id, - address[] calldata nodes, - bytes calldata publicKey - ) external onlyOwner { - IRegistryFilter.Committee storage committee = committees[e3Id]; - require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); - committee.nodes = nodes; - committee.publicKey = keccak256(publicKey); - ICiphernodeRegistry(registry).publishCommittee( - e3Id, - abi.encode(nodes), - publicKey - ); - } - - //////////////////////////////////////////////////////////// - // // - // Set Functions // - // // - //////////////////////////////////////////////////////////// - - /// @notice Sets the registry contract address - /// @dev Only callable by owner - /// @param _registry Address of the ciphernode registry contract - function setRegistry(address _registry) public onlyOwner { - registry = _registry; - } - - //////////////////////////////////////////////////////////// - // // - // Get Functions // - // // - //////////////////////////////////////////////////////////// - - /// @inheritdoc IRegistryFilter - function getCommittee( - uint256 e3Id - ) external view returns (IRegistryFilter.Committee memory) { - IRegistryFilter.Committee memory committee = committees[e3Id]; - require(committee.publicKey != bytes32(0), CommitteeNotPublished()); - return committee; - } -} diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index c31d567d85..2d3c057518 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -6,19 +6,13 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { function requestCommittee( uint256, - address filter, uint32[2] calldata ) external pure returns (bool success) { - if (filter == address(2)) { - success = false; - } else { - success = true; - } + success = true; } function isEnabled(address) external pure returns (bool) { @@ -45,20 +39,16 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { function publishCommittee( uint256, - bytes calldata, + address[] calldata, bytes calldata ) external pure {} // solhint-disable-line no-empty-blocks - function getFilter(uint256) external pure returns (address) { - return address(0); - } - function getCommittee( uint256 - ) external pure returns (IRegistryFilter.Committee memory) { + ) external pure returns (ICiphernodeRegistry.Committee memory) { address[] memory nodes = new address[](0); uint32[2] memory threshold = [uint32(0), uint32(0)]; - return IRegistryFilter.Committee(nodes, threshold, bytes32(0)); + return ICiphernodeRegistry.Committee(nodes, threshold, bytes32(0)); } function root() external pure returns (uint256) { @@ -87,14 +77,9 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function requestCommittee( uint256, - address filter, uint32[2] calldata ) external pure returns (bool success) { - if (filter == address(2)) { - success = false; - } else { - success = true; - } + success = true; } function isEnabled(address) external pure returns (bool) { @@ -117,20 +102,16 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function publishCommittee( uint256, - bytes calldata, + address[] calldata, bytes calldata ) external pure {} // solhint-disable-line no-empty-blocks - function getFilter(uint256) external pure returns (address) { - return address(0); - } - function getCommittee( uint256 - ) external pure returns (IRegistryFilter.Committee memory) { + ) external pure returns (ICiphernodeRegistry.Committee memory) { address[] memory nodes = new address[](0); uint32[2] memory threshold = [uint32(0), uint32(0)]; - return IRegistryFilter.Committee(nodes, threshold, bytes32(0)); + return ICiphernodeRegistry.Committee(nodes, threshold, bytes32(0)); } function root() external pure returns (uint256) { diff --git a/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol b/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol deleted file mode 100644 index e9f32f2d6f..0000000000 --- a/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -pragma solidity >=0.8.27; - -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -interface IRegistry { - function publishCommittee( - uint256 e3Id, - address[] calldata ciphernodes, - bytes calldata publicKey - ) external; -} - -contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { - //////////////////////////////////////////////////////////// - // // - // Storage Variables // - // // - //////////////////////////////////////////////////////////// - - address public registry; - - mapping(uint256 e3 => IRegistryFilter.Committee committee) - public committees; - - //////////////////////////////////////////////////////////// - // // - // Errors // - // // - //////////////////////////////////////////////////////////// - - error CommitteeAlreadyExists(); - error CommitteeAlreadyPublished(); - error CommitteeDoesNotExist(); - error CommitteeNotPublished(); - error OnlyRegistry(); - - //////////////////////////////////////////////////////////// - // // - // Modifiers // - // // - //////////////////////////////////////////////////////////// - - modifier onlyRegistry() { - require(msg.sender == registry, OnlyRegistry()); - _; - } - - //////////////////////////////////////////////////////////// - // // - // Initialization // - // // - //////////////////////////////////////////////////////////// - - constructor(address _owner, address _enclave) { - initialize(_owner, _enclave); - } - - function initialize(address _owner, address _registry) public initializer { - __Ownable_init(msg.sender); - setRegistry(_registry); - if (_owner != owner()) transferOwnership(_owner); - } - - //////////////////////////////////////////////////////////// - // // - // Core Entrypoints // - // // - //////////////////////////////////////////////////////////// - - function requestCommittee( - uint256 e3Id, - uint32[2] calldata threshold - ) external onlyRegistry returns (bool success) { - IRegistryFilter.Committee storage committee = committees[e3Id]; - require(committee.threshold.length == 0, CommitteeAlreadyExists()); - committee.threshold = threshold; - success = true; - } - - function publishCommittee( - uint256 e3Id, - address[] memory nodes, - bytes memory publicKey - ) external onlyOwner { - IRegistryFilter.Committee storage committee = committees[e3Id]; - require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); - committee.nodes = nodes; - committee.publicKey = keccak256(publicKey); - IRegistry(registry).publishCommittee(e3Id, nodes, publicKey); - } - - //////////////////////////////////////////////////////////// - // // - // Set Functions // - // // - //////////////////////////////////////////////////////////// - - function setRegistry(address _registry) public onlyOwner { - registry = _registry; - } - - //////////////////////////////////////////////////////////// - // // - // Get Functions // - // // - //////////////////////////////////////////////////////////// - - function getCommittee( - uint256 e3Id - ) external view returns (IRegistryFilter.Committee memory) { - IRegistryFilter.Committee memory committee = committees[e3Id]; - require(committee.publicKey != bytes32(0), CommitteeNotPublished()); - return committee; - } -} diff --git a/packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts b/packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts deleted file mode 100644 index c1a9a8913c..0000000000 --- a/packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("NaiveRegistryFilter", (m) => { - const ciphernodeRegistryAddress = m.getParameter("ciphernodeRegistryAddress"); - const owner = m.getParameter("owner"); - - const naiveRegistryFilter = m.contract("NaiveRegistryFilter", [ - owner, - ciphernodeRegistryAddress, - ]); - - return { naiveRegistryFilter }; -}) as any; diff --git a/packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts b/packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts deleted file mode 100644 index 913d7310b3..0000000000 --- a/packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; - -import NaiveRegistryFilterModule from "../../ignition/modules/naiveRegistryFilter"; -import { - NaiveRegistryFilter, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, -} from "../../types"; -import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; - -export interface NaiveRegistryFilterArgs { - ciphernodeRegistryAddress?: string; - owner?: string; - hre: HardhatRuntimeEnvironment; -} - -export const deployAndSaveNaiveRegistryFilter = async ({ - ciphernodeRegistryAddress, - owner, - hre, -}: NaiveRegistryFilterArgs): Promise<{ - naiveRegistryFilter: NaiveRegistryFilter; -}> => { - const { ignition, ethers } = await hre.network.connect(); - const [signer] = await ethers.getSigners(); - const chain = hre.globalOptions.network; - - const preDeployedArgs = readDeploymentArgs("NaiveRegistryFilter", chain); - if ( - !ciphernodeRegistryAddress || - !owner || - (preDeployedArgs?.constructorArgs?.ciphernodeRegistryAddress === - ciphernodeRegistryAddress && - preDeployedArgs?.constructorArgs?.owner === owner) - ) { - if (!preDeployedArgs?.address) { - throw new Error( - "NaiveRegistryFilter address not found, it must be deployed first", - ); - } - const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( - preDeployedArgs.address, - signer, - ); - return { naiveRegistryFilter: naiveRegistryFilterContract }; - } - - const naiveRegistryFilter = await ignition.deploy(NaiveRegistryFilterModule, { - parameters: { - NaiveRegistryFilter: { - ciphernodeRegistryAddress, - owner, - }, - }, - }); - - await naiveRegistryFilter.naiveRegistryFilter.waitForDeployment(); - - const naiveRegistryFilterAddress = - await naiveRegistryFilter.naiveRegistryFilter.getAddress(); - - const blockNumber = await ethers.provider.getBlockNumber(); - - storeDeploymentArgs( - { - constructorArgs: { - ciphernodeRegistryAddress: ciphernodeRegistryAddress, - owner, - }, - blockNumber, - address: naiveRegistryFilterAddress, - }, - "NaiveRegistryFilter", - chain, - ); - - const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( - naiveRegistryFilterAddress, - signer, - ); - - return { naiveRegistryFilter: naiveRegistryFilterContract }; -}; diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 2b93acc6f7..607aec5321 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -11,7 +11,6 @@ import { deployAndSaveEnclave } from "./deployAndSave/enclave"; import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; import { deployAndSaveMockStableToken } from "./deployAndSave/mockStableToken"; -import { deployAndSaveNaiveRegistryFilter } from "./deployAndSave/naiveRegistryFilter"; import { deployAndSaveSlashingManager } from "./deployAndSave/slashingManager"; import { deployMocks } from "./deployMocks"; @@ -119,15 +118,6 @@ export const deployEnclave = async (withMocks?: boolean) => { const enclaveAddress = await enclave.getAddress(); console.log("Enclave deployed to:", enclaveAddress); - console.log("Deploying NaiveRegistryFilter..."); - const { naiveRegistryFilter } = await deployAndSaveNaiveRegistryFilter({ - ciphernodeRegistryAddress: ciphernodeRegistryAddress, - owner: ownerAddress, - hre, - }); - const naiveRegistryFilterAddress = await naiveRegistryFilter.getAddress(); - console.log("NaiveRegistryFilter deployed to:", naiveRegistryFilterAddress); - /////////////////////////////////////////// // Configure cross-contract dependencies /////////////////////////////////////////// @@ -195,7 +185,6 @@ export const deployEnclave = async (withMocks?: boolean) => { BondingRegistry: ${bondingRegistryAddress} CiphernodeRegistry: ${ciphernodeRegistryAddress} Enclave: ${enclaveAddress} - NaiveRegistryFilter: ${naiveRegistryFilterAddress} ============================================ `); }; diff --git a/packages/enclave-contracts/scripts/index.ts b/packages/enclave-contracts/scripts/index.ts index 85f5499b89..7e66e8b1f2 100644 --- a/packages/enclave-contracts/scripts/index.ts +++ b/packages/enclave-contracts/scripts/index.ts @@ -13,7 +13,6 @@ export * from "./deployAndSave/enclave"; export * from "./deployAndSave/enclaveTicketToken"; export * from "./deployAndSave/enclaveToken"; export * from "./deployAndSave/mockStableToken"; -export * from "./deployAndSave/naiveRegistryFilter"; export * from "./deployAndSave/slashingManager"; export * from "./deployAndSave/mockComputeProvider"; export * from "./deployAndSave/mockDecryptionVerifier"; diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index a0975fb773..21551d7f43 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -20,13 +20,11 @@ import MockDecryptionVerifierModule from "../ignition/modules/mockDecryptionVeri import MockE3ProgramModule from "../ignition/modules/mockE3Program"; import MockInputValidatorModule from "../ignition/modules/mockInputValidator"; import MockStableTokenModule from "../ignition/modules/mockStableToken"; -import NaiveRegistryFilterModule from "../ignition/modules/naiveRegistryFilter"; import SlashingManagerModule from "../ignition/modules/slashingManager"; import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, Enclave__factory as EnclaveFactory, MockUSDC__factory as MockUSDCFactory, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, } from "../types"; import type { Enclave } from "../types/contracts/Enclave"; import type { MockUSDC } from "../types/contracts/test/MockStableToken.sol/MockUSDC"; @@ -176,30 +174,11 @@ describe("Enclave", function () { const ciphernodeRegistryAddress = await ciphernodeRegistry.cipherNodeRegistry.getAddress(); - const naiveRegistryFilter = await ignition.deploy( - NaiveRegistryFilterModule, - { - parameters: { - NaiveRegistryFilter: { - ciphernodeRegistryAddress, - owner: ownerAddress, - }, - }, - }, - ); - - const naiveRegistryFilterAddress = - await naiveRegistryFilter.naiveRegistryFilter.getAddress(); - const enclave = EnclaveFactory.connect(enclaveAddress, owner); const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, owner, ); - const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( - naiveRegistryFilterAddress, - owner, - ); const registryAddress = await enclave.ciphernodeRegistry(); if (registryAddress !== ciphernodeRegistryAddress) { @@ -258,7 +237,6 @@ describe("Enclave", function () { ); const request = { - filter: await naiveRegistryFilterContract.getAddress(), threshold: [2, 2] as [number, number], startWindow: [await time.latest(), (await time.latest()) + 100] as [ number, @@ -296,7 +274,6 @@ describe("Enclave", function () { return { enclave, ciphernodeRegistryContract, - naiveRegistryFilterContract, bondingRegistry: bondingRegistryContract.bondingRegistry, ticketToken: ticketTokenContract.enclaveTicketToken, licenseToken: enclTokenContract.enclaveToken, @@ -481,16 +458,10 @@ describe("Enclave", function () { }); it("returns correct E3 details", async function () { - const { - enclave, - request, - mocks, - naiveRegistryFilterContract, - usdcToken, - } = await loadFixture(setup); + const { enclave, request, mocks, ciphernodeRegistryContract, usdcToken } = + await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: await naiveRegistryFilterContract.getAddress(), threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -739,7 +710,6 @@ describe("Enclave", function () { const { enclave, request, usdcToken } = await loadFixture(setup); await expect( enclave.request({ - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -753,7 +723,6 @@ describe("Enclave", function () { it("reverts if threshold is 0", async function () { const { enclave, request, usdcToken } = await loadFixture(setup); const fee = await enclave.getE3Quote({ - filter: request.filter, threshold: [0, 2], startWindow: request.startWindow, duration: request.duration, @@ -765,7 +734,6 @@ describe("Enclave", function () { await usdcToken.approve(await enclave.getAddress(), fee); await expect( enclave.request({ - filter: request.filter, threshold: [0, 2], startWindow: request.startWindow, duration: request.duration, @@ -779,12 +747,11 @@ describe("Enclave", function () { .withArgs([0, 2]); }); it("reverts if threshold is greater than number", async function () { - const { enclave, request, naiveRegistryFilterContract, usdcToken } = + const { enclave, request, ciphernodeRegistryContract, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { - filter: await naiveRegistryFilterContract.getAddress(), threshold: [3, 2], startWindow: request.startWindow, duration: request.duration, @@ -798,12 +765,11 @@ describe("Enclave", function () { .withArgs([3, 2]); }); it("reverts if duration is 0", async function () { - const { enclave, request, naiveRegistryFilterContract, usdcToken } = + const { enclave, request, ciphernodeRegistryContract, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { - filter: await naiveRegistryFilterContract.getAddress(), threshold: request.threshold, startWindow: request.startWindow, duration: 0, @@ -817,12 +783,11 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if duration is greater than maxDuration", async function () { - const { enclave, request, naiveRegistryFilterContract, usdcToken } = + const { enclave, request, ciphernodeRegistryContract, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { - filter: await naiveRegistryFilterContract.getAddress(), threshold: request.threshold, startWindow: request.startWindow, duration: time.duration.days(31), @@ -836,12 +801,11 @@ describe("Enclave", function () { .withArgs(time.duration.days(31)); }); it("reverts if E3 Program is not enabled", async function () { - const { enclave, request, naiveRegistryFilterContract, usdcToken } = + const { enclave, request, ciphernodeRegistryContract, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { - filter: await naiveRegistryFilterContract.getAddress(), threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -855,12 +819,11 @@ describe("Enclave", function () { .withArgs(ethers.ZeroAddress); }); it("reverts if given encryption scheme is not enabled", async function () { - const { enclave, request, naiveRegistryFilterContract, usdcToken } = + const { enclave, request, ciphernodeRegistryContract, usdcToken } = await loadFixture(setup); await enclave.disableEncryptionScheme(encryptionSchemeId); await expect( makeRequest(enclave, usdcToken, { - filter: await naiveRegistryFilterContract.getAddress(), threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -874,23 +837,15 @@ describe("Enclave", function () { .withArgs(encryptionSchemeId); }); - it("reverts if committee selection fails", async function () { - const { enclave, request, usdcToken } = await loadFixture(setup); - - const invalidFilterAddress = "0x0000000000000000000000000000000000000002"; - - await expect( - makeRequest(enclave, usdcToken, { - ...request, - filter: invalidFilterAddress, - }), - ).to.be.revert(ethers); + it.skip("reverts if committee selection fails", async function () { + // This test is obsolete after removing NaiveRegistryFilter + // Committee selection no longer happens during request(), + // it happens externally before activate() is called }); it("instantiates a new E3", async function () { const { enclave, request, mocks, usdcToken } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -920,7 +875,6 @@ describe("Enclave", function () { it("emits E3Requested event", async function () { const { enclave, request, usdcToken } = await loadFixture(setup); const tx = await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -933,7 +887,7 @@ describe("Enclave", function () { await expect(tx) .to.emit(enclave, "E3Requested") - .withArgs(0, e3, request.filter, request.e3Program); + .withArgs(0, e3, request.e3Program); }); }); @@ -946,11 +900,10 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if E3 has already been activated", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -960,7 +913,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 0, [addressOne, AddressTwo], data, @@ -980,7 +933,6 @@ describe("Enclave", function () { ] as [number, number]; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: startTime, duration: request.duration, @@ -995,7 +947,7 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; const currentTime = await time.latest(); @@ -1009,7 +961,7 @@ describe("Enclave", function () { startWindow: startTime, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1030,7 +982,6 @@ describe("Enclave", function () { ] as [number, number]; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: startTime, duration: request.duration, @@ -1045,7 +996,7 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; const currentTime = await time.latest(); @@ -1056,7 +1007,7 @@ describe("Enclave", function () { startWindow: startTime, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1070,7 +1021,7 @@ describe("Enclave", function () { ); }); it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request, naiveRegistryFilterContract, usdcToken } = + const { enclave, request, ciphernodeRegistryContract, usdcToken } = await loadFixture(setup); await makeRequest(enclave, usdcToken, request); @@ -1082,16 +1033,14 @@ describe("Enclave", function () { await reg.mockCiphernodeRegistryEmptyKey.getAddress(); await enclave.setCiphernodeRegistry(nextRegistry); - await naiveRegistryFilterContract.setRegistry(nextRegistry); await expect( enclave.activate(0, ethers.ZeroHash), ).to.be.revertedWithCustomError(enclave, "CommitteeSelectionFailed"); await enclave.setCiphernodeRegistry(prevRegistry); - await naiveRegistryFilterContract.setRegistry(prevRegistry); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 0, [addressOne, AddressTwo], data, @@ -1101,16 +1050,10 @@ describe("Enclave", function () { }); it("sets committeePublicKey correctly", async () => { - const { - enclave, - request, - ciphernodeRegistryContract, - usdcToken, - naiveRegistryFilterContract, - } = await loadFixture(setup); + const { enclave, request, ciphernodeRegistryContract, usdcToken } = + await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1122,7 +1065,7 @@ describe("Enclave", function () { const e3Id = 0; - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1140,11 +1083,10 @@ describe("Enclave", function () { expect(e3.committeePublicKey).to.equal(publicKey); }); it("returns true if E3 is activated successfully", async () => { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1156,7 +1098,7 @@ describe("Enclave", function () { const e3Id = 0; - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1165,11 +1107,10 @@ describe("Enclave", function () { expect(await enclave.activate.staticCall(e3Id, data)).to.be.equal(true); }); it("emits E3Activated event", async () => { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1181,7 +1122,7 @@ describe("Enclave", function () { const e3Id = 0; - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1207,7 +1148,6 @@ describe("Enclave", function () { const { enclave, request, usdcToken } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1226,11 +1166,10 @@ describe("Enclave", function () { }); it("reverts if input is not valid", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1240,7 +1179,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 0, [addressOne, AddressTwo], data, @@ -1252,11 +1191,10 @@ describe("Enclave", function () { }); it("reverts if outside of input window", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1266,7 +1204,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 0, [addressOne, AddressTwo], data, @@ -1283,12 +1221,11 @@ describe("Enclave", function () { it("it allows publishing input to different requests", async function () { const fixtureSetup = () => setup(); - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(fixtureSetup); const inputData = "0x12345678"; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1298,7 +1235,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 0, [addressOne, AddressTwo], data, @@ -1307,7 +1244,6 @@ describe("Enclave", function () { await enclave.publishInput(0, inputData); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1317,7 +1253,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 1, [addressOne, AddressTwo], data, @@ -1326,12 +1262,11 @@ describe("Enclave", function () { await enclave.publishInput(1, inputData); }); it("returns true if input is published successfully", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const inputData = "0x12345678"; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1341,7 +1276,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( 0, [addressOne, AddressTwo], data, @@ -1354,14 +1289,13 @@ describe("Enclave", function () { }); it("adds inputHash to merkle tree", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); const tree = new LeanIMT(hash); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1373,7 +1307,7 @@ describe("Enclave", function () { const e3Id = 0; - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1391,11 +1325,10 @@ describe("Enclave", function () { expect(await enclave.getInputRoot(e3Id)).to.equal(tree.root); }); it("emits InputPublished event", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1408,7 +1341,7 @@ describe("Enclave", function () { const e3Id = 0; const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1435,7 +1368,6 @@ describe("Enclave", function () { const e3Id = 0; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: request.startWindow, duration: request.duration, @@ -1449,7 +1381,7 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if input deadline has not passed", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const currentTime = await time.latest(); await makeRequest(enclave, usdcToken, { @@ -1458,7 +1390,7 @@ describe("Enclave", function () { }); const e3Id = 0; - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1470,12 +1402,11 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "InputDeadlineNotPassed"); }); it("reverts if output has already been published", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: [await time.latest(), (await time.latest()) + 100], duration: request.duration, @@ -1485,7 +1416,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1501,12 +1432,11 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { - filter: request.filter, threshold: request.threshold, startWindow: [await time.latest(), (await time.latest()) + 100], duration: request.duration, @@ -1516,7 +1446,7 @@ describe("Enclave", function () { customParams: request.customParams, }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1528,7 +1458,7 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "InvalidOutput"); }); it("sets ciphertextOutput correctly", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1537,7 +1467,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1549,7 +1479,7 @@ describe("Enclave", function () { expect(e3.ciphertextOutput).to.equal(dataHash); }); it("returns true if output is published successfully", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1558,7 +1488,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1570,7 +1500,7 @@ describe("Enclave", function () { ).to.equal(true); }); it("emits CiphertextOutputPublished event", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1579,7 +1509,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1614,7 +1544,7 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if ciphertextOutput has not been published", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1623,7 +1553,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1634,7 +1564,7 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if plaintextOutput has already been published", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1643,7 +1573,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1660,7 +1590,7 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1669,7 +1599,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1682,7 +1612,7 @@ describe("Enclave", function () { .withArgs(data); }); it("sets plaintextOutput correctly", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1691,7 +1621,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1705,7 +1635,7 @@ describe("Enclave", function () { expect(e3.plaintextOutput).to.equal(data); }); it("returns true if output is published successfully", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1714,7 +1644,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, @@ -1727,7 +1657,7 @@ describe("Enclave", function () { ).to.equal(true); }); it("emits PlaintextOutputPublished event", async function () { - const { enclave, request, usdcToken, naiveRegistryFilterContract } = + const { enclave, request, usdcToken, ciphernodeRegistryContract } = await loadFixture(setup); const e3Id = 0; @@ -1736,7 +1666,7 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await naiveRegistryFilterContract.publishCommittee( + await ciphernodeRegistryContract.publishCommittee( e3Id, [addressOne, AddressTwo], data, diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 1acec05b61..e6d7893bf4 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -9,11 +9,7 @@ import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; -import NaiveRegistryFilterModule from "../../ignition/modules/naiveRegistryFilter"; -import { - CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, -} from "../../types"; +import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory } from "../../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; @@ -41,24 +37,10 @@ describe("CiphernodeRegistryOwnable", function () { }, }); - const filterContract = await ignition.deploy(NaiveRegistryFilterModule, { - parameters: { - NaiveRegistryFilter: { - owner: await owner.getAddress(), - ciphernodeRegistryAddress: - await registryContract.cipherNodeRegistry.getAddress(), - }, - }, - }); - const registry = CiphernodeRegistryFactory.connect( await registryContract.cipherNodeRegistry.getAddress(), owner, ); - const filter = NaiveRegistryFilterFactory.connect( - await filterContract.naiveRegistryFilter.getAddress(), - owner, - ); const tree = new LeanIMT(hash); await registry.addCiphernode(AddressOne); @@ -70,11 +52,9 @@ describe("CiphernodeRegistryOwnable", function () { owner, notTheOwner, registry, - filter, tree, request: { e3Id: 1, - filter: await filter.getAddress(), threshold: [2, 2] as [number, number], }, }; @@ -106,81 +86,29 @@ describe("CiphernodeRegistryOwnable", function () { describe("requestCommittee()", function () { it("reverts if committee has already been requested for given e3Id", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); + await registry.requestCommittee(request.e3Id, request.threshold); await expect( - registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), + registry.requestCommittee(request.e3Id, request.threshold), ).to.be.revertedWithCustomError(registry, "CommitteeAlreadyRequested"); }); - it("stores the registry filter for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.getFilter(request.e3Id)).to.equal(request.filter); - }); it("stores the root of the ciphernode registry at the time of the request", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); + await registry.requestCommittee(request.e3Id, request.threshold); expect(await registry.rootAt(request.e3Id)).to.equal( await registry.root(), ); }); - it("requests a committee from the given filter", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.getFilter(request.e3Id)).to.equal(request.filter); - }); it("emits a CommitteeRequested event", async function () { const { registry, request } = await loadFixture(setup); - await expect( - registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ) + await expect(registry.requestCommittee(request.e3Id, request.threshold)) .to.emit(registry, "CommitteeRequested") - .withArgs(request.e3Id, request.filter, request.threshold); - }); - it("reverts if filter.requestCommittee() fails", async function () { - const { owner, registry, filter, request } = await loadFixture(setup); - - await filter.setRegistry(await owner.getAddress()); - await filter.requestCommittee(request.e3Id, request.threshold); - await filter.setRegistry(await registry.getAddress()); - - await expect( - registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyExists"); + .withArgs(request.e3Id, request.threshold); }); it("returns true if the request is successful", async function () { const { registry, request } = await loadFixture(setup); expect( await registry.requestCommittee.staticCall( request.e3Id, - request.filter, request.threshold, ), ).to.be.true; @@ -188,25 +116,20 @@ describe("CiphernodeRegistryOwnable", function () { }); describe("publishCommittee()", function () { - it("reverts if the caller is not the filter for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); + it("reverts if the caller is not the owner", async function () { + const { registry, request, notTheOwner } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, request.threshold); + await expect( - registry.publishCommittee(request.e3Id, "0xc0de", data), - ).to.be.revertedWithCustomError(registry, "OnlyFilter"); + registry + .connect(notTheOwner) + .publishCommittee(request.e3Id, [AddressOne, AddressTwo], data), + ).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount"); }); it("stores the public key of the committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await filter.publishCommittee( + const { registry, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, request.threshold); + await registry.publishCommittee( request.e3Id, [AddressOne, AddressTwo], data, @@ -216,21 +139,17 @@ describe("CiphernodeRegistryOwnable", function () { ); }); it("emits a CommitteePublished event", async function () { - const { filter, registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); + const { registry, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, request.threshold); await expect( - await filter.publishCommittee( + await registry.publishCommittee( request.e3Id, [AddressOne, AddressTwo], data, ), ) .to.emit(registry, "CommitteePublished") - .withArgs(request.e3Id, data); + .withArgs(request.e3Id, [AddressOne, AddressTwo], data); }); }); @@ -333,13 +252,9 @@ describe("CiphernodeRegistryOwnable", function () { describe("committeePublicKey()", function () { it("returns the public key of the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await filter.publishCommittee( + const { registry, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, request.threshold); + await registry.publishCommittee( request.e3Id, [AddressOne, AddressTwo], data, @@ -350,11 +265,7 @@ describe("CiphernodeRegistryOwnable", function () { }); it("reverts if the committee has not been published", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); + await registry.requestCommittee(request.e3Id, request.threshold); await expect( registry.committeePublicKey(request.e3Id), ).to.be.revertedWithCustomError(registry, "CommitteeNotPublished"); @@ -393,27 +304,11 @@ describe("CiphernodeRegistryOwnable", function () { describe("rootAt()", function () { it("returns the root of the ciphernode registry merkle tree at the given e3Id", async function () { const { registry, tree, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); + await registry.requestCommittee(request.e3Id, request.threshold); expect(await registry.rootAt(request.e3Id)).to.equal(tree.root); }); }); - describe("getFilter()", function () { - it("returns the registry filter for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.getFilter(request.e3Id)).to.equal(request.filter); - }); - }); - describe("treeSize()", function () { it("returns the size of the ciphernode registry merkle tree", async function () { const { registry, tree } = await loadFixture(setup); diff --git a/packages/enclave-contracts/test/Registry/NaiveRegistryFilter.spec.ts b/packages/enclave-contracts/test/Registry/NaiveRegistryFilter.spec.ts deleted file mode 100644 index 83eeaadd25..0000000000 --- a/packages/enclave-contracts/test/Registry/NaiveRegistryFilter.spec.ts +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -import { LeanIMT } from "@zk-kit/lean-imt"; -import { expect } from "chai"; -import { network } from "hardhat"; -import { poseidon2 } from "poseidon-lite"; - -import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; -import NaiveRegistryFilterModule from "../../ignition/modules/naiveRegistryFilter"; -import { - CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, -} from "../../types"; - -const AddressOne = "0x0000000000000000000000000000000000000001"; -const AddressTwo = "0x0000000000000000000000000000000000000002"; -const AddressThree = "0x0000000000000000000000000000000000000003"; - -const { ethers, networkHelpers, ignition } = await network.connect(); -const { loadFixture } = networkHelpers; - -// Hash function used to compute the tree nodes. -const hash = (a: bigint, b: bigint) => poseidon2([a, b]); - -describe("NaiveRegistryFilter", function () { - async function setup() { - const [owner, notTheOwner] = await ethers.getSigners(); - - const registryContract = await ignition.deploy(CiphernodeRegistryModule, { - parameters: { - CiphernodeRegistry: { - enclaveAddress: await owner.getAddress(), - owner: await owner.getAddress(), - }, - }, - }); - - const filterContract = await ignition.deploy(NaiveRegistryFilterModule, { - parameters: { - NaiveRegistryFilter: { - owner: await owner.getAddress(), - ciphernodeRegistryAddress: - await registryContract.cipherNodeRegistry.getAddress(), - }, - }, - }); - - const registry = CiphernodeRegistryFactory.connect( - await registryContract.cipherNodeRegistry.getAddress(), - owner, - ); - const filter = NaiveRegistryFilterFactory.connect( - await filterContract.naiveRegistryFilter.getAddress(), - owner, - ); - - const tree = new LeanIMT(hash); - await registry.addCiphernode(AddressOne); - tree.insert(BigInt(AddressOne)); - await registry.addCiphernode(AddressTwo); - tree.insert(BigInt(AddressTwo)); - - return { - owner, - notTheOwner, - registry, - filter, - tree, - request: { - e3Id: 1, - filter: await filter.getAddress(), - threshold: [2, 2] as [number, number], - }, - }; - } - - describe("constructor / initialize()", function () { - it("should set the owner", async function () { - const { owner, filter } = await loadFixture(setup); - expect(await filter.owner()).to.equal(await owner.getAddress()); - }); - it("should set the registry", async function () { - const { registry, filter } = await loadFixture(setup); - expect(await filter.registry()).to.equal(await registry.getAddress()); - }); - }); - - describe("requestCommittee()", function () { - it("should revert if the caller is not the registry", async function () { - const { filter, request } = await loadFixture(setup); - await expect( - filter.requestCommittee(request.e3Id, request.threshold), - ).to.be.revertedWithCustomError(filter, "OnlyRegistry"); - }); - it("should revert if a committee has already been requested for the given e3Id", async function () { - const { filter, request, owner } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - await filter.requestCommittee(request.e3Id, request.threshold); - await expect( - filter.requestCommittee(request.e3Id, request.threshold), - ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyExists"); - }); - it("should set the threshold for the requested committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - await filter.setRegistry(await registry.getAddress()); - - await registry.requestCommittee( - request.e3Id, - await filter.getAddress(), - request.threshold, - ); - - const nodes = [AddressOne, AddressTwo]; - const publicKey = "0x1234567890abcdef"; - await filter.publishCommittee(request.e3Id, nodes, publicKey); - - const committee = await filter.getCommittee(request.e3Id); - expect(committee.threshold).to.deep.equal(request.threshold); - }); - it("should return true when a committee is requested", async function () { - const { filter, owner, request } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - const result = await filter.requestCommittee.staticCall( - request.e3Id, - request.threshold, - ); - expect(result).to.equal(true); - }); - }); - - describe("publishCommittee()", function () { - it("should revert if the caller is not owner", async function () { - const { filter, notTheOwner, request } = await loadFixture(setup); - await expect( - filter - .connect(notTheOwner) - .publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ), - ).to.be.revertedWithCustomError(filter, "OwnableUnauthorizedAccount"); - }); - it("should revert if committee already published", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - await expect( - filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ), - ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyPublished"); - }); - it("should store the node addresses of the committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.nodes).to.deep.equal([AddressOne, AddressTwo]); - }); - it("should store the public key of the committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.publicKey).to.equal(ethers.keccak256(AddressThree)); - }); - it("should publish the correct node addresses of the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.nodes).to.deep.equal([AddressOne, AddressTwo]); - }); - it("should publish the public key of the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.publicKey).to.equal(ethers.keccak256(AddressThree)); - }); - }); - - describe("setRegistry()", function () { - it("should revert if the caller is not the owner", async function () { - const { filter, notTheOwner } = await loadFixture(setup); - await expect( - filter.connect(notTheOwner).setRegistry(await notTheOwner.getAddress()), - ) - .to.be.revertedWithCustomError(filter, "OwnableUnauthorizedAccount") - .withArgs(await notTheOwner.getAddress()); - }); - it("should set the registry", async function () { - const { filter, owner } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - expect(await filter.registry()).to.equal(await owner.getAddress()); - }); - }); - - describe("getCommittee()", function () { - it("should return the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - expect( - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ), - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.threshold).to.deep.equal(request.threshold); - expect(committee.nodes).to.deep.equal([AddressOne, AddressTwo]); - expect(committee.publicKey).to.equal(ethers.keccak256(AddressThree)); - }); - }); -}); diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index e47c63cb74..11b078c4fb 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -5,7 +5,7 @@ chains: e3_program: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - filter_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + bonding_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" program: dev: true diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index ffe410bcac..665fd8955e 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -4,7 +4,7 @@ chains: contracts: enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - filter_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + bonding_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" nodes: cn1: From bade2f7aff7054455e7a612815712dd0da7238d6 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 14 Oct 2025 20:30:52 +0500 Subject: [PATCH 23/88] fix: crisp cli request fix --- crates/events/src/enclave_event/mod.rs | 9 ++ .../operator_activation_changed.rs | 9 ++ crates/evm-helpers/src/contracts.rs | 12 +- crates/evm-helpers/src/events.rs | 4 +- crates/evm/src/bonding_registry_sol.rs | 24 ++++ crates/sortition/src/node_state.rs | 54 ++++++++- deploy/.env.example | 2 +- deploy/agg.yaml | 2 +- deploy/cn1.yaml | 2 +- deploy/cn2.yaml | 2 +- deploy/cn3.yaml | 2 +- deploy/swarm_deployment.md | 12 +- examples/CRISP/Readme.md | 4 + examples/CRISP/enclave.config.yaml | 2 +- examples/CRISP/server/.env.example | 12 +- examples/CRISP/server/src/cli/commands.rs | 5 +- examples/CRISP/server/src/cli/main.rs | 10 +- examples/CRISP/server/src/config.rs | 1 - .../CRISP/server/src/server/routes/rounds.rs | 2 - .../enclave-contracts/deployed_contracts.json | 105 +++++++++++++++++- tests/integration/enclave.config.yaml | 2 +- tests/integration/fns.sh | 7 -- 22 files changed, 227 insertions(+), 57 deletions(-) create mode 100644 crates/events/src/enclave_event/operator_activation_changed.rs diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index f0d199e73c..9f8c589d72 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -16,6 +16,7 @@ mod e3_request_complete; mod e3_requested; mod enclave_error; mod keyshare_created; +mod operator_activation_changed; mod plaintext_aggregated; mod plaintext_output_published; mod publickey_aggregated; @@ -36,6 +37,7 @@ pub use e3_request_complete::*; pub use e3_requested::*; pub use enclave_error::*; pub use keyshare_created::*; +pub use operator_activation_changed::*; pub use plaintext_aggregated::*; pub use plaintext_output_published::*; pub use publickey_aggregated::*; @@ -115,6 +117,10 @@ pub enum EnclaveEvent { id: EventId, data: TicketBalanceUpdated, }, + OperatorActivationChanged { + id: EventId, + data: OperatorActivationChanged, + }, CommitteePublished { id: EventId, data: CommitteePublished, @@ -202,6 +208,7 @@ impl From for EventId { EnclaveEvent::CiphernodeAdded { id, .. } => id, EnclaveEvent::CiphernodeRemoved { id, .. } => id, EnclaveEvent::TicketBalanceUpdated { id, .. } => id, + EnclaveEvent::OperatorActivationChanged { id, .. } => id, EnclaveEvent::CommitteePublished { id, .. } => id, EnclaveEvent::PlaintextOutputPublished { id, .. } => id, EnclaveEvent::EnclaveError { id, .. } => id, @@ -240,6 +247,7 @@ impl EnclaveEvent { EnclaveEvent::CiphernodeAdded { data, .. } => format!("{}", data), EnclaveEvent::CiphernodeRemoved { data, .. } => format!("{}", data), EnclaveEvent::TicketBalanceUpdated { data, .. } => format!("{:?}", data), + EnclaveEvent::OperatorActivationChanged { data, .. } => format!("{:?}", data), EnclaveEvent::CommitteePublished { data, .. } => format!("{:?}", data), EnclaveEvent::PlaintextOutputPublished { data, .. } => format!("{:?}", data), EnclaveEvent::E3RequestComplete { data, .. } => format!("{}", data), @@ -264,6 +272,7 @@ impl_from_event!( CiphernodeAdded, CiphernodeRemoved, TicketBalanceUpdated, + OperatorActivationChanged, CommitteePublished, PlaintextOutputPublished, EnclaveError, diff --git a/crates/events/src/enclave_event/operator_activation_changed.rs b/crates/events/src/enclave_event/operator_activation_changed.rs new file mode 100644 index 0000000000..7ac161cf40 --- /dev/null +++ b/crates/events/src/enclave_event/operator_activation_changed.rs @@ -0,0 +1,9 @@ +use actix::Message; +use serde::{Deserialize, Serialize}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct OperatorActivationChanged { + pub operator: String, + pub active: bool, +} diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index 21ba7ae652..8c68b01a48 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -59,7 +59,6 @@ sol! { #[derive(Debug)] struct E3RequestParams { - address filter; uint32[2] threshold; uint256[2] startWindow; uint256 duration; @@ -115,7 +114,6 @@ pub trait EnclaveRead { /// Get the fee quote for an E3 request async fn get_e3_quote( &self, - filter: Address, threshold: [u32; 2], start_window: [U256; 2], duration: U256, @@ -131,7 +129,6 @@ pub trait EnclaveWrite { /// Request a new E3 async fn request_e3( &self, - filter: Address, threshold: [u32; 2], start_window: [U256; 2], duration: U256, @@ -349,7 +346,6 @@ where async fn get_e3_quote( &self, - filter: Address, threshold: [u32; 2], start_window: [U256; 2], duration: U256, @@ -358,7 +354,6 @@ where compute_provider_params: Bytes, ) -> Result { let e3_request = E3RequestParams { - filter, threshold, startWindow: start_window, duration, @@ -379,7 +374,6 @@ where impl EnclaveWrite for EnclaveContract { async fn request_e3( &self, - filter: Address, threshold: [u32; 2], start_window: [U256; 2], duration: U256, @@ -392,7 +386,6 @@ impl EnclaveWrite for EnclaveContract { let nonce = next_pending_nonce(&*self.provider).await?; let e3_request = E3RequestParams { - filter, threshold, startWindow: start_window, duration, @@ -403,10 +396,7 @@ impl EnclaveWrite for EnclaveContract { }; let contract = Enclave::new(self.contract_address, &self.provider); - let builder = contract - .request(e3_request) - .value(U256::from(1)) - .nonce(nonce); + let builder = contract.request(e3_request).nonce(nonce); let receipt = builder.send().await?.get_receipt().await?; Ok(receipt) diff --git a/crates/evm-helpers/src/events.rs b/crates/evm-helpers/src/events.rs index c455bf3f8c..56566c8b71 100644 --- a/crates/evm-helpers/src/events.rs +++ b/crates/evm-helpers/src/events.rs @@ -13,7 +13,7 @@ sol! { event E3Activated(uint256 e3Id, uint256 expiration, bytes committeePublicKey); #[derive(Debug)] - event E3Requested(uint256 e3Id, E3 e3, address filter, IE3Program indexed e3Program); + event E3Requested(uint256 e3Id, E3 e3, IE3Program indexed e3Program); #[derive(Debug)] interface IE3Program { @@ -59,5 +59,5 @@ sol! { event PlaintextOutputPublished(uint256 indexed e3Id, bytes plaintextOutput); #[derive(Debug)] - event CommitteePublished(uint256 indexed e3Id, bytes publicKey); + event CommitteePublished(uint256 indexed e3Id, address[] nodes, bytes publicKey); } diff --git a/crates/evm/src/bonding_registry_sol.rs b/crates/evm/src/bonding_registry_sol.rs index 794cad99dd..6f012e250e 100644 --- a/crates/evm/src/bonding_registry_sol.rs +++ b/crates/evm/src/bonding_registry_sol.rs @@ -45,6 +45,22 @@ impl From for EnclaveEvent { } } +impl From for e3_events::OperatorActivationChanged { + fn from(value: IBondingRegistry::OperatorActivationChanged) -> Self { + e3_events::OperatorActivationChanged { + operator: value.operator.to_string(), + active: value.active, + } + } +} + +impl From for EnclaveEvent { + fn from(value: IBondingRegistry::OperatorActivationChanged) -> Self { + let payload: e3_events::OperatorActivationChanged = value.into(); + EnclaveEvent::from(payload) + } +} + pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { match topic { Some(&IBondingRegistry::TicketBalanceUpdated::SIGNATURE_HASH) => { @@ -56,6 +72,14 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< event, chain_id, ))) } + Some(&IBondingRegistry::OperatorActivationChanged::SIGNATURE_HASH) => { + let Ok(event) = IBondingRegistry::OperatorActivationChanged::decode_log_data(data) + else { + error!("Error parsing event OperatorActivationChanged after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(event)) + } _topic => { trace!( topic=?_topic, diff --git a/crates/sortition/src/node_state.rs b/crates/sortition/src/node_state.rs index 1f585b579a..c96ecb21e6 100644 --- a/crates/sortition/src/node_state.rs +++ b/crates/sortition/src/node_state.rs @@ -10,19 +10,31 @@ use anyhow::Result; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ BusError, CommitteePublished, EnclaveErrorType, EnclaveEvent, EventBus, - PlaintextOutputPublished, Subscribe, TicketBalanceUpdated, + OperatorActivationChanged, PlaintextOutputPublished, Subscribe, TicketBalanceUpdated, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use tracing::{info, trace}; +use tracing::info; /// State for a single ciphernode -#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct NodeState { /// Current ticket balance for this node pub ticket_balance: U256, /// Number of active E3 jobs this node is currently participating in pub active_jobs: u64, + /// Whether this node is active (has met minimum requirements) + pub active: bool, +} + +impl Default for NodeState { + fn default() -> Self { + Self { + ticket_balance: U256::ZERO, + active_jobs: 0, + active: false, + } + } } /// State for all nodes across all chains @@ -64,10 +76,11 @@ impl NodeStateStore { } /// Get all nodes for a chain with their available tickets + /// Only includes active nodes pub fn get_nodes_with_tickets(&self, chain_id: u64) -> Vec<(String, u64)> { self.nodes .iter() - .filter(|((cid, _), _)| *cid == chain_id) + .filter(|((cid, _), node_state)| *cid == chain_id && node_state.active) .map(|((_, addr), _)| (addr.clone(), self.available_tickets(chain_id, addr))) .filter(|(_, tickets)| *tickets > 0) .collect() @@ -98,6 +111,11 @@ impl NodeStateManager { bus.send(Subscribe::new("TicketBalanceUpdated", addr.clone().into())) .await?; + bus.send(Subscribe::new( + "OperatorActivationChanged", + addr.clone().into(), + )) + .await?; bus.send(Subscribe::new("CommitteePublished", addr.clone().into())) .await?; bus.send(Subscribe::new( @@ -150,6 +168,9 @@ impl Handler for NodeStateManager { EnclaveEvent::TicketBalanceUpdated { data, .. } => { ctx.notify(data); } + EnclaveEvent::OperatorActivationChanged { data, .. } => { + ctx.notify(data); + } EnclaveEvent::CommitteePublished { data, .. } => { ctx.notify(data); } @@ -187,6 +208,31 @@ impl Handler for NodeStateManager { } } +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: OperatorActivationChanged, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + // We don't have chain_id in this event, so we need to update all entries for this operator + // In practice, an operator should only be registered on one chain, but we handle all just in case + for ((_, addr), node) in state.nodes.iter_mut() { + if addr == &msg.operator { + node.active = msg.active; + info!( + operator = %msg.operator, + active = msg.active, + "Updated operator active status" + ); + } + } + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} + impl Handler for NodeStateManager { type Result = (); diff --git a/deploy/.env.example b/deploy/.env.example index 14723a665a..5dd7d26b21 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,4 +1,4 @@ RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/API_KEY SEPOLIA_ENCLAVE_ADDRESS=0xCe087F31e20E2F76b6544A2E4A74D4557C8fDf77 SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS=0x0952388f6028a9Eda93a5041a3B216Ea331d97Ab -SEPOLIA_FILTER_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 +SEPOLIA_BONDING_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 diff --git a/deploy/agg.yaml b/deploy/agg.yaml index 806a2cc7bd..839d7ea8f7 100644 --- a/deploy/agg.yaml +++ b/deploy/agg.yaml @@ -13,4 +13,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" \ No newline at end of file + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/cn1.yaml b/deploy/cn1.yaml index 3bfc6f97c6..901e6662eb 100644 --- a/deploy/cn1.yaml +++ b/deploy/cn1.yaml @@ -7,4 +7,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/cn2.yaml b/deploy/cn2.yaml index 821bfa09e3..b05344e93b 100644 --- a/deploy/cn2.yaml +++ b/deploy/cn2.yaml @@ -11,4 +11,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/cn3.yaml b/deploy/cn3.yaml index 7112f0c2d6..cc889447f0 100644 --- a/deploy/cn3.yaml +++ b/deploy/cn3.yaml @@ -11,4 +11,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - bonding_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/swarm_deployment.md b/deploy/swarm_deployment.md index 6ea6377932..d850d69c53 100644 --- a/deploy/swarm_deployment.md +++ b/deploy/swarm_deployment.md @@ -13,7 +13,6 @@ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plug sudo docker run hello-world ``` - Initialize swarm ``` @@ -22,7 +21,6 @@ docker swarm init NOTE: If you get an error about not being able to choose between IP addresses choose the more private IP address. - ``` docker swarm init --advertise-addr 10.49.0.5 ``` @@ -86,7 +84,7 @@ Alter the variables to reflect the correct values required for the stack: export RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/ export SEPOLIA_ENCLAVE_ADDRESS=0xCe087F31e20E2F76b6544A2E4A74D4557C8fDf77 export SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS=0x0952388f6028a9Eda93a5041a3B216Ea331d97Ab -export SEPOLIA_FILTER_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 +export SEPOLIA_BONDING_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 ``` Pay special attention to the `RPC_URL` vars as here we use a standin API key value. @@ -96,20 +94,23 @@ You can peruse the yaml config files for the nodes to see how the vars are used # Secrets Setup Utils Script We have created a secrets setup utility to aid setting up the secrets for each node. - + To deploy with swarm we need to set up the secrets file for our cluster. ## Run + ```bash ./deploy/copy-secrets.sh ``` ## What it does + - Copies `example.secrets.json` to create `cn1/2/3` and `agg.secrets.json` files - Skips existing files - Warns with yellow arrows (==>) if any files are identical to the example ## Example output + ```bash Created cn1.secrets.json Skipping cn2.secrets.json - file already exists @@ -121,7 +122,7 @@ Remember to modify any highlighted files before use with unique secrets. # Deploy a version to the stack -To deploy +To deploy ``` ./deploy/deploy.sh enclave ghcr.io/gnosisguild/ciphernode:latest @@ -153,4 +154,3 @@ enclave_cn2.1.zom4r645ophf@nixos | 2024-12-19T23:47:08.582536Z INFO enclave: ``` This can help you identify which compilation you are looking at. This works by generating a unique ID based on the complication time. - diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index f0828a9f52..765e543d20 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -239,6 +239,7 @@ ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" NAIVE_REGISTRY_FILTER_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address +FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" # Mock ERC20 Token Address # E3 Config E3_WINDOW_SIZE=600 @@ -249,6 +250,9 @@ E3_DURATION=600 E3_COMPUTE_PROVIDER_NAME="RISC0" E3_COMPUTE_PROVIDER_PARALLEL=false E3_COMPUTE_PROVIDER_BATCH_SIZE=4 # Must be a power of 2 + +# Bitquery API Key (optional, leave empty if not using) +BITQUERY_API_KEY="" ``` ## Running Ciphernodes diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 8e34ceb846..0610115a58 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -4,7 +4,7 @@ chains: contracts: enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" program: dev: true diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 57ab1dc803..c3e4f2e262 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -13,20 +13,12 @@ BITQUERY_API_KEY="" CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -<<<<<<< HEAD ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -NAIVE_REGISTRY_FILTER_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" -E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address +BONDING_REGISTRY="0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" +E3_PROGRAM_ADDRESS="0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" -======= -ENCLAVE_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -CIPHERNODE_REGISTRY_ADDRESS="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -NAIVE_REGISTRY_FILTER_ADDRESS="0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -E3_PROGRAM_ADDRESS="0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" # CRISPProgram Contract Address ->>>>>>> dev - # E3 Config E3_WINDOW_SIZE=40 E3_THRESHOLD_MIN=1 diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index 1609cf19b1..2910981f24 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -74,7 +74,7 @@ pub async fn initialize_crisp_round( .await?; let e3_program: Address = CONFIG.e3_program_address.parse()?; - info!("Enabling E3 Program..."); + info!("Enabling E3 Program with address: {}", e3_program); match contract.is_e3_program_enabled(e3_program).await { Ok(enabled) => { if !enabled { @@ -100,7 +100,6 @@ pub async fn initialize_crisp_round( // Serialize the custom parameters to bytes. let custom_params_bytes = Bytes::from(serde_json::to_vec(&custom_params)?); - let filter: Address = CONFIG.naive_registry_filter_address.parse()?; let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; let start_window: [U256; 2] = [ U256::from(Utc::now().timestamp()), @@ -118,7 +117,6 @@ pub async fn initialize_crisp_round( info!("Getting fee quote..."); let fee_amount = contract .get_e3_quote( - filter, threshold, start_window, duration, @@ -141,7 +139,6 @@ pub async fn initialize_crisp_round( let res = contract .request_e3( - filter, threshold, start_window, duration, diff --git a/examples/CRISP/server/src/cli/main.rs b/examples/CRISP/server/src/cli/main.rs index e7669d90df..7a10be5e15 100644 --- a/examples/CRISP/server/src/cli/main.rs +++ b/examples/CRISP/server/src/cli/main.rs @@ -40,9 +40,13 @@ struct Cli { enum Commands { /// Initialize new E3 round Init { - #[arg(short, long)] + #[arg( + short, + long, + default_value = "0x0000000000000000000000000000000000000000" + )] token_address: String, - #[arg(short, long)] + #[arg(short, long, default_value = "1000000000000000000")] balance_threshold: String, }, } @@ -109,11 +113,13 @@ fn select_action() -> Result> { fn get_token_address() -> Result> { Ok(Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter the token contract address for the voting round") + .default("0x0000000000000000000000000000000000000000".to_string()) .interact_text()?) } fn get_balance_threshold() -> Result> { Ok(Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter the balance threshold for the voting round") + .default("1000000000000000000".to_string()) .interact_text()?) } diff --git a/examples/CRISP/server/src/config.rs b/examples/CRISP/server/src/config.rs index a677090693..2137118d2c 100644 --- a/examples/CRISP/server/src/config.rs +++ b/examples/CRISP/server/src/config.rs @@ -19,7 +19,6 @@ pub struct Config { pub enclave_address: String, pub e3_program_address: String, pub ciphernode_registry_address: String, - pub naive_registry_filter_address: String, pub fee_token_address: String, pub chain_id: u64, pub cron_api_key: String, diff --git a/examples/CRISP/server/src/server/routes/rounds.rs b/examples/CRISP/server/src/server/routes/rounds.rs index 2a74cc56b7..ccebc09d91 100644 --- a/examples/CRISP/server/src/server/routes/rounds.rs +++ b/examples/CRISP/server/src/server/routes/rounds.rs @@ -195,7 +195,6 @@ pub async fn initialize_crisp_round( let custom_params_bytes = Bytes::from(serde_json::to_vec(&custom_params)?); info!("Requesting E3..."); - let filter: Address = CONFIG.naive_registry_filter_address.parse()?; let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; let start_window: [U256; 2] = [ U256::from(Utc::now().timestamp()), @@ -211,7 +210,6 @@ pub async fn initialize_crisp_round( let compute_provider_params = Bytes::from(bincode::serialize(&compute_provider_params)?); let res = contract .request_e3( - filter, threshold, start_window, duration, diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 906896bb54..64b40f92fa 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -26,5 +26,108 @@ "blockNumber": 9181753, "address": "0x58708A1bf1AEdf8e75755FDa1882F8dc46985009" } + }, + "hardhat": { + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + } + }, + "localhost": { + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 4, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 5, + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "licenseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "blockNumber": 6, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "CiphernodeRegistry": { + "constructorArgs": { + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 8, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "Enclave": { + "constructorArgs": { + "params": "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "maxDuration": "2592000", + "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "bondingRegistry": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "feeToken": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "blockNumber": 10, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "MockComputeProvider": { + "blockNumber": 18, + "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + }, + "MockDecryptionVerifier": { + "blockNumber": 19, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + }, + "MockInputValidator": { + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, + "MockE3Program": { + "constructorArgs": { + "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, + "blockNumber": 21, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + } } -} +} \ No newline at end of file diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 665fd8955e..1943ea6987 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -4,7 +4,7 @@ chains: contracts: enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" nodes: cn1: diff --git a/tests/integration/fns.sh b/tests/integration/fns.sh index b63d37b935..8e799ce03e 100644 --- a/tests/integration/fns.sh +++ b/tests/integration/fns.sh @@ -19,13 +19,6 @@ PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" NETWORK_PRIVATE_KEY_AG="0x51a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams." -# These contracts are based on the deterministic order of hardhat deploy -# We _may_ wish to get these off the hardhat environment somehow? -ENCLAVE_CONTRACT="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" -REGISTRY_CONTRACT="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -REGISTRY_FILTER_CONTRACT="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" -INPUT_VALIDATOR_CONTRACT="0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" - # These are random addresses for now CIPHERNODE_ADDRESS_1="0x2546BcD3c84621e976D8185a91A922aE77ECEc30" CIPHERNODE_ADDRESS_2="0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" From 4005ae43da7cb81fb607e17db40b3c5216252a0b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 14 Oct 2025 20:34:53 +0500 Subject: [PATCH 24/88] chore: lint --- .../enclave-contracts/test/Enclave.spec.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 21551d7f43..3ff373b6b6 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -458,8 +458,7 @@ describe("Enclave", function () { }); it("returns correct E3 details", async function () { - const { enclave, request, mocks, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { enclave, request, mocks, usdcToken } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -747,8 +746,7 @@ describe("Enclave", function () { .withArgs([0, 2]); }); it("reverts if threshold is greater than number", async function () { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { @@ -765,8 +763,7 @@ describe("Enclave", function () { .withArgs([3, 2]); }); it("reverts if duration is 0", async function () { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { @@ -783,8 +780,7 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if duration is greater than maxDuration", async function () { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { @@ -801,8 +797,7 @@ describe("Enclave", function () { .withArgs(time.duration.days(31)); }); it("reverts if E3 Program is not enabled", async function () { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { @@ -819,8 +814,7 @@ describe("Enclave", function () { .withArgs(ethers.ZeroAddress); }); it("reverts if given encryption scheme is not enabled", async function () { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await enclave.disableEncryptionScheme(encryptionSchemeId); await expect( makeRequest(enclave, usdcToken, { From 6c95d1a958cffc9be3e61ee10da11c6f77bb2241 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 14 Oct 2025 20:36:23 +0500 Subject: [PATCH 25/88] chore: lint --- crates/cli/src/print_env.rs | 5 ++++- crates/sortition/src/ticket_bonding_sortition.rs | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/print_env.rs b/crates/cli/src/print_env.rs index 6da77762eb..072c5d932d 100644 --- a/crates/cli/src/print_env.rs +++ b/crates/cli/src/print_env.rs @@ -41,7 +41,10 @@ pub fn extract_env_vars(config: &AppConfig, chain: &str) -> String { env_vars.push(format!("ENCLAVE_ADDRESS={}", enclave_addr.address())); env_vars.push(format!("RPC_URL={}", chain.rpc_url)); env_vars.push(format!("REGISTRY_ADDRESS={}", registry_addr.address())); - env_vars.push(format!("BONDING_REGISTRY_ADDRESS={}", bonding_registry_addr.address())); + env_vars.push(format!( + "BONDING_REGISTRY_ADDRESS={}", + bonding_registry_addr.address() + )); if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("E3_PROGRAM_ADDRESS={}", e3_program.address())); } diff --git a/crates/sortition/src/ticket_bonding_sortition.rs b/crates/sortition/src/ticket_bonding_sortition.rs index 9b507aaae8..2c416463c6 100644 --- a/crates/sortition/src/ticket_bonding_sortition.rs +++ b/crates/sortition/src/ticket_bonding_sortition.rs @@ -56,7 +56,7 @@ impl TicketBondingSortition { message.extend_from_slice(&ticket_number.to_be_bytes()); message.extend_from_slice(&e3_id.to_be_bytes()); message.extend_from_slice(&seed.to_be_bytes()); - + let hash = keccak256(&message); BigUint::from_bytes_be(&hash.0) } @@ -274,4 +274,3 @@ mod tests { // Both have same available tickets, result is deterministic based on scores } } - From ee767e0ad36c6b64d710a1e576507b6ceb5272f0 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 14 Oct 2025 21:04:09 +0500 Subject: [PATCH 26/88] chore: merge dev --- crates/events/src/enclave_event/mod.rs | 2 ++ crates/sortition/src/sortition.rs | 32 ++++++++++++++++++++++---- examples/CRISP/client/.env.example | 2 +- examples/CRISP/server/.env.example | 2 +- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 3dfe71d30e..10ae4aef5a 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -45,6 +45,8 @@ pub use publickey_aggregated::*; pub use publish_document::*; pub use shutdown::*; pub use test_event::*; +pub use threshold_share_created::*; +pub use ticket_balance_updated::*; use crate::{E3id, ErrorEvent, Event, EventId}; use actix::Message; diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index f2cd5514b4..e5267254aa 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -383,6 +383,17 @@ impl SortitionList for BondingBackend { Ok(false) } + /// Get index using bonding-based sortition. + /// + /// Note: This implementation cannot access NodeStateStore directly, + /// so it returns None. For proper bonding sortition, use the + /// `Sortition` actor's `GetNodeIndex` message which has access to state. + fn get_index(&self, _seed: Seed, _size: usize, _address: String) -> Result> { + // BondingBackend requires NodeStateStore which isn't available here + // The Sortition actor will handle this by querying the node state + Ok(None) + } + fn add(&mut self, address: String) { self.nodes.insert(address); } @@ -409,6 +420,7 @@ impl SortitionList for SortitionBackend { match self { SortitionBackend::Distance(backend) => backend.get_index(seed, size, address), SortitionBackend::Score(backend) => backend.get_index(seed, size, address), + SortitionBackend::Bonding(backend) => backend.get_index(seed, size, address), } } fn add(&mut self, address: String) { @@ -608,7 +620,7 @@ impl Handler for Sortition { /// /// Errors while accessing persisted state or parsing the address are /// reported on the event bus and surfaced here as `None`. - #[instrument(name = "sortition_contains", skip_all)] + #[instrument(name = "sortition_get_index", skip_all)] fn handle(&mut self, msg: GetNodeIndex, _ctx: &mut Self::Context) -> Self::Result { self.list .try_with(|map| { @@ -625,20 +637,30 @@ impl Handler for Sortition { let sortition = TicketBondingSortition::new(msg.size); let target_addr: Address = msg.address.parse()?; - // Get committee and check if address is included + // Get committee and find the index of the address let committee = sortition.get_committee( &nodes_with_tickets, msg.chain_id, msg.seed.into(), )?; - Ok(committee.contains(&target_addr)) + // Find the index of the target address in the committee + let maybe_index = + committee.iter().enumerate().find_map(|(index, addr)| { + if addr == &target_addr { + Some(index as u64) + } else { + None + } + }); + + Ok(maybe_index) }); } } - // For other backends, use their native contains method - backend.contains(msg.seed, msg.size, msg.address.clone()) + // For other backends, use their native get_index method + backend.get_index(msg.seed, msg.size, msg.address.clone()) } else { Ok(None) } diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 322771dcb7..d7bd291d0a 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,4 +1,4 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9 # Default E3 program address from anvil +VITE_E3_PROGRAM_ADDRESS=0x09635F643e140090A9A8Dcd712eD6285858ceBef # Default E3 program address from anvil diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index c3e4f2e262..6fa078bcff 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -16,7 +16,7 @@ CRON_API_KEY=1234567890 ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" BONDING_REGISTRY="0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" -E3_PROGRAM_ADDRESS="0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" # E3 Config From c083619372074d19fe33bba145770601105b461c Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 14 Oct 2025 21:05:37 +0500 Subject: [PATCH 27/88] chore: add header --- .../events/src/enclave_event/operator_activation_changed.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/events/src/enclave_event/operator_activation_changed.rs b/crates/events/src/enclave_event/operator_activation_changed.rs index 7ac161cf40..2d528f9499 100644 --- a/crates/events/src/enclave_event/operator_activation_changed.rs +++ b/crates/events/src/enclave_event/operator_activation_changed.rs @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + use actix::Message; use serde::{Deserialize, Serialize}; From d79b5712363b4c1d852f93c4040e2e04434d6ac9 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 15 Oct 2025 16:42:24 +0500 Subject: [PATCH 28/88] fix: remove bonding registry artifacts from gitignore --- examples/CRISP/Readme.md | 3 +- examples/CRISP/server/.env.example | 1 - examples/CRISP/server/Readme.md | 2 +- packages/enclave-contracts/.gitignore | 4 +- .../IBondingRegistry.json | 855 ++++++++++++++++++ packages/enclave-contracts/package.json | 1 - packages/enclave-contracts/tasks/enclave.ts | 23 +- .../enclave-contracts/test/Enclave.spec.ts | 6 - .../default/client/src/pages/WizardSDK.tsx | 1 - .../default/client/src/utils/env-config.ts | 42 +- templates/default/hardhat.config.ts | 17 +- templates/default/tests/integration.spec.ts | 4 +- 12 files changed, 895 insertions(+), 64 deletions(-) create mode 100644 packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index 765e543d20..0162505c38 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -157,7 +157,7 @@ After deployment, you will see the addresses for the following contracts: - Enclave - Ciphernode Registry -- Naive Registry Filter +- Bonding Registry Filter - Mock Input Validator - Mock E3 Program - Mock Decryption Verifier @@ -237,7 +237,6 @@ CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -NAIVE_REGISTRY_FILTER_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" # Mock ERC20 Token Address diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 6fa078bcff..c529235a11 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -15,7 +15,6 @@ CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -BONDING_REGISTRY="0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" diff --git a/examples/CRISP/server/Readme.md b/examples/CRISP/server/Readme.md index ee1b7282bf..ce1e14e8e5 100644 --- a/examples/CRISP/server/Readme.md +++ b/examples/CRISP/server/Readme.md @@ -34,7 +34,7 @@ This is a Rust-based server implementation for CRISP, which is built on top of t ENCLAVE_ADDRESS=your_enclave_contract_address E3_PROGRAM_ADDRESS=your_e3_program_address CIPHERNODE_REGISTRY_ADDRESS=your_ciphernode_registry_address - NAIVE_REGISTRY_FILTER_ADDRESS=your_naive_registry_filter_address + FEE_TOKEN_ADDRESS=free_token_address CHAIN_ID=your_chain_id CRON_API_KEY=your_cron_api_key ``` diff --git a/packages/enclave-contracts/.gitignore b/packages/enclave-contracts/.gitignore index 1671543cfc..d778ae5d7a 100644 --- a/packages/enclave-contracts/.gitignore +++ b/packages/enclave-contracts/.gitignore @@ -10,10 +10,10 @@ !/artifacts/contracts/registry/ !/artifacts/contracts/interfaces/IEnclave.sol/ !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ -!/artifacts/contracts/registry/NaiveRegistryFilter.sol/ +!/artifacts/contracts/interfaces/IBondingRegistry.sol/ !/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json -!/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +!/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json build cache coverage diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json new file mode 100644 index 0000000000..eb7bc983b0 --- /dev/null +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -0,0 +1,855 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "IBondingRegistry", + "sourceName": "contracts/interfaces/IBondingRegistry.sol", + "abi": [ + { + "inputs": [], + "name": "AlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "CiphernodeBanned", + "type": "error" + }, + { + "inputs": [], + "name": "ExitInProgress", + "type": "error" + }, + { + "inputs": [], + "name": "ExitNotReady", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidConfiguration", + "type": "error" + }, + { + "inputs": [], + "name": "NoPendingDeregistration", + "type": "error" + }, + { + "inputs": [], + "name": "NotLicensed", + "type": "error" + }, + { + "inputs": [], + "name": "NotRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyRewardDistributor", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "unlockAt", + "type": "uint64" + } + ], + "name": "CiphernodeDeregistrationRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "parameter", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldValue", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newValue", + "type": "uint256" + } + ], + "name": "ConfigurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "delta", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBond", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "LicenseBondUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "name": "OperatorActivationChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + } + ], + "name": "SlashedFundsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "delta", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "TicketBalanceUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "availableTickets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "bondLicense", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxTicketAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLicenseAmount", + "type": "uint256" + } + ], + "name": "claimExits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "siblingNodes", + "type": "uint256[]" + } + ], + "name": "deregisterOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "rewardToken", + "type": "address" + }, + { + "internalType": "address[]", + "name": "operators", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "distributeRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exitDelay", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "getLicenseBond", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "getTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getTicketBalanceAtBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "hasExitInProgress", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isLicensed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "licenseRequiredBond", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "pendingExits", + "outputs": [ + { + "internalType": "uint256", + "name": "ticket", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "license", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "previewClaimable", + "outputs": [ + { + "internalType": "uint256", + "name": "ticket", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "license", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registerOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "removeTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "newExitDelay", + "type": "uint64" + } + ], + "name": "setExitDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newBps", + "type": "uint256" + } + ], + "name": "setLicenseActiveBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newLicenseRequiredBond", + "type": "uint256" + } + ], + "name": "setLicenseRequiredBond", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "newLicenseToken", + "type": "address" + } + ], + "name": "setLicenseToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinTicketBalance", + "type": "uint256" + } + ], + "name": "setMinTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ICiphernodeRegistry", + "name": "newRegistry", + "type": "address" + } + ], + "name": "setRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newRewardDistributor", + "type": "address" + } + ], + "name": "setRewardDistributor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newSlashedFundsTreasury", + "type": "address" + } + ], + "name": "setSlashedFundsTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newSlashingManager", + "type": "address" + } + ], + "name": "setSlashingManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newTicketPrice", + "type": "uint256" + } + ], + "name": "setTicketPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract EnclaveTicketToken", + "name": "newTicketToken", + "type": "address" + } + ], + "name": "setTicketToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "slashLicenseBond", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "slashTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "slashedFundsTreasury", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "slashedLicenseBond", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "slashedTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ticketPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unbondLicense", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + } + ], + "name": "withdrawSlashedFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", + "buildInfoId": "solc-0_8_27-944178351e7c6a2ade6cf3019319b93f86f65a3f" +} \ No newline at end of file diff --git a/packages/enclave-contracts/package.json b/packages/enclave-contracts/package.json index de8096b7c2..128f3fa76b 100644 --- a/packages/enclave-contracts/package.json +++ b/packages/enclave-contracts/package.json @@ -162,7 +162,6 @@ "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", "test": "hardhat test mocha", "test:report-gas": "REPORT_GAS=true hardhat test mocha", - "test:registryFilter": "pnpm run test test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts", "test:enclave": "pnpm run test test/Enclave.spec.ts", "test:ciphernodeRegistry": "pnpm run test test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts", "prerelease": "pnpm clean && pnpm compile && pnpm typechain", diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index f70b970f15..404400641b 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -127,15 +127,6 @@ export const requestCommittee = task( throw new Error("CiphernodeRegistry deployment arguments not found"); } - const filterArgs = readDeploymentArgs( - "NaiveRegistryFilter", - hre.globalOptions.network, - ); - - if (!filterArgs) { - throw new Error("NaiveRegistryFilter deployment arguments not found"); - } - const mockE3ProgramArgs = readDeploymentArgs( "MockE3Program", hre.globalOptions.network, @@ -172,7 +163,6 @@ export const requestCommittee = task( } const requestParams = { - filter: filter === ZeroAddress ? filterArgs.address : filter, threshold: [thresholdQuorum, thresholdTotal] as [number, number], startWindow: [windowStart, windowEnd] as [number, number], duration: duration, @@ -266,13 +256,14 @@ export const publishCommittee = task( }) .setAction(async () => ({ default: async ({ e3Id, nodes, publicKey }, hre) => { - const { deployAndSaveNaiveRegistryFilter } = await import( - "../scripts/deployAndSave/naiveRegistryFilter" + const { deployAndSaveCiphernodeRegistryOwnable } = await import( + "../scripts/deployAndSave/ciphernodeRegistryOwnable" ); - const { naiveRegistryFilter } = await deployAndSaveNaiveRegistryFilter({ - hre, - }); + const { ciphernodeRegistry } = + await deployAndSaveCiphernodeRegistryOwnable({ + hre, + }); const nodesToSend = nodes .split(",") @@ -283,7 +274,7 @@ export const publishCommittee = task( throw new Error("Invalid nodes format: no valid addresses found"); } - const tx = await naiveRegistryFilter.publishCommittee( + const tx = await ciphernodeRegistry.publishCommittee( e3Id, nodesToSend, publicKey, diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 3ff373b6b6..bc46ff8eeb 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -830,12 +830,6 @@ describe("Enclave", function () { .to.be.revertedWithCustomError(enclave, "InvalidEncryptionScheme") .withArgs(encryptionSchemeId); }); - - it.skip("reverts if committee selection fails", async function () { - // This test is obsolete after removing NaiveRegistryFilter - // Committee selection no longer happens during request(), - // it happens externally before activate() is called - }); it("instantiates a new E3", async function () { const { enclave, request, mocks, usdcToken } = await loadFixture(setup); diff --git a/templates/default/client/src/pages/WizardSDK.tsx b/templates/default/client/src/pages/WizardSDK.tsx index 2b62b65d64..1213933e06 100644 --- a/templates/default/client/src/pages/WizardSDK.tsx +++ b/templates/default/client/src/pages/WizardSDK.tsx @@ -725,7 +725,6 @@ const WizardSDK: React.FC = () => { console.log('requestE3') const hash = await requestE3({ - filter: contracts.filterRegistry, threshold, startWindow, duration, diff --git a/templates/default/client/src/utils/env-config.ts b/templates/default/client/src/utils/env-config.ts index 464edc1064..05b414b313 100644 --- a/templates/default/client/src/utils/env-config.ts +++ b/templates/default/client/src/utils/env-config.ts @@ -7,19 +7,19 @@ export const ENCLAVE_ADDRESS = import.meta.env.VITE_ENCLAVE_ADDRESS export const E3_PROGRAM_ADDRESS = import.meta.env.VITE_E3_PROGRAM_ADDRESS export const REGISTRY_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS -export const FILTER_REGISTRY_ADDRESS = import.meta.env.VITE_FILTER_REGISTRY_ADDRESS +export const BONDING_REGISTRY_ADDRESS = import.meta.env.VITE_BONDING_REGISTRY_ADDRESS export const RPC_URL = import.meta.env.VITE_RPC_URL || 'http://localhost:8545' const requiredEnvVars = { - VITE_ENCLAVE_ADDRESS: ENCLAVE_ADDRESS, - VITE_E3_PROGRAM_ADDRESS: E3_PROGRAM_ADDRESS, - VITE_REGISTRY_ADDRESS: REGISTRY_ADDRESS, - VITE_FILTER_REGISTRY_ADDRESS: FILTER_REGISTRY_ADDRESS, + VITE_ENCLAVE_ADDRESS: ENCLAVE_ADDRESS, + VITE_E3_PROGRAM_ADDRESS: E3_PROGRAM_ADDRESS, + VITE_REGISTRY_ADDRESS: REGISTRY_ADDRESS, + VITE_BONDING_REGISTRY_ADDRESS: BONDING_REGISTRY_ADDRESS, } export const MISSING_ENV_VARS = Object.entries(requiredEnvVars) - .filter(([, value]) => !value) - .map(([key]) => key) + .filter(([, value]) => !value) + .map(([key]) => key) export const HAS_MISSING_ENV_VARS = MISSING_ENV_VARS.length > 0 @@ -27,23 +27,23 @@ export const HAS_MISSING_ENV_VARS = MISSING_ENV_VARS.length > 0 * Validate environment variables and throw an error if any are missing */ export function validateEnvVars(): void { - if (HAS_MISSING_ENV_VARS) { - throw new Error( - `Missing required environment variables: ${MISSING_ENV_VARS.join(', ')}\n` + - 'Please check your .env file and ensure all required variables are set.' - ) - } + if (HAS_MISSING_ENV_VARS) { + throw new Error( + `Missing required environment variables: ${MISSING_ENV_VARS.join(', ')}\n` + + 'Please check your .env file and ensure all required variables are set.', + ) + } } /** * Get validated contract addresses */ export function getContractAddresses() { - validateEnvVars() - return { - enclave: ENCLAVE_ADDRESS as `0x${string}`, - ciphernodeRegistry: REGISTRY_ADDRESS as `0x${string}`, - filterRegistry: FILTER_REGISTRY_ADDRESS as `0x${string}`, - e3Program: E3_PROGRAM_ADDRESS as `0x${string}`, - } -} \ No newline at end of file + validateEnvVars() + return { + enclave: ENCLAVE_ADDRESS as `0x${string}`, + ciphernodeRegistry: REGISTRY_ADDRESS as `0x${string}`, + bondingRegistry: BONDING_REGISTRY_ADDRESS as `0x${string}`, + e3Program: E3_PROGRAM_ADDRESS as `0x${string}`, + } +} diff --git a/templates/default/hardhat.config.ts b/templates/default/hardhat.config.ts index 47eefe39e4..5636d732d9 100644 --- a/templates/default/hardhat.config.ts +++ b/templates/default/hardhat.config.ts @@ -76,10 +76,7 @@ function getChainConfig(chain: keyof typeof chainIds, apiUrl: string) { } const config: HardhatUserConfig = { - tasks: [ - ciphernodeAdd, - cleanDeploymentsTask, - ], + tasks: [ciphernodeAdd, cleanDeploymentsTask], plugins: [ hardhatTypechainPlugin, hardhatEthersChaiMatchers, @@ -106,32 +103,32 @@ const config: HardhatUserConfig = { }, arbitrum: getChainConfig( "arbitrum-mainnet", - process.env.ARBISCAN_API_KEY || "", + process.env.ARBISCAN_API_KEY || "" ), avalanche: getChainConfig("avalanche", process.env.SNOWTRACE_API_KEY || ""), bsc: getChainConfig("bsc", process.env.BSCSCAN_API_KEY || ""), mainnet: getChainConfig("mainnet", process.env.ETHERSCAN_API_KEY || ""), optimism: getChainConfig( "optimism-mainnet", - process.env.OPTIMISM_API_KEY || "", + process.env.OPTIMISM_API_KEY || "" ), "polygon-mainnet": getChainConfig( "polygon-mainnet", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), "polygon-mumbai": getChainConfig( "polygon-mumbai", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), sepolia: getChainConfig("sepolia", process.env.ETHERSCAN_API_KEY || ""), goerli: getChainConfig("goerli", process.env.ETHERSCAN_API_KEY || ""), }, solidity: { npmFilesToBuild: [ - "poseidon-solidity/PoseidonT3.sol", + "poseidon-solidity/PoseidonT3.sol", "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", - "@enclave-e3/contracts/contracts/registry/NaiveRegistryFilter.sol", + "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index 23a08b55be..70df6c8f30 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -27,7 +27,7 @@ export function getContractAddresses() { return { enclave: process.env.ENCLAVE_ADDRESS as `0x${string}`, ciphernodeRegistry: process.env.REGISTRY_ADDRESS as `0x${string}`, - filterRegistry: process.env.FILTER_REGISTRY_ADDRESS as `0x${string}`, + bondingRegistry: process.env.BONDING_REGISTRY_ADDRESS as `0x${string}`, e3Program: process.env.E3_PROGRAM_ADDRESS as `0x${string}`, }; } @@ -199,7 +199,6 @@ describe("Integration", () => { await waitForEvent(EnclaveEventType.E3_REQUESTED, async () => { console.log("Requested E3..."); await sdk.requestE3({ - filter: contracts.filterRegistry, threshold, startWindow, duration, @@ -212,7 +211,6 @@ describe("Integration", () => { state = store.get(0n); assert(state); assert.strictEqual(state.e3Id, 0n); - assert.strictEqual(state.filter, contracts.filterRegistry); assert.strictEqual(state.type, "requested"); // Ciphernodes will publish a public key within the COMMITTEE_PUBLISHED event From 59f64d5bc9e96cddd9b627c377d0338b846010a2 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 15 Oct 2025 16:45:42 +0500 Subject: [PATCH 29/88] fix: remove filter --- docs/pages/setting-up-server.mdx | 2 -- packages/enclave-contracts/tasks/enclave.ts | 1 - packages/enclave-react/README.md | 1 - packages/enclave-sdk/README.md | 2 -- packages/enclave-sdk/src/contract-client.ts | 3 +-- packages/enclave-sdk/src/enclave-sdk.ts | 4 +--- 6 files changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/pages/setting-up-server.mdx b/docs/pages/setting-up-server.mdx index 8f6923419f..cbbd12f783 100644 --- a/docs/pages/setting-up-server.mdx +++ b/docs/pages/setting-up-server.mdx @@ -74,7 +74,6 @@ await sdk.initialize() // Request a new E3 computation const hash = await sdk.requestE3({ - filter: '0x0000000000000000000000000000000000000000', threshold: [2, 3], startWindow: [BigInt(0), BigInt(100)], duration: BigInt(3600), @@ -139,7 +138,6 @@ function E3Dashboard() { const handleRequestE3 = async () => { try { const hash = await requestE3({ - filter: '0x0000000000000000000000000000000000000000', threshold: [2, 3], startWindow: [BigInt(Date.now()), BigInt(Date.now() + 300000)], duration: BigInt(1800), diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index 404400641b..c52c98596f 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -77,7 +77,6 @@ export const requestCommittee = task( .setAction(async () => ({ default: async ( { - filter, thresholdQuorum, thresholdTotal, windowStart, diff --git a/packages/enclave-react/README.md b/packages/enclave-react/README.md index eaf1c33e2c..8343a11207 100644 --- a/packages/enclave-react/README.md +++ b/packages/enclave-react/README.md @@ -61,7 +61,6 @@ function MyComponent() { const handleRequest = async () => { try { const hash = await requestE3({ - filter: "0x...", threshold: [2, 3], startWindow: [BigInt(Date.now()), BigInt(Date.now() + 300000)], duration: BigInt(1800), diff --git a/packages/enclave-sdk/README.md b/packages/enclave-sdk/README.md index 611e1cf841..dff910667f 100644 --- a/packages/enclave-sdk/README.md +++ b/packages/enclave-sdk/README.md @@ -66,7 +66,6 @@ sdk.onEnclaveEvent(RegistryEventType.CIPHERNODE_ADDED, (event) => { // Interact with contracts const hash = await sdk.requestE3({ - filter: "0x...", threshold: [1, 3], startWindow: [BigInt(0), BigInt(100)], duration: BigInt(3600), @@ -212,7 +211,6 @@ function MyComponent() { ```typescript // Request a new E3 computation await sdk.requestE3({ - filter: `0x${string}`, threshold: [number, number], startWindow: [bigint, bigint], duration: bigint, diff --git a/packages/enclave-sdk/src/contract-client.ts b/packages/enclave-sdk/src/contract-client.ts index 93f2e18fda..3fcc93b576 100644 --- a/packages/enclave-sdk/src/contract-client.ts +++ b/packages/enclave-sdk/src/contract-client.ts @@ -75,7 +75,6 @@ export class ContractClient { * request(address filter, uint32[2] threshold, uint256[2] startWindow, uint256 duration, IE3Program e3Program, bytes e3ProgramParams, bytes computeProviderParams, bytes customParams) */ public async requestE3( - filter: `0x${string}`, threshold: [number, number], startWindow: [bigint, bigint], duration: bigint, @@ -109,13 +108,13 @@ export class ContractClient { functionName: "request", args: [ { - filter, threshold, startWindow, duration, e3Program, e3ProgramParams, computeProviderParams, + customParams: customParams || "0x", }, ], account, diff --git a/packages/enclave-sdk/src/enclave-sdk.ts b/packages/enclave-sdk/src/enclave-sdk.ts index 348f846c3d..75ae5ed235 100644 --- a/packages/enclave-sdk/src/enclave-sdk.ts +++ b/packages/enclave-sdk/src/enclave-sdk.ts @@ -177,7 +177,6 @@ export class EnclaveSDK { * Request a new E3 computation */ public async requestE3(params: { - filter: `0x${string}`; threshold: [number, number]; startWindow: [bigint, bigint]; duration: bigint; @@ -194,14 +193,13 @@ export class EnclaveSDK { } return this.contractClient.requestE3( - params.filter, params.threshold, params.startWindow, params.duration, params.e3Program, params.e3ProgramParams, params.computeProviderParams, - params.value, + params.customParams, params.gasLimit ); } From a2074e0ee4621e9a3c4c44e24c23fa2bc734817b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 16 Oct 2025 00:55:26 +0500 Subject: [PATCH 30/88] chore: fix test --- crates/events/src/enclave_event/mod.rs | 3 +++ .../interfaces/IBondingRegistry.sol/IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.sol/ICiphernodeRegistry.json | 2 +- .../artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 10ae4aef5a..4fbac70bc4 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -232,6 +232,9 @@ impl EnclaveEvent { EnclaveEvent::DecryptionshareCreated { data, .. } => Some(data.e3_id), EnclaveEvent::PlaintextAggregated { data, .. } => Some(data.e3_id), EnclaveEvent::CiphernodeSelected { data, .. } => Some(data.e3_id), + EnclaveEvent::ThresholdShareCreated { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteePublished { data, .. } => Some(data.e3_id), + EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), _ => None, } } diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index eb7bc983b0..f08fd5bdb5 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_27-944178351e7c6a2ade6cf3019319b93f86f65a3f" + "buildInfoId": "solc-0_8_27-4fe15502d3a80277a9221a66fbbdc6c08701e71b" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 72389614a7..f4458984a7 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -403,5 +403,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_27-944178351e7c6a2ade6cf3019319b93f86f65a3f" + "buildInfoId": "solc-0_8_27-4fe15502d3a80277a9221a66fbbdc6c08701e71b" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 25f61f8588..e7434250e1 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_27-944178351e7c6a2ade6cf3019319b93f86f65a3f" + "buildInfoId": "solc-0_8_27-4fe15502d3a80277a9221a66fbbdc6c08701e71b" } \ No newline at end of file From a4b219c1e8f9ec520a2af2a62f153e46b4013e7d Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 15:43:10 +0500 Subject: [PATCH 31/88] feat: onchain committee sortition --- .../src/threshold_plaintext_aggregator.rs | 2 +- crates/config/src/contract.rs | 1 + crates/config/src/store_keys.rs | 4 + .../src/enclave_event/ciphernode_selected.rs | 2 + .../src/enclave_event/committee_finalized.rs | 24 + .../enclave_event/configuration_updated.rs | 29 + crates/events/src/enclave_event/mod.rs | 29 + .../src/enclave_event/ticket_submitted.rs | 27 + crates/evm/src/bonding_registry_sol.rs | 29 + crates/evm/src/committee_sortition_sol.rs | 310 ++++++++++ crates/evm/src/lib.rs | 4 + crates/evm/src/repo.rs | 13 + crates/sortition/src/ciphernode_selector.rs | 16 +- crates/sortition/src/lib.rs | 2 - crates/sortition/src/node_state.rs | 56 +- crates/sortition/src/sortition.rs | 361 +++++------- .../sortition/src/ticket_bonding_sortition.rs | 276 --------- crates/sortition/src/ticket_sortition.rs | 7 +- examples/CRISP/deploy/config.toml | 2 +- examples/CRISP/enclave.config.yaml | 4 +- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../registry/CiphernodeRegistryOwnable.sol | 58 ++ .../sortition/CommitteeSortition.sol | 392 +++++++++++++ .../ignition/modules/committeeSortition.ts | 17 + .../deployAndSave/committeeSortition.ts | 102 ++++ .../scripts/deployEnclave.ts | 14 + .../test/CommitteeSortition.spec.ts | 537 ++++++++++++++++++ .../enclave-contracts/test/Enclave.spec.ts | 16 + .../CiphernodeRegistryOwnable.spec.ts | 106 +++- templates/default/enclave.config.yaml | 7 +- templates/default/hardhat.config.ts | 2 +- tests/integration/enclave.config.yaml | 4 +- 34 files changed, 1923 insertions(+), 536 deletions(-) create mode 100644 crates/events/src/enclave_event/committee_finalized.rs create mode 100644 crates/events/src/enclave_event/configuration_updated.rs create mode 100644 crates/events/src/enclave_event/ticket_submitted.rs create mode 100644 crates/evm/src/committee_sortition_sol.rs delete mode 100644 crates/sortition/src/ticket_bonding_sortition.rs create mode 100644 packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol create mode 100644 packages/enclave-contracts/ignition/modules/committeeSortition.ts create mode 100644 packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts create mode 100644 packages/enclave-contracts/test/CommitteeSortition.spec.ts diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index de6056ea14..773e8d0ced 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -278,7 +278,7 @@ impl Handler for ThresholdPlaintextAggregator { .into_actor(self) .map(move |res, act, ctx| { let maybe_found_index = res?; - let Some(party) = maybe_found_index else { + let Some((party, _ticket_number)) = maybe_found_index else { error!("Attempting to aggregate share but party not found in committee"); return Ok(()); }; diff --git a/crates/config/src/contract.rs b/crates/config/src/contract.rs index 17ee69f4d7..fb1add3198 100644 --- a/crates/config/src/contract.rs +++ b/crates/config/src/contract.rs @@ -39,5 +39,6 @@ pub struct ContractAddresses { pub enclave: Contract, pub ciphernode_registry: Contract, pub bonding_registry: Contract, + pub committee_sortition: Option, pub e3_program: Option, } diff --git a/crates/config/src/store_keys.rs b/crates/config/src/store_keys.rs index a85dba71fc..111f2a2831 100644 --- a/crates/config/src/store_keys.rs +++ b/crates/config/src/store_keys.rs @@ -65,6 +65,10 @@ impl StoreKeys { format!("//evm_readers/bonding_registry/{chain_id}") } + pub fn committee_sortition_reader(chain_id: u64) -> String { + format!("//evm_readers/committee_sortition/{chain_id}") + } + pub fn node_state() -> String { String::from("//node_state") } diff --git a/crates/events/src/enclave_event/ciphernode_selected.rs b/crates/events/src/enclave_event/ciphernode_selected.rs index 96626eeb8d..281cb12d9e 100644 --- a/crates/events/src/enclave_event/ciphernode_selected.rs +++ b/crates/events/src/enclave_event/ciphernode_selected.rs @@ -21,6 +21,7 @@ pub struct CiphernodeSelected { pub esi_per_ct: usize, pub params: ArcBytes, pub party_id: u64, + pub ticket_id: Option, } impl Default for CiphernodeSelected { @@ -34,6 +35,7 @@ impl Default for CiphernodeSelected { seed: Seed([0u8; 32]), threshold_m: 0, threshold_n: 0, + ticket_id: None, } } } diff --git a/crates/events/src/enclave_event/committee_finalized.rs b/crates/events/src/enclave_event/committee_finalized.rs new file mode 100644 index 0000000000..69bdb9cfa3 --- /dev/null +++ b/crates/events/src/enclave_event/committee_finalized.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeFinalized { + pub e3_id: E3id, + pub committee: Vec, + pub chain_id: u64, +} + +impl Display for CommitteeFinalized { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/configuration_updated.rs b/crates/events/src/enclave_event/configuration_updated.rs new file mode 100644 index 0000000000..2185e53ea9 --- /dev/null +++ b/crates/events/src/enclave_event/configuration_updated.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use actix::Message; +use alloy::primitives::U256; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct ConfigurationUpdated { + pub parameter: String, + pub old_value: U256, + pub new_value: U256, + pub chain_id: u64, +} + +impl Display for ConfigurationUpdated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "parameter: {}, old_value: {}, new_value: {}, chain_id: {}", + self.parameter, self.old_value, self.new_value, self.chain_id + ) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 4fbac70bc4..8c47bafe81 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -8,8 +8,10 @@ mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; mod ciphertext_output_published; +mod committee_finalized; mod committee_published; mod compute_request; +mod configuration_updated; mod decryptionshare_created; mod die; mod e3_request_complete; @@ -25,13 +27,16 @@ mod shutdown; mod test_event; mod threshold_share_created; mod ticket_balance_updated; +mod ticket_submitted; pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; pub use ciphertext_output_published::*; +pub use committee_finalized::*; pub use committee_published::*; pub use compute_request::*; +pub use configuration_updated::*; pub use decryptionshare_created::*; pub use die::*; pub use e3_request_complete::*; @@ -47,6 +52,7 @@ pub use shutdown::*; pub use test_event::*; pub use threshold_share_created::*; pub use ticket_balance_updated::*; +pub use ticket_submitted::*; use crate::{E3id, ErrorEvent, Event, EventId}; use actix::Message; @@ -119,6 +125,10 @@ pub enum EnclaveEvent { id: EventId, data: TicketBalanceUpdated, }, + ConfigurationUpdated { + id: EventId, + data: ConfigurationUpdated, + }, OperatorActivationChanged { id: EventId, data: OperatorActivationChanged, @@ -127,6 +137,14 @@ pub enum EnclaveEvent { id: EventId, data: CommitteePublished, }, + CommitteeFinalized { + id: EventId, + data: CommitteeFinalized, + }, + TicketSubmitted { + id: EventId, + data: TicketSubmitted, + }, PlaintextOutputPublished { id: EventId, data: PlaintextOutputPublished, @@ -210,6 +228,7 @@ impl From for EventId { EnclaveEvent::CiphernodeAdded { id, .. } => id, EnclaveEvent::CiphernodeRemoved { id, .. } => id, EnclaveEvent::TicketBalanceUpdated { id, .. } => id, + EnclaveEvent::ConfigurationUpdated { id, .. } => id, EnclaveEvent::OperatorActivationChanged { id, .. } => id, EnclaveEvent::CommitteePublished { id, .. } => id, EnclaveEvent::PlaintextOutputPublished { id, .. } => id, @@ -218,6 +237,8 @@ impl From for EventId { EnclaveEvent::Shutdown { id, .. } => id, EnclaveEvent::TestEvent { id, .. } => id, EnclaveEvent::ThresholdShareCreated { id, .. } => id, + EnclaveEvent::CommitteeFinalized { id, .. } => id, + EnclaveEvent::TicketSubmitted { id, .. } => id, } } } @@ -235,6 +256,8 @@ impl EnclaveEvent { EnclaveEvent::ThresholdShareCreated { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteePublished { data, .. } => Some(data.e3_id), EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteeFinalized { data, .. } => Some(data.e3_id), + EnclaveEvent::TicketSubmitted { data, .. } => Some(data.e3_id), _ => None, } } @@ -251,6 +274,7 @@ impl EnclaveEvent { EnclaveEvent::CiphernodeAdded { data, .. } => format!("{}", data), EnclaveEvent::CiphernodeRemoved { data, .. } => format!("{}", data), EnclaveEvent::TicketBalanceUpdated { data, .. } => format!("{:?}", data), + EnclaveEvent::ConfigurationUpdated { data, .. } => format!("{:?}", data), EnclaveEvent::OperatorActivationChanged { data, .. } => format!("{:?}", data), EnclaveEvent::CommitteePublished { data, .. } => format!("{:?}", data), EnclaveEvent::PlaintextOutputPublished { data, .. } => format!("{:?}", data), @@ -259,6 +283,8 @@ impl EnclaveEvent { EnclaveEvent::Shutdown { data, .. } => format!("{:?}", data), EnclaveEvent::ThresholdShareCreated { data, .. } => format!("{:?}", data), EnclaveEvent::TestEvent { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteeFinalized { data, .. } => format!("{:?}", data), + EnclaveEvent::TicketSubmitted { data, .. } => format!("{:?}", data), // _ => "".to_string(), } } @@ -277,8 +303,11 @@ impl_from_event!( CiphernodeAdded, CiphernodeRemoved, TicketBalanceUpdated, + ConfigurationUpdated, OperatorActivationChanged, CommitteePublished, + CommitteeFinalized, + TicketSubmitted, PlaintextOutputPublished, EnclaveError, Shutdown, diff --git a/crates/events/src/enclave_event/ticket_submitted.rs b/crates/events/src/enclave_event/ticket_submitted.rs new file mode 100644 index 0000000000..b454a3758d --- /dev/null +++ b/crates/events/src/enclave_event/ticket_submitted.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct TicketSubmitted { + pub e3_id: E3id, + pub node: String, + pub ticket_number: u64, + pub score: String, + pub added_to_committee: bool, + pub chain_id: u64, +} + +impl Display for TicketSubmitted { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/evm/src/bonding_registry_sol.rs b/crates/evm/src/bonding_registry_sol.rs index 6f012e250e..f860910e40 100644 --- a/crates/evm/src/bonding_registry_sol.rs +++ b/crates/evm/src/bonding_registry_sol.rs @@ -45,6 +45,19 @@ impl From for EnclaveEvent { } } +struct ConfigurationUpdatedWithChainId(pub IBondingRegistry::ConfigurationUpdated, pub u64); + +impl From for e3_events::ConfigurationUpdated { + fn from(value: ConfigurationUpdatedWithChainId) -> Self { + e3_events::ConfigurationUpdated { + parameter: value.0.parameter.to_string(), + old_value: value.0.oldValue, + new_value: value.0.newValue, + chain_id: value.1, + } + } +} + impl From for e3_events::OperatorActivationChanged { fn from(value: IBondingRegistry::OperatorActivationChanged) -> Self { e3_events::OperatorActivationChanged { @@ -61,6 +74,13 @@ impl From for EnclaveEvent { } } +impl From for EnclaveEvent { + fn from(value: ConfigurationUpdatedWithChainId) -> Self { + let payload: e3_events::ConfigurationUpdated = value.into(); + EnclaveEvent::from(payload) + } +} + pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { match topic { Some(&IBondingRegistry::TicketBalanceUpdated::SIGNATURE_HASH) => { @@ -80,6 +100,15 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< }; Some(EnclaveEvent::from(event)) } + Some(&IBondingRegistry::ConfigurationUpdated::SIGNATURE_HASH) => { + let Ok(event) = IBondingRegistry::ConfigurationUpdated::decode_log_data(data) else { + error!("Error parsing event ConfigurationUpdated after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(ConfigurationUpdatedWithChainId( + event, chain_id, + ))) + } _topic => { trace!( topic=?_topic, diff --git a/crates/evm/src/committee_sortition_sol.rs b/crates/evm/src/committee_sortition_sol.rs new file mode 100644 index 0000000000..9f34868205 --- /dev/null +++ b/crates/evm/src/committee_sortition_sol.rs @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::{helpers::EthProvider, EvmEventReader, EvmEventReaderState}; +use actix::prelude::*; +use alloy::{ + primitives::{Address, LogData, B256, U256}, + providers::{Provider, WalletProvider}, + rpc::types::TransactionReceipt, + sol, + sol_types::SolEvent, +}; +use anyhow::Result; +use e3_data::Repository; +use e3_events::{BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, Shutdown, Subscribe}; +use tracing::{error, info, trace}; + +sol!( + #[sol(rpc)] + #[derive(Debug)] + CommitteeSortition, + "../../packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json" +); + +struct TicketSubmittedWithChainId(pub CommitteeSortition::TicketSubmitted, pub u64); + +impl From for e3_events::TicketSubmitted { + fn from(value: TicketSubmittedWithChainId) -> Self { + e3_events::TicketSubmitted { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + node: value.0.node.to_string(), + ticket_number: value.0.ticketNumber.try_into().unwrap_or(0), + score: value.0.score.to_string(), + added_to_committee: value.0.addedToCommittee, + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: TicketSubmittedWithChainId) -> Self { + let payload: e3_events::TicketSubmitted = value.into(); + EnclaveEvent::from(payload) + } +} + +struct CommitteeFinalizedWithChainId(pub CommitteeSortition::CommitteeFinalized, pub u64); + +impl From for e3_events::CommitteeFinalized { + fn from(value: CommitteeFinalizedWithChainId) -> Self { + e3_events::CommitteeFinalized { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + committee: value + .0 + .committee + .iter() + .map(|addr| addr.to_string()) + .collect(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: CommitteeFinalizedWithChainId) -> Self { + let payload: e3_events::CommitteeFinalized = value.into(); + EnclaveEvent::from(payload) + } +} + +pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { + match topic { + Some(&CommitteeSortition::TicketSubmitted::SIGNATURE_HASH) => { + let Ok(event) = CommitteeSortition::TicketSubmitted::decode_log_data(data) else { + error!("Error parsing event TicketSubmitted after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(TicketSubmittedWithChainId( + event, chain_id, + ))) + } + Some(&CommitteeSortition::CommitteeFinalized::SIGNATURE_HASH) => { + let Ok(event) = CommitteeSortition::CommitteeFinalized::decode_log_data(data) else { + error!("Error parsing event CommitteeFinalized after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(CommitteeFinalizedWithChainId( + event, chain_id, + ))) + } + _topic => { + trace!( + topic=?_topic, + "Unknown event was received by CommitteeSortition.sol parser but was ignored" + ); + None + } + } +} + +pub struct CommitteeSortitionSolReader; + +impl CommitteeSortitionSolReader { + pub async fn attach( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + ) -> Result>> { + let addr = EvmEventReader::attach( + provider, + extractor, + contract_address, + start_block, + &bus.clone().into(), + repository, + rpc_url, + ) + .await?; + + info!(address=%contract_address, "CommitteeSortitionSolReader is listening to address"); + + Ok(addr) + } +} + +/// Writer for CommitteeSortition contract +pub struct CommitteeSortitionSolWriter

{ + provider: EthProvider

, + contract_address: Address, + bus: Addr>, +} + +impl CommitteeSortitionSolWriter

{ + pub fn new( + bus: &Addr>, + provider: EthProvider

, + contract_address: Address, + ) -> Result { + Ok(Self { + provider, + contract_address, + bus: bus.clone(), + }) + } + + pub async fn attach( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + ) -> Result>> { + let addr = + CommitteeSortitionSolWriter::new(bus, provider, contract_address.parse()?)?.start(); + + bus.send(Subscribe::new("CiphernodeSelected", addr.clone().into())) + .await?; + + bus.send(Subscribe::new("Shutdown", addr.clone().into())) + .await?; + + Ok(addr) + } + + async fn submit_ticket(&self, e3_id: E3id, ticket_number: u64) -> Result { + let e3_id_u256: U256 = e3_id.clone().try_into()?; + let ticket_number_u256 = U256::from(ticket_number); + + let from_address = self.provider.provider().default_signer_address(); + let current_nonce = self + .provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + + let contract = CommitteeSortition::new(self.contract_address, self.provider.provider()); + let builder = contract + .submitTicket(e3_id_u256, ticket_number_u256) + .nonce(current_nonce); + + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) + } +} + +impl Actor for CommitteeSortitionSolWriter

{ + type Context = actix::Context; +} + +impl Handler + for CommitteeSortitionSolWriter

+{ + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::CiphernodeSelected { data, .. } => { + ctx.notify(data); + } + EnclaveEvent::Shutdown { data, .. } => { + ctx.notify(data); + } + _ => {} + } + } +} + +impl Handler + for CommitteeSortitionSolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle( + &mut self, + data: e3_events::CiphernodeSelected, + _: &mut Self::Context, + ) -> Self::Result { + let e3_id = data.e3_id.clone(); + let provider = self.provider.clone(); + let contract_address = self.contract_address; + let bus = self.bus.clone(); + + let ticket_number = data.ticket_id; + + Box::pin(async move { + let Some(ticket) = ticket_number else { + info!( + "CiphernodeSelected: No ticket number (non-bonding backend), skipping ticket submission for E3 {:?}", + e3_id + ); + return; + }; + + info!( + "CiphernodeSelected: Submitting ticket {} for E3 {:?}", + ticket, e3_id + ); + + // Get the node's wallet address + let node_address = provider.provider().default_signer_address(); + + info!( + "Node {:?} submitting ticket {} for E3 {:?}", + node_address, ticket, e3_id + ); + + let writer = CommitteeSortitionSolWriter::new(&bus, provider.clone(), contract_address) + .expect("Failed to create writer"); + + match writer.submit_ticket(e3_id.clone(), ticket).await { + Ok(receipt) => { + info!( + "Successfully submitted ticket for E3 {:?}, tx: {:?}", + e3_id, receipt.transaction_hash + ); + } + Err(e) => { + error!("Failed to submit ticket for E3 {:?}: {:?}", e3_id, e); + bus.err(EnclaveErrorType::Evm, e); + } + } + }) + } +} + +impl Handler + for CommitteeSortitionSolWriter

+{ + type Result = (); + + fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { + ctx.stop(); + } +} + +/// Wrapper for reader and writer +pub struct CommitteeSortitionSol; + +impl CommitteeSortitionSol { + pub async fn attach

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + ) -> Result>> + where + P: Provider + WalletProvider + Clone + 'static, + { + CommitteeSortitionSolReader::attach( + bus, + provider.clone(), + contract_address, + repository, + start_block, + rpc_url, + ) + .await?; + + let writer = CommitteeSortitionSolWriter::attach(bus, provider, contract_address).await?; + + Ok(writer) + } +} diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 477e20798c..c473568502 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -6,6 +6,7 @@ mod bonding_registry_sol; mod ciphernode_registry_sol; +mod committee_sortition_sol; mod enclave_sol; mod enclave_sol_reader; mod enclave_sol_writer; @@ -17,6 +18,9 @@ pub use bonding_registry_sol::{BondingRegistrySol, BondingRegistrySolReader}; pub use ciphernode_registry_sol::{ CiphernodeRegistrySol, CiphernodeRegistrySolReader, CiphernodeRegistrySolWriter, }; +pub use committee_sortition_sol::{ + CommitteeSortitionSol, CommitteeSortitionSolReader, CommitteeSortitionSolWriter, +}; pub use enclave_sol::EnclaveSol; pub use enclave_sol_reader::EnclaveSolReader; pub use enclave_sol_writer::EnclaveSolWriter; diff --git a/crates/evm/src/repo.rs b/crates/evm/src/repo.rs index 0455c09f04..c21f21a920 100644 --- a/crates/evm/src/repo.rs +++ b/crates/evm/src/repo.rs @@ -54,3 +54,16 @@ impl BondingRegistryReaderRepositoryFactory for Repositories { ) } } + +pub trait CommitteeSortitionReaderRepositoryFactory { + fn committee_sortition_reader(&self, chain_id: u64) -> Repository; +} + +impl CommitteeSortitionReaderRepositoryFactory for Repositories { + fn committee_sortition_reader(&self, chain_id: u64) -> Repository { + Repository::new( + self.store + .scope(StoreKeys::committee_sortition_reader(chain_id)), + ) + } +} diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index b4d7a0da11..09a2e9ec7b 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -78,7 +78,7 @@ impl Handler for CiphernodeSelector { seed, size ); - if let Ok(found_index) = sortition + if let Ok(found_result) = sortition .send(GetNodeIndex { chain_id, seed, @@ -87,13 +87,23 @@ impl Handler for CiphernodeSelector { }) .await { - let Some(party_id) = found_index else { + let Some((party_id, ticket_id)) = found_result else { info!(node = address, "Ciphernode was not selected"); return; }; - info!("CIPHERNODE SELECTED: node={} address={}", party_id, address); + match ticket_id { + Some(ticket) => info!( + "CIPHERNODE SELECTED: node={} address={} ticket={}", + party_id, address, ticket + ), + None => info!( + "CIPHERNODE SELECTED: node={} address={} (no ticket)", + party_id, address + ), + } bus.do_send(EnclaveEvent::from(CiphernodeSelected { party_id, + ticket_id, e3_id: data.e3_id, threshold_m: data.threshold_m, threshold_n: data.threshold_n, diff --git a/crates/sortition/src/lib.rs b/crates/sortition/src/lib.rs index 7b3b6b5a36..f1645dcfd3 100644 --- a/crates/sortition/src/lib.rs +++ b/crates/sortition/src/lib.rs @@ -10,12 +10,10 @@ mod node_state; mod repo; mod sortition; mod ticket; -mod ticket_bonding_sortition; mod ticket_sortition; pub use ciphernode_selector::*; pub use node_state::*; pub use repo::*; pub use sortition::*; -pub use ticket_bonding_sortition::*; pub use ticket_sortition::*; diff --git a/crates/sortition/src/node_state.rs b/crates/sortition/src/node_state.rs index c96ecb21e6..8e3481323a 100644 --- a/crates/sortition/src/node_state.rs +++ b/crates/sortition/src/node_state.rs @@ -9,7 +9,7 @@ use alloy::primitives::U256; use anyhow::Result; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ - BusError, CommitteePublished, EnclaveErrorType, EnclaveEvent, EventBus, + BusError, CommitteePublished, ConfigurationUpdated, EnclaveErrorType, EnclaveEvent, EventBus, OperatorActivationChanged, PlaintextOutputPublished, Subscribe, TicketBalanceUpdated, }; use serde::{Deserialize, Serialize}; @@ -116,6 +116,8 @@ impl NodeStateManager { addr.clone().into(), )) .await?; + bus.send(Subscribe::new("ConfigurationUpdated", addr.clone().into())) + .await?; bus.send(Subscribe::new("CommitteePublished", addr.clone().into())) .await?; bus.send(Subscribe::new( @@ -133,33 +135,6 @@ impl Actor for NodeStateManager { type Context = Context; } -/// Message to set ticket price for a chain -#[derive(Message)] -#[rtype(result = "()")] -pub struct SetTicketPrice { - pub chain_id: u64, - pub price: U256, -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: SetTicketPrice, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - state.ticket_prices.insert(msg.chain_id, msg.price); - info!( - chain_id = msg.chain_id, - price = ?msg.price, - "Updated ticket price" - ); - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - impl Handler for NodeStateManager { type Result = (); @@ -171,6 +146,9 @@ impl Handler for NodeStateManager { EnclaveEvent::OperatorActivationChanged { data, .. } => { ctx.notify(data); } + EnclaveEvent::ConfigurationUpdated { data, .. } => { + ctx.notify(data); + } EnclaveEvent::CommitteePublished { data, .. } => { ctx.notify(data); } @@ -233,6 +211,28 @@ impl Handler for NodeStateManager { } } +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: ConfigurationUpdated, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + if msg.parameter == "ticketPrice" { + state.ticket_prices.insert(msg.chain_id, msg.new_value); + info!( + chain_id = msg.chain_id, + old_ticket_price = ?msg.old_value, + new_ticket_price = ?msg.new_value, + "ConfigurationUpdated - ticket price updated" + ); + } + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} + impl Handler for NodeStateManager { type Result = (); diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index e5267254aa..bf9ea7701b 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -7,7 +7,6 @@ use crate::distance::DistanceSortition; use crate::node_state::NodeStateStore; use crate::ticket::{RegisteredNode, Ticket}; -use crate::ticket_bonding_sortition::{NodeWithTickets, TicketBondingSortition}; use crate::ticket_sortition::ScoreSortition; use actix::prelude::*; use alloy::primitives::Address; @@ -28,10 +27,9 @@ use tracing::{info, instrument, trace}; /// Membership semantics depend on the backend for that chain: /// - **Distance backend**: computes a committee using address distance. /// - **Score backend**: computes each node’s best ticket score and sorts globally. -/// /// Returns `true` if `address` appears in the resulting top-`size` selection. #[derive(Message, Clone, Debug, PartialEq, Eq)] -#[rtype(result = "Option")] +#[rtype(result = "Option<(u64, Option)>")] pub struct GetNodeIndex { /// Round seed / randomness used by the sortition algorithm. pub seed: Seed, @@ -63,13 +61,27 @@ pub trait SortitionList { /// /// Implementations should return `Ok(false)` if the backend has no nodes /// or if `size == 0`. - fn contains(&self, seed: Seed, size: usize, address: T) -> Result; + fn contains( + &self, + seed: Seed, + size: usize, + address: T, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result; /// Return an index if `address` appears in the committee under `seed`. /// /// Implementations should return `Ok(None)` if the backend has no nodes /// or if `size == 0`. - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result>; + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> Result)>>; /// Add a node to the backend. Backends should be idempotent on duplicates. fn add(&mut self, address: T); @@ -104,7 +116,14 @@ impl SortitionList for DistanceBackend { /// then check whether `address` is in the result. /// /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn contains(&self, seed: Seed, size: usize, address: String) -> Result { + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + _node_state: Option<&NodeStateStore>, + _chain_id: u64, + ) -> anyhow::Result { if size == 0 { return Err(anyhow!("Size cannot be 0")); } @@ -120,7 +139,14 @@ impl SortitionList for DistanceBackend { .any(|(_, addr)| addr.to_string() == address)) } - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result> { + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + _node_state: Option<&NodeStateStore>, + _chain_id: u64, + ) -> Result)>> { if size == 0 { return Err(anyhow!("Size cannot be 0")); } @@ -131,14 +157,12 @@ impl SortitionList for DistanceBackend { let committee = get_committee(seed, size, self.nodes.clone())?; - let maybe_index = committee.iter().enumerate().find_map(|(index, (_, addr))| { - if addr.to_string() == address { - return Some(index as u64); - } - None - }); + let maybe = committee + .iter() + .enumerate() + .find_map(|(i, (_, addr))| (addr.to_string() == address).then(|| (i as u64, None))); - Ok(maybe_index) + Ok(maybe) } /// Insert a node address (hex). Duplicate inserts are harmless. @@ -175,8 +199,6 @@ fn get_committee( /// Score-sortition backend. /// /// Stores richer `RegisteredNode` entries (address + per-node ticket set). -/// Tickets use **local, per-node** IDs in the range `1..=k`, assigned by -/// [`ScoreBackend::set_ticket_count_addr`]. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ScoreBackend { /// Nodes with their ticket sets (used by score-based committee selection). @@ -192,20 +214,38 @@ impl Default for ScoreBackend { } impl ScoreBackend { - /// Set (or replace) a node’s ticket *count* using local IDs `1..=count`. + /// Build a vector of ephemeral nodes from the node state. /// - /// - If the node already exists, its entire ticket vector is replaced. - /// - If the node doesn’t exist, a new `RegisteredNode` is created. - /// - Passing `count == 0` clears the ticket vector for that node. - /// - /// This does **not** attempt to deduplicate across nodes; IDs are local. - pub fn set_ticket_count_addr(&mut self, address: Address, count: u64) { - let tickets: Vec = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); - if let Some(existing) = self.registered.iter_mut().find(|n| n.address == address) { - existing.tickets = tickets; - } else { - self.registered.push(RegisteredNode { address, tickets }); - } + /// The nodes are built from the node state and the registered nodes. + fn build_nodes_from_state( + &self, + chain_id: u64, + node_state: &NodeStateStore, + ) -> Vec { + self.registered + .iter() + .filter_map(|n| { + let addr_str = n.address.to_string(); + let key = (chain_id, addr_str.clone()); + let Some(ns) = node_state.nodes.get(&key) else { + return None; + }; + if !ns.active { + return None; + } + + let count = node_state.available_tickets(chain_id, &addr_str) as u64; + if count == 0 { + return None; + } + + let tickets = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); + Some(RegisteredNode { + address: n.address, + tickets, + }) + }) + .collect() } } @@ -213,11 +253,27 @@ impl SortitionList for ScoreBackend { /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. /// /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn contains(&self, seed: Seed, size: usize, address: String) -> Result { - if self.registered.is_empty() || size == 0 { + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result { + if size == 0 { return Ok(false); } - let winners = ScoreSortition::new(size).get_committee(seed.into(), &self.registered)?; + let Some(state) = node_state else { + return Ok(false); + }; + + let nodes = self.build_nodes_from_state(chain_id, state); + if nodes.is_empty() { + return Ok(false); + } + + let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; let want: Address = address.parse()?; Ok(winners.iter().any(|w| w.address == want)) } @@ -225,27 +281,39 @@ impl SortitionList for ScoreBackend { /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. /// /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result> { - if self.registered.is_empty() || size == 0 { + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result)>> { + if size == 0 { return Ok(None); } - let winners = ScoreSortition::new(size).get_committee(seed.into(), &self.registered)?; - let want: Address = address.parse()?; - let maybe_index = winners.iter().enumerate().find_map(|(index, w)| { - if w.address == want { - return Some(index as u64); - } - None - }); + if node_state.is_none() { + return Ok(None); + } + + let nodes: Vec = self.build_nodes_from_state(chain_id, node_state.unwrap()); + + if nodes.is_empty() { + return Ok(None); + } + + let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; + let want: alloy::primitives::Address = address.parse()?; - Ok(maybe_index) + let maybe = winners + .iter() + .enumerate() + .find_map(|(i, w)| (w.address == want).then(|| (i as u64, Some(w.ticket_id)))); + Ok(maybe) } /// Add a node, creating an empty ticket set when first seen. - /// - /// To set tickets, call [`ScoreBackend::set_ticket_count_addr`] (or another - /// initialization path) after the node is added. fn add(&mut self, address: String) { match address.parse::

() { Ok(addr) => { @@ -283,66 +351,17 @@ impl SortitionList for ScoreBackend { } } -/// Bonding-based sortition backend. -/// -/// Stores a set of hex-encoded addresses and delegates committee selection -/// to `TicketBondingSortition`. Ticket availability is calculated from -/// NodeStateManager: `floor(ticket_balance / ticket_price) - active_jobs` -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BondingBackend { - /// Set of registered node addresses (hex strings). - nodes: HashSet, -} - -impl Default for BondingBackend { - fn default() -> Self { - Self { - nodes: HashSet::new(), - } - } -} - -impl BondingBackend { - /// Get nodes with their available tickets from NodeStateStore - pub fn get_nodes_with_tickets( - &self, - chain_id: u64, - node_state: &NodeStateStore, - ) -> Vec { - self.nodes - .iter() - .filter_map(|addr_str| { - let addr = addr_str.parse::
().ok()?; - let available_tickets = node_state.available_tickets(chain_id, addr_str); - - // Only include nodes with available tickets - if available_tickets > 0 { - Some(NodeWithTickets { - address: addr, - available_tickets, - }) - } else { - None - } - }) - .collect() - } -} - /// Enum wrapper around the supported backends. /// /// New chains should default to `Distance`. If a chain is intended to /// use score selection, construct it as `SortitionBackend::Score(ScoreBackend::default())` -/// and then populate tickets explicitly. For bonding-based sortition with -/// dynamic ticket calculation, use `SortitionBackend::Bonding(BondingBackend::default())`. +/// and then populate tickets explicitly. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum SortitionBackend { /// Distance-based selection (stores a simple set of addresses). Distance(DistanceBackend), /// Score-based selection (stores `RegisteredNode`s with tickets). Score(ScoreBackend), - /// Bonding-based selection (uses NodeStateManager for dynamic tickets). - Bonding(BondingBackend), } impl SortitionBackend { @@ -350,84 +369,41 @@ impl SortitionBackend { pub fn default() -> Self { SortitionBackend::Distance(DistanceBackend::default()) } - - /// Helper for Score backends: assign local ticket IDs `1..=count` for `address`. - /// - /// # Errors - /// Returns an error if called on a `Distance` or `Bonding` backend. - pub fn set_ticket_count_addr(&mut self, address: Address, count: u64) -> Result<()> { - match self { - SortitionBackend::Score(b) => { - b.set_ticket_count_addr(address, count); - Ok(()) - } - SortitionBackend::Distance(_) => { - anyhow::bail!("set_ticket_count_addr is only valid for Score backend") - } - SortitionBackend::Bonding(_) => { - anyhow::bail!("set_ticket_count_addr is not applicable to Bonding backend (uses NodeStateStore)") - } - } - } -} - -impl SortitionList for BondingBackend { - /// Check membership using bonding-based sortition. - /// - /// Note: This implementation cannot access NodeStateStore directly, - /// so it returns false. For proper bonding sortition, use the - /// `Sortition` actor's `GetHasNode` message which has access to state. - fn contains(&self, _seed: Seed, _size: usize, _address: String) -> Result { - // BondingBackend requires NodeStateStore which isn't available here - // The Sortition actor will handle this by querying the node state - Ok(false) - } - - /// Get index using bonding-based sortition. - /// - /// Note: This implementation cannot access NodeStateStore directly, - /// so it returns None. For proper bonding sortition, use the - /// `Sortition` actor's `GetNodeIndex` message which has access to state. - fn get_index(&self, _seed: Seed, _size: usize, _address: String) -> Result> { - // BondingBackend requires NodeStateStore which isn't available here - // The Sortition actor will handle this by querying the node state - Ok(None) - } - - fn add(&mut self, address: String) { - self.nodes.insert(address); - } - - fn remove(&mut self, address: String) { - self.nodes.remove(&address); - } - - fn nodes(&self) -> Vec { - self.nodes.iter().cloned().collect() - } } impl SortitionList for SortitionBackend { - fn contains(&self, seed: Seed, size: usize, address: String) -> Result { + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result { match self { - SortitionBackend::Distance(backend) => backend.contains(seed, size, address), - SortitionBackend::Score(backend) => backend.contains(seed, size, address), - SortitionBackend::Bonding(backend) => backend.contains(seed, size, address), + SortitionBackend::Distance(b) => b.contains(seed, size, address, None, chain_id), + SortitionBackend::Score(b) => b.contains(seed, size, address, node_state, chain_id), } } - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result> { + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result)>> { match self { - SortitionBackend::Distance(backend) => backend.get_index(seed, size, address), - SortitionBackend::Score(backend) => backend.get_index(seed, size, address), - SortitionBackend::Bonding(backend) => backend.get_index(seed, size, address), + SortitionBackend::Distance(b) => b.get_index(seed, size, address, None, chain_id), + SortitionBackend::Score(b) => b.get_index(seed, size, address, node_state, chain_id), } } + fn add(&mut self, address: String) { match self { SortitionBackend::Distance(backend) => backend.add(address), SortitionBackend::Score(backend) => backend.add(address), - SortitionBackend::Bonding(backend) => backend.add(address), } } @@ -435,7 +411,6 @@ impl SortitionList for SortitionBackend { match self { SortitionBackend::Distance(backend) => backend.remove(address), SortitionBackend::Score(backend) => backend.remove(address), - SortitionBackend::Bonding(backend) => backend.remove(address), } } @@ -443,7 +418,6 @@ impl SortitionList for SortitionBackend { match self { SortitionBackend::Distance(backend) => backend.nodes(), SortitionBackend::Score(backend) => backend.nodes(), - SortitionBackend::Bonding(backend) => backend.nodes(), } } } @@ -461,7 +435,7 @@ pub struct Sortition { list: Persistable>, /// Event bus for error reporting and enclave event subscription. bus: Addr>, - /// Optional reference to node state for bonding-based sortition + /// Optional reference to node state for score-based sortition node_state: Option>, } @@ -472,7 +446,7 @@ pub struct SortitionParams { pub bus: Addr>, /// Persisted per-chain backend map. pub list: Persistable>, - /// Optional node state for bonding-based sortition + /// Optional node state for score-based sortition pub node_state: Option>, } @@ -506,9 +480,9 @@ impl Sortition { Ok(addr) } - /// Load persisted state with node state support for bonding-based sortition. + /// Load persisted state with node state support for score-based sortition. /// - /// This version allows bonding-based backends to query ticket availability. + /// This version allows score-based backends to query ticket availability. #[instrument(name = "sortition_attach_with_node_state", skip_all)] pub async fn attach_with_node_state( bus: &Addr>, @@ -613,54 +587,22 @@ impl Handler for Sortition { } impl Handler for Sortition { - type Result = Option; + type Result = Option<(u64, Option)>; - /// Return the index of `address` in the size-`size` committee for `seed` - /// on `chain_id`. If the chain has not been initialized, returns `None`. - /// - /// Errors while accessing persisted state or parsing the address are - /// reported on the event bus and surfaced here as `None`. - #[instrument(name = "sortition_get_index", skip_all)] fn handle(&mut self, msg: GetNodeIndex, _ctx: &mut Self::Context) -> Self::Result { + let node_state_snapshot = self.node_state.as_ref().and_then(|p| p.get()); + let node_state_ref = node_state_snapshot.as_ref(); + self.list .try_with(|map| { if let Some(backend) = map.get(&msg.chain_id) { - // For bonding backends, we need to use the node state - if let SortitionBackend::Bonding(bonding_backend) = backend { - if let Some(node_state_ref) = &self.node_state { - return node_state_ref.try_with(|node_state| { - // Get nodes with their available tickets - let nodes_with_tickets = bonding_backend - .get_nodes_with_tickets(msg.chain_id, node_state); - - // Use ticket bonding sortition - let sortition = TicketBondingSortition::new(msg.size); - let target_addr: Address = msg.address.parse()?; - - // Get committee and find the index of the address - let committee = sortition.get_committee( - &nodes_with_tickets, - msg.chain_id, - msg.seed.into(), - )?; - - // Find the index of the target address in the committee - let maybe_index = - committee.iter().enumerate().find_map(|(index, addr)| { - if addr == &target_addr { - Some(index as u64) - } else { - None - } - }); - - Ok(maybe_index) - }); - } - } - - // For other backends, use their native get_index method - backend.get_index(msg.seed, msg.size, msg.address.clone()) + backend.get_index( + msg.seed, + msg.size, + msg.address.clone(), + node_state_ref, + msg.chain_id, + ) } else { Ok(None) } @@ -671,7 +613,6 @@ impl Handler for Sortition { }) } } - impl Handler for Sortition { type Result = Vec; diff --git a/crates/sortition/src/ticket_bonding_sortition.rs b/crates/sortition/src/ticket_bonding_sortition.rs deleted file mode 100644 index 2c416463c6..0000000000 --- a/crates/sortition/src/ticket_bonding_sortition.rs +++ /dev/null @@ -1,276 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use alloy::primitives::{keccak256, Address}; -use anyhow::{anyhow, Result}; -use num_bigint::BigUint; -use serde::{Deserialize, Serialize}; - -/// A node with its available tickets (after subtracting active jobs) -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NodeWithTickets { - pub address: Address, - pub available_tickets: u64, -} - -/// A winning ticket with its score -#[derive(Clone, Debug)] -pub struct WinningTicket { - pub address: Address, - pub ticket_number: u64, - pub score: BigUint, -} - -/// Ticket-based sortition using bonding registry state -/// -/// Algorithm: -/// 1. Each node has N available tickets (ticket_balance / ticket_price - active_jobs) -/// 2. For each ticket of each node, compute hash(node_address, ticket_number, e3_id, seed) -/// 3. Find the lowest score ticket for each node -/// 4. Sort all nodes by their lowest ticket score -/// 5. Select the top M nodes -pub struct TicketBondingSortition { - /// Desired committee size - pub size: usize, -} - -impl TicketBondingSortition { - pub fn new(size: usize) -> Self { - Self { size } - } - - /// Compute the score for a specific ticket - /// - /// Score = keccak256(node_address || ticket_number || e3_id || seed) - fn compute_ticket_score( - node_address: Address, - ticket_number: u64, - e3_id: u64, - seed: u64, - ) -> BigUint { - let mut message = Vec::with_capacity(20 + 8 + 8 + 8); - message.extend_from_slice(node_address.as_slice()); - message.extend_from_slice(&ticket_number.to_be_bytes()); - message.extend_from_slice(&e3_id.to_be_bytes()); - message.extend_from_slice(&seed.to_be_bytes()); - - let hash = keccak256(&message); - BigUint::from_bytes_be(&hash.0) - } - - /// Find the lowest scoring ticket for a given node - fn find_best_ticket_for_node( - node: &NodeWithTickets, - e3_id: u64, - seed: u64, - ) -> Option { - if node.available_tickets == 0 { - return None; - } - - let mut best: Option = None; - - for ticket_num in 1..=node.available_tickets { - let score = Self::compute_ticket_score(node.address, ticket_num, e3_id, seed); - - match &best { - None => { - best = Some(WinningTicket { - address: node.address, - ticket_number: ticket_num, - score, - }); - } - Some(current_best) => { - if score < current_best.score - || (score == current_best.score && ticket_num < current_best.ticket_number) - { - best = Some(WinningTicket { - address: node.address, - ticket_number: ticket_num, - score, - }); - } - } - } - } - - best - } - - /// Determine the committee from a list of nodes with their available tickets - /// - /// Returns the sorted list of winning nodes (top M by lowest ticket score) - pub fn get_committee( - &self, - nodes: &[NodeWithTickets], - e3_id: u64, - seed: u64, - ) -> Result> { - if nodes.is_empty() || self.size == 0 { - return Ok(Vec::new()); - } - - // Find the best ticket for each node - let mut winning_tickets: Vec = nodes - .iter() - .filter_map(|node| Self::find_best_ticket_for_node(node, e3_id, seed)) - .collect(); - - if winning_tickets.is_empty() { - return Err(anyhow!("No nodes with available tickets")); - } - - // Sort by score (ascending), then by ticket number if scores are equal - winning_tickets.sort_unstable_by(|a, b| { - a.score - .cmp(&b.score) - .then(a.ticket_number.cmp(&b.ticket_number)) - }); - - // Select top M nodes - let selected_size = self.size.min(winning_tickets.len()); - Ok(winning_tickets - .into_iter() - .take(selected_size) - .map(|w| w.address) - .collect()) - } - - /// Check if a specific node is in the committee - pub fn is_node_in_committee( - &self, - nodes: &[NodeWithTickets], - e3_id: u64, - seed: u64, - target_address: Address, - ) -> Result { - let committee = self.get_committee(nodes, e3_id, seed)?; - Ok(committee.contains(&target_address)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy::primitives::keccak256; - - fn address(i: u64) -> Address { - let h = keccak256([b"addr".as_slice(), &i.to_be_bytes()].concat()); - let mut bytes20 = [0u8; 20]; - bytes20.copy_from_slice(&h.0[12..32]); - Address::from(bytes20) - } - - #[test] - fn test_ticket_bonding_sortition() { - let nodes = vec![ - NodeWithTickets { - address: address(1), - available_tickets: 5, - }, - NodeWithTickets { - address: address(2), - available_tickets: 3, - }, - NodeWithTickets { - address: address(3), - available_tickets: 7, - }, - NodeWithTickets { - address: address(4), - available_tickets: 2, - }, - NodeWithTickets { - address: address(5), - available_tickets: 0, // No available tickets - }, - ]; - - let sortition = TicketBondingSortition::new(3); - let e3_id = 12345; - let seed = 0xABCDEF; - - let committee = sortition - .get_committee(&nodes, e3_id, seed) - .expect("Should get committee"); - - assert_eq!(committee.len(), 3); - println!("Committee: {:?}", committee); - - // Verify the committee is deterministic - let committee2 = sortition - .get_committee(&nodes, e3_id, seed) - .expect("Should get committee"); - assert_eq!(committee, committee2); - - // Verify node with 0 tickets is not selected - assert!(!committee.contains(&address(5))); - } - - #[test] - fn test_is_node_in_committee() { - let nodes = vec![ - NodeWithTickets { - address: address(1), - available_tickets: 5, - }, - NodeWithTickets { - address: address(2), - available_tickets: 3, - }, - NodeWithTickets { - address: address(3), - available_tickets: 7, - }, - ]; - - let sortition = TicketBondingSortition::new(2); - let e3_id = 12345; - let seed = 0xABCDEF; - - let committee = sortition - .get_committee(&nodes, e3_id, seed) - .expect("Should get committee"); - - for node in &nodes { - let is_in = sortition - .is_node_in_committee(&nodes, e3_id, seed, node.address) - .expect("Should check membership"); - assert_eq!( - is_in, - committee.contains(&node.address), - "Membership check should match committee" - ); - } - } - - #[test] - fn test_active_jobs_penalty() { - // Node 1 has more tickets but they're reduced by active jobs - let nodes = vec![ - NodeWithTickets { - address: address(1), - available_tickets: 10, // e.g., had 15 tickets but 5 active jobs - }, - NodeWithTickets { - address: address(2), - available_tickets: 10, // e.g., had 10 tickets but 0 active jobs - }, - ]; - - let sortition = TicketBondingSortition::new(1); - let e3_id = 12345; - let seed = 0xABCDEF; - - let committee = sortition - .get_committee(&nodes, e3_id, seed) - .expect("Should get committee"); - - assert_eq!(committee.len(), 1); - // Both have same available tickets, result is deterministic based on scores - } -} diff --git a/crates/sortition/src/ticket_sortition.rs b/crates/sortition/src/ticket_sortition.rs index 45051eabaa..ea5a655703 100644 --- a/crates/sortition/src/ticket_sortition.rs +++ b/crates/sortition/src/ticket_sortition.rs @@ -66,7 +66,12 @@ impl ScoreSortition { let mut items: Vec = best_map.into_values().collect(); // Sort ascending by (score, ticket_id) - items.sort_unstable_by(|a, b| a.score.cmp(&b.score).then(a.ticket_id.cmp(&b.ticket_id))); + items.sort_unstable_by(|a, b| { + a.score + .cmp(&b.score) + .then(a.ticket_id.cmp(&b.ticket_id)) + .then(a.address.as_slice().cmp(b.address.as_slice())) + }); let k = self.size.min(items.len()); items.truncate(k); diff --git a/examples/CRISP/deploy/config.toml b/examples/CRISP/deploy/config.toml index 6173c80a83..172313fb6d 100644 --- a/examples/CRISP/deploy/config.toml +++ b/examples/CRISP/deploy/config.toml @@ -14,4 +14,4 @@ enclaveAddress = "0xE3000000000000000000000000000000000000E3" [profile.custom] chainId = 31337 riscZeroVerifierAddress = "0x0000000000000000000000000000000000000000" # Deployed with the script. Don't set or it will be skipped. -enclaveAddress = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" # Based on default deployment address using local anvil node +enclaveAddress = "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" # Based on default deployment address using local anvil node diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 0610115a58..aed9fbb9f3 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -2,9 +2,11 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + e3_program: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + enclave: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + committee_sortition: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" program: dev: true diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index f08fd5bdb5..1c74d101bd 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_27-4fe15502d3a80277a9221a66fbbdc6c08701e71b" + "buildInfoId": "solc-0_8_27-0d27918dccf41f92d21008dc1c7a071e1b0a90b0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index f4458984a7..15fcc99964 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -403,5 +403,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_27-4fe15502d3a80277a9221a66fbbdc6c08701e71b" + "buildInfoId": "solc-0_8_27-0d27918dccf41f92d21008dc1c7a071e1b0a90b0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index e7434250e1..cad57e6e26 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_27-4fe15502d3a80277a9221a66fbbdc6c08701e71b" + "buildInfoId": "solc-0_8_27-0d27918dccf41f92d21008dc1c7a071e1b0a90b0" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 0471cac748..ff6757ea5b 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -7,6 +7,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; +import { CommitteeSortition } from "../sortition/CommitteeSortition.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -33,6 +34,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @param bondingRegistry Address of the bonding registry contract event BondingRegistrySet(address indexed bondingRegistry); + /// @notice Emitted when the committee sortition address is set + /// @param committeeSortition Address of the committee sortition contract + event CommitteeSortitionSet(address indexed committeeSortition); + //////////////////////////////////////////////////////////// // // // Storage Variables // @@ -45,6 +50,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Address of the bonding registry for checking node eligibility address public bondingRegistry; + /// @notice Address of the committee sortition contract + address public committeeSortition; + /// @notice Current number of registered ciphernodes uint256 public numCiphernodes; @@ -99,6 +107,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Bonding registry has not been set error BondingRegistryNotSet(); + /// @notice Committee sortition has not been set + error CommitteeSortitionNotSet(); + /// @notice Caller is not authorized error Unauthorized(); @@ -170,9 +181,22 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { committees[e3Id].threshold[1] == 0, CommitteeAlreadyRequested() ); + require(committeeSortition != address(0), CommitteeSortitionNotSet()); + committees[e3Id].threshold = threshold; roots[e3Id] = root(); + // Initialize sortition in CommitteeSortition contract + // Get seed from Enclave contract (will be passed via E3Requested event) + // For now, we'll generate it here - note: should match E3.seed from Enclave + uint256 seed = uint256(keccak256(abi.encode(block.prevrandao, e3Id))); + CommitteeSortition(committeeSortition).initializeSortition( + e3Id, + threshold[1], // Use N (total committee size) + seed, + block.number + ); + emit CommitteeRequested(e3Id, threshold); success = true; } @@ -196,6 +220,29 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit CommitteePublished(e3Id, nodes, publicKey); } + /// @notice Finalizes committee from sortition and publishes it + /// @dev Can be called by anyone after sortition deadline. Gets committee from CommitteeSortition. + /// @param e3Id ID of the E3 computation + /// @param publicKey Aggregated public key of the committee + function finalizeAndPublishCommittee( + uint256 e3Id, + bytes calldata publicKey + ) external { + require(committeeSortition != address(0), CommitteeSortitionNotSet()); + ICiphernodeRegistry.Committee storage committee = committees[e3Id]; + require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); + + // Finalize sortition and get committee + address[] memory nodes = CommitteeSortition(committeeSortition) + .finalizeCommittee(e3Id); + + committee.nodes = nodes; + bytes32 publicKeyHash = keccak256(publicKey); + committee.publicKey = publicKeyHash; + publicKeyHashes[e3Id] = publicKeyHash; + emit CommitteePublished(e3Id, nodes, publicKey); + } + /// @inheritdoc ICiphernodeRegistry function addCiphernode(address node) external onlyOwnerOrBondingVault { if (isEnabled(node)) { @@ -251,6 +298,17 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit BondingRegistrySet(_bondingRegistry); } + /// @notice Sets the committee sortition contract address + /// @dev Only callable by owner + /// @param _committeeSortition Address of the committee sortition contract + function setCommitteeSortition( + address _committeeSortition + ) public onlyOwner { + require(_committeeSortition != address(0), ZeroAddress()); + committeeSortition = _committeeSortition; + emit CommitteeSortitionSet(_committeeSortition); + } + //////////////////////////////////////////////////////////// // // // Get Functions // diff --git a/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol b/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol new file mode 100644 index 0000000000..cdb4090db4 --- /dev/null +++ b/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; + +/** + * @title CommitteeSortition + * @notice Simple on-chain verification of ticket-based sortition + * @dev Validates ticket submissions and tracks committee members + * + * Flow: + * 1. Nodes perform sortition off-chain + * 2. Selected nodes submit their winning ticket via submitTicket() + * 3. Contract validates ticket against snapshot balance + * 4. Contract tracks top N nodes by score + */ +contract CommitteeSortition { + // ====================== + // Errors + // ====================== + + error InvalidTicketNumber(); + error NodeNotEligible(); + error NodeAlreadySubmitted(); + error SubmissionWindowClosed(); + error SubmissionWindowNotClosed(); + error CommitteeNotInitialized(); + error CommitteeAlreadyFinalized(); + error OnlyCiphernodeRegistry(); + + // ====================== + // Events + // ====================== + + event TicketSubmitted( + uint256 indexed e3Id, + address indexed node, + uint256 ticketNumber, + uint256 score, + bool addedToCommittee + ); + + event CommitteeFinalized(uint256 indexed e3Id, address[] committee); + + // ====================== + // Structs + // ====================== + + /// @notice Represents a node's ticket submission + struct TicketSubmission { + address node; + uint256 ticketNumber; + uint256 score; + bool exists; + } + + /// @notice Sortition state for an E3 + struct SortitionState { + uint256 threshold; // Number of nodes needed + uint256 seed; // Random seed for this E3 + uint256 requestBlock; // Block number when E3 was requested (for snapshot) + uint256 submissionDeadline; // Timestamp when submission window closes + bool finalized; // Whether committee has been finalized + address[] topNodes; // Current top N nodes (sorted by score) + mapping(address => TicketSubmission) submissions; + } + + // ====================== + // Storage + // ====================== + + /// @notice Bonding registry for checking ticket balances + IBondingRegistry public immutable bondingRegistry; + + /// @notice Ciphernode registry that can initialize sortitions + address public immutable ciphernodeRegistry; + + /// @notice Default submission window duration (in seconds) + uint256 public immutable submissionWindow; + + /// @notice Maps E3 ID to its sortition state + mapping(uint256 => SortitionState) public sortitions; + + // ====================== + // Constructor + // ====================== + + constructor( + address _bondingRegistry, + address _ciphernodeRegistry, + uint256 _submissionWindow + ) { + bondingRegistry = IBondingRegistry(_bondingRegistry); + ciphernodeRegistry = _ciphernodeRegistry; + submissionWindow = _submissionWindow; + } + + // ====================== + // Main Functions + // ====================== + + /** + * @notice Initialize sortition for an E3 + * @dev Only callable by ciphernode registry when committee is requested + * @param e3Id The E3 identifier + * @param threshold Number of committee members needed + * @param seed Random seed for score computation + * @param requestBlock Block number for snapshot validation + */ + function initializeSortition( + uint256 e3Id, + uint256 threshold, + uint256 seed, + uint256 requestBlock + ) external { + require(msg.sender == ciphernodeRegistry, OnlyCiphernodeRegistry()); + SortitionState storage state = sortitions[e3Id]; + require(state.threshold == 0, CommitteeAlreadyFinalized()); + + state.threshold = threshold; + state.seed = seed; + state.requestBlock = requestBlock; + state.submissionDeadline = block.timestamp + submissionWindow; + state.finalized = false; + } + + /** + * @notice Submit a ticket for sortition + * @dev Nodes call this to submit their best ticket. Score is computed and verified on-chain. + * @param e3Id The E3 identifier + * @param ticketNumber The ticket number to submit (1 to available_tickets at snapshot) + */ + function submitTicket(uint256 e3Id, uint256 ticketNumber) external { + SortitionState storage state = sortitions[e3Id]; + + // Check sortition is initialized + require(state.threshold > 0, CommitteeNotInitialized()); + + // Check submission window is still open + require( + block.timestamp <= state.submissionDeadline, + SubmissionWindowClosed() + ); + + // Check not finalized + require(!state.finalized, CommitteeAlreadyFinalized()); + + // Check node hasn't already submitted + if (state.submissions[msg.sender].exists) revert NodeAlreadySubmitted(); + + // Check node is eligible (has ticket balance at snapshot) + _validateNodeEligibility(msg.sender, ticketNumber, e3Id); + + // Compute score + uint256 score = _computeTicketScore( + msg.sender, + ticketNumber, + e3Id, + state.seed + ); + + // Store submission + state.submissions[msg.sender] = TicketSubmission({ + node: msg.sender, + ticketNumber: ticketNumber, + score: score, + exists: true + }); + + // Try to insert into top N + bool added = _tryInsertIntoTopN(state, msg.sender, score); + + emit TicketSubmitted(e3Id, msg.sender, ticketNumber, score, added); + } + + /** + * @notice Finalize the committee after submission window closes + * @dev Can be called by anyone after the deadline. Sets finalized flag. + * @param e3Id The E3 identifier + * @return committee The final committee addresses + */ + function finalizeCommittee( + uint256 e3Id + ) external returns (address[] memory committee) { + SortitionState storage state = sortitions[e3Id]; + + require(state.threshold > 0, CommitteeNotInitialized()); + require(!state.finalized, CommitteeAlreadyFinalized()); + require( + block.timestamp > state.submissionDeadline, + SubmissionWindowNotClosed() + ); + + state.finalized = true; + committee = state.topNodes; + + emit CommitteeFinalized(e3Id, committee); + } + + // ====================== + // View Functions + // ====================== + + /** + * @notice Get the current top N nodes for an E3 + * @param e3Id The E3 identifier + * @return Array of top N node addresses + */ + function getTopNodes( + uint256 e3Id + ) external view returns (address[] memory) { + return sortitions[e3Id].topNodes; + } + + /** + * @notice Get a node's submission for an E3 + * @param e3Id The E3 identifier + * @param node The node address + * @return The ticket submission + */ + function getSubmission( + uint256 e3Id, + address node + ) external view returns (TicketSubmission memory) { + return sortitions[e3Id].submissions[node]; + } + + /** + * @notice Compute the score for a ticket + * @dev Public function to allow off-chain computation verification + * @param node Node address + * @param ticketNumber Ticket number (1 to N) + * @param e3Id E3 identifier + * @param seed Random seed + * @return The computed score + */ + function computeTicketScore( + address node, + uint256 ticketNumber, + uint256 e3Id, + uint256 seed + ) external pure returns (uint256) { + return _computeTicketScore(node, ticketNumber, e3Id, seed); + } + + /** + * @notice Get sortition information for an E3 + * @param e3Id The E3 identifier + * @return threshold Number of committee members needed + * @return seed Random seed + * @return requestBlock Block number when E3 was requested + * @return submissionDeadline Timestamp when submission window closes + * @return finalized Whether committee has been finalized + */ + function getSortitionInfo( + uint256 e3Id + ) + external + view + returns ( + uint256 threshold, + uint256 seed, + uint256 requestBlock, + uint256 submissionDeadline, + bool finalized + ) + { + SortitionState storage state = sortitions[e3Id]; + return ( + state.threshold, + state.seed, + state.requestBlock, + state.submissionDeadline, + state.finalized + ); + } + + // ====================== + // Internal Functions + // ====================== + + /** + * @notice Computes score = keccak256(node || ticketNumber || e3Id || seed) + */ + function _computeTicketScore( + address node, + uint256 ticketNumber, + uint256 e3Id, + uint256 seed + ) internal pure returns (uint256) { + bytes32 hash = keccak256( + abi.encodePacked(node, ticketNumber, e3Id, seed) + ); + return uint256(hash); + } + + /** + * @notice Validates that a node is eligible to participate + * @dev Uses snapshot of ticket balance at E3 request block for deterministic validation + */ + function _validateNodeEligibility( + address node, + uint256 ticketNumber, + uint256 e3Id + ) internal view { + if (ticketNumber == 0) revert InvalidTicketNumber(); + + SortitionState storage state = sortitions[e3Id]; + + // Get ticket balance at the time E3 was requested (snapshot) + uint256 ticketBalance = bondingRegistry.getTicketBalanceAtBlock( + node, + state.requestBlock + ); + uint256 ticketPrice = bondingRegistry.ticketPrice(); + + if (ticketPrice == 0) revert InvalidTicketNumber(); + + // Calculate available tickets at snapshot + uint256 availableTickets = ticketBalance / ticketPrice; + + // Check ticket number is valid + if (ticketNumber > availableTickets) revert InvalidTicketNumber(); + + // Check node is eligible (has tickets at snapshot) + if (availableTickets == 0) revert NodeNotEligible(); + } + + /** + * @notice Try to insert node into top N sorted list + * @dev Maintains sorted order by score (lowest first) + * @return Whether node was added to top N + */ + function _tryInsertIntoTopN( + SortitionState storage state, + address node, + uint256 score + ) internal returns (bool) { + address[] storage topNodes = state.topNodes; + + // If list not full, insert in sorted order + if (topNodes.length < state.threshold) { + _insertSorted(state, node, score); + return true; + } + + // If list is full, only add if score is better than worst + uint256 worstScore = state + .submissions[topNodes[topNodes.length - 1]] + .score; + if (score < worstScore) { + topNodes.pop(); // Remove worst + _insertSorted(state, node, score); + return true; + } + + return false; + } + + /** + * @notice Insert node into sorted position (ascending by score) + */ + function _insertSorted( + SortitionState storage state, + address node, + uint256 score + ) internal { + address[] storage topNodes = state.topNodes; + + // Find insertion position + uint256 insertPos = topNodes.length; + for (uint256 i = 0; i < topNodes.length; i++) { + uint256 existingScore = state.submissions[topNodes[i]].score; + if (score < existingScore) { + insertPos = i; + break; + } + } + + // Insert at position + topNodes.push(address(0)); // Extend array + for (uint256 i = topNodes.length - 1; i > insertPos; i--) { + topNodes[i] = topNodes[i - 1]; + } + topNodes[insertPos] = node; + } +} diff --git a/packages/enclave-contracts/ignition/modules/committeeSortition.ts b/packages/enclave-contracts/ignition/modules/committeeSortition.ts new file mode 100644 index 0000000000..f479beb29e --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/committeeSortition.ts @@ -0,0 +1,17 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("CommitteeSortition", (m) => { + const bondingRegistry = m.getParameter("bondingRegistry"); + const ciphernodeRegistry = m.getParameter("ciphernodeRegistry"); + + // TODO: 5 minutes is the default submission window + const submissionWindow = m.getParameter("submissionWindow", 300); + + const committeeSortition = m.contract("CommitteeSortition", [ + bondingRegistry, + ciphernodeRegistry, + submissionWindow, + ]); + + return { committeeSortition }; +}) as any; diff --git a/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts b/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts new file mode 100644 index 0000000000..4306ddd5d7 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import CommitteeSortitionModule from "../../ignition/modules/committeeSortition"; +import { + CommitteeSortition, + CommitteeSortition__factory as CommitteeSortitionFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveCommitteeSortition function + */ +export interface CommitteeSortitionArgs { + bondingRegistry?: string; + ciphernodeRegistry?: string; + submissionWindow?: number; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the CommitteeSortition contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed CommitteeSortition contract + */ +export const deployAndSaveCommitteeSortition = async ({ + bondingRegistry, + ciphernodeRegistry, + submissionWindow = 300, // Default 5 minutes + hre, +}: CommitteeSortitionArgs): Promise<{ + committeeSortition: CommitteeSortition; +}> => { + const { ignition, ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("CommitteeSortition", chain); + + if ( + !bondingRegistry || + !ciphernodeRegistry || + (preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.ciphernodeRegistry === + ciphernodeRegistry && + Number(preDeployedArgs?.constructorArgs?.submissionWindow) === + submissionWindow) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "CommitteeSortition address not found, it must be deployed first", + ); + } + const committeeSortitionContract = CommitteeSortitionFactory.connect( + preDeployedArgs.address, + signer, + ); + return { committeeSortition: committeeSortitionContract }; + } + + const committeeSortition = await ignition.deploy(CommitteeSortitionModule, { + parameters: { + CommitteeSortition: { + bondingRegistry, + ciphernodeRegistry, + submissionWindow, + }, + }, + }); + + await committeeSortition.committeeSortition.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const committeeSortitionAddress = + await committeeSortition.committeeSortition.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + bondingRegistry, + ciphernodeRegistry, + submissionWindow: submissionWindow.toString(), + }, + blockNumber, + address: committeeSortitionAddress, + }, + "CommitteeSortition", + chain, + ); + + const committeeSortitionContract = CommitteeSortitionFactory.connect( + committeeSortitionAddress, + signer, + ); + + return { committeeSortition: committeeSortitionContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 607aec5321..25cf0ba06d 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -7,6 +7,7 @@ import hre from "hardhat"; import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; +import { deployAndSaveCommitteeSortition } from "./deployAndSave/committeeSortition"; import { deployAndSaveEnclave } from "./deployAndSave/enclave"; import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; @@ -105,6 +106,15 @@ export const deployEnclave = async (withMocks?: boolean) => { const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); console.log("CiphernodeRegistry deployed to:", ciphernodeRegistryAddress); + console.log("Deploying CommitteeSortition..."); + const { committeeSortition } = await deployAndSaveCommitteeSortition({ + bondingRegistry: bondingRegistryAddress, + ciphernodeRegistry: ciphernodeRegistryAddress, + hre, + }); + const committeeSortitionAddress = await committeeSortition.getAddress(); + console.log("CommitteeSortition deployed to:", committeeSortitionAddress); + console.log("Deploying Enclave..."); const { enclave } = await deployAndSaveEnclave({ params: encoded, @@ -145,6 +155,9 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting Enclave as reward distributor in BondingRegistry..."); await bondingRegistry.setRewardDistributor(enclaveAddress); + console.log("Setting CommitteeSortition address in CiphernodeRegistry..."); + await ciphernodeRegistry.setCommitteeSortition(committeeSortitionAddress); + if (shouldDeployMocks) { const { decryptionVerifierAddress, e3ProgramAddress } = await deployMocks(); @@ -183,6 +196,7 @@ export const deployEnclave = async (withMocks?: boolean) => { EnclaveTicketToken: ${enclaveTicketTokenAddress} SlashingManager: ${slashingManagerAddress} BondingRegistry: ${bondingRegistryAddress} + CommitteeSortition: ${committeeSortitionAddress} CiphernodeRegistry: ${ciphernodeRegistryAddress} Enclave: ${enclaveAddress} ============================================ diff --git a/packages/enclave-contracts/test/CommitteeSortition.spec.ts b/packages/enclave-contracts/test/CommitteeSortition.spec.ts new file mode 100644 index 0000000000..2dd00e040a --- /dev/null +++ b/packages/enclave-contracts/test/CommitteeSortition.spec.ts @@ -0,0 +1,537 @@ +import { expect } from "chai"; +import { network } from "hardhat"; + +import BondingRegistryModule from "../ignition/modules/bondingRegistry"; +import CommitteeSortitionModule from "../ignition/modules/committeeSortition"; +import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../ignition/modules/enclaveToken"; +import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; +import MockStableTokenModule from "../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + CommitteeSortition__factory as CommitteeSortitionFactory, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, + MockUSDC__factory as MockUSDCFactory, +} from "../types"; + +const { ethers, networkHelpers, ignition } = await network.connect(); +const { loadFixture } = networkHelpers; + +describe("CommitteeSortition", function () { + const SUBMISSION_WINDOW = 300; // 5 minutes + const TICKET_PRICE = ethers.parseEther("10"); + const E3_ID = 1; + const THRESHOLD = 3; + const SEED = 12345; + const AddressOne = "0x0000000000000000000000000000000000000001"; + + async function deployFixture() { + const [owner, ciphernodeRegistry, node1, node2, node3, node4] = + await ethers.getSigners(); + + const ownerAddress = await owner.getAddress(); + + // Deploy token contracts + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, + }, + }, + }); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcContract.mockUSDC.getAddress(), + registry: AddressOne, + owner: ownerAddress, + }, + }, + }, + ); + + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: AddressOne, + }, + }, + }, + ); + + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: AddressOne, + slashedFundsTreasury: ownerAddress, + ticketPrice: TICKET_PRICE, + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 1, + exitDelay: 7 * 24 * 60 * 60, + }, + }, + }, + ); + + const committeeSortitionContract = await ignition.deploy( + CommitteeSortitionModule, + { + parameters: { + CommitteeSortition: { + bondingRegistry: + await bondingRegistryContract.bondingRegistry.getAddress(), + ciphernodeRegistry: ciphernodeRegistry.address, + submissionWindow: SUBMISSION_WINDOW, + }, + }, + }, + ); + + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + const committeeSortition = CommitteeSortitionFactory.connect( + await committeeSortitionContract.committeeSortition.getAddress(), + owner, + ); + + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + const ticketToken = EnclaveTicketTokenFactory.connect( + await ticketTokenContract.enclaveTicketToken.getAddress(), + owner, + ); + + // Deploy a mock ciphernode registry for testing + const mockRegistry = await ignition.deploy( + MockCiphernodeRegistryEmptyKeyModule, + ); + + // Set up cross-contract dependencies + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await bondingRegistry.setRegistry( + await mockRegistry.mockCiphernodeRegistryEmptyKey.getAddress(), + ); + await bondingRegistry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); + await slashingManagerContract.slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + + // Set up licensed operators with ticket balances + const licenseToken = EnclaveTicketTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + const licenseAmount = ethers.parseEther("1000"); // Min license bond + + // Whitelist bonding registry for license token transfers + await enclTokenContract.enclaveToken.whitelistContracts( + await bondingRegistry.getAddress(), + ethers.ZeroAddress, + ); + + for (const node of [node1, node2, node3, node4]) { + const nodeTickets = + node === node1 ? 5 : node === node2 ? 3 : node === node3 ? 7 : 2; + const ticketAmount = TICKET_PRICE * BigInt(nodeTickets); + + // Bond license first + await enclTokenContract.enclaveToken.mintAllocation( + node.address, + licenseAmount, + "Test allocation", + ); + await licenseToken + .connect(node) + .approve(await bondingRegistry.getAddress(), licenseAmount); + await bondingRegistry.connect(node).bondLicense(licenseAmount); + + // Then register operator + await bondingRegistry.connect(node).registerOperator(); + + // Mint USDC to node and have them add ticket balance through bonding registry + await usdcToken.mint(node.address, ticketAmount); + + // Node approves ticket token to spend USDC (needed for depositFrom) + await usdcToken + .connect(node) + .approve(await ticketToken.getAddress(), ticketAmount); + + // Node adds ticket balance (this will call ticketToken.depositFrom internally) + await bondingRegistry.connect(node).addTicketBalance(ticketAmount); + } + + return { + committeeSortition, + bondingRegistry, + owner, + ciphernodeRegistry, + node1, + node2, + node3, + node4, + }; + } + + describe("Initialization", function () { + it("Should initialize sortition correctly", async function () { + const { committeeSortition, ciphernodeRegistry } = + await loadFixture(deployFixture); + const requestBlock = await ethers.provider.getBlockNumber(); + + await committeeSortition + .connect(ciphernodeRegistry) + .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + + const [threshold, seed, reqBlock, deadline, finalized] = + await committeeSortition.getSortitionInfo(E3_ID); + + expect(threshold).to.equal(THRESHOLD); + expect(seed).to.equal(SEED); + expect(reqBlock).to.equal(requestBlock); + expect(finalized).to.be.false; + expect(deadline).to.be.gt(0); + }); + + it("Should revert if not called by ciphernode registry", async function () { + const { committeeSortition, owner } = await loadFixture(deployFixture); + const requestBlock = await ethers.provider.getBlockNumber(); + + await expect( + committeeSortition + .connect(owner) + .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), + ).to.be.revertedWithCustomError( + committeeSortition, + "OnlyCiphernodeRegistry", + ); + }); + + it("Should revert if already initialized", async function () { + const { committeeSortition, ciphernodeRegistry } = + await loadFixture(deployFixture); + const requestBlock = await ethers.provider.getBlockNumber(); + + await committeeSortition + .connect(ciphernodeRegistry) + .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + + await expect( + committeeSortition + .connect(ciphernodeRegistry) + .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), + ).to.be.revertedWithCustomError( + committeeSortition, + "CommitteeAlreadyFinalized", + ); + }); + }); + + describe("Ticket Submission", function () { + async function initializeFixture() { + const fixture = await deployFixture(); + const requestBlock = await ethers.provider.getBlockNumber(); + await fixture.committeeSortition + .connect(fixture.ciphernodeRegistry) + .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + return { ...fixture, requestBlock }; + } + + it("Should submit ticket successfully", async function () { + const { committeeSortition, node1 } = + await loadFixture(initializeFixture); + const ticketNumber = 1; + + const tx = await committeeSortition + .connect(node1) + .submitTicket(E3_ID, ticketNumber); + await expect(tx).to.emit(committeeSortition, "TicketSubmitted"); + + const submission = await committeeSortition.getSubmission( + E3_ID, + node1.address, + ); + expect(submission.exists).to.be.true; + expect(submission.ticketNumber).to.equal(ticketNumber); + }); + + it("Should track top N nodes correctly", async function () { + const { committeeSortition, node1, node2, node3, node4 } = + await loadFixture(initializeFixture); + // Submit tickets from multiple nodes + await committeeSortition.connect(node1).submitTicket(E3_ID, 1); + await committeeSortition.connect(node2).submitTicket(E3_ID, 1); + await committeeSortition.connect(node3).submitTicket(E3_ID, 1); + await committeeSortition.connect(node4).submitTicket(E3_ID, 1); + + const topNodes = await committeeSortition.getTopNodes(E3_ID); + expect(topNodes.length).to.equal(THRESHOLD); + }); + + it("Should revert if ticket number is 0", async function () { + const { committeeSortition, node1 } = + await loadFixture(initializeFixture); + await expect( + committeeSortition.connect(node1).submitTicket(E3_ID, 0), + ).to.be.revertedWithCustomError( + committeeSortition, + "InvalidTicketNumber", + ); + }); + + it("Should revert if ticket number exceeds available tickets", async function () { + const { committeeSortition, node1 } = + await loadFixture(initializeFixture); + await expect( + committeeSortition.connect(node1).submitTicket(E3_ID, 100), + ).to.be.revertedWithCustomError( + committeeSortition, + "InvalidTicketNumber", + ); + }); + + it("Should revert if node already submitted", async function () { + const { committeeSortition, node1 } = + await loadFixture(initializeFixture); + await committeeSortition.connect(node1).submitTicket(E3_ID, 1); + + await expect( + committeeSortition.connect(node1).submitTicket(E3_ID, 2), + ).to.be.revertedWithCustomError( + committeeSortition, + "NodeAlreadySubmitted", + ); + }); + + it("Should revert if node has no tickets", async function () { + const { committeeSortition } = await loadFixture(initializeFixture); + // Create a completely fresh wallet + const nodeWithNoTickets = ethers.Wallet.createRandom().connect( + ethers.provider, + ); + + // Fund it with ETH for gas but don't set ticket balance + const [funder] = await ethers.getSigners(); + await funder.sendTransaction({ + to: nodeWithNoTickets.address, + value: ethers.parseEther("1"), + }); + + // When a node has 0 tickets and tries to submit ticket 1, + // it will revert with InvalidTicketNumber (since 1 > 0 available tickets) + await expect( + committeeSortition.connect(nodeWithNoTickets).submitTicket(E3_ID, 1), + ).to.be.revertedWithCustomError( + committeeSortition, + "InvalidTicketNumber", + ); + }); + + it("Should revert if submission window closed", async function () { + const { committeeSortition, node1 } = + await loadFixture(initializeFixture); + // Fast forward time beyond submission window + await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); + await ethers.provider.send("evm_mine", []); + + await expect( + committeeSortition.connect(node1).submitTicket(E3_ID, 1), + ).to.be.revertedWithCustomError( + committeeSortition, + "SubmissionWindowClosed", + ); + }); + + it("Should compute scores correctly", async function () { + const { committeeSortition, node1 } = + await loadFixture(initializeFixture); + const ticketNumber = 1; + + // Compute expected score off-chain + const expectedScore = await committeeSortition.computeTicketScore( + node1.address, + ticketNumber, + E3_ID, + SEED, + ); + + await committeeSortition.connect(node1).submitTicket(E3_ID, ticketNumber); + + const submission = await committeeSortition.getSubmission( + E3_ID, + node1.address, + ); + expect(submission.score).to.equal(expectedScore); + }); + }); + + describe("Committee Finalization", function () { + async function finalizeFixture() { + const fixture = await deployFixture(); + const requestBlock = await ethers.provider.getBlockNumber(); + await fixture.committeeSortition + .connect(fixture.ciphernodeRegistry) + .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + + // Submit tickets from nodes + await fixture.committeeSortition + .connect(fixture.node1) + .submitTicket(E3_ID, 1); + await fixture.committeeSortition + .connect(fixture.node2) + .submitTicket(E3_ID, 1); + await fixture.committeeSortition + .connect(fixture.node3) + .submitTicket(E3_ID, 1); + return { ...fixture, requestBlock }; + } + + it("Should finalize committee after deadline", async function () { + const { committeeSortition, owner } = await loadFixture(finalizeFixture); + // Fast forward time + await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); + await ethers.provider.send("evm_mine", []); + + const tx = await committeeSortition + .connect(owner) + .finalizeCommittee(E3_ID); + await expect(tx).to.emit(committeeSortition, "CommitteeFinalized"); + + const [, , , , finalized] = + await committeeSortition.getSortitionInfo(E3_ID); + expect(finalized).to.be.true; + }); + + it("Should revert if finalized before deadline", async function () { + const { committeeSortition, owner } = await loadFixture(finalizeFixture); + await expect( + committeeSortition.connect(owner).finalizeCommittee(E3_ID), + ).to.be.revertedWithCustomError( + committeeSortition, + "SubmissionWindowNotClosed", + ); + }); + + it("Should revert if already finalized", async function () { + const { committeeSortition, owner } = await loadFixture(finalizeFixture); + // Fast forward time + await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); + await ethers.provider.send("evm_mine", []); + + await committeeSortition.connect(owner).finalizeCommittee(E3_ID); + + await expect( + committeeSortition.connect(owner).finalizeCommittee(E3_ID), + ).to.be.revertedWithCustomError( + committeeSortition, + "CommitteeAlreadyFinalized", + ); + }); + + it("Should return correct committee", async function () { + const { committeeSortition, owner } = await loadFixture(finalizeFixture); + // Fast forward time + await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); + await ethers.provider.send("evm_mine", []); + + const committee = await committeeSortition + .connect(owner) + .finalizeCommittee.staticCall(E3_ID); + + expect(committee.length).to.equal(THRESHOLD); + }); + + it("Should prevent submissions after finalization", async function () { + const { committeeSortition, owner, node4 } = + await loadFixture(finalizeFixture); + // Fast forward time + await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); + await ethers.provider.send("evm_mine", []); + + await committeeSortition.connect(owner).finalizeCommittee(E3_ID); + + // Try to submit - should fail because submission window is closed + // Note: The contract checks submission window before checking if finalized + await expect( + committeeSortition.connect(node4).submitTicket(E3_ID, 1), + ).to.be.revertedWithCustomError( + committeeSortition, + "SubmissionWindowClosed", + ); + }); + }); + + describe("Score Sorting", function () { + async function scoreSortingFixture() { + const fixture = await deployFixture(); + const requestBlock = await ethers.provider.getBlockNumber(); + await fixture.committeeSortition + .connect(fixture.ciphernodeRegistry) + .initializeSortition(E3_ID, 2, SEED, requestBlock); // Threshold of 2 + return { ...fixture, requestBlock }; + } + + it("Should maintain sorted order (lowest scores first)", async function () { + const { committeeSortition, node1, node2, node3 } = + await loadFixture(scoreSortingFixture); + // Submit tickets + await committeeSortition.connect(node1).submitTicket(E3_ID, 1); + await committeeSortition.connect(node2).submitTicket(E3_ID, 1); + await committeeSortition.connect(node3).submitTicket(E3_ID, 1); + + const topNodes = await committeeSortition.getTopNodes(E3_ID); + expect(topNodes.length).to.equal(2); + + // Verify scores are in ascending order + const score1 = ( + await committeeSortition.getSubmission(E3_ID, topNodes[0]) + ).score; + const score2 = ( + await committeeSortition.getSubmission(E3_ID, topNodes[1]) + ).score; + + expect(score1).to.be.lte(score2); + }); + + it("Should replace worst node when better score arrives", async function () { + const { committeeSortition, node1, node2, node3 } = + await loadFixture(scoreSortingFixture); + await committeeSortition.connect(node1).submitTicket(E3_ID, 1); + await committeeSortition.connect(node2).submitTicket(E3_ID, 1); + + const topNodesBefore = await committeeSortition.getTopNodes(E3_ID); + + // Submit from node3 - should replace worst if score is better + await committeeSortition.connect(node3).submitTicket(E3_ID, 1); + + const topNodesAfter = await committeeSortition.getTopNodes(E3_ID); + expect(topNodesAfter.length).to.equal(2); + }); + }); +}); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index bc46ff8eeb..d8a0582cee 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -11,6 +11,7 @@ import { poseidon2 } from "poseidon-lite"; import BondingRegistryModule from "../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../ignition/modules/ciphernodeRegistry"; +import CommitteeSortitionModule from "../ignition/modules/committeeSortition"; import EnclaveModule from "../ignition/modules/enclave"; import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../ignition/modules/enclaveToken"; @@ -174,6 +175,17 @@ describe("Enclave", function () { const ciphernodeRegistryAddress = await ciphernodeRegistry.cipherNodeRegistry.getAddress(); + const committeeSortition = await ignition.deploy(CommitteeSortitionModule, { + parameters: { + CommitteeSortition: { + bondingRegistry: + await bondingRegistryContract.bondingRegistry.getAddress(), + ciphernodeRegistry: ciphernodeRegistryAddress, + submissionWindow: 300, + }, + }, + }); + const enclave = EnclaveFactory.connect(enclaveAddress, owner); const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, @@ -185,6 +197,10 @@ describe("Enclave", function () { await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); } + await ciphernodeRegistryContract.setCommitteeSortition( + await committeeSortition.committeeSortition.getAddress(), + ); + await ticketTokenContract.enclaveTicketToken.setRegistry( await bondingRegistryContract.bondingRegistry.getAddress(), ); diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index e6d7893bf4..b32d91486c 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -8,7 +8,13 @@ import { expect } from "chai"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; +import CommitteeSortitionModule from "../../ignition/modules/committeeSortition"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory } from "../../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; @@ -27,19 +33,109 @@ const hash = (a: bigint, b: bigint) => poseidon2([a, b]); describe("CiphernodeRegistryOwnable", function () { async function setup() { const [owner, notTheOwner] = await ethers.getSigners(); + const ownerAddress = await owner.getAddress(); + + // Deploy token contracts + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, + }, + }, + }); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcContract.mockUSDC.getAddress(), + registry: AddressOne, + owner: ownerAddress, + }, + }, + }, + ); + + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: AddressOne, + }, + }, + }, + ); + + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: AddressOne, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseEther("10"), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: 7 * 24 * 60 * 60, + }, + }, + }, + ); const registryContract = await ignition.deploy(CiphernodeRegistryModule, { parameters: { CiphernodeRegistry: { - enclaveAddress: await owner.getAddress(), - owner: await owner.getAddress(), + enclaveAddress: ownerAddress, + owner: ownerAddress, }, }, }); - const registry = CiphernodeRegistryFactory.connect( - await registryContract.cipherNodeRegistry.getAddress(), - owner, + const registryAddress = + await registryContract.cipherNodeRegistry.getAddress(); + + const committeeSortition = await ignition.deploy(CommitteeSortitionModule, { + parameters: { + CommitteeSortition: { + bondingRegistry: + await bondingRegistryContract.bondingRegistry.getAddress(), + ciphernodeRegistry: registryAddress, + submissionWindow: 300, + }, + }, + }); + + const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); + + // Set up cross-contract dependencies + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + await bondingRegistryContract.bondingRegistry.setRegistry(registryAddress); + await bondingRegistryContract.bondingRegistry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); + await slashingManagerContract.slashingManager.setBondingRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + + await registry.setCommitteeSortition( + await committeeSortition.committeeSortition.getAddress(), ); const tree = new LeanIMT(hash); diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 11b078c4fb..07fb05cce2 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,10 +2,11 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" - enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + e3_program: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + enclave: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + committee_sortition: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" program: dev: true diff --git a/templates/default/hardhat.config.ts b/templates/default/hardhat.config.ts index 5636d732d9..12c4b4d6b3 100644 --- a/templates/default/hardhat.config.ts +++ b/templates/default/hardhat.config.ts @@ -134,7 +134,7 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/test/MockRegistryFilter.sol", + "@enclave-e3/contracts/contracts/sortition/CommitteeSortition.sol", ], compilers: [ { diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 1943ea6987..9546277fc8 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,9 +2,11 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - enclave: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + e3_program: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + enclave: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + committee_sortition: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" nodes: cn1: From 2b78cc43ce11da69ef60787800cc30ab2a487ba2 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 15:44:10 +0500 Subject: [PATCH 32/88] chore: header --- .../ignition/modules/committeeSortition.ts | 6 ++++++ packages/enclave-contracts/test/CommitteeSortition.spec.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/enclave-contracts/ignition/modules/committeeSortition.ts b/packages/enclave-contracts/ignition/modules/committeeSortition.ts index f479beb29e..80d471c9e8 100644 --- a/packages/enclave-contracts/ignition/modules/committeeSortition.ts +++ b/packages/enclave-contracts/ignition/modules/committeeSortition.ts @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("CommitteeSortition", (m) => { diff --git a/packages/enclave-contracts/test/CommitteeSortition.spec.ts b/packages/enclave-contracts/test/CommitteeSortition.spec.ts index 2dd00e040a..85b75b558c 100644 --- a/packages/enclave-contracts/test/CommitteeSortition.spec.ts +++ b/packages/enclave-contracts/test/CommitteeSortition.spec.ts @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + import { expect } from "chai"; import { network } from "hardhat"; From b63d6bb8133ad00d110f9341c19e0c2f1e584e42 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 15:50:23 +0500 Subject: [PATCH 33/88] chore: linter fix --- .../enclave-contracts/ignition/modules/committeeSortition.ts | 1 + packages/enclave-contracts/test/CommitteeSortition.spec.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/enclave-contracts/ignition/modules/committeeSortition.ts b/packages/enclave-contracts/ignition/modules/committeeSortition.ts index 80d471c9e8..61d583eab5 100644 --- a/packages/enclave-contracts/ignition/modules/committeeSortition.ts +++ b/packages/enclave-contracts/ignition/modules/committeeSortition.ts @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +/* eslint-disable @typescript-eslint/no-explicit-any */ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("CommitteeSortition", (m) => { diff --git a/packages/enclave-contracts/test/CommitteeSortition.spec.ts b/packages/enclave-contracts/test/CommitteeSortition.spec.ts index 85b75b558c..51ee1a8f59 100644 --- a/packages/enclave-contracts/test/CommitteeSortition.spec.ts +++ b/packages/enclave-contracts/test/CommitteeSortition.spec.ts @@ -3,7 +3,6 @@ // This file is provided WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. - import { expect } from "chai"; import { network } from "hardhat"; @@ -531,7 +530,7 @@ describe("CommitteeSortition", function () { await committeeSortition.connect(node1).submitTicket(E3_ID, 1); await committeeSortition.connect(node2).submitTicket(E3_ID, 1); - const topNodesBefore = await committeeSortition.getTopNodes(E3_ID); + // const topNodesBefore = await committeeSortition.getTopNodes(E3_ID); // Submit from node3 - should replace worst if score is better await committeeSortition.connect(node3).submitTicket(E3_ID, 1); From d524e35c7f980845cbcf12a2db8839b82c21eaff Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 19:28:13 +0500 Subject: [PATCH 34/88] chore: merge dev to hmza/economics --- .../src/ciphernode_builder.rs | 31 ++++++++----- .../entrypoint/src/start/aggregator_start.rs | 2 +- examples/CRISP/hardhat.config.ts | 18 +++----- examples/CRISP/server/src/server/indexer.rs | 14 +++--- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 4 +- .../interfaces/IEnclave.sol/IEnclave.json | 6 +-- .../enclave-contracts/deployed_contracts.json | 35 +++++++++++--- .../scripts/deployAndSave/enclave.ts | 26 ++++------- .../scripts/deployEnclave.ts | 46 ++----------------- 10 files changed, 80 insertions(+), 104 deletions(-) diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 4d6008f970..944ed346e7 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -22,8 +22,9 @@ use e3_evm::{ load_signer_from_repository, ConcreteReadProvider, ConcreteWriteProvider, EthProvider, ProviderConfig, }, + BondingRegistryReaderRepositoryFactory, BondingRegistrySol, CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, EnclaveSol, EnclaveSolReader, - EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, RegistryFilterSol, + EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, }; use e3_fhe::ext::FheExtension; use e3_keyshare::ext::{KeyshareExtension, ThresholdKeyshareExtension}; @@ -64,8 +65,9 @@ pub struct CiphernodeBuilder { pub struct ContractComponents { enclave_reader: bool, enclave: bool, - registry_filter: bool, ciphernode_registry: bool, + bonding_registry: bool, + committee_sortition: bool, } #[derive(Clone, Debug)] @@ -211,8 +213,14 @@ impl CiphernodeBuilder { } /// Setup a writable RegistryFilter for every evm chain provided - pub fn with_contract_registry_filter(mut self) -> Self { - self.contract_components.registry_filter = true; + pub fn with_contract_bonding_registry(mut self) -> Self { + self.contract_components.bonding_registry = true; + self + } + + /// Setup a writable CommitteeSortition for every evm chain provided + pub fn with_contract_committee_sortition(mut self) -> Self { + self.contract_components.committee_sortition = true; self } @@ -319,14 +327,15 @@ impl CiphernodeBuilder { .await?; } - if self.contract_components.registry_filter { - let write_provider = provider_cache - .ensure_write_provider(&repositories, chain, cipher) - .await?; - RegistryFilterSol::attach( + if self.contract_components.bonding_registry { + let read_provider = provider_cache.ensure_read_provider(chain).await?; + BondingRegistrySol::attach( &local_bus, - write_provider.clone(), - &chain.contracts.filter_registry.address(), + read_provider.clone(), + &chain.contracts.bonding_registry.address(), + &repositories.bonding_registry_reader(read_provider.chain_id()), + chain.contracts.bonding_registry.deploy_block(), + chain.rpc_url.clone(), ) .await?; } diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 9154d9d884..857dc9e57c 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -39,7 +39,7 @@ pub async fn execute( .with_datastore(store) .with_chains(&config.chains()) .with_contract_enclave_full() - .with_contract_registry_filter() + .with_contract_bonding_registry() .with_contract_ciphernode_registry() .with_plaintext_aggregation() .with_pubkey_aggregation() diff --git a/examples/CRISP/hardhat.config.ts b/examples/CRISP/hardhat.config.ts index 9ca0501460..3a353863cd 100644 --- a/examples/CRISP/hardhat.config.ts +++ b/examples/CRISP/hardhat.config.ts @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatUserConfig } from "hardhat/config"; -import { cleanDeploymentsTask} from "@enclave-e3/contracts/tasks/utils"; +import { cleanDeploymentsTask } from "@enclave-e3/contracts/tasks/utils"; import { ciphernodeAdd } from "@enclave-e3/contracts/tasks/ciphernode"; import dotenv from "dotenv"; @@ -69,10 +69,7 @@ const config: HardhatUserConfig = { hardhatNetworkHelpers, hardhatToolboxMochaEthersPlugin, ], - tasks: [ - cleanDeploymentsTask, - ciphernodeAdd, - ], + tasks: [cleanDeploymentsTask, ciphernodeAdd], networks: { hardhat: { type: "edr-simulated", @@ -88,22 +85,22 @@ const config: HardhatUserConfig = { }, arbitrum: getChainConfig( "arbitrum-mainnet", - process.env.ARBISCAN_API_KEY || "", + process.env.ARBISCAN_API_KEY || "" ), avalanche: getChainConfig("avalanche", process.env.SNOWTRACE_API_KEY || ""), bsc: getChainConfig("bsc", process.env.BSCSCAN_API_KEY || ""), mainnet: getChainConfig("mainnet", process.env.ETHERSCAN_API_KEY || ""), optimism: getChainConfig( "optimism-mainnet", - process.env.OPTIMISM_API_KEY || "", + process.env.OPTIMISM_API_KEY || "" ), "polygon-mainnet": getChainConfig( "polygon-mainnet", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), "polygon-mumbai": getChainConfig( "polygon-mumbai", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), sepolia: getChainConfig("sepolia", process.env.ETHERSCAN_API_KEY || ""), goerli: getChainConfig("goerli", process.env.ETHERSCAN_API_KEY || ""), @@ -121,7 +118,7 @@ const config: HardhatUserConfig = { solidity: { version: "0.8.28", npmFilesToBuild: [ - "poseidon-solidity/PoseidonT3.sol", + "poseidon-solidity/PoseidonT3.sol", "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", "@enclave-e3/contracts/contracts/registry/NaiveRegistryFilter.sol", @@ -130,7 +127,6 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/test/MockRegistryFilter.sol", ], settings: { optimizer: { diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index a00dc9f533..34fd5ec70b 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -47,10 +47,9 @@ pub async fn register_e3_requested( info!("E3Requested: {:?}", event); async move { - // Convert custom params bytes back to token address and balance threshold. - let custom_params: CustomParams = - serde_json::from_slice(&event.e3.customParams) - .with_context(|| "Failed to parse custom params from E3 event")?; + // Convert custom params bytes back to token address and balance threshold. + let custom_params: CustomParams = serde_json::from_slice(&event.e3.customParams) + .with_context(|| "Failed to parse custom params from E3 event")?; let token_address: Address = custom_params .token_address @@ -62,7 +61,8 @@ pub async fn register_e3_requested( .ok_or_else(|| eyre::eyre!("Invalid balance threshold"))?; // save the e3 details - repo.initialize_round(custom_params.token_address, custom_params.balance_threshold).await?; + repo.initialize_round(custom_params.token_address, custom_params.balance_threshold) + .await?; // Get token holders from Bitquery API or mocked data. let token_holders = if matches!(CONFIG.chain_id, 31337 | 1337) { @@ -294,7 +294,7 @@ pub async fn register_committee_published( pub async fn start_indexer( ws_url: &str, contract_address: &str, - registry_filter_address: &str, + registry_address: &str, store: impl DataStore, private_key: &str, ) -> Result<()> { @@ -317,7 +317,7 @@ pub async fn start_indexer( // Registry Listener let registry_contract_listener = - EventListener::create_contract_listener(&ws_url, registry_filter_address).await?; + EventListener::create_contract_listener(&ws_url, registry_address).await?; let registry_listener = register_committee_published(registry_contract_listener, readwrite_contract).await?; registry_listener.start(); diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index 1c74d101bd..047c0cfbd6 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_27-0d27918dccf41f92d21008dc1c7a071e1b0a90b0" + "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 9a673587b4..e49a5c5262 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -403,5 +403,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_27-0d27918dccf41f92d21008dc1c7a071e1b0a90b0" -} + "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" +} \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 63bd4192f9..aba14551e3 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,9 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", -<<<<<<< HEAD - "buildInfoId": "solc-0_8_27-0d27918dccf41f92d21008dc1c7a071e1b0a90b0" -======= - "buildInfoId": "solc-0_8_28-5c8e4a4cdd9ec90fb8a4640bf873c02f6e1ad388" ->>>>>>> dev + "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" } \ No newline at end of file diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 013256ab7c..5e10cef656 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -125,15 +125,17 @@ }, "Enclave": { "constructorArgs": { - "params": "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "maxDuration": "2592000", - "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "registry": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "bondingRegistry": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "feeToken": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + "feeToken": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "maxDuration": "2592000", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] }, - "blockNumber": 10, - "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + "blockNumber": 4, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" }, "MockComputeProvider": { "blockNumber": 18, @@ -153,6 +155,27 @@ }, "blockNumber": 21, "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "PoseidonT3": { + "blockNumber": 1, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + }, + "CommitteeSortition": { + "constructorArgs": { + "bondingRegistry": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "ciphernodeRegistry": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "submissionWindow": "300" + }, + "blockNumber": 3, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" } } } \ No newline at end of file diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index 45df6bb778..1b814b9e93 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -20,9 +20,9 @@ export interface EnclaveArgs { owner?: string; maxDuration?: string; registry?: string; - poseidonT3Address: string; bondingRegistry?: string; feeToken?: string; + poseidonT3Address: string; hre: HardhatRuntimeEnvironment; } @@ -55,10 +55,11 @@ export const deployAndSaveEnclave = async ({ !registry || !bondingRegistry || !feeToken || - (preDeployedArgs?.constructorArgs?.params === params && - preDeployedArgs?.constructorArgs?.owner === owner && + (preDeployedArgs?.constructorArgs?.owner === owner && preDeployedArgs?.constructorArgs?.maxDuration === maxDuration && preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.feeToken === feeToken && areArraysEqual( preDeployedArgs?.constructorArgs?.params as string[], params, @@ -86,21 +87,11 @@ export const deployAndSaveEnclave = async ({ const enclave = await enclaveFactory.deploy( owner, registry, + bondingRegistry, + feeToken, maxDuration, params, ); - const enclave = await ignition.deploy(EnclaveModule, { - parameters: { - Enclave: { - params, - owner, - maxDuration, - registry, - bondingRegistry, - feeToken, - }, - }, - }); await enclave.waitForDeployment(); @@ -110,14 +101,13 @@ export const deployAndSaveEnclave = async ({ storeDeploymentArgs( { constructorArgs: { - params, owner, - maxDuration, registry, bondingRegistry, feeToken, + maxDuration, + params, }, - constructorArgs: { owner, registry, maxDuration, params }, blockNumber, address: enclaveAddress, }, diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 0743258b3f..6cc38404d8 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -9,11 +9,10 @@ import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; import { deployAndSaveCommitteeSortition } from "./deployAndSave/committeeSortition"; import { deployAndSaveEnclave } from "./deployAndSave/enclave"; -import { deployAndSaveNaiveRegistryFilter } from "./deployAndSave/naiveRegistryFilter"; -import { deployAndSavePoseidonT3 } from "./deployAndSave/poseidonT3"; import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; import { deployAndSaveMockStableToken } from "./deployAndSave/mockStableToken"; +import { deployAndSavePoseidonT3 } from "./deployAndSave/poseidonT3"; import { deployAndSaveSlashingManager } from "./deployAndSave/slashingManager"; import { deployMocks } from "./deployMocks"; @@ -41,9 +40,6 @@ export const deployEnclave = async (withMocks?: boolean) => { const poseidonT3 = await deployAndSavePoseidonT3({ hre }); - console.log("Deploying Enclave"); - const { enclave } = await deployAndSaveEnclave({ - params: [encoded], const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; let feeTokenAddress: string; @@ -83,7 +79,6 @@ export const deployEnclave = async (withMocks?: boolean) => { const { slashingManager } = await deployAndSaveSlashingManager({ admin: ownerAddress, bondingRegistry: addressOne, - poseidonT3Address: poseidonT3, hre, }); const slashingManagerAddress = await slashingManager.getAddress(); @@ -106,14 +101,10 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("BondingRegistry deployed to:", bondingRegistryAddress); console.log("Deploying CiphernodeRegistry..."); - - const enclaveAddress = await enclave.getAddress(); - - console.log("Deploying CiphernodeRegistry"); const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ + poseidonT3Address: poseidonT3, enclaveAddress: addressOne, owner: ownerAddress, - poseidonT3Address: poseidonT3, hre, }); const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); @@ -130,16 +121,13 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Deploying Enclave..."); const { enclave } = await deployAndSaveEnclave({ - params: encoded, - - console.log("Deploying NaiveRegistryFilter"); - const { naiveRegistryFilter } = await deployAndSaveNaiveRegistryFilter({ - ciphernodeRegistryAddress: ciphernodeRegistryAddress, + params: [encoded], owner: ownerAddress, maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), registry: ciphernodeRegistryAddress, bondingRegistry: bondingRegistryAddress, feeToken: feeTokenAddress, + poseidonT3Address: poseidonT3, hre, }); const enclaveAddress = await enclave.getAddress(); @@ -175,33 +163,7 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting CommitteeSortition address in CiphernodeRegistry..."); await ciphernodeRegistry.setCommitteeSortition(committeeSortitionAddress); - const naiveRegistryFilterAddress = await naiveRegistryFilter.getAddress(); - - const registryAddress = await enclave.ciphernodeRegistry(); - - console.log("Setting CiphernodeRegistry in Enclave"); - if (registryAddress === ciphernodeRegistryAddress) { - console.log(`Enclave contract already has registry`); - } else { - const tx = await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); - await tx.wait(); - - console.log(`Enclave contract updated with registry`); - } - - console.log(` - Deployments: - ---------------------------------------------------------------------- - Enclave: ${enclaveAddress} - CiphernodeRegistry: ${ciphernodeRegistryAddress} - NaiveRegistryFilter: ${naiveRegistryFilterAddress} - `); - - // Deploy mocks only if specified - const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; - if (shouldDeployMocks) { - console.log("Deploying Mocks"); const { decryptionVerifierAddress, e3ProgramAddress } = await deployMocks(); const encryptionSchemeId = ethers.keccak256( From 225e445c16eb7e4233ee1ac18143dfc86bef2725 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 19:52:03 +0500 Subject: [PATCH 35/88] chore: add CommitteeSortition to gitigore --- packages/enclave-contracts/.gitignore | 2 + .../CommitteeSortition.json | 447 ++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json diff --git a/packages/enclave-contracts/.gitignore b/packages/enclave-contracts/.gitignore index d778ae5d7a..bc722a45a3 100644 --- a/packages/enclave-contracts/.gitignore +++ b/packages/enclave-contracts/.gitignore @@ -8,12 +8,14 @@ !/artifacts/contracts/ !/artifacts/contracts/interfaces/ !/artifacts/contracts/registry/ +!/artifacts/contracts/sortition/ !/artifacts/contracts/interfaces/IEnclave.sol/ !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ !/artifacts/contracts/interfaces/IBondingRegistry.sol/ !/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json !/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +!/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json build cache coverage diff --git a/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json b/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json new file mode 100644 index 0000000000..d24b1126e4 --- /dev/null +++ b/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json @@ -0,0 +1,447 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "CommitteeSortition", + "sourceName": "contracts/sortition/CommitteeSortition.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_bondingRegistry", + "type": "address" + }, + { + "internalType": "address", + "name": "_ciphernodeRegistry", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_submissionWindow", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "CommitteeAlreadyFinalized", + "type": "error" + }, + { + "inputs": [], + "name": "CommitteeNotInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTicketNumber", + "type": "error" + }, + { + "inputs": [], + "name": "NodeAlreadySubmitted", + "type": "error" + }, + { + "inputs": [], + "name": "NodeNotEligible", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyCiphernodeRegistry", + "type": "error" + }, + { + "inputs": [], + "name": "SubmissionWindowClosed", + "type": "error" + }, + { + "inputs": [], + "name": "SubmissionWindowNotClosed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "committee", + "type": "address[]" + } + ], + "name": "CommitteeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "addedToCommittee", + "type": "bool" + } + ], + "name": "TicketSubmitted", + "type": "event" + }, + { + "inputs": [], + "name": "bondingRegistry", + "outputs": [ + { + "internalType": "contract IBondingRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ciphernodeRegistry", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "uint256", + "name": "ticketNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seed", + "type": "uint256" + } + ], + "name": "computeTicketScore", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "finalizeCommittee", + "outputs": [ + { + "internalType": "address[]", + "name": "committee", + "type": "address[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getSortitionInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requestBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "submissionDeadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "finalized", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "getSubmission", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "uint256", + "name": "ticketNumber", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "exists", + "type": "bool" + } + ], + "internalType": "struct CommitteeSortition.TicketSubmission", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getTopNodes", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requestBlock", + "type": "uint256" + } + ], + "name": "initializeSortition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "sortitions", + "outputs": [ + { + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requestBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "submissionDeadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "finalized", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "submissionWindow", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ticketNumber", + "type": "uint256" + } + ], + "name": "submitTicket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60e060405234801561000f575f5ffd5b50604051610ea3380380610ea383398101604081905261002e91610064565b6001600160a01b03928316608052911660a05260c05261009d565b80516001600160a01b038116811461005f575f5ffd5b919050565b5f5f5f60608486031215610076575f5ffd5b61007f84610049565b925061008d60208501610049565b9150604084015190509250925092565b60805160a05160c051610dc06100e35f395f81816102b1015261043401525f81816101d201526103b301525f81816101930152818161083501526108a30152610dc05ff3fe608060405234801561000f575f5ffd5b50600436106100c4575f3560e01c8063b64f05921161007d578063e621dbc711610058578063e621dbc7146102ac578063e6745e13146102d3578063e7a5c098146102e6575f5ffd5b8063b64f059214610209578063da881e5a14610279578063e243eaf014610299575f5ffd5b806385814243116100ad578063858142431461018e5780638dcdd86b146101cd5780638e28a380146101f4575f5ffd5b806355f0e221146100c85780636c58e4eb14610122575b5f5ffd5b6100db6100d6366004610bf5565b610325565b604051610119919081516001600160a01b03168152602080830151908201526040808301519082015260609182015115159181019190915260800190565b60405180910390f35b610164610130366004610c1f565b5f9081526020819052604090208054600182015460028301546003840154600490940154929491939092909160ff90911690565b6040805195865260208601949094529284019190915260608301521515608082015260a001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b610207610202366004610c36565b6103a8565b005b61026b610217366004610c65565b6040805160609590951b6bffffffffffffffffffffffff1916602080870191909152603486019490945260548501929092526074808501919091528151808503909101815260949093019052815191012090565b604051908152602001610119565b61028c610287366004610c1f565b61046f565b6040516101199190610c9b565b61028c6102a7366004610c1f565b610590565b61026b7f000000000000000000000000000000000000000000000000000000000000000081565b6102076102e1366004610ce6565b6105fa565b6101646102f4366004610c1f565b5f60208190529081526040902080546001820154600283015460038401546004909401549293919290919060ff1685565b604080516080810182525f808252602082018190529181018290526060810191909152505f828152602081815260408083206001600160a01b03808616855260069091018352928190208151608081018352815490941684526001810154928401929092526002820154908301526003015460ff16151560608201525b92915050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146103f15760405163b56831db60e01b815260040160405180910390fd5b5f84815260208190526040902080541561041e57604051631860f69960e31b815260040160405180910390fd5b83815560018101839055600281018290556104597f000000000000000000000000000000000000000000000000000000000000000042610d1a565b6003820155600401805460ff1916905550505050565b5f81815260208190526040902080546060919061049f5760405163ad0d953f60e01b815260040160405180910390fd5b600481015460ff16156104c557604051631860f69960e31b815260040160405180910390fd5b806003015442116104e957604051632f021e8d60e11b815260040160405180910390fd5b60048101805460ff191660011790556005810180546040805160208084028201810190925282815292919083018282801561054b57602002820191905f5260205f20905b81546001600160a01b0316815260019091019060200180831161052d575b50505050509150827fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132836040516105829190610c9b565b60405180910390a250919050565b5f81815260208181526040918290206005018054835181840281018401909452808452606093928301828280156105ee57602002820191905f5260205f20905b81546001600160a01b031681526001909101906020018083116105d0575b50505050509050919050565b5f82815260208190526040902080546106265760405163ad0d953f60e01b815260040160405180910390fd5b806003015442111561064b576040516332999ab560e11b815260040160405180910390fd5b600481015460ff161561067157604051631860f69960e31b815260040160405180910390fd5b335f90815260068201602052604090206003015460ff16156106a65760405163257309f160e11b815260040160405180910390fd5b6106b13383856107d9565b600181810154604080516bffffffffffffffffffffffff1933606081901b91909116602080840191909152603483018890526054830189905260748084019590955283518084039095018552609483018085528551958201959095206101148401855282865260b4840189815260d4850182815260f49095018881525f85815260068b01909452958320965187546001600160a01b0319166001600160a01b0390911617875551968601969096559151600285015591516003909301805460ff1916931515939093179092556107899084908461099a565b6040805186815260208101859052821515818301529051919250339187917f2669da55d0606fd95b54298fc8fa777246cc83b04141d983fa5a6553c09bf071919081900360600190a35050505050565b815f036107f95760405163aeaddff160e01b815260040160405180910390fd5b5f818152602081905260408082206002810154915163bb03bd7160e01b81526001600160a01b03878116600483015260248201939093529092917f0000000000000000000000000000000000000000000000000000000000000000169063bb03bd7190604401602060405180830381865afa15801561087a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061089e9190610d2d565b90505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316631209b1f66040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fd573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906109219190610d2d565b9050805f036109435760405163aeaddff160e01b815260040160405180910390fd5b5f61094e8284610d44565b9050808611156109715760405163aeaddff160e01b815260040160405180910390fd5b805f036109915760405163149fbcfd60e11b815260040160405180910390fd5b50505050505050565b82546005840180545f9211156109bf576109b5858585610a6b565b6001915050610a64565b5f856006015f83600185805490506109d79190610d63565b815481106109e7576109e7610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080841015610a5e5781805480610a2857610a28610d8a565b5f8281526020902081015f1990810180546001600160a01b0319169055019055610a53868686610a6b565b600192505050610a64565b5f925050505b9392505050565b6005830180545f5b8254811015610ad7575f866006015f858481548110610a9457610a94610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080851015610ace5781925050610ad7565b50600101610a73565b508154600181810184555f8481526020812090920180546001600160a01b03191690558354610b069190610d63565b90505b81811115610b945782610b1d600183610d63565b81548110610b2d57610b2d610d76565b905f5260205f20015f9054906101000a90046001600160a01b0316838281548110610b5a57610b5a610d76565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610b8c81610d9e565b915050610b09565b5083828281548110610ba857610ba8610d76565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b80356001600160a01b0381168114610bf0575f5ffd5b919050565b5f5f60408385031215610c06575f5ffd5b82359150610c1660208401610bda565b90509250929050565b5f60208284031215610c2f575f5ffd5b5035919050565b5f5f5f5f60808587031215610c49575f5ffd5b5050823594602084013594506040840135936060013592509050565b5f5f5f5f60808587031215610c78575f5ffd5b610c8185610bda565b966020860135965060408601359560600135945092505050565b602080825282518282018190525f918401906040840190835b81811015610cdb5783516001600160a01b0316835260209384019390920191600101610cb4565b509095945050505050565b5f5f60408385031215610cf7575f5ffd5b50508035926020909101359150565b634e487b7160e01b5f52601160045260245ffd5b808201808211156103a2576103a2610d06565b5f60208284031215610d3d575f5ffd5b5051919050565b5f82610d5e57634e487b7160e01b5f52601260045260245ffd5b500490565b818103818111156103a2576103a2610d06565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52603160045260245ffd5b5f81610dac57610dac610d06565b505f19019056fea164736f6c634300081c000a", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100c4575f3560e01c8063b64f05921161007d578063e621dbc711610058578063e621dbc7146102ac578063e6745e13146102d3578063e7a5c098146102e6575f5ffd5b8063b64f059214610209578063da881e5a14610279578063e243eaf014610299575f5ffd5b806385814243116100ad578063858142431461018e5780638dcdd86b146101cd5780638e28a380146101f4575f5ffd5b806355f0e221146100c85780636c58e4eb14610122575b5f5ffd5b6100db6100d6366004610bf5565b610325565b604051610119919081516001600160a01b03168152602080830151908201526040808301519082015260609182015115159181019190915260800190565b60405180910390f35b610164610130366004610c1f565b5f9081526020819052604090208054600182015460028301546003840154600490940154929491939092909160ff90911690565b6040805195865260208601949094529284019190915260608301521515608082015260a001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b610207610202366004610c36565b6103a8565b005b61026b610217366004610c65565b6040805160609590951b6bffffffffffffffffffffffff1916602080870191909152603486019490945260548501929092526074808501919091528151808503909101815260949093019052815191012090565b604051908152602001610119565b61028c610287366004610c1f565b61046f565b6040516101199190610c9b565b61028c6102a7366004610c1f565b610590565b61026b7f000000000000000000000000000000000000000000000000000000000000000081565b6102076102e1366004610ce6565b6105fa565b6101646102f4366004610c1f565b5f60208190529081526040902080546001820154600283015460038401546004909401549293919290919060ff1685565b604080516080810182525f808252602082018190529181018290526060810191909152505f828152602081815260408083206001600160a01b03808616855260069091018352928190208151608081018352815490941684526001810154928401929092526002820154908301526003015460ff16151560608201525b92915050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146103f15760405163b56831db60e01b815260040160405180910390fd5b5f84815260208190526040902080541561041e57604051631860f69960e31b815260040160405180910390fd5b83815560018101839055600281018290556104597f000000000000000000000000000000000000000000000000000000000000000042610d1a565b6003820155600401805460ff1916905550505050565b5f81815260208190526040902080546060919061049f5760405163ad0d953f60e01b815260040160405180910390fd5b600481015460ff16156104c557604051631860f69960e31b815260040160405180910390fd5b806003015442116104e957604051632f021e8d60e11b815260040160405180910390fd5b60048101805460ff191660011790556005810180546040805160208084028201810190925282815292919083018282801561054b57602002820191905f5260205f20905b81546001600160a01b0316815260019091019060200180831161052d575b50505050509150827fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132836040516105829190610c9b565b60405180910390a250919050565b5f81815260208181526040918290206005018054835181840281018401909452808452606093928301828280156105ee57602002820191905f5260205f20905b81546001600160a01b031681526001909101906020018083116105d0575b50505050509050919050565b5f82815260208190526040902080546106265760405163ad0d953f60e01b815260040160405180910390fd5b806003015442111561064b576040516332999ab560e11b815260040160405180910390fd5b600481015460ff161561067157604051631860f69960e31b815260040160405180910390fd5b335f90815260068201602052604090206003015460ff16156106a65760405163257309f160e11b815260040160405180910390fd5b6106b13383856107d9565b600181810154604080516bffffffffffffffffffffffff1933606081901b91909116602080840191909152603483018890526054830189905260748084019590955283518084039095018552609483018085528551958201959095206101148401855282865260b4840189815260d4850182815260f49095018881525f85815260068b01909452958320965187546001600160a01b0319166001600160a01b0390911617875551968601969096559151600285015591516003909301805460ff1916931515939093179092556107899084908461099a565b6040805186815260208101859052821515818301529051919250339187917f2669da55d0606fd95b54298fc8fa777246cc83b04141d983fa5a6553c09bf071919081900360600190a35050505050565b815f036107f95760405163aeaddff160e01b815260040160405180910390fd5b5f818152602081905260408082206002810154915163bb03bd7160e01b81526001600160a01b03878116600483015260248201939093529092917f0000000000000000000000000000000000000000000000000000000000000000169063bb03bd7190604401602060405180830381865afa15801561087a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061089e9190610d2d565b90505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316631209b1f66040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fd573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906109219190610d2d565b9050805f036109435760405163aeaddff160e01b815260040160405180910390fd5b5f61094e8284610d44565b9050808611156109715760405163aeaddff160e01b815260040160405180910390fd5b805f036109915760405163149fbcfd60e11b815260040160405180910390fd5b50505050505050565b82546005840180545f9211156109bf576109b5858585610a6b565b6001915050610a64565b5f856006015f83600185805490506109d79190610d63565b815481106109e7576109e7610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080841015610a5e5781805480610a2857610a28610d8a565b5f8281526020902081015f1990810180546001600160a01b0319169055019055610a53868686610a6b565b600192505050610a64565b5f925050505b9392505050565b6005830180545f5b8254811015610ad7575f866006015f858481548110610a9457610a94610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080851015610ace5781925050610ad7565b50600101610a73565b508154600181810184555f8481526020812090920180546001600160a01b03191690558354610b069190610d63565b90505b81811115610b945782610b1d600183610d63565b81548110610b2d57610b2d610d76565b905f5260205f20015f9054906101000a90046001600160a01b0316838281548110610b5a57610b5a610d76565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610b8c81610d9e565b915050610b09565b5083828281548110610ba857610ba8610d76565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b80356001600160a01b0381168114610bf0575f5ffd5b919050565b5f5f60408385031215610c06575f5ffd5b82359150610c1660208401610bda565b90509250929050565b5f60208284031215610c2f575f5ffd5b5035919050565b5f5f5f5f60808587031215610c49575f5ffd5b5050823594602084013594506040840135936060013592509050565b5f5f5f5f60808587031215610c78575f5ffd5b610c8185610bda565b966020860135965060408601359560600135945092505050565b602080825282518282018190525f918401906040840190835b81811015610cdb5783516001600160a01b0316835260209384019390920191600101610cb4565b509095945050505050565b5f5f60408385031215610cf7575f5ffd5b50508035926020909101359150565b634e487b7160e01b5f52601160045260245ffd5b808201808211156103a2576103a2610d06565b5f60208284031215610d3d575f5ffd5b5051919050565b5f82610d5e57634e487b7160e01b5f52601260045260245ffd5b500490565b818103818111156103a2576103a2610d06565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52603160045260245ffd5b5f81610dac57610dac610d06565b505f19019056fea164736f6c634300081c000a", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": { + "19668": [ + { + "length": 32, + "start": 403 + }, + { + "length": 32, + "start": 2101 + }, + { + "length": 32, + "start": 2211 + } + ], + "19671": [ + { + "length": 32, + "start": 466 + }, + { + "length": 32, + "start": 947 + } + ], + "19674": [ + { + "length": 32, + "start": 689 + }, + { + "length": 32, + "start": 1076 + } + ] + }, + "inputSourceName": "project/contracts/sortition/CommitteeSortition.sol", + "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" +} \ No newline at end of file From 649125230147d10b2fd7c1d869099e0204197961 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 20:00:50 +0500 Subject: [PATCH 36/88] fix: pass posideonT3 to Ciphernode deployment --- packages/enclave-contracts/tasks/enclave.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index 219c389c68..6cd029d075 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -275,9 +275,15 @@ export const publishCommittee = task( "../scripts/deployAndSave/ciphernodeRegistryOwnable" ); + const { deployAndSavePoseidonT3 } = await import( + "../scripts/deployAndSave/poseidonT3" + ); + const poseidonT3 = await deployAndSavePoseidonT3({ hre }); + const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ hre, + poseidonT3Address: poseidonT3, }); const nodesToSend = nodes From 492bf7cd7f9283b62339436ad748dc43a0bd027f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 20:37:41 +0500 Subject: [PATCH 37/88] chore: remove old contracts from hardhat config --- examples/CRISP/hardhat.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/CRISP/hardhat.config.ts b/examples/CRISP/hardhat.config.ts index 3a353863cd..69c9d1271b 100644 --- a/examples/CRISP/hardhat.config.ts +++ b/examples/CRISP/hardhat.config.ts @@ -121,7 +121,8 @@ const config: HardhatUserConfig = { "poseidon-solidity/PoseidonT3.sol", "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", - "@enclave-e3/contracts/contracts/registry/NaiveRegistryFilter.sol", + "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", + "@enclave-e3/contracts/contracts/sortition/CommitteeSortition.sol", "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", From f73621622d07e2f5103fc2559724650734bc26ab Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 22:14:53 +0500 Subject: [PATCH 38/88] chore: add contracts to hh config --- examples/CRISP/hardhat.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/CRISP/hardhat.config.ts b/examples/CRISP/hardhat.config.ts index 69c9d1271b..2f5543a716 100644 --- a/examples/CRISP/hardhat.config.ts +++ b/examples/CRISP/hardhat.config.ts @@ -122,12 +122,17 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", + "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", "@enclave-e3/contracts/contracts/sortition/CommitteeSortition.sol", + "@enclave-e3/contracts/contracts/token/EnclaveToken.sol", + "@enclave-e3/contracts/contracts/token/EnclaveTicketToken.sol", "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", + "@enclave-e3/contracts/contracts/test/MockStableToken.sol", + "@enclave-e3/contracts/contracts/test/MockSlashingVerifier.sol", ], settings: { optimizer: { From 56eec15ae970c0f48277d9b1e5b962f2525ad04f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 23 Oct 2025 22:43:20 +0500 Subject: [PATCH 39/88] chore: update deployment addresses --- examples/CRISP/client/.env.example | 2 +- examples/CRISP/deployed_contracts.json | 124 ++++++++++++++++++ examples/CRISP/enclave.config.yaml | 10 +- examples/CRISP/server/.env.example | 8 +- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../enclave-contracts/deployed_contracts.json | 90 ++++++------- templates/default/enclave.config.yaml | 10 +- tests/integration/enclave.config.yaml | 10 +- 9 files changed, 187 insertions(+), 71 deletions(-) diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 24be538821..75fa09b0cc 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,4 +1,4 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1 # Default E3 program address from hardhat +VITE_E3_PROGRAM_ADDRESS=0xc6e7DF5E7b4f2A278906862b61205850344D4e7d # Default E3 program address from hardhat diff --git a/examples/CRISP/deployed_contracts.json b/examples/CRISP/deployed_contracts.json index 829bc69c13..e8807a4ba8 100644 --- a/examples/CRISP/deployed_contracts.json +++ b/examples/CRISP/deployed_contracts.json @@ -8,5 +8,129 @@ "RiscZeroGroth16Verifier": { "address": "0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187" } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 1, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 24, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 24, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 24, + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 24, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "blockNumber": 24, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 25, + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + }, + "CommitteeSortition": { + "constructorArgs": { + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "ciphernodeRegistry": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "submissionWindow": "300" + }, + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "maxDuration": "2592000", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] + }, + "blockNumber": 3, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "MockComputeProvider": { + "blockNumber": 12, + "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + }, + "MockDecryptionVerifier": { + "blockNumber": 13, + "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + }, + "MockInputValidator": { + "blockNumber": 14, + "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "MockE3Program": { + "constructorArgs": { + "mockInputValidator": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 15, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, + "MockRISC0Verifier": { + "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + }, + "CRISPInputValidatorFactory": { + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "constructorArgs": { + "inputValidator": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + } + }, + "HonkVerifier": { + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, + "CRISPProgram": { + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "constructorArgs": { + "enclave": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "verifierAddress": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE", + "inputValidatorAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", + "honkVerifierAddress": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", + "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" + } + } } } \ No newline at end of file diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index aed9fbb9f3..9da355cfa2 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -2,11 +2,11 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" - enclave: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" - ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" - committee_sortition: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + e3_program: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + ciphernode_registry: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + committee_sortition: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" program: dev: true diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index c529235a11..4ae3566924 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -13,10 +13,10 @@ BITQUERY_API_KEY="" CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" -CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address -FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" +ENCLAVE_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" +CIPHERNODE_REGISTRY_ADDRESS="0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" +E3_PROGRAM_ADDRESS="0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" # CRISPProgram Contract Address +FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # E3 Config E3_WINDOW_SIZE=40 diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 073c56849e..e49a5c5262 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -404,4 +404,4 @@ "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" -} +} \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 8ff6ae2fa1..aba14551e3 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -959,4 +959,4 @@ "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" -} +} \ No newline at end of file diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 77a6d5ea19..fa23e01909 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -69,42 +69,46 @@ } }, "localhost": { + "PoseidonT3": { + "blockNumber": 1, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 1, - "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 3, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 4, - "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + "blockNumber": 5, + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 5, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 6, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", - "licenseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -112,70 +116,58 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 6, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 7, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, - "CiphernodeRegistry": { + "CiphernodeRegistryOwnable": { "constructorArgs": { - "enclaveAddress": "0x0000000000000000000000000000000000000001", - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001" }, "blockNumber": 8, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, + "CommitteeSortition": { + "constructorArgs": { + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "ciphernodeRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "submissionWindow": "300" + }, + "blockNumber": 9, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - "bondingRegistry": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "feeToken": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 4, - "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + "blockNumber": 10, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "MockComputeProvider": { - "blockNumber": 18, - "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" - }, - "MockDecryptionVerifier": { "blockNumber": 19, "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, - "MockInputValidator": { + "MockDecryptionVerifier": { "blockNumber": 20, "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, - "MockE3Program": { - "constructorArgs": { - "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" - }, + "MockInputValidator": { "blockNumber": 21, "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, - "PoseidonT3": { - "blockNumber": 1, - "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" - }, - "CiphernodeRegistryOwnable": { - "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "enclaveAddress": "0x0000000000000000000000000000000000000001" - }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" - }, - "CommitteeSortition": { + "MockE3Program": { "constructorArgs": { - "bondingRegistry": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "ciphernodeRegistry": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - "submissionWindow": "300" + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 22, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" } } -} +} \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 07fb05cce2..02ced93e30 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,11 +2,11 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" - enclave: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" - ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" - committee_sortition: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + e3_program: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + ciphernode_registry: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + committee_sortition: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" program: dev: true diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 9546277fc8..df38e2dcfb 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,11 +2,11 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" - enclave: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" - ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" - committee_sortition: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + e3_program: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + ciphernode_registry: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + committee_sortition: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" nodes: cn1: From 367817b99222f671d2d0a619907635c7b46db3f8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 24 Oct 2025 12:54:13 +0500 Subject: [PATCH 40/88] fix: minor deployment errors --- deploy/local/contracts.sh | 5 +- examples/CRISP/deployed_contracts.json | 62 ++++++------- examples/CRISP/hardhat.config.ts | 18 +++- examples/CRISP/package.json | 4 +- examples/CRISP/server/.env.example | 6 +- packages/enclave-contracts/hardhat.config.ts | 11 +++ .../scripts/cleanIgnitionState.ts | 89 +++++++++++++++++++ .../scripts/deployAndSave/enclaveToken.ts | 2 + .../scripts/deployEnclave.ts | 8 ++ packages/enclave-contracts/scripts/index.ts | 1 + tests/integration/enclave.config.yaml | 2 +- 11 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 packages/enclave-contracts/scripts/cleanIgnitionState.ts diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index 257792c8a6..ef36d19eca 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -3,11 +3,8 @@ # Install the enclave binary # cargo install --locked --path ./crates/cli --bin enclave -f -# Deploy Contacts -(cd packages/enclave-contracts && rm -rf deployments/localhost && pnpm deploy:mocks --network localhost) - # Deploy CRISP Contracts -(cd examples/CRISP && ETH_WALLET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 FOUNDRY_PROFILE=local forge script --rpc-url http://localhost:8545 --broadcast deploy/Deploy.s.sol) +(cd examples/CRISP && pnpm deploy:contracts:full:mock --network localhost) # Add Ciphernodes to Enclave sleep 2 # wait for enclave to start diff --git a/examples/CRISP/deployed_contracts.json b/examples/CRISP/deployed_contracts.json index e8807a4ba8..9014f1f64f 100644 --- a/examples/CRISP/deployed_contracts.json +++ b/examples/CRISP/deployed_contracts.json @@ -18,14 +18,14 @@ "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 24, + "blockNumber": 2, "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 24, + "blockNumber": 3, "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { @@ -34,7 +34,7 @@ "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 24, + "blockNumber": 5, "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { @@ -42,7 +42,7 @@ "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 24, + "blockNumber": 6, "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "BondingRegistry": { @@ -57,7 +57,7 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 24, + "blockNumber": 7, "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, "CiphernodeRegistryOwnable": { @@ -65,22 +65,22 @@ "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "enclaveAddress": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 25, - "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "blockNumber": 8, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "CommitteeSortition": { "constructorArgs": { "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "ciphernodeRegistry": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "ciphernodeRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", "submissionWindow": "300" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 9, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "maxDuration": "2592000", @@ -88,47 +88,47 @@ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 10, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "MockComputeProvider": { - "blockNumber": 12, - "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + "blockNumber": 19, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockDecryptionVerifier": { - "blockNumber": 13, - "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, "MockInputValidator": { - "blockNumber": 14, - "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + "blockNumber": 21, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, - "blockNumber": 15, - "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + "blockNumber": 22, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, "MockRISC0Verifier": { - "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidatorFactory": { - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", "constructorArgs": { - "inputValidator": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + "inputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" } }, "HonkVerifier": { - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" }, "CRISPProgram": { - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "constructorArgs": { - "enclave": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - "verifierAddress": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE", - "inputValidatorAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", - "honkVerifierAddress": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", + "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "inputValidatorAddress": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/examples/CRISP/hardhat.config.ts b/examples/CRISP/hardhat.config.ts index 2f5543a716..dfd423998b 100644 --- a/examples/CRISP/hardhat.config.ts +++ b/examples/CRISP/hardhat.config.ts @@ -6,7 +6,10 @@ import type { HardhatUserConfig } from "hardhat/config"; import { cleanDeploymentsTask } from "@enclave-e3/contracts/tasks/utils"; -import { ciphernodeAdd } from "@enclave-e3/contracts/tasks/ciphernode"; +import { + ciphernodeAdd, + ciphernodeAdminAdd, +} from "@enclave-e3/contracts/tasks/ciphernode"; import dotenv from "dotenv"; import hardhatEthersChaiMatchers from "@nomicfoundation/hardhat-ethers-chai-matchers"; @@ -69,12 +72,22 @@ const config: HardhatUserConfig = { hardhatNetworkHelpers, hardhatToolboxMochaEthersPlugin, ], - tasks: [cleanDeploymentsTask, ciphernodeAdd], + tasks: [cleanDeploymentsTask, ciphernodeAdd, ciphernodeAdminAdd], networks: { hardhat: { type: "edr-simulated", chainType: "l1", }, + localhost: { + accounts: { + mnemonic, + }, + chainId: chainIds.hardhat, + url: "http://localhost:8545", + type: "http", + chainType: "l1", + timeout: 60000, + }, ganache: { accounts: { mnemonic, @@ -82,6 +95,7 @@ const config: HardhatUserConfig = { chainId: chainIds.ganache, url: "http://localhost:8545", type: "http", + timeout: 60000, }, arbitrum: getChainConfig( "arbitrum-mainnet", diff --git a/examples/CRISP/package.json b/examples/CRISP/package.json index d4592d407b..385d5bef99 100644 --- a/examples/CRISP/package.json +++ b/examples/CRISP/package.json @@ -10,7 +10,7 @@ }, "scripts": { "compile": "forge compile", - "ciphernode:add": "hardhat ciphernode:add", + "ciphernode:add": "hardhat ciphernode:admin-add", "cli": "bash ./scripts/cli.sh", "clean:deployments": "hardhat utils:clean-deployments", "dev:setup": "bash ./scripts/setup.sh", @@ -18,7 +18,7 @@ "dev:up": "bash ./scripts/dev.sh", "deploy:contracts": "pnpm hardhat run deploy/deploy.ts", "deploy:contracts:full": "export DEPLOY_ENCLAVE=true && pnpm deploy:contracts", - "deploy:contracts:full:mock": "export DEPLOY_ENCLAVE=true && export USE_MOCK_VERIFIER=true && export USE_MOCK_INPUT_VALIDATOR=true && pnpm deploy:contracts", + "deploy:contracts:full:mock": "export DEPLOY_ENCLAVE=true && export USE_MOCK_VERIFIER=true && export USE_MOCK_INPUT_VALIDATOR=true && pnpm deploy:contracts", "test:e2e": "bash ./scripts/test_e2e.sh", "test": "pnpm test:e2e", "test:contracts": "hardhat test tests/crisp.contracts.test.ts --network localhost", diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 4ae3566924..3bc1ee0fee 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -13,9 +13,9 @@ BITQUERY_API_KEY="" CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" -CIPHERNODE_REGISTRY_ADDRESS="0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" -E3_PROGRAM_ADDRESS="0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" # CRISPProgram Contract Address +ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" +E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # E3 Config diff --git a/packages/enclave-contracts/hardhat.config.ts b/packages/enclave-contracts/hardhat.config.ts index 285931878d..186f504277 100644 --- a/packages/enclave-contracts/hardhat.config.ts +++ b/packages/enclave-contracts/hardhat.config.ts @@ -108,6 +108,16 @@ const config: HardhatUserConfig = { type: "edr-simulated", chainType: "l1", }, + localhost: { + accounts: { + mnemonic, + }, + chainId: chainIds.hardhat, + url: "http://localhost:8545", + type: "http", + chainType: "l1", + timeout: 60000, + }, ganache: { accounts: { mnemonic, @@ -115,6 +125,7 @@ const config: HardhatUserConfig = { chainId: chainIds.ganache, url: "http://localhost:8545", type: "http", + timeout: 60000, }, arbitrum: getChainConfig( "arbitrum-mainnet", diff --git a/packages/enclave-contracts/scripts/cleanIgnitionState.ts b/packages/enclave-contracts/scripts/cleanIgnitionState.ts new file mode 100644 index 0000000000..ca353b2b92 --- /dev/null +++ b/packages/enclave-contracts/scripts/cleanIgnitionState.ts @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import fs from "fs"; +import path from "path"; + +/** + * Cleans Hardhat Ignition state for a given chain ID. + * This is useful when working with Anvil or other local nodes where chain state + * is reset but Ignition state persists, causing reconciliation errors. + * + * @param chainId - The chain ID to clean state for (default: 31337 for Anvil/Hardhat) + */ +export const cleanIgnitionState = (chainId: number = 31337): void => { + const ignitionPath = path.join(process.cwd(), "ignition", "deployments"); + const chainFolder = path.join(ignitionPath, `chain-${chainId}`); + + if (fs.existsSync(chainFolder)) { + console.log(`Cleaning Hardhat Ignition state for chain ${chainId}...`); + fs.rmSync(chainFolder, { recursive: true, force: true }); + console.log(`Cleaned Ignition state at: ${chainFolder}`); + } else { + console.log(`No Ignition state found for chain ${chainId}`); + } +}; + +/** + * Cleans deployment records for a specific network from deployed_contracts.json + * + * @param networkName - The network name (e.g., "localhost", "hardhat") + */ +export const cleanDeploymentRecords = (networkName: string): void => { + const deploymentsFile = path.join(process.cwd(), "deployed_contracts.json"); + + if (!fs.existsSync(deploymentsFile)) { + return; + } + + try { + const deployments = JSON.parse(fs.readFileSync(deploymentsFile, "utf8")); + + if (deployments[networkName]) { + console.log( + `Cleaning deployment records for network '${networkName}'...`, + ); + delete deployments[networkName]; + fs.writeFileSync(deploymentsFile, JSON.stringify(deployments, null, 2)); + console.log(`Cleaned deployment records for '${networkName}'`); + } + } catch (error) { + console.warn("Failed to clean deployment records:", error); + } +}; + +/** + * Automatically clean Ignition state and deployment records for localhost/hardhat networks before deployment. + * This prevents stale state issues when Anvil is restarted. + */ +export const autoCleanIgnitionForLocalhost = async ( + networkName: string, + chainId: number, +): Promise => { + const localNetworks = ["localhost", "hardhat", "anvil", "ganache"]; + if (localNetworks.includes(networkName)) { + console.log( + `Detected local network '${networkName}', auto-cleaning stale deployment state...`, + ); + cleanIgnitionState(chainId); + cleanDeploymentRecords(networkName); + } +}; + +/** + * + * Usage: pnpm hardhat run scripts/cleanIgnitionState.ts + */ +async function main() { + console.log("Manually cleaning Ignition state for localhost (chainId 31337)"); + cleanIgnitionState(31337); + cleanDeploymentRecords("localhost"); + console.log("Done! You can now run deployments again."); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts index 70ced195e5..c272cf63cc 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts @@ -30,6 +30,8 @@ async function disableTransferRestrictionsForLocal( if (chain !== "localhost" && chain !== "hardhat") { return; } + console.log("Disabling transfer restrictions for chain", chain); + console.log("Contract address", await contract.getAddress()); try { const isRestricted = await contract.transfersRestricted(); diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 6cc38404d8..6b78260e5f 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import hre from "hardhat"; +import { autoCleanIgnitionForLocalhost } from "./cleanIgnitionState"; import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; import { deployAndSaveCommitteeSortition } from "./deployAndSave/committeeSortition"; @@ -22,6 +23,13 @@ import { deployMocks } from "./deployMocks"; export const deployEnclave = async (withMocks?: boolean) => { const { ethers } = await hre.network.connect(); + // Auto-clean Ignition state for local networks to prevent stale state issues + const [signer] = await ethers.getSigners(); + const network = await signer.provider?.getNetwork(); + const chainId = Number(network?.chainId ?? 31337); + const networkName = hre.globalOptions.network ?? "localhost"; + await autoCleanIgnitionForLocalhost(networkName, chainId); + const [owner] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); diff --git a/packages/enclave-contracts/scripts/index.ts b/packages/enclave-contracts/scripts/index.ts index 7e66e8b1f2..0235d8a03c 100644 --- a/packages/enclave-contracts/scripts/index.ts +++ b/packages/enclave-contracts/scripts/index.ts @@ -7,6 +7,7 @@ export * from "./deployEnclave"; export * from "./deployMocks"; export * from "./utils"; +export * from "./cleanIgnitionState"; export * from "./deployAndSave/bondingRegistry"; export * from "./deployAndSave/ciphernodeRegistryOwnable"; export * from "./deployAndSave/enclave"; diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index df38e2dcfb..ed92ac63cc 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -30,7 +30,7 @@ nodes: autonetkey: true autopassword: true ag: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" + address: "0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097" quic_port: 9095 autonetkey: true autopassword: true From b30eebcc5a120191639421b0d603e1754b48105f Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:22:39 +0100 Subject: [PATCH 41/88] chore: remove ignition (#887) --- .../enclave-contracts/deployed_contracts.json | 155 ++++++------------ .../scripts/cleanIgnitionState.ts | 40 +---- .../scripts/deployAndSave/bondingRegistry.ts | 37 ++--- .../deployAndSave/committeeSortition.ts | 25 ++- .../deployAndSave/enclaveTicketToken.ts | 24 ++- .../scripts/deployAndSave/enclaveToken.ts | 16 +- .../scripts/deployAndSave/mockStableToken.ts | 16 +- .../scripts/deployAndSave/slashingManager.ts | 22 +-- .../scripts/deployEnclave.ts | 9 +- 9 files changed, 112 insertions(+), 232 deletions(-) diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index fa23e01909..9628c55c6d 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -1,173 +1,120 @@ { "sepolia": { "PoseidonT3": { - "blockNumber": 9461441, + "blockNumber": 9479393, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "Enclave": { "constructorArgs": { "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", - "registry": "0x0000000000000000000000000000000000000001", + "registry": "0xEC98074C1F64f820f897842d266e1091A0f47Ad8", + "bondingRegistry": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61", + "feeToken": "0xB58B762748c64f1a36B34012d1C52503617f4De0", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 9461442, - "address": "0x966eC3eC1b46158f5e310B5a28aFc967C68De90a" + "blockNumber": 9479401, + "address": "0x787e6e63EF62ad4a40ea19D117bD7343ee8F1eC0" }, "CiphernodeRegistryOwnable": { "constructorArgs": { "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", - "enclaveAddress": "0x966eC3eC1b46158f5e310B5a28aFc967C68De90a" - }, - "blockNumber": 9461443, - "address": "0xC458d854cbC93D389CDaCB0FAAC8B25A59c3b74E" - }, - "NaiveRegistryFilter": { - "constructorArgs": { - "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", - "ciphernodeRegistryAddress": "0xC458d854cbC93D389CDaCB0FAAC8B25A59c3b74E" + "enclaveAddress": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 9461444, - "address": "0x0DC777566d1255B871dda65dD770cDb0E0280223" + "blockNumber": 9479399, + "address": "0xEC98074C1F64f820f897842d266e1091A0f47Ad8" }, "MockComputeProvider": { - "blockNumber": 9461446, - "address": "0x15b4159E8Cc37ce91c1C89cAcDCE71592A3D12cf" + "blockNumber": 9479402, + "address": "0xf428dc63Ef8df4AdB3C202983Ea3b00B6985a03b" }, "MockDecryptionVerifier": { - "blockNumber": 9461448, - "address": "0x4697A79238A021c17Ffb76cf0995d9ec41383692" + "blockNumber": 9479403, + "address": "0x09E79Fbcc3A5d9dc9f45920F7A8D15c7208Dd568" }, "MockInputValidator": { - "blockNumber": 9461452, - "address": "0x826C5Eb0C388fbCe7Ef483b9595749D42ACe14ad" + "blockNumber": 9479404, + "address": "0x885F5D69D2aA3ec34bb2e2598529213D308B12CF" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x826C5Eb0C388fbCe7Ef483b9595749D42ACe14ad" - }, - "blockNumber": 9461453, - "address": "0x1375c3dB8De6F0b77B8AE2789DE2C953E96ca15e" - } - }, - "hardhat": { - "MockUSDC": { - "constructorArgs": { - "initialSupply": "1000000" - }, - "blockNumber": 1, - "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" - }, - "EnclaveToken": { - "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + "mockInputValidator": "0x885F5D69D2aA3ec34bb2e2598529213D308B12CF" }, - "blockNumber": 1, - "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" - } - }, - "localhost": { - "PoseidonT3": { - "blockNumber": 1, - "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + "blockNumber": 9479405, + "address": "0x5a196784e60A6A18b86Af7a9e564A969F6d2bC76" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 9479394, + "address": "0xB58B762748c64f1a36B34012d1C52503617f4De0" }, "EnclaveToken": { "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676" }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 9479395, + "address": "0x9B3D470a2937c500632d382574EbD1D8FfCE3D63" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "baseToken": "0xB58B762748c64f1a36B34012d1C52503617f4De0", "registry": "0x0000000000000000000000000000000000000001", - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676" }, - "blockNumber": 5, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 9479396, + "address": "0xceb48a1bc9a3F160Ca687Da0a2A624F7a09a6158" }, "SlashingManager": { "constructorArgs": { - "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "admin": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 6, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 9479397, + "address": "0x4752200Fc26747672d6CD3abe2e9072152D46f04" }, "BondingRegistry": { "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", + "ticketToken": "0xceb48a1bc9a3F160Ca687Da0a2A624F7a09a6158", + "licenseToken": "0x9B3D470a2937c500632d382574EbD1D8FfCE3D63", "registry": "0x0000000000000000000000000000000000000001", - "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "slashedFundsTreasury": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", "ticketPrice": "10000000", "licenseRequiredBond": "100000000000000000000", "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 7, - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" - }, - "CiphernodeRegistryOwnable": { - "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "enclaveAddress": "0x0000000000000000000000000000000000000001" - }, - "blockNumber": 8, - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + "blockNumber": 9479398, + "address": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61" }, "CommitteeSortition": { "constructorArgs": { - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "ciphernodeRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "bondingRegistry": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61", + "ciphernodeRegistry": "0xEC98074C1F64f820f897842d266e1091A0f47Ad8", "submissionWindow": "300" }, - "blockNumber": 9, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - }, - "Enclave": { + "blockNumber": 9479400, + "address": "0xB48207B7faAf3504025552ba2db43d3b4aD74E04" + } + }, + "hardhat": { + "MockUSDC": { "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - "maxDuration": "2592000", - "params": [ - "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" - ] + "initialSupply": "1000000" }, - "blockNumber": 10, - "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" - }, - "MockComputeProvider": { - "blockNumber": 19, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" - }, - "MockDecryptionVerifier": { - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" - }, - "MockInputValidator": { - "blockNumber": 21, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" }, - "MockE3Program": { + "EnclaveToken": { "constructorArgs": { - "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 22, - "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" } } } \ No newline at end of file diff --git a/packages/enclave-contracts/scripts/cleanIgnitionState.ts b/packages/enclave-contracts/scripts/cleanIgnitionState.ts index ca353b2b92..e9995ab99e 100644 --- a/packages/enclave-contracts/scripts/cleanIgnitionState.ts +++ b/packages/enclave-contracts/scripts/cleanIgnitionState.ts @@ -6,26 +6,6 @@ import fs from "fs"; import path from "path"; -/** - * Cleans Hardhat Ignition state for a given chain ID. - * This is useful when working with Anvil or other local nodes where chain state - * is reset but Ignition state persists, causing reconciliation errors. - * - * @param chainId - The chain ID to clean state for (default: 31337 for Anvil/Hardhat) - */ -export const cleanIgnitionState = (chainId: number = 31337): void => { - const ignitionPath = path.join(process.cwd(), "ignition", "deployments"); - const chainFolder = path.join(ignitionPath, `chain-${chainId}`); - - if (fs.existsSync(chainFolder)) { - console.log(`Cleaning Hardhat Ignition state for chain ${chainId}...`); - fs.rmSync(chainFolder, { recursive: true, force: true }); - console.log(`Cleaned Ignition state at: ${chainFolder}`); - } else { - console.log(`No Ignition state found for chain ${chainId}`); - } -}; - /** * Cleans deployment records for a specific network from deployed_contracts.json * @@ -58,32 +38,14 @@ export const cleanDeploymentRecords = (networkName: string): void => { * Automatically clean Ignition state and deployment records for localhost/hardhat networks before deployment. * This prevents stale state issues when Anvil is restarted. */ -export const autoCleanIgnitionForLocalhost = async ( +export const autoCleanForLocalhost = async ( networkName: string, - chainId: number, ): Promise => { const localNetworks = ["localhost", "hardhat", "anvil", "ganache"]; if (localNetworks.includes(networkName)) { console.log( `Detected local network '${networkName}', auto-cleaning stale deployment state...`, ); - cleanIgnitionState(chainId); cleanDeploymentRecords(networkName); } }; - -/** - * - * Usage: pnpm hardhat run scripts/cleanIgnitionState.ts - */ -async function main() { - console.log("Manually cleaning Ignition state for localhost (chainId 31337)"); - cleanIgnitionState(31337); - cleanDeploymentRecords("localhost"); - console.log("Done! You can now run deployments again."); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts b/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts index 59d3c9d2b6..0a44f35ce6 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; -import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import { BondingRegistry, BondingRegistry__factory as BondingRegistryFactory, @@ -47,7 +46,7 @@ export const deployAndSaveBondingRegistry = async ({ }: BondingRegistryArgs): Promise<{ bondingRegistry: BondingRegistry; }> => { - const { ignition, ethers } = await hre.network.connect(); + const { ethers } = await hre.network.connect(); const [signer] = await ethers.getSigners(); const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; @@ -88,28 +87,26 @@ export const deployAndSaveBondingRegistry = async ({ return { bondingRegistry: bondingRegistryContract }; } - const bondingRegistry = await ignition.deploy(BondingRegistryModule, { - parameters: { - BondingRegistry: { - owner, - ticketToken, - licenseToken, - registry, - slashedFundsTreasury, - ticketPrice, - licenseRequiredBond, - minTicketBalance, - exitDelay, - }, - }, - }); + const bondingRegistryFactory = + await ethers.getContractFactory("BondingRegistry"); + + const bondingRegistry = await bondingRegistryFactory.deploy( + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + ); - await bondingRegistry.bondingRegistry.waitForDeployment(); + await bondingRegistry.waitForDeployment(); const blockNumber = await ethers.provider.getBlockNumber(); - const bondingRegistryAddress = - await bondingRegistry.bondingRegistry.getAddress(); + const bondingRegistryAddress = await bondingRegistry.getAddress(); storeDeploymentArgs( { diff --git a/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts b/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts index 4306ddd5d7..dcee0c852d 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; -import CommitteeSortitionModule from "../../ignition/modules/committeeSortition"; import { CommitteeSortition, CommitteeSortition__factory as CommitteeSortitionFactory, @@ -35,7 +34,7 @@ export const deployAndSaveCommitteeSortition = async ({ }: CommitteeSortitionArgs): Promise<{ committeeSortition: CommitteeSortition; }> => { - const { ignition, ethers } = await hre.network.connect(); + const { ethers } = await hre.network.connect(); const [signer] = await ethers.getSigners(); const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; @@ -62,22 +61,20 @@ export const deployAndSaveCommitteeSortition = async ({ return { committeeSortition: committeeSortitionContract }; } - const committeeSortition = await ignition.deploy(CommitteeSortitionModule, { - parameters: { - CommitteeSortition: { - bondingRegistry, - ciphernodeRegistry, - submissionWindow, - }, - }, - }); + const committeeSortitionFactory = + await ethers.getContractFactory("CommitteeSortition"); + + const committeeSortition = await committeeSortitionFactory.deploy( + bondingRegistry, + ciphernodeRegistry, + submissionWindow, + ); - await committeeSortition.committeeSortition.waitForDeployment(); + await committeeSortition.waitForDeployment(); const blockNumber = await ethers.provider.getBlockNumber(); - const committeeSortitionAddress = - await committeeSortition.committeeSortition.getAddress(); + const committeeSortitionAddress = await committeeSortition.getAddress(); storeDeploymentArgs( { diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts index a5ee11c305..36123181b8 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; -import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; import { EnclaveTicketToken, EnclaveTicketToken__factory as EnclaveTicketTokenFactory, @@ -35,7 +34,7 @@ export const deployAndSaveEnclaveTicketToken = async ({ }: EnclaveTicketTokenArgs): Promise<{ enclaveTicketToken: EnclaveTicketToken; }> => { - const { ignition, ethers } = await hre.network.connect(); + const { ethers } = await hre.network.connect(); const [signer] = await ethers.getSigners(); const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; @@ -61,22 +60,19 @@ export const deployAndSaveEnclaveTicketToken = async ({ return { enclaveTicketToken: enclaveTicketTokenContract }; } - const enclaveTicketToken = await ignition.deploy(EnclaveTicketTokenModule, { - parameters: { - EnclaveTicketToken: { - baseToken, - registry, - owner, - }, - }, - }); + const enclaveTicketTokenFactory = + await ethers.getContractFactory("EnclaveTicketToken"); + const enclaveTicketToken = await enclaveTicketTokenFactory.deploy( + baseToken, + registry, + owner, + ); - await enclaveTicketToken.enclaveTicketToken.waitForDeployment(); + await enclaveTicketToken.waitForDeployment(); const blockNumber = await ethers.provider.getBlockNumber(); - const enclaveTicketTokenAddress = - await enclaveTicketToken.enclaveTicketToken.getAddress(); + const enclaveTicketTokenAddress = await enclaveTicketToken.getAddress(); storeDeploymentArgs( { diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts index c272cf63cc..d3ef2919d6 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; -import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; import { EnclaveToken, EnclaveToken__factory as EnclaveTokenFactory, @@ -56,7 +55,7 @@ export const deployAndSaveEnclaveToken = async ({ }: EnclaveTokenArgs): Promise<{ enclaveToken: EnclaveToken; }> => { - const { ignition, ethers } = await hre.network.connect(); + const { ethers } = await hre.network.connect(); const [signer] = await ethers.getSigners(); const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; @@ -78,19 +77,14 @@ export const deployAndSaveEnclaveToken = async ({ return { enclaveToken: enclaveTokenContract }; } - const enclaveToken = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner, - }, - }, - }); + const enclaveTokenFactory = await ethers.getContractFactory("EnclaveToken"); + const enclaveToken = await enclaveTokenFactory.deploy(owner); - await enclaveToken.enclaveToken.waitForDeployment(); + await enclaveToken.waitForDeployment(); const blockNumber = await ethers.provider.getBlockNumber(); - const enclaveTokenAddress = await enclaveToken.enclaveToken.getAddress(); + const enclaveTokenAddress = await enclaveToken.getAddress(); storeDeploymentArgs( { diff --git a/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts b/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts index 6a4db972b0..539599a433 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; -import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import { MockUSDC, MockUSDC__factory as MockUSDCFactory } from "../../types"; import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; @@ -28,7 +27,7 @@ export const deployAndSaveMockStableToken = async ({ }: MockStableTokenArgs): Promise<{ mockStableToken: MockUSDC; }> => { - const { ignition, ethers } = await hre.network.connect(); + const { ethers } = await hre.network.connect(); const [signer] = await ethers.getSigners(); const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; @@ -49,19 +48,14 @@ export const deployAndSaveMockStableToken = async ({ return { mockStableToken: mockStableTokenContract }; } - const mockStableToken = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply, - }, - }, - }); + const mockStableTokenFactory = await ethers.getContractFactory("MockUSDC"); + const mockStableToken = await mockStableTokenFactory.deploy(initialSupply); - await mockStableToken.mockUSDC.waitForDeployment(); + await mockStableToken.waitForDeployment(); const blockNumber = await ethers.provider.getBlockNumber(); - const mockStableTokenAddress = await mockStableToken.mockUSDC.getAddress(); + const mockStableTokenAddress = await mockStableToken.getAddress(); storeDeploymentArgs( { diff --git a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts index a294559c00..47b7e179e6 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; -import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { SlashingManager, SlashingManager__factory as SlashingManagerFactory, @@ -33,7 +32,7 @@ export const deployAndSaveSlashingManager = async ({ }: SlashingManagerArgs): Promise<{ slashingManager: SlashingManager; }> => { - const { ignition, ethers } = await hre.network.connect(); + const { ethers } = await hre.network.connect(); const [signer] = await ethers.getSigners(); const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; @@ -57,21 +56,18 @@ export const deployAndSaveSlashingManager = async ({ return { slashingManager: slashingManagerContract }; } - const slashingManager = await ignition.deploy(SlashingManagerModule, { - parameters: { - SlashingManager: { - admin, - bondingRegistry, - }, - }, - }); + const slashingManagerFactory = + await ethers.getContractFactory("SlashingManager"); + const slashingManager = await slashingManagerFactory.deploy( + admin, + bondingRegistry, + ); - await slashingManager.slashingManager.waitForDeployment(); + await slashingManager.waitForDeployment(); const blockNumber = await ethers.provider.getBlockNumber(); - const slashingManagerAddress = - await slashingManager.slashingManager.getAddress(); + const slashingManagerAddress = await slashingManager.getAddress(); storeDeploymentArgs( { diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 6b78260e5f..43921657ba 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import hre from "hardhat"; -import { autoCleanIgnitionForLocalhost } from "./cleanIgnitionState"; +import { autoCleanForLocalhost } from "./cleanIgnitionState"; import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; import { deployAndSaveCommitteeSortition } from "./deployAndSave/committeeSortition"; @@ -23,12 +23,9 @@ import { deployMocks } from "./deployMocks"; export const deployEnclave = async (withMocks?: boolean) => { const { ethers } = await hre.network.connect(); - // Auto-clean Ignition state for local networks to prevent stale state issues - const [signer] = await ethers.getSigners(); - const network = await signer.provider?.getNetwork(); - const chainId = Number(network?.chainId ?? 31337); + // Auto-clean state for local networks to prevent stale state issues const networkName = hre.globalOptions.network ?? "localhost"; - await autoCleanIgnitionForLocalhost(networkName, chainId); + await autoCleanForLocalhost(networkName); const [owner] = await ethers.getSigners(); From 0d3e17fce19f3154a0231a223095f5632e3dd5f8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 24 Oct 2025 15:36:58 +0500 Subject: [PATCH 42/88] fix: interface fix --- crates/evm-helpers/src/contracts.rs | 2 +- deploy/local/contracts.sh | 6 +++--- examples/CRISP/server/src/cli/commands.rs | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index 90c40eaccf..db56937b86 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -75,7 +75,7 @@ sol! { mapping(uint256 e3Id => uint256 inputCount) public inputCounts; mapping(uint256 e3Id => bytes params) public e3Params; mapping(address e3Program => bool allowed) public e3Programs; - function request(E3RequestParams memory request) external payable returns (uint256 e3Id, E3 memory e3); + function request(E3RequestParams calldata requestParams) external returns (uint256 e3Id, E3 memory e3); function activate(uint256 e3Id,bytes calldata publicKey) external returns (bool success); function enableE3Program(address e3Program) public onlyOwner returns (bool success); function publishInput(uint256 e3Id, bytes calldata data) external returns (bool success); diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index ef36d19eca..cc5c818be3 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -15,9 +15,9 @@ CN2=0xdD2FD4581271e230360230F9337D5c0430Bf44C0 CN3=0x2546BcD3c84621e976D8185a91A922aE77ECEc30 # Add the ciphernodes to the enclave -pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost" -pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost" -pnpm ciphernode:add --ciphernode-address "$CN3" --network "localhost" +(cd examples/CRISP && pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost") +(cd examples/CRISP && pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost") +(cd examples/CRISP && pnpm ciphernode:add --ciphernode-address "$CN3" --network "localhost") # Delete local DB diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index 4b0c08e3ef..5991ec1d77 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -77,7 +77,9 @@ pub async fn initialize_crisp_round( info!("Enabling E3 Program with address: {}", e3_program); match contract.is_e3_program_enabled(e3_program).await { Ok(enabled) => { + info!("Debug - E3 Program enabled status: {}", enabled); if !enabled { + info!("E3 Program not enabled, attempting to enable..."); match contract.enable_e3_program(e3_program).await { Ok(res) => info!("E3 Program enabled. TxHash: {:?}", res.transaction_hash), Err(e) => info!("Error enabling E3 Program: {:?}", e), @@ -137,6 +139,19 @@ pub async fn initialize_crisp_round( ) .await?; + info!("Requesting E3 on contract: {}", CONFIG.enclave_address); + + info!("Debug - threshold: {:?}", threshold); + info!("Debug - start_window: {:?}", start_window); + info!("Debug - duration: {}", duration); + info!("Debug - e3_program: {}", e3_program); + info!("Debug - current timestamp: {}", Utc::now().timestamp()); + + info!( + "Debug - Checking ciphernode registry at: {}", + CONFIG.ciphernode_registry_address + ); + let res = contract .request_e3( threshold, From b8fd610e4310a2e82273ccbce2d93c3ed6ef676f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 24 Oct 2025 19:11:22 +0500 Subject: [PATCH 43/88] fix: ciphernode builder contract registry writer --- .../src/ciphernode_builder.rs | 18 +++++++++++++++++ crates/sortition/src/ciphernode_selector.rs | 11 ---------- deploy/local/contracts.sh | 2 +- deploy/local/nodes.sh | 7 ++++++- examples/CRISP/deployed_contracts.json | 17 +++++++++------- examples/CRISP/enclave.config.yaml | 20 ++++++++++++++----- 6 files changed, 50 insertions(+), 25 deletions(-) diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 944ed346e7..19a34c66c7 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -351,6 +351,24 @@ impl CiphernodeBuilder { chain.rpc_url.clone(), ) .await?; + + match provider_cache + .ensure_write_provider(&repositories, chain, cipher) + .await + { + Ok(write_provider) => { + CiphernodeRegistrySol::attach_writer( + &local_bus, + write_provider.clone(), + &chain.contracts.ciphernode_registry.address(), + ) + .await?; + info!("CiphernodeRegistrySolWriter attached for publishing committees"); + } + Err(_) => { + info!("No wallet configured for this node, skipping writer attachment"); + } + } } } diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index 09a2e9ec7b..387dc41c51 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -63,7 +63,6 @@ impl Handler for CiphernodeSelector { type Result = ResponseFuture<()>; fn handle(&mut self, data: E3Requested, _ctx: &mut Self::Context) -> Self::Result { - info!("CiphernodeSelector is handling E3Requested!!!"); let address = self.address.clone(); let sortition = self.sortition.clone(); let bus = self.bus.clone(); @@ -91,16 +90,6 @@ impl Handler for CiphernodeSelector { info!(node = address, "Ciphernode was not selected"); return; }; - match ticket_id { - Some(ticket) => info!( - "CIPHERNODE SELECTED: node={} address={} ticket={}", - party_id, address, ticket - ), - None => info!( - "CIPHERNODE SELECTED: node={} address={} (no ticket)", - party_id, address - ), - } bus.do_send(EnclaveEvent::from(CiphernodeSelected { party_id, ticket_id, diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index cc5c818be3..061acb2ba8 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -4,7 +4,7 @@ # cargo install --locked --path ./crates/cli --bin enclave -f # Deploy CRISP Contracts -(cd examples/CRISP && pnpm deploy:contracts:full:mock --network localhost) +(cd examples/CRISP && pnpm deploy:contracts:full --network localhost) # Add Ciphernodes to Enclave sleep 2 # wait for enclave to start diff --git a/deploy/local/nodes.sh b/deploy/local/nodes.sh index 32789b87b0..d7deaa9d1a 100755 --- a/deploy/local/nodes.sh +++ b/deploy/local/nodes.sh @@ -7,4 +7,9 @@ concurrently \ --names "ANVIL,NODES" \ --prefix-colors "blue,yellow" \ "anvil" \ - "cd examples/CRISP && enclave wallet set --name ag --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" && enclave nodes up -v" \ No newline at end of file + "cd examples/CRISP && \ + enclave wallet set --name ag --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" && + enclave wallet set --name cn1 --private-key "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" && + enclave wallet set --name cn2 --private-key "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" && + enclave wallet set --name cn3 --private-key "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" && + enclave nodes up -v" \ No newline at end of file diff --git a/examples/CRISP/deployed_contracts.json b/examples/CRISP/deployed_contracts.json index fa9ca306e2..5a93472962 100644 --- a/examples/CRISP/deployed_contracts.json +++ b/examples/CRISP/deployed_contracts.json @@ -183,25 +183,28 @@ "blockNumber": 22, "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, - "MockRISC0Verifier": { + "RiscZeroGroth16Verifier": { "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, + "CRISPInputValidator": { + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + }, "CRISPInputValidatorFactory": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "constructorArgs": { - "inputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "inputValidator": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" } }, "HonkVerifier": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" }, "CRISPProgram": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", "constructorArgs": { "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - "inputValidatorAddress": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "inputValidatorAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "honkVerifierAddress": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 9da355cfa2..d5a93ad0bf 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -2,11 +2,21 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" - enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - ciphernode_registry: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" - bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" - committee_sortition: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + e3_program: + address: "0xc5a5C42992dECbae36851359345FE25997F5C42d" + deploy_block: 1 # Set to actual deploy block + enclave: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 1 # Set to actual deploy block + ciphernode_registry: + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 1 # Set to actual deploy block + bonding_registry: + address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + deploy_block: 1 # Set to actual deploy block + committee_sortition: + address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + deploy_block: 1 # Set to actual deploy block program: dev: true From 005e80360a221eec20ad93f0d5a0bf54473863c8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sun, 26 Oct 2025 19:17:11 +0500 Subject: [PATCH 44/88] chore: committee sorition --- .../src/ciphernode_builder.rs | 45 +- .../entrypoint/src/start/aggregator_start.rs | 1 + crates/entrypoint/src/start/start.rs | 1 + .../src/enclave_event/ticket_submitted.rs | 3 +- crates/evm/src/ciphernode_registry_sol.rs | 83 +- crates/evm/src/committee_sortition_sol.rs | 152 ++- crates/keyshare/src/threshold_keyshare.rs | 61 +- docs/score-sortition-flow.md | 377 ++++++ examples/CRISP/deployed_contracts.json | 17 +- examples/CRISP/enclave.config.yaml | 2 +- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 19 +- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../CommitteeSortition.json | 246 ++-- .../enclave-contracts/contracts/Enclave.sol | 27 +- .../interfaces/ICiphernodeRegistry.sol | 42 +- .../contracts/interfaces/IEnclave.sol | 11 + .../registry/CiphernodeRegistryOwnable.sol | 119 +- .../sortition/CommitteeSortition.sol | 478 +++----- .../contracts/sortition/OldCS.sol | 392 ++++++ .../contracts/test/MockCiphernodeRegistry.sol | 2 + .../test/CommitteeSortition.spec.ts | 1084 ++++++++--------- .../CiphernodeRegistryOwnable.spec.ts | 26 +- 23 files changed, 2002 insertions(+), 1190 deletions(-) create mode 100644 docs/score-sortition-flow.md create mode 100644 packages/enclave-contracts/contracts/sortition/OldCS.sol diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 19a34c66c7..1f2e882765 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -23,7 +23,8 @@ use e3_evm::{ ProviderConfig, }, BondingRegistryReaderRepositoryFactory, BondingRegistrySol, - CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, EnclaveSol, EnclaveSolReader, + CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, + CommitteeSortitionReaderRepositoryFactory, CommitteeSortitionSol, EnclaveSol, EnclaveSolReader, EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, }; use e3_fhe::ext::FheExtension; @@ -370,6 +371,48 @@ impl CiphernodeBuilder { } } } + + if self.contract_components.committee_sortition { + if let Some(committee_sortition_contract) = &chain.contracts.committee_sortition { + match provider_cache + .ensure_write_provider(&repositories, chain, cipher) + .await + { + Ok(write_provider) => { + let enable_finalizer = self.pubkey_agg; + CommitteeSortitionSol::attach_with_finalizer( + &local_bus, + write_provider.clone(), + &committee_sortition_contract.address(), + &repositories.committee_sortition_reader(write_provider.chain_id()), + committee_sortition_contract.deploy_block(), + chain.rpc_url.clone(), + enable_finalizer, + ) + .await?; + } + Err(e) => { + return Err(anyhow::anyhow!( + "Score sortition enabled but no wallet configured for node. \ + All nodes must have wallets to submit tickets. Error: {}", + e + )); + } + } + } else { + info!( + "📍 DISTANCE SORTITION MODE (CommitteeSortition contract not configured)" + ); + if self.pubkey_agg { + info!(" Role: AGGREGATOR (will publish committees immediately)"); + } + } + } else { + info!("📍 DISTANCE SORTITION MODE"); + if self.pubkey_agg { + info!(" Role: AGGREGATOR (will publish committees immediately)"); + } + } } // E3 specific setup diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 857dc9e57c..e6797df854 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -41,6 +41,7 @@ pub async fn execute( .with_contract_enclave_full() .with_contract_bonding_registry() .with_contract_ciphernode_registry() + .with_contract_committee_sortition() .with_plaintext_aggregation() .with_pubkey_aggregation() .build() diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index c88d0d026c..1537ad7743 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -42,6 +42,7 @@ pub async fn execute( .with_chains(&config.chains()) .with_contract_enclave_reader() .with_contract_ciphernode_registry() + .with_contract_committee_sortition() .build() .await?; diff --git a/crates/events/src/enclave_event/ticket_submitted.rs b/crates/events/src/enclave_event/ticket_submitted.rs index b454a3758d..4b969f3de6 100644 --- a/crates/events/src/enclave_event/ticket_submitted.rs +++ b/crates/events/src/enclave_event/ticket_submitted.rs @@ -14,9 +14,8 @@ use std::fmt::{self, Display}; pub struct TicketSubmitted { pub e3_id: E3id, pub node: String, - pub ticket_number: u64, + pub ticket_id: u64, pub score: String, - pub added_to_committee: bool, pub chain_id: u64, } diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index 831dc0a393..9a3ecb460c 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -16,9 +16,10 @@ use alloy::{ use anyhow::Result; use e3_data::Repository; use e3_events::{ - BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, PublicKeyAggregated, - Shutdown, Subscribe, + BusError, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, + PublicKeyAggregated, Shutdown, Subscribe, }; +use std::collections::HashMap; use tracing::{error, info, trace}; sol!( @@ -152,6 +153,9 @@ pub struct CiphernodeRegistrySolWriter

{ provider: EthProvider

, contract_address: Address, bus: Addr>, + /// Store finalized committees for score sortition + /// Maps E3id to the finalized committee nodes + finalized_committees: HashMap>, } impl CiphernodeRegistrySolWriter

{ @@ -164,6 +168,7 @@ impl CiphernodeRegistrySolWriter provider, contract_address, bus: bus.clone(), + finalized_committees: HashMap::new(), }) } @@ -180,6 +185,11 @@ impl CiphernodeRegistrySolWriter .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) .await; + // Subscribe to CommitteeFinalized for score sortition + let _ = bus + .send(Subscribe::new("CommitteeFinalized", addr.clone().into())) + .await; + Ok(addr) } } @@ -201,36 +211,71 @@ impl Handler ctx.notify(data); } } + EnclaveEvent::CommitteeFinalized { data, .. } => { + // Only store if chain matches + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), _ => (), } } } +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = (); + + fn handle(&mut self, msg: CommitteeFinalized, _: &mut Self::Context) -> Self::Result { + info!( + "Storing finalized committee for E3 {:?} (score sortition)", + msg.e3_id + ); + self.finalized_committees + .insert(msg.e3_id.clone(), msg.committee); + } +} + impl Handler for CiphernodeRegistrySolWriter

{ type Result = ResponseFuture<()>; fn handle(&mut self, msg: PublicKeyAggregated, _: &mut Self::Context) -> Self::Result { - Box::pin({ - let e3_id = msg.e3_id.clone(); - let pubkey = msg.pubkey.clone(); - let contract_address = self.contract_address; - let provider = self.provider.clone(); - let bus = self.bus.clone(); - let nodes = msg.nodes.clone(); - - async move { - let result = - publish_committee_to_registry(provider, contract_address, e3_id, nodes, pubkey) - .await; - match result { - Ok(receipt) => { - info!(tx=%receipt.transaction_hash, "Committee published to registry"); - } - Err(err) => bus.err(EnclaveErrorType::Evm, err), + let e3_id = msg.e3_id.clone(); + let pubkey = msg.pubkey.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + // Check if we have a finalized committee for this E3 (score sortition) + // Otherwise use the nodes from PublicKeyAggregated (distance sortition) + let nodes = if let Some(finalized_nodes) = self.finalized_committees.remove(&e3_id) { + info!( + "Using finalized committee nodes for E3 {:?} (score sortition)", + e3_id + ); + // Convert Vec to OrderedSet + OrderedSet::from_iter(finalized_nodes) + } else { + info!( + "Using aggregated nodes for E3 {:?} (distance sortition)", + e3_id + ); + msg.nodes.clone() + }; + + Box::pin(async move { + let result = + publish_committee_to_registry(provider, contract_address, e3_id, nodes, pubkey) + .await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Committee published to registry"); } + Err(err) => bus.err(EnclaveErrorType::Evm, err), } }) } diff --git a/crates/evm/src/committee_sortition_sol.rs b/crates/evm/src/committee_sortition_sol.rs index 9f34868205..d129b87ff8 100644 --- a/crates/evm/src/committee_sortition_sol.rs +++ b/crates/evm/src/committee_sortition_sol.rs @@ -9,14 +9,13 @@ use actix::prelude::*; use alloy::{ primitives::{Address, LogData, B256, U256}, providers::{Provider, WalletProvider}, - rpc::types::TransactionReceipt, sol, sol_types::SolEvent, }; use anyhow::Result; use e3_data::Repository; use e3_events::{BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, Shutdown, Subscribe}; -use tracing::{error, info, trace}; +use tracing::{error, info, trace, warn}; sol!( #[sol(rpc)] @@ -32,9 +31,8 @@ impl From for e3_events::TicketSubmitted { e3_events::TicketSubmitted { e3_id: E3id::new(value.0.e3Id.to_string(), value.1), node: value.0.node.to_string(), - ticket_number: value.0.ticketNumber.try_into().unwrap_or(0), + ticket_id: value.0.ticketId.try_into().unwrap_or(0), score: value.0.score.to_string(), - added_to_committee: value.0.addedToCommittee, chain_id: value.1, } } @@ -131,67 +129,34 @@ impl CommitteeSortitionSolReader { /// Writer for CommitteeSortition contract pub struct CommitteeSortitionSolWriter

{ + bus: Addr>, provider: EthProvider

, contract_address: Address, - bus: Addr>, + is_aggregator: bool, } impl CommitteeSortitionSolWriter

{ - pub fn new( + pub async fn attach_with_finalizer( bus: &Addr>, provider: EthProvider

, contract_address: Address, - ) -> Result { - Ok(Self { + is_aggregator: bool, + ) -> Result> { + let writer = Self { + bus: bus.clone(), provider, contract_address, - bus: bus.clone(), - }) - } - - pub async fn attach( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - ) -> Result>> { - let addr = - CommitteeSortitionSolWriter::new(bus, provider, contract_address.parse()?)?.start(); - - bus.send(Subscribe::new("CiphernodeSelected", addr.clone().into())) - .await?; - - bus.send(Subscribe::new("Shutdown", addr.clone().into())) - .await?; - - Ok(addr) - } - - async fn submit_ticket(&self, e3_id: E3id, ticket_number: u64) -> Result { - let e3_id_u256: U256 = e3_id.clone().try_into()?; - let ticket_number_u256 = U256::from(ticket_number); + is_aggregator, + } + .start(); - let from_address = self.provider.provider().default_signer_address(); - let current_nonce = self - .provider - .provider() - .get_transaction_count(from_address) - .pending() + bus.send(Subscribe::new("E3Requested", writer.clone().recipient())) .await?; - let contract = CommitteeSortition::new(self.contract_address, self.provider.provider()); - let builder = contract - .submitTicket(e3_id_u256, ticket_number_u256) - .nonce(current_nonce); - - let receipt = builder.send().await?.get_receipt().await?; - Ok(receipt) + Ok(writer) } } -impl Actor for CommitteeSortitionSolWriter

{ - type Context = actix::Context; -} - impl Handler for CommitteeSortitionSolWriter

{ @@ -202,6 +167,16 @@ impl Handler EnclaveEvent::CiphernodeSelected { data, .. } => { ctx.notify(data); } + EnclaveEvent::E3Requested { data, .. } => { + if self.enable_finalizer { + ctx.notify(data); + } + } + EnclaveEvent::CommitteeFinalized { data, .. } => { + if self.enable_finalizer { + ctx.notify(data); + } + } EnclaveEvent::Shutdown { data, .. } => { ctx.notify(data); } @@ -249,8 +224,14 @@ impl Handler { @@ -268,6 +249,40 @@ impl Handler Handler + for CommitteeSortitionSolWriter

+{ + type Result = (); + + fn handle(&mut self, data: e3_events::E3Requested, ctx: &mut Self::Context) -> Self::Result { + info!( + "E3Requested for E3 {:?}, scheduling committee finalization", + data.e3_id + ); + self.schedule_finalization(data.e3_id, ctx); + } +} + +impl Handler + for CommitteeSortitionSolWriter

+{ + type Result = (); + + fn handle( + &mut self, + data: e3_events::CommitteeFinalized, + _: &mut Self::Context, + ) -> Self::Result { + // Remove from pending tracking since it's already finalized + if self.pending_e3s.remove(&data.e3_id).is_some() { + info!( + "CommitteeFinalized received for E3 {:?}, removed from pending", + data.e3_id + ); + } + } +} + impl Handler for CommitteeSortitionSolWriter

{ @@ -282,6 +297,7 @@ impl Handler pub struct CommitteeSortitionSol; impl CommitteeSortitionSol { + /// Attach reader and writer (no automatic finalization) pub async fn attach

( bus: &Addr>, provider: EthProvider

, @@ -290,6 +306,32 @@ impl CommitteeSortitionSol { start_block: Option, rpc_url: String, ) -> Result>> + where + P: Provider + WalletProvider + Clone + 'static, + { + Self::attach_with_finalizer( + bus, + provider, + contract_address, + repository, + start_block, + rpc_url, + false, + ) + .await + } + + /// Attach reader and writer with optional automatic committee finalization + /// The submission window is automatically fetched from the contract + pub async fn attach_with_finalizer

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + enable_finalizer: bool, + ) -> Result>> where P: Provider + WalletProvider + Clone + 'static, { @@ -303,7 +345,13 @@ impl CommitteeSortitionSol { ) .await?; - let writer = CommitteeSortitionSolWriter::attach(bus, provider, contract_address).await?; + let writer = CommitteeSortitionSolWriter::attach_with_finalizer( + bus, + provider, + contract_address, + enable_finalizer, + ) + .await?; Ok(writer) } diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 401363b278..83be7a3404 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -9,9 +9,9 @@ use anyhow::{anyhow, bail, Result}; use e3_crypto::{Cipher, SensitiveBytes}; use e3_data::Persistable; use e3_events::{ - CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, ComputeResponse, - DecryptionshareCreated, E3id, EnclaveEvent, EventBus, KeyshareCreated, PartyId, ThresholdShare, - ThresholdShareCreated, + CiphernodeSelected, CiphertextOutputPublished, CommitteeFinalized, ComputeRequest, + ComputeResponse, DecryptionshareCreated, E3id, EnclaveEvent, EventBus, KeyshareCreated, + PartyId, ThresholdShare, ThresholdShareCreated, }; use e3_fhe::create_crp; use e3_multithread::Multithread; @@ -281,6 +281,8 @@ pub struct ThresholdKeyshare { decryption_key_collector: Option>, multithread: Addr, state: Persistable, + /// Store pending E3 selections waiting for committee finalization (for score sortition) + pending_selections: HashMap, } impl ThresholdKeyshare { @@ -291,6 +293,7 @@ impl ThresholdKeyshare { decryption_key_collector: None, multithread: params.multithread, state: params.state, + pending_selections: HashMap::new(), } } } @@ -755,6 +758,7 @@ impl Handler for ThresholdKeyshare { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { EnclaveEvent::CiphernodeSelected { data, .. } => ctx.notify(data), + EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data), EnclaveEvent::CiphertextOutputPublished { data, .. } => ctx.notify(data), EnclaveEvent::ThresholdShareCreated { data, .. } => { let _ = self.handle_threshold_share_created(data, ctx.address()); @@ -767,6 +771,12 @@ impl Handler for ThresholdKeyshare { impl Handler for ThresholdKeyshare { type Result = (); fn handle(&mut self, msg: CiphernodeSelected, ctx: &mut Self::Context) -> Self::Result { + // Store selection for potential committee finalization + self.pending_selections + .insert(msg.e3_id.clone(), msg.clone()); + + // Start keygen immediately (for distance sortition or if no CommitteeFinalized comes) + // If CommitteeFinalized arrives later, it will verify committee membership match self.handle_ciphernode_selected(msg, ctx.address()) { Err(e) => error!("{e}"), Ok(_) => (), @@ -774,6 +784,51 @@ impl Handler for ThresholdKeyshare { } } +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle(&mut self, msg: CommitteeFinalized, _: &mut Self::Context) -> Self::Result { + // Check if we have a pending selection for this E3 + let Some(_selection) = self.pending_selections.get(&msg.e3_id) else { + info!( + "CommitteeFinalized for E3 {:?} but no pending selection found", + msg.e3_id + ); + return; + }; + + // Get our node address from state + let Some(state) = self.state.get() else { + error!("State not found when handling CommitteeFinalized"); + return; + }; + + // Check if we're in the finalized committee + let our_address = state.address.to_lowercase(); + let in_committee = msg + .committee + .iter() + .any(|addr| addr.to_lowercase() == our_address); + + if in_committee { + info!( + "Node {} is in finalized committee for E3 {:?}, keygen already started", + our_address, msg.e3_id + ); + // Keygen already started in CiphernodeSelected handler + // Clean up pending selection + self.pending_selections.remove(&msg.e3_id); + } else { + info!( + "Node {} was selected but NOT in finalized committee for E3 {:?}", + our_address, msg.e3_id + ); + // TODO: Should we stop/cancel the keygen that was started? + // For now, just remove from pending + self.pending_selections.remove(&msg.e3_id); + } + } +} + impl Handler for ThresholdKeyshare { type Result = ResponseActFuture; fn handle(&mut self, msg: GenEsiSss, _: &mut Self::Context) -> Self::Result { diff --git a/docs/score-sortition-flow.md b/docs/score-sortition-flow.md new file mode 100644 index 0000000000..499e71339d --- /dev/null +++ b/docs/score-sortition-flow.md @@ -0,0 +1,377 @@ +# Complete Score Sortition Flow - End to End + +This document explains the entire score sortition process, from when an E3 is requested to when the committee and public key are published. + +## Overview + +**Score sortition** is an alternative to distance sortition where: +- ALL eligible nodes can participate (not just closest in merkle tree) +- Nodes submit "lottery tickets" with computed scores +- Contract selects top N nodes with lowest scores +- More decentralized and fair + +--- + +## Step-by-Step Flow + +### 1. **E3 Requested** (User creates computation request) + +**Contract: Enclave.sol** +```solidity +requestCompute() → emits E3Requested(e3Id, threshold, seed, ...) +``` + +**All Ciphernodes:** +- `EnclaveSolReader` picks up `E3Requested` event +- Converts to `EnclaveEvent::E3Requested` and broadcasts on event bus + +**CiphernodeRegistry.sol** (if score sortition enabled): +```solidity +requestCommittee() is called by Enclave + → Calls CommitteeSortition.initializeSortition(e3Id, threshold, seed, block.number) + → Sets submission deadline = now + submissionWindow (e.g., 60 seconds) +``` + +**All Ciphernodes:** +- `CiphernodeSelector` receives `E3Requested` +- Checks eligibility (bonding, ticket balance) +- Performs ticket sortition locally (computes scores for all owned tickets) +- Finds best ticket (lowest score) +- Emits `CiphernodeSelected` event with ticket_id + +**Aggregator:** +- `CommitteeSortitionSolWriter` (with `enable_finalizer=true`) receives `E3Requested` +- Calls `schedule_finalization(e3_id)`: + - Sets deadline = now + submission_window (fetched from contract) + - Stores in `pending_e3s` HashMap + - Schedules timer to check after submission_window expires + +--- + +### 2. **Ticket Submission Window** (Selected nodes submit tickets) + +**Selected Ciphernodes:** +- `CommitteeSortitionSolWriter` receives `CiphernodeSelected` event +- Calls `submitTicket(e3Id, ticketNumber)` on contract + +**Contract: CommitteeSortition.sol** +```solidity +submitTicket(e3Id, ticketNumber) + 1. Validates submission window still open (block.timestamp <= deadline) + 2. Validates node hasn't submitted before + 3. Validates node has ticket balance at snapshot block + 4. Computes score = keccak256(node || ticketNumber || e3Id || seed) + 5. Tries to insert into topNodes sorted array (size = threshold) + 6. Emits TicketSubmitted(e3Id, node, ticketNumber, score, addedToCommittee) +``` + +**All Ciphernodes:** +- `CommitteeSortitionSolReader` reads `TicketSubmitted` events +- Broadcasts as `EnclaveEvent::TicketSubmitted` +- Nodes can see who submitted and whether they made it into top N + +--- + +### 3. **Submission Window Closes** (After 60 seconds) + +**Aggregator:** +- `CommitteeSortitionSolWriter` has a timer running (10-second interval checks) +- `CheckDeadlines` handler finds expired E3s in `pending_e3s` +- Calls `finalize_committee(e3_id)` + +**Contract: CommitteeSortition.sol** +```solidity +finalizeCommittee(e3Id) + 1. Validates submission window has closed (block.timestamp > deadline) + 2. Validates not already finalized + 3. Sets finalized = true + 4. Returns topNodes array + 5. Emits CommitteeFinalized(e3Id, topNodes[]) +``` + +**All Ciphernodes:** +- `CommitteeSortitionSolReader` picks up `CommitteeFinalized` event +- Broadcasts as `EnclaveEvent::CommitteeFinalized(e3_id, committee[])` + +**Aggregator:** +- `CommitteeSortitionSolWriter` receives `CommitteeFinalized` +- Removes e3_id from `pending_e3s` (cleanup) + +--- + +### 4. **Keygen Starts** (Committee members generate key shares) + +**Committee Members Only:** +- Receive `CommitteeFinalized` event +- Check if their address is in the committee array +- `ThresholdKeyshareExtension` starts DKG (Distributed Key Generation): + - Generates local secret share + - Broadcasts commitments to other committee members + - Receives and validates shares from others + - Computes public key share + +**All Committee Members:** +- Complete DKG protocol +- Each member now has: + - Their secret share (stored locally) + - The aggregated public key (same for all) + +--- + +### 5. **Public Key Aggregation** (Aggregator collects and publishes) + +**Committee Members:** +- `ThresholdKeyshareExtension` emits `PublicKeyGenerated` event locally +- Contains their computed public key + +**Aggregator:** +- `PublicKeyAggregatorExtension` receives multiple `PublicKeyGenerated` events +- Waits for threshold M nodes to report same public key +- Once threshold reached, emits `PublicKeyAggregated` event with: + - `e3_id` + - `publicKey` (aggregated) + +--- + +### 6. **Committee Published to Registry** (Aggregator writes to blockchain) + +**Aggregator:** +- `CiphernodeRegistrySolWriter` receives `PublicKeyAggregated` event +- Calls `publishCommittee(e3Id, nodes[], publicKey)` on contract + +> **Note:** For score sortition, this needs an update (still TODO): +> - Should track committee from earlier `CommitteeFinalized` event +> - Use those stored nodes when publishing +> - Current code works for distance sortition + +**Contract: CiphernodeRegistryOwnable.sol** +```solidity +publishCommittee(e3Id, nodes[], publicKey) + 1. Validates not already published + 2. Stores committee data + 3. Stores publicKey hash + 4. Emits CommitteePublished(e3Id, nodes[], publicKey) +``` + +**All Ciphernodes:** +- `CiphernodeRegistrySolReader` picks up `CommitteePublished` event +- Broadcasts as `EnclaveEvent::CommitteePublished` +- Committee is now officially registered and ready for computations + +--- + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. E3 REQUESTED │ +│ Enclave.sol → E3Requested event │ +│ ↓ │ +│ All Nodes: CiphernodeSelector checks eligibility │ +│ ↓ │ +│ Selected Nodes: Emit CiphernodeSelected(ticket_id) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. TICKET SUBMISSION (60 second window) │ +│ Selected Nodes → CommitteeSortition.submitTicket() │ +│ ↓ │ +│ Contract: Validates, computes score, inserts into topN │ +│ ↓ │ +│ Contract: Emits TicketSubmitted for each submission │ +│ │ +│ [Meanwhile: Aggregator schedules finalization timer] │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. FINALIZATION (after 60s) │ +│ Aggregator → CommitteeSortition.finalizeCommittee() │ +│ ↓ │ +│ Contract: Returns topN nodes, emits CommitteeFinalized │ +│ ↓ │ +│ All Nodes: Receive CommitteeFinalized(e3Id, nodes[]) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. KEYGEN (Committee members only) │ +│ Committee Members: Run DKG protocol │ +│ ↓ │ +│ Each generates secret share + aggregated public key │ +│ ↓ │ +│ Emit PublicKeyGenerated locally │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 5. PUBLIC KEY AGGREGATION │ +│ Aggregator: Collects PublicKeyGenerated from M+ nodes │ +│ ↓ │ +│ Emits PublicKeyAggregated(e3Id, publicKey) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 6. COMMITTEE PUBLISHED │ +│ Aggregator → CiphernodeRegistry.publishCommittee() │ +│ ↓ │ +│ Contract: Stores committee + pubkey, emits CommitteePublished│ +│ ↓ │ +│ ✅ Committee is now registered and ready! │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Key Components & Their Roles + +### **CommitteeSortitionSolWriter** (`crates/evm/src/committee_sortition_sol.rs`) +- **All Nodes:** Submits tickets when `CiphernodeSelected` +- **Aggregator Only** (if `enable_finalizer=true`): + - Tracks submission deadlines + - Auto-calls `finalizeCommittee()` after window + - Cleans up after `CommitteeFinalized` + +### **CommitteeSortition.sol** (Solidity contract) +- Validates ticket submissions +- Maintains sorted topN array (by score) +- Enforces submission window +- Finalizes committee selection + +### **CiphernodeSelector** (`crates/sortition/`) +- Checks if node is eligible (bonding, tickets) +- Computes scores for all owned tickets +- Finds best ticket to submit +- Emits `CiphernodeSelected` if should participate + +### **ThresholdKeyshareExtension** (`crates/keyshare/`) +- Waits for `CommitteeFinalized` event +- Runs DKG with other committee members +- Generates secret shares and public key + +### **PublicKeyAggregatorExtension** (`crates/aggregator/`) +- Collects public keys from committee members +- Validates threshold reached +- Emits `PublicKeyAggregated` + +### **CiphernodeRegistrySolWriter** (`crates/evm/`) +- Receives `PublicKeyAggregated` +- Publishes committee + pubkey to blockchain +- Makes committee official + +--- + +## What Makes Score Sortition Different from Distance Sortition? + +| Aspect | Distance Sortition | Score Sortition | +|--------|-------------------|-----------------| +| **Participation** | Only closest N nodes in merkle tree | ALL eligible nodes can try | +| **Selection** | Aggregator computes distances | Nodes self-select via lottery | +| **Submission** | Aggregator publishes immediately | 60s window for submissions | +| **Fairness** | Depends on tree position | Equal chance based on tickets | +| **Finalization** | Immediate | After submission window | +| **Contract** | CiphernodeRegistry only | CiphernodeRegistry + CommitteeSortition | + +--- + +## Event Flow Summary + +``` +E3Requested + ↓ +CiphernodeSelected (local, selected nodes only) + ↓ +TicketSubmitted (on-chain, for each submission) + ↓ +CommitteeFinalized (on-chain, after window closes) + ↓ +PublicKeyGenerated (local, committee members) + ↓ +PublicKeyAggregated (local, aggregator) + ↓ +CommitteePublished (on-chain, final registration) +``` + +--- + +## Configuration + +To enable score sortition in your builder: +```rust +CiphernodeBuilder::new() + .with_contract_committee_sortition() // ← Enable score sortition + .with_pubkey_agg() // ← Makes this node an aggregator + .build() +``` + +- **Regular nodes:** Submit tickets only +- **Aggregator nodes:** Submit tickets + auto-finalize committees + +The submission window is automatically fetched from the contract's `submissionWindow` immutable variable (typically 60 seconds). + +--- + +## Implementation Files + +### Core Implementation +- `crates/evm/src/committee_sortition_sol.rs` - Contract interaction and finalization logic +- `crates/ciphernode-builder/src/ciphernode_builder.rs:375-410` - Integration and attachment +- `packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol` - On-chain sortition logic + +### Events +- `crates/events/src/enclave_event/committee_finalized.rs` - CommitteeFinalized event +- `crates/events/src/enclave_event/committee_published.rs` - CommitteePublished event + +### Related Components +- `crates/sortition/` - Node selection and ticket computation +- `crates/keyshare/` - DKG and key generation +- `crates/aggregator/` - Public key aggregation + +--- + +## TODOs for Complete Score Sortition Support + +1. **Update CiphernodeRegistrySolWriter** to track finalized committees: + - Store committee nodes from `CommitteeFinalized` event + - Use stored nodes when publishing (not aggregated nodes) + +2. **Make ThresholdKeyshareExtension wait for CommitteeFinalized**: + - Currently starts on `CiphernodeSelected` + - Should wait for `CommitteeFinalized` in score sortition + - Add mode detection or configuration + +3. **Make submission window configurable**: + - Currently fetched from contract (good!) + - Consider adding override in config for testing + +--- + +## Testing Score Sortition + +1. Deploy contracts with `CommitteeSortition` enabled +2. Start aggregator node with `with_contract_committee_sortition()` and `with_pubkey_agg()` +3. Start multiple regular nodes with `with_contract_committee_sortition()` +4. Request E3 computation +5. Watch logs for: + - Ticket submissions + - Finalization after 60s + - Committee members starting keygen + - Public key aggregation + - Final committee publication + +--- + +## Advantages of Score Sortition + +1. **Fair Participation**: All bonded nodes have equal chance based on tickets owned +2. **Decentralized Selection**: No single node controls selection +3. **Transparent**: All submissions on-chain, verifiable +4. **Secure**: Uses cryptographic randomness (seed + ticket numbers) +5. **Flexible**: Can adjust committee size via threshold parameter + +--- + +## Security Considerations + +1. **Submission Window**: Must be long enough for honest nodes but short enough to prevent attacks +2. **Ticket Balance Snapshot**: Uses block number at E3 request to prevent manipulation +3. **One Submission per Node**: Prevents spam and ensures fair distribution +4. **Score Verification**: Contract recomputes score on-chain (not trusted input) +5. **Finalization Permission**: Anyone can finalize (no central authority) diff --git a/examples/CRISP/deployed_contracts.json b/examples/CRISP/deployed_contracts.json index 5a93472962..fa9ca306e2 100644 --- a/examples/CRISP/deployed_contracts.json +++ b/examples/CRISP/deployed_contracts.json @@ -183,28 +183,25 @@ "blockNumber": 22, "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, - "RiscZeroGroth16Verifier": { + "MockRISC0Verifier": { "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, - "CRISPInputValidator": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" - }, "CRISPInputValidatorFactory": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", "constructorArgs": { - "inputValidator": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + "inputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" } }, "HonkVerifier": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" }, "CRISPProgram": { - "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "constructorArgs": { "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - "inputValidatorAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", - "honkVerifierAddress": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "inputValidatorAddress": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index d5a93ad0bf..0becf549c1 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -15,7 +15,7 @@ chains: address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" deploy_block: 1 # Set to actual deploy block committee_sortition: - address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block program: diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index 047c0cfbd6..17ba2a6b65 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" + "buildInfoId": "solc-0_8_28-332be8f9366934dd9b42983a9b2838dffc905ed0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index e49a5c5262..11ce641987 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -118,11 +118,23 @@ "name": "e3Id", "type": "uint256" }, + { + "indexed": false, + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, { "indexed": false, "internalType": "uint32[2]", "name": "threshold", "type": "uint32[2]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "requestBlock", + "type": "uint256" } ], "name": "CommitteeRequested", @@ -308,6 +320,11 @@ "name": "e3Id", "type": "uint256" }, + { + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, { "internalType": "uint32[2]", "name": "threshold", @@ -403,5 +420,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" + "buildInfoId": "solc-0_8_28-332be8f9366934dd9b42983a9b2838dffc905ed0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index aba14551e3..2453d06f4b 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" + "buildInfoId": "solc-0_8_28-332be8f9366934dd9b42983a9b2838dffc905ed0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json b/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json index d24b1126e4..0c76208801 100644 --- a/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json +++ b/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json @@ -7,17 +7,17 @@ "inputs": [ { "internalType": "address", - "name": "_bondingRegistry", + "name": "_ciphernodeRegistry", "type": "address" }, { "internalType": "address", - "name": "_ciphernodeRegistry", + "name": "_bondingRegistry", "type": "address" }, { "internalType": "uint256", - "name": "_submissionWindow", + "name": "_submissionWindowSeconds", "type": "uint256" } ], @@ -31,27 +31,27 @@ }, { "inputs": [], - "name": "CommitteeNotInitialized", + "name": "NodeAlreadySubmitted", "type": "error" }, { "inputs": [], - "name": "InvalidTicketNumber", + "name": "NodeNotEligible", "type": "error" }, { "inputs": [], - "name": "NodeAlreadySubmitted", + "name": "OnlyCiphernodeRegistry", "type": "error" }, { "inputs": [], - "name": "NodeNotEligible", + "name": "RoundAlreadyInitialized", "type": "error" }, { "inputs": [], - "name": "OnlyCiphernodeRegistry", + "name": "RoundNotInitialized", "type": "error" }, { @@ -83,6 +83,43 @@ "name": "CommitteeFinalized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "committeeSize", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "seed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "startTime", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "endTime", + "type": "uint64" + } + ], + "name": "SortitionInitialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -101,7 +138,7 @@ { "indexed": false, "internalType": "uint256", - "name": "ticketNumber", + "name": "ticketId", "type": "uint256" }, { @@ -109,12 +146,6 @@ "internalType": "uint256", "name": "score", "type": "uint256" - }, - { - "indexed": false, - "internalType": "bool", - "name": "addedToCommittee", - "type": "bool" } ], "name": "TicketSubmitted", @@ -146,40 +177,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "node", - "type": "address" - }, - { - "internalType": "uint256", - "name": "ticketNumber", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "seed", - "type": "uint256" - } - ], - "name": "computeTicketScore", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "pure", - "type": "function" - }, { "inputs": [ { @@ -189,13 +186,7 @@ } ], "name": "finalizeCommittee", - "outputs": [ - { - "internalType": "address[]", - "name": "committee", - "type": "address[]" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -207,78 +198,12 @@ "type": "uint256" } ], - "name": "getSortitionInfo", - "outputs": [ - { - "internalType": "uint256", - "name": "threshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "seed", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "requestBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "submissionDeadline", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "finalized", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "address", - "name": "node", - "type": "address" - } - ], - "name": "getSubmission", + "name": "getCommittee", "outputs": [ { - "components": [ - { - "internalType": "address", - "name": "node", - "type": "address" - }, - { - "internalType": "uint256", - "name": "ticketNumber", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "score", - "type": "uint256" - }, - { - "internalType": "bool", - "name": "exists", - "type": "bool" - } - ], - "internalType": "struct CommitteeSortition.TicketSubmission", + "internalType": "address[]", "name": "", - "type": "tuple" + "type": "address[]" } ], "stateMutability": "view", @@ -292,7 +217,7 @@ "type": "uint256" } ], - "name": "getTopNodes", + "name": "getTopSoFar", "outputs": [ { "internalType": "address[]", @@ -311,9 +236,9 @@ "type": "uint256" }, { - "internalType": "uint256", - "name": "threshold", - "type": "uint256" + "internalType": "uint32", + "name": "committeeSize", + "type": "uint32" }, { "internalType": "uint256", @@ -322,7 +247,7 @@ }, { "internalType": "uint256", - "name": "requestBlock", + "name": "", "type": "uint256" } ], @@ -335,35 +260,15 @@ "inputs": [ { "internalType": "uint256", - "name": "", + "name": "e3Id", "type": "uint256" } ], - "name": "sortitions", + "name": "isOpen", "outputs": [ - { - "internalType": "uint256", - "name": "threshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "seed", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "requestBlock", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "submissionDeadline", - "type": "uint256" - }, { "internalType": "bool", - "name": "finalized", + "name": "", "type": "bool" } ], @@ -392,7 +297,12 @@ }, { "internalType": "uint256", - "name": "ticketNumber", + "name": "ticketId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "score", "type": "uint256" } ], @@ -402,46 +312,42 @@ "type": "function" } ], - "bytecode": "0x60e060405234801561000f575f5ffd5b50604051610ea3380380610ea383398101604081905261002e91610064565b6001600160a01b03928316608052911660a05260c05261009d565b80516001600160a01b038116811461005f575f5ffd5b919050565b5f5f5f60608486031215610076575f5ffd5b61007f84610049565b925061008d60208501610049565b9150604084015190509250925092565b60805160a05160c051610dc06100e35f395f81816102b1015261043401525f81816101d201526103b301525f81816101930152818161083501526108a30152610dc05ff3fe608060405234801561000f575f5ffd5b50600436106100c4575f3560e01c8063b64f05921161007d578063e621dbc711610058578063e621dbc7146102ac578063e6745e13146102d3578063e7a5c098146102e6575f5ffd5b8063b64f059214610209578063da881e5a14610279578063e243eaf014610299575f5ffd5b806385814243116100ad578063858142431461018e5780638dcdd86b146101cd5780638e28a380146101f4575f5ffd5b806355f0e221146100c85780636c58e4eb14610122575b5f5ffd5b6100db6100d6366004610bf5565b610325565b604051610119919081516001600160a01b03168152602080830151908201526040808301519082015260609182015115159181019190915260800190565b60405180910390f35b610164610130366004610c1f565b5f9081526020819052604090208054600182015460028301546003840154600490940154929491939092909160ff90911690565b6040805195865260208601949094529284019190915260608301521515608082015260a001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b610207610202366004610c36565b6103a8565b005b61026b610217366004610c65565b6040805160609590951b6bffffffffffffffffffffffff1916602080870191909152603486019490945260548501929092526074808501919091528151808503909101815260949093019052815191012090565b604051908152602001610119565b61028c610287366004610c1f565b61046f565b6040516101199190610c9b565b61028c6102a7366004610c1f565b610590565b61026b7f000000000000000000000000000000000000000000000000000000000000000081565b6102076102e1366004610ce6565b6105fa565b6101646102f4366004610c1f565b5f60208190529081526040902080546001820154600283015460038401546004909401549293919290919060ff1685565b604080516080810182525f808252602082018190529181018290526060810191909152505f828152602081815260408083206001600160a01b03808616855260069091018352928190208151608081018352815490941684526001810154928401929092526002820154908301526003015460ff16151560608201525b92915050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146103f15760405163b56831db60e01b815260040160405180910390fd5b5f84815260208190526040902080541561041e57604051631860f69960e31b815260040160405180910390fd5b83815560018101839055600281018290556104597f000000000000000000000000000000000000000000000000000000000000000042610d1a565b6003820155600401805460ff1916905550505050565b5f81815260208190526040902080546060919061049f5760405163ad0d953f60e01b815260040160405180910390fd5b600481015460ff16156104c557604051631860f69960e31b815260040160405180910390fd5b806003015442116104e957604051632f021e8d60e11b815260040160405180910390fd5b60048101805460ff191660011790556005810180546040805160208084028201810190925282815292919083018282801561054b57602002820191905f5260205f20905b81546001600160a01b0316815260019091019060200180831161052d575b50505050509150827fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132836040516105829190610c9b565b60405180910390a250919050565b5f81815260208181526040918290206005018054835181840281018401909452808452606093928301828280156105ee57602002820191905f5260205f20905b81546001600160a01b031681526001909101906020018083116105d0575b50505050509050919050565b5f82815260208190526040902080546106265760405163ad0d953f60e01b815260040160405180910390fd5b806003015442111561064b576040516332999ab560e11b815260040160405180910390fd5b600481015460ff161561067157604051631860f69960e31b815260040160405180910390fd5b335f90815260068201602052604090206003015460ff16156106a65760405163257309f160e11b815260040160405180910390fd5b6106b13383856107d9565b600181810154604080516bffffffffffffffffffffffff1933606081901b91909116602080840191909152603483018890526054830189905260748084019590955283518084039095018552609483018085528551958201959095206101148401855282865260b4840189815260d4850182815260f49095018881525f85815260068b01909452958320965187546001600160a01b0319166001600160a01b0390911617875551968601969096559151600285015591516003909301805460ff1916931515939093179092556107899084908461099a565b6040805186815260208101859052821515818301529051919250339187917f2669da55d0606fd95b54298fc8fa777246cc83b04141d983fa5a6553c09bf071919081900360600190a35050505050565b815f036107f95760405163aeaddff160e01b815260040160405180910390fd5b5f818152602081905260408082206002810154915163bb03bd7160e01b81526001600160a01b03878116600483015260248201939093529092917f0000000000000000000000000000000000000000000000000000000000000000169063bb03bd7190604401602060405180830381865afa15801561087a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061089e9190610d2d565b90505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316631209b1f66040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fd573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906109219190610d2d565b9050805f036109435760405163aeaddff160e01b815260040160405180910390fd5b5f61094e8284610d44565b9050808611156109715760405163aeaddff160e01b815260040160405180910390fd5b805f036109915760405163149fbcfd60e11b815260040160405180910390fd5b50505050505050565b82546005840180545f9211156109bf576109b5858585610a6b565b6001915050610a64565b5f856006015f83600185805490506109d79190610d63565b815481106109e7576109e7610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080841015610a5e5781805480610a2857610a28610d8a565b5f8281526020902081015f1990810180546001600160a01b0319169055019055610a53868686610a6b565b600192505050610a64565b5f925050505b9392505050565b6005830180545f5b8254811015610ad7575f866006015f858481548110610a9457610a94610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080851015610ace5781925050610ad7565b50600101610a73565b508154600181810184555f8481526020812090920180546001600160a01b03191690558354610b069190610d63565b90505b81811115610b945782610b1d600183610d63565b81548110610b2d57610b2d610d76565b905f5260205f20015f9054906101000a90046001600160a01b0316838281548110610b5a57610b5a610d76565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610b8c81610d9e565b915050610b09565b5083828281548110610ba857610ba8610d76565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b80356001600160a01b0381168114610bf0575f5ffd5b919050565b5f5f60408385031215610c06575f5ffd5b82359150610c1660208401610bda565b90509250929050565b5f60208284031215610c2f575f5ffd5b5035919050565b5f5f5f5f60808587031215610c49575f5ffd5b5050823594602084013594506040840135936060013592509050565b5f5f5f5f60808587031215610c78575f5ffd5b610c8185610bda565b966020860135965060408601359560600135945092505050565b602080825282518282018190525f918401906040840190835b81811015610cdb5783516001600160a01b0316835260209384019390920191600101610cb4565b509095945050505050565b5f5f60408385031215610cf7575f5ffd5b50508035926020909101359150565b634e487b7160e01b5f52601160045260245ffd5b808201808211156103a2576103a2610d06565b5f60208284031215610d3d575f5ffd5b5051919050565b5f82610d5e57634e487b7160e01b5f52601260045260245ffd5b500490565b818103818111156103a2576103a2610d06565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52603160045260245ffd5b5f81610dac57610dac610d06565b505f19019056fea164736f6c634300081c000a", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100c4575f3560e01c8063b64f05921161007d578063e621dbc711610058578063e621dbc7146102ac578063e6745e13146102d3578063e7a5c098146102e6575f5ffd5b8063b64f059214610209578063da881e5a14610279578063e243eaf014610299575f5ffd5b806385814243116100ad578063858142431461018e5780638dcdd86b146101cd5780638e28a380146101f4575f5ffd5b806355f0e221146100c85780636c58e4eb14610122575b5f5ffd5b6100db6100d6366004610bf5565b610325565b604051610119919081516001600160a01b03168152602080830151908201526040808301519082015260609182015115159181019190915260800190565b60405180910390f35b610164610130366004610c1f565b5f9081526020819052604090208054600182015460028301546003840154600490940154929491939092909160ff90911690565b6040805195865260208601949094529284019190915260608301521515608082015260a001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610119565b6101b57f000000000000000000000000000000000000000000000000000000000000000081565b610207610202366004610c36565b6103a8565b005b61026b610217366004610c65565b6040805160609590951b6bffffffffffffffffffffffff1916602080870191909152603486019490945260548501929092526074808501919091528151808503909101815260949093019052815191012090565b604051908152602001610119565b61028c610287366004610c1f565b61046f565b6040516101199190610c9b565b61028c6102a7366004610c1f565b610590565b61026b7f000000000000000000000000000000000000000000000000000000000000000081565b6102076102e1366004610ce6565b6105fa565b6101646102f4366004610c1f565b5f60208190529081526040902080546001820154600283015460038401546004909401549293919290919060ff1685565b604080516080810182525f808252602082018190529181018290526060810191909152505f828152602081815260408083206001600160a01b03808616855260069091018352928190208151608081018352815490941684526001810154928401929092526002820154908301526003015460ff16151560608201525b92915050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146103f15760405163b56831db60e01b815260040160405180910390fd5b5f84815260208190526040902080541561041e57604051631860f69960e31b815260040160405180910390fd5b83815560018101839055600281018290556104597f000000000000000000000000000000000000000000000000000000000000000042610d1a565b6003820155600401805460ff1916905550505050565b5f81815260208190526040902080546060919061049f5760405163ad0d953f60e01b815260040160405180910390fd5b600481015460ff16156104c557604051631860f69960e31b815260040160405180910390fd5b806003015442116104e957604051632f021e8d60e11b815260040160405180910390fd5b60048101805460ff191660011790556005810180546040805160208084028201810190925282815292919083018282801561054b57602002820191905f5260205f20905b81546001600160a01b0316815260019091019060200180831161052d575b50505050509150827fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132836040516105829190610c9b565b60405180910390a250919050565b5f81815260208181526040918290206005018054835181840281018401909452808452606093928301828280156105ee57602002820191905f5260205f20905b81546001600160a01b031681526001909101906020018083116105d0575b50505050509050919050565b5f82815260208190526040902080546106265760405163ad0d953f60e01b815260040160405180910390fd5b806003015442111561064b576040516332999ab560e11b815260040160405180910390fd5b600481015460ff161561067157604051631860f69960e31b815260040160405180910390fd5b335f90815260068201602052604090206003015460ff16156106a65760405163257309f160e11b815260040160405180910390fd5b6106b13383856107d9565b600181810154604080516bffffffffffffffffffffffff1933606081901b91909116602080840191909152603483018890526054830189905260748084019590955283518084039095018552609483018085528551958201959095206101148401855282865260b4840189815260d4850182815260f49095018881525f85815260068b01909452958320965187546001600160a01b0319166001600160a01b0390911617875551968601969096559151600285015591516003909301805460ff1916931515939093179092556107899084908461099a565b6040805186815260208101859052821515818301529051919250339187917f2669da55d0606fd95b54298fc8fa777246cc83b04141d983fa5a6553c09bf071919081900360600190a35050505050565b815f036107f95760405163aeaddff160e01b815260040160405180910390fd5b5f818152602081905260408082206002810154915163bb03bd7160e01b81526001600160a01b03878116600483015260248201939093529092917f0000000000000000000000000000000000000000000000000000000000000000169063bb03bd7190604401602060405180830381865afa15801561087a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061089e9190610d2d565b90505f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316631209b1f66040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fd573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906109219190610d2d565b9050805f036109435760405163aeaddff160e01b815260040160405180910390fd5b5f61094e8284610d44565b9050808611156109715760405163aeaddff160e01b815260040160405180910390fd5b805f036109915760405163149fbcfd60e11b815260040160405180910390fd5b50505050505050565b82546005840180545f9211156109bf576109b5858585610a6b565b6001915050610a64565b5f856006015f83600185805490506109d79190610d63565b815481106109e7576109e7610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080841015610a5e5781805480610a2857610a28610d8a565b5f8281526020902081015f1990810180546001600160a01b0319169055019055610a53868686610a6b565b600192505050610a64565b5f925050505b9392505050565b6005830180545f5b8254811015610ad7575f866006015f858481548110610a9457610a94610d76565b5f9182526020808320909101546001600160a01b03168352820192909252604001902060020154905080851015610ace5781925050610ad7565b50600101610a73565b508154600181810184555f8481526020812090920180546001600160a01b03191690558354610b069190610d63565b90505b81811115610b945782610b1d600183610d63565b81548110610b2d57610b2d610d76565b905f5260205f20015f9054906101000a90046001600160a01b0316838281548110610b5a57610b5a610d76565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610b8c81610d9e565b915050610b09565b5083828281548110610ba857610ba8610d76565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b80356001600160a01b0381168114610bf0575f5ffd5b919050565b5f5f60408385031215610c06575f5ffd5b82359150610c1660208401610bda565b90509250929050565b5f60208284031215610c2f575f5ffd5b5035919050565b5f5f5f5f60808587031215610c49575f5ffd5b5050823594602084013594506040840135936060013592509050565b5f5f5f5f60808587031215610c78575f5ffd5b610c8185610bda565b966020860135965060408601359560600135945092505050565b602080825282518282018190525f918401906040840190835b81811015610cdb5783516001600160a01b0316835260209384019390920191600101610cb4565b509095945050505050565b5f5f60408385031215610cf7575f5ffd5b50508035926020909101359150565b634e487b7160e01b5f52601160045260245ffd5b808201808211156103a2576103a2610d06565b5f60208284031215610d3d575f5ffd5b5051919050565b5f82610d5e57634e487b7160e01b5f52601260045260245ffd5b500490565b818103818111156103a2576103a2610d06565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52603160045260245ffd5b5f81610dac57610dac610d06565b505f19019056fea164736f6c634300081c000a", + "bytecode": "0x60e060405234801561000f575f5ffd5b50604051610f1d380380610f1d83398101604081905261002e9161012e565b6001600160a01b0383166100785760405162461bcd60e51b815260206004820152600c60248201526b62616420726567697374727960a01b60448201526064015b60405180910390fd5b6001600160a01b0382166100bc5760405162461bcd60e51b815260206004820152600b60248201526a62616420626f6e64696e6760a81b604482015260640161006f565b5f81116100f85760405162461bcd60e51b815260206004820152600a6024820152696261642077696e646f7760b01b604482015260640161006f565b6001600160a01b0392831660c052911660a052608052610167565b80516001600160a01b0381168114610129575f5ffd5b919050565b5f5f5f60608486031215610140575f5ffd5b61014984610113565b925061015760208501610113565b9150604084015190509250925092565b60805160a05160c051610d786101a55f395f8181610132015261045b01525f818160f3015261031701525f818161019401526105330152610d785ff3fe608060405234801561000f575f5ffd5b506004361061009e575f3560e01c80638e6c157311610072578063da881e5a11610058578063da881e5a1461017c578063e621dbc71461018f578063ea1514a3146101c4575f5ffd5b80638e6c157314610154578063c88f60ae14610169575f5ffd5b806218449a146100a25780634d6861a6146100cb57806385814243146100ee5780638dcdd86b1461012d575b5f5ffd5b6100b56100b0366004610bed565b6101d7565b6040516100c29190610c04565b60405180910390f35b6100de6100d9366004610bed565b610241565b60405190151581526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b610167610162366004610c4f565b610294565b005b610167610177366004610c78565b610450565b61016761018a366004610bed565b6105f4565b6101b67f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020016100c2565b6100b56101d2366004610bed565b6107a4565b5f818152602081815260409182902060030180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610217575b50505050509050919050565b5f818152602081905260408120805460ff16158061026557508054610100900460ff165b1561027257505f92915050565b546a0100000000000000000000900467ffffffffffffffff1642111592915050565b5f838152602081905260409020805460ff166102dc576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6102e584610241565b610302576040516332999ab560e11b815260040160405180910390fd5b604051639f8a13d760e01b81523360048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690639f8a13d790602401602060405180830381865afa158015610364573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103889190610cba565b6103a55760405163149fbcfd60e11b815260040160405180910390fd5b335f90815260048201602052604090205460ff16156103d75760405163257309f160e11b815260040160405180910390fd5b335f8181526004830160209081526040808320805460ff1916600117905560058501909152902083905561040d9082908461080c565b6040805184815260208101849052339186917f52999628fb1cb05707e842278833b22e511f11746202cecdf221968b0b89e8bd910160405180910390a350505050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146104995760405163b56831db60e01b815260040160405180910390fd5b5f848152602081905260409020805460ff16156104c9576040516306e1765960e21b815260040160405180910390fd5b805460018083018590557fffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff0000909116600160901b63ffffffff871602171769ffffffffffffffff00001916620100004267ffffffffffffffff811691909102919091178255610558907f000000000000000000000000000000000000000000000000000000000000000090610cf4565b815471ffffffffffffffff0000000000000000000019166a010000000000000000000067ffffffffffffffff9283168102919091178084556040805163ffffffff8916815260208101889052620100008304851691810191909152919004909116606082015285907f298ce6cec139152f1e7b795aeee6b9e8c11c7cdf0650f99dc5315f822eb4bba99060800160405180910390a25050505050565b5f818152602081905260409020805460ff1661063c576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61064582610241565b1561066357604051632f021e8d60e11b815260040160405180910390fd5b8054610100900460ff161561068b57604051631860f69960e31b815260040160405180910390fd5b805461ff00191661010017815560028101545f8167ffffffffffffffff8111156106b7576106b7610d07565b6040519080825280602002602001820160405280156106e0578160200160208202803683370190505b5090505f5b8281101561074f5783600201818154811061070257610702610d1b565b905f5260205f20015f9054906101000a90046001600160a01b031682828151811061072f5761072f610d1b565b6001600160a01b03909216602092830291909101909101526001016106e5565b5080516107659060038501906020840190610b76565b50837fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132826040516107969190610c04565b60405180910390a250505050565b5f818152602081815260409182902060020180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f209081546001600160a01b031681526001909101906020018083116102175750505050509050919050565b60028301548354600160901b900463ffffffff1681101561094d575f6108328584610b02565b600286018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038a161790559154929350916108739190610d2f565b90505b81811115610904576002860161088d600183610d2f565b8154811061089d5761089d610d1b565b5f918252602090912001546002870180546001600160a01b0390921691839081106108ca576108ca610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b0392909216919091179055806108fc81610d42565b915050610876565b508385600201828154811061091b5761091b610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b5f6002850161095d600184610d2f565b8154811061096d5761096d610d1b565b5f9182526020808320909101546001600160a01b0316808352600588019091526040909120549091508084106109a557505050505050565b856002018054806109b8576109b8610d57565b5f8281526020812082015f1990810180546001600160a01b03191690559091019091556109e58786610b02565b600288018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038c16179055915492935091610a269190610d2f565b90505b81811115610ab75760028801610a40600183610d2f565b81548110610a5057610a50610d1b565b5f918252602090912001546002890180546001600160a01b039092169183908110610a7d57610a7d610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610aaf81610d42565b915050610a29565b5085876002018281548110610ace57610ace610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b0316021790555050505050505050565b60028201545f90815b81811015610b6c575f856002018281548110610b2957610b29610d1b565b5f9182526020808320909101546001600160a01b031680835260058901909152604090912054909150851015610b6357509150610b709050565b50600101610b0b565b5090505b92915050565b828054828255905f5260205f20908101928215610bc9579160200282015b82811115610bc957825182546001600160a01b0319166001600160a01b03909116178255602090920191600190910190610b94565b50610bd5929150610bd9565b5090565b5b80821115610bd5575f8155600101610bda565b5f60208284031215610bfd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610c445783516001600160a01b0316835260209384019390920191600101610c1d565b509095945050505050565b5f5f5f60608486031215610c61575f5ffd5b505081359360208301359350604090920135919050565b5f5f5f5f60808587031215610c8b575f5ffd5b84359350602085013563ffffffff81168114610ca5575f5ffd5b93969395505050506040820135916060013590565b5f60208284031215610cca575f5ffd5b81518015158114610cd9575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610b7057610b70610ce0565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b81810381811115610b7057610b70610ce0565b5f81610d5057610d50610ce0565b505f190190565b634e487b7160e01b5f52603160045260245ffdfea164736f6c634300081c000a", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b506004361061009e575f3560e01c80638e6c157311610072578063da881e5a11610058578063da881e5a1461017c578063e621dbc71461018f578063ea1514a3146101c4575f5ffd5b80638e6c157314610154578063c88f60ae14610169575f5ffd5b806218449a146100a25780634d6861a6146100cb57806385814243146100ee5780638dcdd86b1461012d575b5f5ffd5b6100b56100b0366004610bed565b6101d7565b6040516100c29190610c04565b60405180910390f35b6100de6100d9366004610bed565b610241565b60405190151581526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b610167610162366004610c4f565b610294565b005b610167610177366004610c78565b610450565b61016761018a366004610bed565b6105f4565b6101b67f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020016100c2565b6100b56101d2366004610bed565b6107a4565b5f818152602081815260409182902060030180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610217575b50505050509050919050565b5f818152602081905260408120805460ff16158061026557508054610100900460ff165b1561027257505f92915050565b546a0100000000000000000000900467ffffffffffffffff1642111592915050565b5f838152602081905260409020805460ff166102dc576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6102e584610241565b610302576040516332999ab560e11b815260040160405180910390fd5b604051639f8a13d760e01b81523360048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690639f8a13d790602401602060405180830381865afa158015610364573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103889190610cba565b6103a55760405163149fbcfd60e11b815260040160405180910390fd5b335f90815260048201602052604090205460ff16156103d75760405163257309f160e11b815260040160405180910390fd5b335f8181526004830160209081526040808320805460ff1916600117905560058501909152902083905561040d9082908461080c565b6040805184815260208101849052339186917f52999628fb1cb05707e842278833b22e511f11746202cecdf221968b0b89e8bd910160405180910390a350505050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146104995760405163b56831db60e01b815260040160405180910390fd5b5f848152602081905260409020805460ff16156104c9576040516306e1765960e21b815260040160405180910390fd5b805460018083018590557fffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff0000909116600160901b63ffffffff871602171769ffffffffffffffff00001916620100004267ffffffffffffffff811691909102919091178255610558907f000000000000000000000000000000000000000000000000000000000000000090610cf4565b815471ffffffffffffffff0000000000000000000019166a010000000000000000000067ffffffffffffffff9283168102919091178084556040805163ffffffff8916815260208101889052620100008304851691810191909152919004909116606082015285907f298ce6cec139152f1e7b795aeee6b9e8c11c7cdf0650f99dc5315f822eb4bba99060800160405180910390a25050505050565b5f818152602081905260409020805460ff1661063c576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61064582610241565b1561066357604051632f021e8d60e11b815260040160405180910390fd5b8054610100900460ff161561068b57604051631860f69960e31b815260040160405180910390fd5b805461ff00191661010017815560028101545f8167ffffffffffffffff8111156106b7576106b7610d07565b6040519080825280602002602001820160405280156106e0578160200160208202803683370190505b5090505f5b8281101561074f5783600201818154811061070257610702610d1b565b905f5260205f20015f9054906101000a90046001600160a01b031682828151811061072f5761072f610d1b565b6001600160a01b03909216602092830291909101909101526001016106e5565b5080516107659060038501906020840190610b76565b50837fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132826040516107969190610c04565b60405180910390a250505050565b5f818152602081815260409182902060020180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f209081546001600160a01b031681526001909101906020018083116102175750505050509050919050565b60028301548354600160901b900463ffffffff1681101561094d575f6108328584610b02565b600286018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038a161790559154929350916108739190610d2f565b90505b81811115610904576002860161088d600183610d2f565b8154811061089d5761089d610d1b565b5f918252602090912001546002870180546001600160a01b0390921691839081106108ca576108ca610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b0392909216919091179055806108fc81610d42565b915050610876565b508385600201828154811061091b5761091b610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b5f6002850161095d600184610d2f565b8154811061096d5761096d610d1b565b5f9182526020808320909101546001600160a01b0316808352600588019091526040909120549091508084106109a557505050505050565b856002018054806109b8576109b8610d57565b5f8281526020812082015f1990810180546001600160a01b03191690559091019091556109e58786610b02565b600288018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038c16179055915492935091610a269190610d2f565b90505b81811115610ab75760028801610a40600183610d2f565b81548110610a5057610a50610d1b565b5f918252602090912001546002890180546001600160a01b039092169183908110610a7d57610a7d610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610aaf81610d42565b915050610a29565b5085876002018281548110610ace57610ace610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b0316021790555050505050505050565b60028201545f90815b81811015610b6c575f856002018281548110610b2957610b29610d1b565b5f9182526020808320909101546001600160a01b031680835260058901909152604090912054909150851015610b6357509150610b709050565b50600101610b0b565b5090505b92915050565b828054828255905f5260205f20908101928215610bc9579160200282015b82811115610bc957825182546001600160a01b0319166001600160a01b03909116178255602090920191600190910190610b94565b50610bd5929150610bd9565b5090565b5b80821115610bd5575f8155600101610bda565b5f60208284031215610bfd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610c445783516001600160a01b0316835260209384019390920191600101610c1d565b509095945050505050565b5f5f5f60608486031215610c61575f5ffd5b505081359360208301359350604090920135919050565b5f5f5f5f60808587031215610c8b575f5ffd5b84359350602085013563ffffffff81168114610ca5575f5ffd5b93969395505050506040820135916060013590565b5f60208284031215610cca575f5ffd5b81518015158114610cd9575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610b7057610b70610ce0565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b81810381811115610b7057610b70610ce0565b5f81610d5057610d50610ce0565b505f190190565b634e487b7160e01b5f52603160045260245ffdfea164736f6c634300081c000a", "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": { - "19668": [ - { - "length": 32, - "start": 403 - }, + "13294": [ { "length": 32, - "start": 2101 + "start": 404 }, { "length": 32, - "start": 2211 + "start": 1331 } ], - "19671": [ + "13297": [ { "length": 32, - "start": 466 + "start": 243 }, { "length": 32, - "start": 947 + "start": 791 } ], - "19674": [ + "13299": [ { "length": 32, - "start": 689 + "start": 306 }, { "length": 32, - "start": 1076 + "start": 1115 } ] }, "inputSourceName": "project/contracts/sortition/CommitteeSortition.sol", - "buildInfoId": "solc-0_8_28-3e03817383dac2d80aaffcb248a668140348a693" + "buildInfoId": "solc-0_8_28-a9cacbeeb68df13e87656122d54fd5cb4ca7e178" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index a5b1512ed6..c1e87fe183 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -58,6 +58,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Incremented after each successful E3 request. uint256 public nexte3Id; + /// @notice Submission Window for an E3 Sortition. + /// @dev The submission window is the time period during which the ciphernodes can submit + /// their tickets to be a part of the committee. + uint256 public sortitionSubmissionWindow; + /// @notice Mapping of allowed E3 Programs. /// @dev Only enabled E3 Programs can be used in computation requests. mapping(IE3Program e3Program => bool allowed) public e3Programs; @@ -203,6 +208,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @param _bondingRegistry The address of the Bonding Registry contract. /// @param _feeToken The address of the ERC20 token used for E3 fees. /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). constructor( address _owner, @@ -210,6 +216,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { IBondingRegistry _bondingRegistry, IERC20 _feeToken, uint256 _maxDuration, + uint256 _sortitionSubmissionWindow, bytes[] memory _e3ProgramsParams ) { initialize( @@ -218,6 +225,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { _bondingRegistry, _feeToken, _maxDuration, + _sortitionSubmissionWindow, _e3ProgramsParams ); } @@ -229,6 +237,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @param _bondingRegistry The address of the Bonding Registry contract. /// @param _feeToken The address of the ERC20 token used for E3 fees. /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). function initialize( address _owner, @@ -236,6 +245,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { IBondingRegistry _bondingRegistry, IERC20 _feeToken, uint256 _maxDuration, + uint256 _sortitionSubmissionWindow, bytes[] memory _e3ProgramsParams ) public initializer { __Ownable_init(msg.sender); @@ -243,6 +253,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { setCiphernodeRegistry(_ciphernodeRegistry); setBondingRegistry(_bondingRegistry); setFeeToken(_feeToken); + setSortitionSubmissionWindow(_sortitionSubmissionWindow); setE3ProgramsParams(_e3ProgramsParams); if (_owner != owner()) transferOwnership(_owner); } @@ -329,7 +340,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); require( - ciphernodeRegistry.requestCommittee(e3Id, requestParams.threshold), + ciphernodeRegistry.requestCommittee( + e3Id, + seed, + requestParams.threshold, + sortitionSubmissionWindow + ), CommitteeSelectionFailed() ); @@ -504,6 +520,15 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit MaxDurationSet(_maxDuration); } + /// @inheritdoc IEnclave + function setSortitionSubmissionWindow( + uint256 _sortitionSubmissionWindow + ) public onlyOwner returns (bool success) { + sortitionSubmissionWindow = _sortitionSubmissionWindow; + success = true; + emit SortitionSubmissionWindowSet(_sortitionSubmissionWindow); + } + /// @inheritdoc IEnclave function setCiphernodeRegistry( ICiphernodeRegistry _ciphernodeRegistry diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index f9d3a131a6..942d0e586b 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -12,19 +12,45 @@ pragma solidity >=0.8.27; * and coordinates committee selection for E3 computations */ interface ICiphernodeRegistry { - /// @notice Struct representing a committee for an E3. - /// @param nodes Array of ciphernode addresses in the committee. + /// @notice Struct representing the sortition state for an E3 round. + /// @param initialized Whether the round has been initialized. + /// @param finalized Whether the round has been finalized. + /// @param requestBlock The block number when the committee was requested. + /// @param submissionDeadline The deadline for submitting tickets. /// @param threshold The M/N threshold for the committee ([M, N]). /// @param publicKey Hash of the committee's public key. + /// @param seed The seed for the round. + /// @param topNodes The top nodes in the round. + /// @param committee The committee for the round. + /// @param submitted Mapping of nodes to their submission status. + /// @param scoreOf Mapping of nodes to their scores. struct Committee { - address[] nodes; - uint32[2] threshold; + bool initialized; + bool finalized; + uint256 seed; + uint256 requestBlock; + uint256 submissionDeadline; bytes32 publicKey; + uint32[2] threshold; + address[] topNodes; + address[] committee; + mapping(address node => bool submitted) submitted; + mapping(address node => uint256 score) scoreOf; } + /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. + /// @param seed Random seed for score computation. /// @param threshold The M/N threshold for the committee. - event CommitteeRequested(uint256 indexed e3Id, uint32[2] threshold); + /// @param requestBlock Block number for snapshot validation. + /// @param submissionDeadline Deadline for submitting tickets. + event CommitteeRequested( + uint256 indexed e3Id, + uint256 seed, + uint32[2] threshold, + uint256 requestBlock, + uint256 submissionDeadline + ); /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. @@ -94,11 +120,15 @@ interface ICiphernodeRegistry { /// @notice Initiates the committee selection process for a specified E3. /// @dev This function MUST revert when not called by the Enclave contract. /// @param e3Id ID of the E3 for which to select the committee. + /// @param seed Random seed for score computation. /// @param threshold The M/N threshold for the committee. + /// @param submissionWindow The submission window for the E3 sortition in seconds. /// @return success True if committee selection was successfully initiated. function requestCommittee( uint256 e3Id, - uint32[2] calldata threshold + uint256 seed, + uint32[2] calldata threshold, + uint256 submissionWindow ) external returns (bool success); /// @notice Publishes the public key resulting from the committee selection process. diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index e6f403b509..786f7e3295 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -64,6 +64,10 @@ interface IEnclave { /// @param maxDuration The maximum duration of a computation in seconds. event MaxDurationSet(uint256 maxDuration); + /// @notice This event MUST be emitted any time the `sortitionSubmissionWindow` is set. + /// @param sortitionSubmissionWindow The submission window for the E3 sortition in seconds. + event SortitionSubmissionWindowSet(uint256 sortitionSubmissionWindow); + /// @notice This event MUST be emitted any time the CiphernodeRegistry is set. /// @param ciphernodeRegistry The address of the CiphernodeRegistry contract. event CiphernodeRegistrySet(address ciphernodeRegistry); @@ -206,6 +210,13 @@ interface IEnclave { uint256 _maxDuration ) external returns (bool success); + /// @notice This function should be called to set the submission window for the E3 sortition. + /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. + /// @return success True if the sortition submission window was successfully set. + function setSortitionSubmissionWindow( + uint256 _sortitionSubmissionWindow + ) external returns (bool success); + /// @notice Sets the Ciphernode Registry contract address. /// @dev This function MUST revert if the address is zero or the same as the current registry. /// @param _ciphernodeRegistry The address of the new Ciphernode Registry contract. diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index ff6757ea5b..c5f03cf469 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -66,8 +66,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; /// @notice Maps E3 ID to its committee data - mapping(uint256 e3Id => ICiphernodeRegistry.Committee committee) - public committees; + mapping(uint256 e3Id => Committee committee) public committees; //////////////////////////////////////////////////////////// // // @@ -84,6 +83,33 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Committee has not been published yet for this E3 error CommitteeNotPublished(); + /// @notice Committee has not been requested yet for this E3 + error CommitteeNotRequested(); + + /// @notice Submission Window Not valid for this E3 + error SubmissionWindowNotValid(); + + /// @notice Submission Window has been closed for this E3 + error SubmissionWindowClosed(); + + /// @notice Submission deadline has been reached for this E3 + error SubmissionDeadlineReached(); + + /// @notice Committee has already been finalized for this E3 + error CommitteeAlreadyFinalized(); + + /// @notice Committee has not been finalized yet for this E3 + error CommitteeNotFinalized(); + + /// @notice Node has already submitted a ticket for this E3 + error NodeAlreadySubmitted(); + + /// @notice Node has not submitted a ticket for this E3 + error NodeNotSubmitted(); + + /// @notice Node is not eligible for this E3 + error NodeNotEligible(); + /// @notice Ciphernode is not enabled in the registry /// @param node Address of the ciphernode error CiphernodeNotEnabled(address node); @@ -175,29 +201,28 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @inheritdoc ICiphernodeRegistry function requestCommittee( uint256 e3Id, - uint32[2] calldata threshold + uint256 seed, + uint32[2] calldata threshold, + uint256 submissionWindow ) external onlyEnclave returns (bool success) { - require( - committees[e3Id].threshold[1] == 0, - CommitteeAlreadyRequested() - ); - require(committeeSortition != address(0), CommitteeSortitionNotSet()); - - committees[e3Id].threshold = threshold; + Committee storage c = committees[e3Id]; + require(!c.initialized, CommitteeAlreadyRequested()); + + c.initialized = true; + c.finalized = false; + c.seed = seed; + c.requestBlock = block.number; + c.submissionDeadline = block.timestamp + submissionWindow; + c.threshold = threshold; roots[e3Id] = root(); - // Initialize sortition in CommitteeSortition contract - // Get seed from Enclave contract (will be passed via E3Requested event) - // For now, we'll generate it here - note: should match E3.seed from Enclave - uint256 seed = uint256(keccak256(abi.encode(block.prevrandao, e3Id))); - CommitteeSortition(committeeSortition).initializeSortition( + emit CommitteeRequested( e3Id, - threshold[1], // Use N (total committee size) seed, - block.number + threshold, + c.requestBlock, + c.submissionDeadline ); - - emit CommitteeRequested(e3Id, threshold); success = true; } @@ -220,29 +245,6 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit CommitteePublished(e3Id, nodes, publicKey); } - /// @notice Finalizes committee from sortition and publishes it - /// @dev Can be called by anyone after sortition deadline. Gets committee from CommitteeSortition. - /// @param e3Id ID of the E3 computation - /// @param publicKey Aggregated public key of the committee - function finalizeAndPublishCommittee( - uint256 e3Id, - bytes calldata publicKey - ) external { - require(committeeSortition != address(0), CommitteeSortitionNotSet()); - ICiphernodeRegistry.Committee storage committee = committees[e3Id]; - require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); - - // Finalize sortition and get committee - address[] memory nodes = CommitteeSortition(committeeSortition) - .finalizeCommittee(e3Id); - - committee.nodes = nodes; - bytes32 publicKeyHash = keccak256(publicKey); - committee.publicKey = publicKeyHash; - publicKeyHashes[e3Id] = publicKeyHash; - emit CommitteePublished(e3Id, nodes, publicKey); - } - /// @inheritdoc ICiphernodeRegistry function addCiphernode(address node) external onlyOwnerOrBondingVault { if (isEnabled(node)) { @@ -274,6 +276,39 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit CiphernodeRemoved(node, index, numCiphernodes, ciphernodes.size); } + //////////////////////////////////////////////////////////// + // // + // Sortition Functions // + // // + //////////////////////////////////////////////////////////// + + /// @inheritdoc ICiphernodeRegistry + function submitTicket( + uint256 e3Id, + uint256 ticketId, + uint256 score + ) external { + Committee storage c = committees[e3Id]; + require(!r.initialized || r.finalized, CommitteeNotRequested()); + require( + block.timestamp <= c.submissionDeadline, + SubmissionDeadlineReached() + ); + + if (!isOpen(e3Id)) revert SubmissionWindowClosed(); + if (!IBondingRegistry(bondingRegistry).isActive(msg.sender)) + revert NodeNotEligible(); + if (r.submitted[msg.sender]) revert NodeAlreadySubmitted(); + + r.submitted[msg.sender] = true; + r.scoreOf[msg.sender] = score; + + // insert into top-N (ascending score) + _insertTopN(r, msg.sender, score); + + emit TicketSubmitted(e3Id, msg.sender, ticketId, score); + } + //////////////////////////////////////////////////////////// // // // Set Functions // diff --git a/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol b/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol index cdb4090db4..1b7715ce7e 100644 --- a/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol +++ b/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol @@ -8,385 +8,209 @@ pragma solidity >=0.8.27; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; -/** - * @title CommitteeSortition - * @notice Simple on-chain verification of ticket-based sortition - * @dev Validates ticket submissions and tracks committee members - * - * Flow: - * 1. Nodes perform sortition off-chain - * 2. Selected nodes submit their winning ticket via submitTicket() - * 3. Contract validates ticket against snapshot balance - * 4. Contract tracks top N nodes by score - */ contract CommitteeSortition { - // ====================== - // Errors - // ====================== + // ============ Config ============ + uint256 public immutable submissionWindow; // seconds + IBondingRegistry public immutable bondingRegistry; + address public immutable ciphernodeRegistry; // who opens rounds + + // ============ Types ============ + struct Round { + // lifecycle + bool initialized; + bool finalized; + uint64 startTime; + uint64 endTime; + // params + uint32 committeeSize; + uint256 seed; + // state + address[] topNodes; // sorted by ascending score + address[] committee; // frozen after finalize + mapping(address => bool) submitted; + mapping(address => uint256) scoreOf; // score for submitted node (lower is better) + } - error InvalidTicketNumber(); - error NodeNotEligible(); - error NodeAlreadySubmitted(); + // e3Id => Round + mapping(uint256 => Round) private rounds; + + // ============ Errors ============ + error OnlyCiphernodeRegistry(); + error RoundNotInitialized(); + error RoundAlreadyInitialized(); error SubmissionWindowClosed(); error SubmissionWindowNotClosed(); - error CommitteeNotInitialized(); + error NodeNotEligible(); + error NodeAlreadySubmitted(); error CommitteeAlreadyFinalized(); - error OnlyCiphernodeRegistry(); - // ====================== - // Events - // ====================== + // ============ Events ============ + event SortitionInitialized( + uint256 indexed e3Id, + uint32 committeeSize, + uint256 seed, + uint64 startTime, + uint64 endTime + ); event TicketSubmitted( uint256 indexed e3Id, address indexed node, - uint256 ticketNumber, - uint256 score, - bool addedToCommittee + uint256 ticketId, + uint256 score ); event CommitteeFinalized(uint256 indexed e3Id, address[] committee); - // ====================== - // Structs - // ====================== - - /// @notice Represents a node's ticket submission - struct TicketSubmission { - address node; - uint256 ticketNumber; - uint256 score; - bool exists; - } - - /// @notice Sortition state for an E3 - struct SortitionState { - uint256 threshold; // Number of nodes needed - uint256 seed; // Random seed for this E3 - uint256 requestBlock; // Block number when E3 was requested (for snapshot) - uint256 submissionDeadline; // Timestamp when submission window closes - bool finalized; // Whether committee has been finalized - address[] topNodes; // Current top N nodes (sorted by score) - mapping(address => TicketSubmission) submissions; - } - - // ====================== - // Storage - // ====================== - - /// @notice Bonding registry for checking ticket balances - IBondingRegistry public immutable bondingRegistry; - - /// @notice Ciphernode registry that can initialize sortitions - address public immutable ciphernodeRegistry; - - /// @notice Default submission window duration (in seconds) - uint256 public immutable submissionWindow; - - /// @notice Maps E3 ID to its sortition state - mapping(uint256 => SortitionState) public sortitions; - - // ====================== - // Constructor - // ====================== - + // ============ Constructor ============ constructor( - address _bondingRegistry, address _ciphernodeRegistry, - uint256 _submissionWindow + address _bondingRegistry, + uint256 _submissionWindowSeconds ) { - bondingRegistry = IBondingRegistry(_bondingRegistry); + require(_ciphernodeRegistry != address(0), "bad registry"); + require(_bondingRegistry != address(0), "bad bonding"); + require(_submissionWindowSeconds > 0, "bad window"); ciphernodeRegistry = _ciphernodeRegistry; - submissionWindow = _submissionWindow; - } - - // ====================== - // Main Functions - // ====================== - - /** - * @notice Initialize sortition for an E3 - * @dev Only callable by ciphernode registry when committee is requested - * @param e3Id The E3 identifier - * @param threshold Number of committee members needed - * @param seed Random seed for score computation - * @param requestBlock Block number for snapshot validation - */ - function initializeSortition( - uint256 e3Id, - uint256 threshold, - uint256 seed, - uint256 requestBlock - ) external { - require(msg.sender == ciphernodeRegistry, OnlyCiphernodeRegistry()); - SortitionState storage state = sortitions[e3Id]; - require(state.threshold == 0, CommitteeAlreadyFinalized()); - - state.threshold = threshold; - state.seed = seed; - state.requestBlock = requestBlock; - state.submissionDeadline = block.timestamp + submissionWindow; - state.finalized = false; - } - - /** - * @notice Submit a ticket for sortition - * @dev Nodes call this to submit their best ticket. Score is computed and verified on-chain. - * @param e3Id The E3 identifier - * @param ticketNumber The ticket number to submit (1 to available_tickets at snapshot) - */ - function submitTicket(uint256 e3Id, uint256 ticketNumber) external { - SortitionState storage state = sortitions[e3Id]; - - // Check sortition is initialized - require(state.threshold > 0, CommitteeNotInitialized()); - - // Check submission window is still open - require( - block.timestamp <= state.submissionDeadline, - SubmissionWindowClosed() - ); - - // Check not finalized - require(!state.finalized, CommitteeAlreadyFinalized()); - - // Check node hasn't already submitted - if (state.submissions[msg.sender].exists) revert NodeAlreadySubmitted(); - - // Check node is eligible (has ticket balance at snapshot) - _validateNodeEligibility(msg.sender, ticketNumber, e3Id); - - // Compute score - uint256 score = _computeTicketScore( - msg.sender, - ticketNumber, - e3Id, - state.seed - ); - - // Store submission - state.submissions[msg.sender] = TicketSubmission({ - node: msg.sender, - ticketNumber: ticketNumber, - score: score, - exists: true - }); - - // Try to insert into top N - bool added = _tryInsertIntoTopN(state, msg.sender, score); - - emit TicketSubmitted(e3Id, msg.sender, ticketNumber, score, added); + bondingRegistry = IBondingRegistry(_bondingRegistry); + submissionWindow = _submissionWindowSeconds; } - /** - * @notice Finalize the committee after submission window closes - * @dev Can be called by anyone after the deadline. Sets finalized flag. - * @param e3Id The E3 identifier - * @return committee The final committee addresses - */ - function finalizeCommittee( + // ============ View helpers ============ + function getCommittee( uint256 e3Id - ) external returns (address[] memory committee) { - SortitionState storage state = sortitions[e3Id]; - - require(state.threshold > 0, CommitteeNotInitialized()); - require(!state.finalized, CommitteeAlreadyFinalized()); - require( - block.timestamp > state.submissionDeadline, - SubmissionWindowNotClosed() - ); - - state.finalized = true; - committee = state.topNodes; - - emit CommitteeFinalized(e3Id, committee); + ) external view returns (address[] memory) { + return rounds[e3Id].committee; } - // ====================== - // View Functions - // ====================== - - /** - * @notice Get the current top N nodes for an E3 - * @param e3Id The E3 identifier - * @return Array of top N node addresses - */ - function getTopNodes( + function getTopSoFar( uint256 e3Id ) external view returns (address[] memory) { - return sortitions[e3Id].topNodes; + return rounds[e3Id].topNodes; } - /** - * @notice Get a node's submission for an E3 - * @param e3Id The E3 identifier - * @param node The node address - * @return The ticket submission - */ - function getSubmission( - uint256 e3Id, - address node - ) external view returns (TicketSubmission memory) { - return sortitions[e3Id].submissions[node]; + function isOpen(uint256 e3Id) public view returns (bool) { + Round storage r = rounds[e3Id]; + if (!r.initialized || r.finalized) return false; + return block.timestamp <= r.endTime; } - /** - * @notice Compute the score for a ticket - * @dev Public function to allow off-chain computation verification - * @param node Node address - * @param ticketNumber Ticket number (1 to N) - * @param e3Id E3 identifier - * @param seed Random seed - * @return The computed score - */ - function computeTicketScore( - address node, - uint256 ticketNumber, - uint256 e3Id, - uint256 seed - ) external pure returns (uint256) { - return _computeTicketScore(node, ticketNumber, e3Id, seed); - } + // ============ Core ============ - /** - * @notice Get sortition information for an E3 - * @param e3Id The E3 identifier - * @return threshold Number of committee members needed - * @return seed Random seed - * @return requestBlock Block number when E3 was requested - * @return submissionDeadline Timestamp when submission window closes - * @return finalized Whether committee has been finalized - */ - function getSortitionInfo( - uint256 e3Id - ) - external - view - returns ( - uint256 threshold, - uint256 seed, - uint256 requestBlock, - uint256 submissionDeadline, - bool finalized - ) - { - SortitionState storage state = sortitions[e3Id]; - return ( - state.threshold, - state.seed, - state.requestBlock, - state.submissionDeadline, - state.finalized + /// called by CiphernodeRegistry when Enclave.requestCommittee(...) happens + function initializeSortition( + uint256 e3Id, + uint32 committeeSize, + uint256 seed, + uint256 /* requestBlock */ // kept for compatibility / auditing, not used here + ) external { + if (msg.sender != ciphernodeRegistry) revert OnlyCiphernodeRegistry(); + Round storage r = rounds[e3Id]; + if (r.initialized) revert RoundAlreadyInitialized(); + + r.initialized = true; + r.finalized = false; + r.committeeSize = committeeSize; + r.seed = seed; + r.startTime = uint64(block.timestamp); + r.endTime = uint64(block.timestamp + submissionWindow); + + emit SortitionInitialized( + e3Id, + committeeSize, + seed, + r.startTime, + r.endTime ); } - // ====================== - // Internal Functions - // ====================== - - /** - * @notice Computes score = keccak256(node || ticketNumber || e3Id || seed) - */ - function _computeTicketScore( - address node, - uint256 ticketNumber, + /// nodes submit their *best* ticket (id, score) if eligible + function submitTicket( uint256 e3Id, - uint256 seed - ) internal pure returns (uint256) { - bytes32 hash = keccak256( - abi.encodePacked(node, ticketNumber, e3Id, seed) - ); - return uint256(hash); - } + uint256 ticketId, + uint256 score + ) external { + Round storage r = rounds[e3Id]; + if (!r.initialized) revert RoundNotInitialized(); + if (!isOpen(e3Id)) revert SubmissionWindowClosed(); + if (!IBondingRegistry(bondingRegistry).isActive(msg.sender)) + revert NodeNotEligible(); + if (r.submitted[msg.sender]) revert NodeAlreadySubmitted(); - /** - * @notice Validates that a node is eligible to participate - * @dev Uses snapshot of ticket balance at E3 request block for deterministic validation - */ - function _validateNodeEligibility( - address node, - uint256 ticketNumber, - uint256 e3Id - ) internal view { - if (ticketNumber == 0) revert InvalidTicketNumber(); + r.submitted[msg.sender] = true; + r.scoreOf[msg.sender] = score; - SortitionState storage state = sortitions[e3Id]; + // insert into top-N (ascending score) + _insertTopN(r, msg.sender, score); - // Get ticket balance at the time E3 was requested (snapshot) - uint256 ticketBalance = bondingRegistry.getTicketBalanceAtBlock( - node, - state.requestBlock - ); - uint256 ticketPrice = bondingRegistry.ticketPrice(); + emit TicketSubmitted(e3Id, msg.sender, ticketId, score); + } - if (ticketPrice == 0) revert InvalidTicketNumber(); + /// anyone can finalize after the window closes + function finalizeCommittee(uint256 e3Id) external { + Round storage r = rounds[e3Id]; + if (!r.initialized) revert RoundNotInitialized(); + if (isOpen(e3Id)) revert SubmissionWindowNotClosed(); + if (r.finalized) revert CommitteeAlreadyFinalized(); - // Calculate available tickets at snapshot - uint256 availableTickets = ticketBalance / ticketPrice; + r.finalized = true; - // Check ticket number is valid - if (ticketNumber > availableTickets) revert InvalidTicketNumber(); + // freeze committee + uint256 n = r.topNodes.length; + address[] memory committee = new address[](n); + for (uint256 i = 0; i < n; i++) committee[i] = r.topNodes[i]; + r.committee = committee; - // Check node is eligible (has tickets at snapshot) - if (availableTickets == 0) revert NodeNotEligible(); + emit CommitteeFinalized(e3Id, committee); } - /** - * @notice Try to insert node into top N sorted list - * @dev Maintains sorted order by score (lowest first) - * @return Whether node was added to top N - */ - function _tryInsertIntoTopN( - SortitionState storage state, + // ============ Internal ============ + + function _insertTopN( + Round storage r, address node, uint256 score - ) internal returns (bool) { - address[] storage topNodes = state.topNodes; - - // If list not full, insert in sorted order - if (topNodes.length < state.threshold) { - _insertSorted(state, node, score); - return true; + ) internal { + uint256 n = r.topNodes.length; + + // if we still have room, just insert in sorted spot + if (n < r.committeeSize) { + uint256 pos = _findInsertPos(r, score); + r.topNodes.push(node); + for (uint256 i = r.topNodes.length - 1; i > pos; i--) { + r.topNodes[i] = r.topNodes[i - 1]; + } + r.topNodes[pos] = node; + return; } - // If list is full, only add if score is better than worst - uint256 worstScore = state - .submissions[topNodes[topNodes.length - 1]] - .score; - if (score < worstScore) { - topNodes.pop(); // Remove worst - _insertSorted(state, node, score); - return true; + // otherwise compare with worst current score + address worst = r.topNodes[n - 1]; + uint256 worstScore = r.scoreOf[worst]; + if (score >= worstScore) { + // not better than worst, ignore + return; } - return false; + // replace worst with node at correct position + r.topNodes.pop(); // drop worst + uint256 pos2 = _findInsertPos(r, score); + r.topNodes.push(node); + for (uint256 i = r.topNodes.length - 1; i > pos2; i--) { + r.topNodes[i] = r.topNodes[i - 1]; + } + r.topNodes[pos2] = node; } - /** - * @notice Insert node into sorted position (ascending by score) - */ - function _insertSorted( - SortitionState storage state, - address node, + function _findInsertPos( + Round storage r, uint256 score - ) internal { - address[] storage topNodes = state.topNodes; - - // Find insertion position - uint256 insertPos = topNodes.length; - for (uint256 i = 0; i < topNodes.length; i++) { - uint256 existingScore = state.submissions[topNodes[i]].score; - if (score < existingScore) { - insertPos = i; - break; - } - } - - // Insert at position - topNodes.push(address(0)); // Extend array - for (uint256 i = topNodes.length - 1; i > insertPos; i--) { - topNodes[i] = topNodes[i - 1]; + ) internal view returns (uint256) { + uint256 n = r.topNodes.length; + for (uint256 i = 0; i < n; i++) { + address a = r.topNodes[i]; + if (score < r.scoreOf[a]) return i; } - topNodes[insertPos] = node; + return n; } } diff --git a/packages/enclave-contracts/contracts/sortition/OldCS.sol b/packages/enclave-contracts/contracts/sortition/OldCS.sol new file mode 100644 index 0000000000..57816fb279 --- /dev/null +++ b/packages/enclave-contracts/contracts/sortition/OldCS.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity >=0.8.27; + +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; + +/** + * @title CommitteeSortition + * @notice Simple on-chain verification of ticket-based sortition + * @dev Validates ticket submissions and tracks committee members + * + * Flow: + * 1. Nodes perform sortition off-chain + * 2. Selected nodes submit their winning ticket via submitTicket() + * 3. Contract validates ticket against snapshot balance + * 4. Contract tracks top N nodes by score + */ +contract OldCS { + // ====================== + // Errors + // ====================== + + error InvalidTicketNumber(); + error NodeNotEligible(); + error NodeAlreadySubmitted(); + error SubmissionWindowClosed(); + error SubmissionWindowNotClosed(); + error CommitteeNotInitialized(); + error CommitteeAlreadyFinalized(); + error OnlyCiphernodeRegistry(); + + // ====================== + // Events + // ====================== + + event TicketSubmitted( + uint256 indexed e3Id, + address indexed node, + uint256 ticketNumber, + uint256 score, + bool addedToCommittee + ); + + event CommitteeFinalized(uint256 indexed e3Id, address[] committee); + + // ====================== + // Structs + // ====================== + + /// @notice Represents a node's ticket submission + struct TicketSubmission { + address node; + uint256 ticketNumber; + uint256 score; + bool exists; + } + + /// @notice Sortition state for an E3 + struct SortitionState { + uint256 threshold; // Number of nodes needed + uint256 seed; // Random seed for this E3 + uint256 requestBlock; // Block number when E3 was requested (for snapshot) + uint256 submissionDeadline; // Timestamp when submission window closes + bool finalized; // Whether committee has been finalized + address[] topNodes; // Current top N nodes (sorted by score) + mapping(address => TicketSubmission) submissions; + } + + // ====================== + // Storage + // ====================== + + /// @notice Bonding registry for checking ticket balances + IBondingRegistry public immutable bondingRegistry; + + /// @notice Ciphernode registry that can initialize sortitions + address public immutable ciphernodeRegistry; + + /// @notice Default submission window duration (in seconds) + uint256 public immutable submissionWindow; + + /// @notice Maps E3 ID to its sortition state + mapping(uint256 => SortitionState) public sortitions; + + // ====================== + // Constructor + // ====================== + + constructor( + address _bondingRegistry, + address _ciphernodeRegistry, + uint256 _submissionWindow + ) { + bondingRegistry = IBondingRegistry(_bondingRegistry); + ciphernodeRegistry = _ciphernodeRegistry; + submissionWindow = _submissionWindow; + } + + // ====================== + // Main Functions + // ====================== + + /** + * @notice Initialize sortition for an E3 + * @dev Only callable by ciphernode registry when committee is requested + * @param e3Id The E3 identifier + * @param threshold Number of committee members needed + * @param seed Random seed for score computation + * @param requestBlock Block number for snapshot validation + */ + function initializeSortition( + uint256 e3Id, + uint256 threshold, + uint256 seed, + uint256 requestBlock + ) external { + require(msg.sender == ciphernodeRegistry, OnlyCiphernodeRegistry()); + SortitionState storage state = sortitions[e3Id]; + require(state.threshold == 0, CommitteeAlreadyFinalized()); + + state.threshold = threshold; + state.seed = seed; + state.requestBlock = requestBlock; + state.submissionDeadline = block.timestamp + submissionWindow; + state.finalized = false; + } + + /** + * @notice Submit a ticket for sortition + * @dev Nodes call this to submit their best ticket. Score is computed and verified on-chain. + * @param e3Id The E3 identifier + * @param ticketNumber The ticket number to submit (1 to available_tickets at snapshot) + */ + function submitTicket(uint256 e3Id, uint256 ticketNumber) external { + SortitionState storage state = sortitions[e3Id]; + + // Check sortition is initialized + require(state.threshold > 0, CommitteeNotInitialized()); + + // Check submission window is still open + require( + block.timestamp <= state.submissionDeadline, + SubmissionWindowClosed() + ); + + // Check not finalized + require(!state.finalized, CommitteeAlreadyFinalized()); + + // Check node hasn't already submitted + if (state.submissions[msg.sender].exists) revert NodeAlreadySubmitted(); + + // Check node is eligible (has ticket balance at snapshot) + _validateNodeEligibility(msg.sender, ticketNumber, e3Id); + + // Compute score + uint256 score = _computeTicketScore( + msg.sender, + ticketNumber, + e3Id, + state.seed + ); + + // Store submission + state.submissions[msg.sender] = TicketSubmission({ + node: msg.sender, + ticketNumber: ticketNumber, + score: score, + exists: true + }); + + // Try to insert into top N + bool added = _tryInsertIntoTopN(state, msg.sender, score); + + emit TicketSubmitted(e3Id, msg.sender, ticketNumber, score, added); + } + + /** + * @notice Finalize the committee after submission window closes + * @dev Can be called by anyone after the deadline. Sets finalized flag. + * @param e3Id The E3 identifier + * @return committee The final committee addresses + */ + function finalizeCommittee( + uint256 e3Id + ) external returns (address[] memory committee) { + SortitionState storage state = sortitions[e3Id]; + + require(state.threshold > 0, CommitteeNotInitialized()); + require(!state.finalized, CommitteeAlreadyFinalized()); + require( + block.timestamp > state.submissionDeadline, + SubmissionWindowNotClosed() + ); + + state.finalized = true; + committee = state.topNodes; + + emit CommitteeFinalized(e3Id, committee); + } + + // ====================== + // View Functions + // ====================== + + /** + * @notice Get the current top N nodes for an E3 + * @param e3Id The E3 identifier + * @return Array of top N node addresses + */ + function getTopNodes( + uint256 e3Id + ) external view returns (address[] memory) { + return sortitions[e3Id].topNodes; + } + + /** + * @notice Get a node's submission for an E3 + * @param e3Id The E3 identifier + * @param node The node address + * @return The ticket submission + */ + function getSubmission( + uint256 e3Id, + address node + ) external view returns (TicketSubmission memory) { + return sortitions[e3Id].submissions[node]; + } + + /** + * @notice Compute the score for a ticket + * @dev Public function to allow off-chain computation verification + * @param node Node address + * @param ticketNumber Ticket number (1 to N) + * @param e3Id E3 identifier + * @param seed Random seed + * @return The computed score + */ + function computeTicketScore( + address node, + uint256 ticketNumber, + uint256 e3Id, + uint256 seed + ) external pure returns (uint256) { + return _computeTicketScore(node, ticketNumber, e3Id, seed); + } + + /** + * @notice Get sortition information for an E3 + * @param e3Id The E3 identifier + * @return threshold Number of committee members needed + * @return seed Random seed + * @return requestBlock Block number when E3 was requested + * @return submissionDeadline Timestamp when submission window closes + * @return finalized Whether committee has been finalized + */ + function getSortitionInfo( + uint256 e3Id + ) + external + view + returns ( + uint256 threshold, + uint256 seed, + uint256 requestBlock, + uint256 submissionDeadline, + bool finalized + ) + { + SortitionState storage state = sortitions[e3Id]; + return ( + state.threshold, + state.seed, + state.requestBlock, + state.submissionDeadline, + state.finalized + ); + } + + // ====================== + // Internal Functions + // ====================== + + /** + * @notice Computes score = keccak256(node || ticketNumber || e3Id || seed) + */ + function _computeTicketScore( + address node, + uint256 ticketNumber, + uint256 e3Id, + uint256 seed + ) internal pure returns (uint256) { + bytes32 hash = keccak256( + abi.encodePacked(node, ticketNumber, e3Id, seed) + ); + return uint256(hash); + } + + /** + * @notice Validates that a node is eligible to participate + * @dev Uses snapshot of ticket balance at E3 request block for deterministic validation + */ + function _validateNodeEligibility( + address node, + uint256 ticketNumber, + uint256 e3Id + ) internal view { + if (ticketNumber == 0) revert InvalidTicketNumber(); + + SortitionState storage state = sortitions[e3Id]; + + // Get ticket balance at the time E3 was requested (snapshot) + uint256 ticketBalance = bondingRegistry.getTicketBalanceAtBlock( + node, + state.requestBlock + ); + uint256 ticketPrice = bondingRegistry.ticketPrice(); + + if (ticketPrice == 0) revert InvalidTicketNumber(); + + // Calculate available tickets at snapshot + uint256 availableTickets = ticketBalance / ticketPrice; + + // Check ticket number is valid + if (ticketNumber > availableTickets) revert InvalidTicketNumber(); + + // Check node is eligible (has tickets at snapshot) + if (availableTickets == 0) revert NodeNotEligible(); + } + + /** + * @notice Try to insert node into top N sorted list + * @dev Maintains sorted order by score (lowest first) + * @return Whether node was added to top N + */ + function _tryInsertIntoTopN( + SortitionState storage state, + address node, + uint256 score + ) internal returns (bool) { + address[] storage topNodes = state.topNodes; + + // If list not full, insert in sorted order + if (topNodes.length < state.threshold) { + _insertSorted(state, node, score); + return true; + } + + // If list is full, only add if score is better than worst + uint256 worstScore = state + .submissions[topNodes[topNodes.length - 1]] + .score; + if (score < worstScore) { + topNodes.pop(); // Remove worst + _insertSorted(state, node, score); + return true; + } + + return false; + } + + /** + * @notice Insert node into sorted position (ascending by score) + */ + function _insertSorted( + SortitionState storage state, + address node, + uint256 score + ) internal { + address[] storage topNodes = state.topNodes; + + // Find insertion position + uint256 insertPos = topNodes.length; + for (uint256 i = 0; i < topNodes.length; i++) { + uint256 existingScore = state.submissions[topNodes[i]].score; + if (score < existingScore) { + insertPos = i; + break; + } + } + + // Insert at position + topNodes.push(address(0)); // Extend array + for (uint256 i = topNodes.length - 1; i > insertPos; i--) { + topNodes[i] = topNodes[i - 1]; + } + topNodes[insertPos] = node; + } +} diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index 2d3c057518..3d02a7ddf6 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -9,6 +9,7 @@ import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { function requestCommittee( + uint256, uint256, uint32[2] calldata ) external pure returns (bool success) { @@ -76,6 +77,7 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function requestCommittee( + uint256, uint256, uint32[2] calldata ) external pure returns (bool success) { diff --git a/packages/enclave-contracts/test/CommitteeSortition.spec.ts b/packages/enclave-contracts/test/CommitteeSortition.spec.ts index 51ee1a8f59..46f123c3f2 100644 --- a/packages/enclave-contracts/test/CommitteeSortition.spec.ts +++ b/packages/enclave-contracts/test/CommitteeSortition.spec.ts @@ -1,542 +1,542 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -import { expect } from "chai"; -import { network } from "hardhat"; - -import BondingRegistryModule from "../ignition/modules/bondingRegistry"; -import CommitteeSortitionModule from "../ignition/modules/committeeSortition"; -import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; -import EnclaveTokenModule from "../ignition/modules/enclaveToken"; -import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; -import MockStableTokenModule from "../ignition/modules/mockStableToken"; -import SlashingManagerModule from "../ignition/modules/slashingManager"; -import { - BondingRegistry__factory as BondingRegistryFactory, - CommitteeSortition__factory as CommitteeSortitionFactory, - EnclaveTicketToken__factory as EnclaveTicketTokenFactory, - MockUSDC__factory as MockUSDCFactory, -} from "../types"; - -const { ethers, networkHelpers, ignition } = await network.connect(); -const { loadFixture } = networkHelpers; - -describe("CommitteeSortition", function () { - const SUBMISSION_WINDOW = 300; // 5 minutes - const TICKET_PRICE = ethers.parseEther("10"); - const E3_ID = 1; - const THRESHOLD = 3; - const SEED = 12345; - const AddressOne = "0x0000000000000000000000000000000000000001"; - - async function deployFixture() { - const [owner, ciphernodeRegistry, node1, node2, node3, node4] = - await ethers.getSigners(); - - const ownerAddress = await owner.getAddress(); - - // Deploy token contracts - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply: 1000000, - }, - }, - }); - - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner: ownerAddress, - }, - }, - }); - - const ticketTokenContract = await ignition.deploy( - EnclaveTicketTokenModule, - { - parameters: { - EnclaveTicketToken: { - baseToken: await usdcContract.mockUSDC.getAddress(), - registry: AddressOne, - owner: ownerAddress, - }, - }, - }, - ); - - const slashingManagerContract = await ignition.deploy( - SlashingManagerModule, - { - parameters: { - SlashingManager: { - admin: ownerAddress, - bondingRegistry: AddressOne, - }, - }, - }, - ); - - const bondingRegistryContract = await ignition.deploy( - BondingRegistryModule, - { - parameters: { - BondingRegistry: { - owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), - licenseToken: await enclTokenContract.enclaveToken.getAddress(), - registry: AddressOne, - slashedFundsTreasury: ownerAddress, - ticketPrice: TICKET_PRICE, - licenseRequiredBond: ethers.parseEther("1000"), - minTicketBalance: 1, - exitDelay: 7 * 24 * 60 * 60, - }, - }, - }, - ); - - const committeeSortitionContract = await ignition.deploy( - CommitteeSortitionModule, - { - parameters: { - CommitteeSortition: { - bondingRegistry: - await bondingRegistryContract.bondingRegistry.getAddress(), - ciphernodeRegistry: ciphernodeRegistry.address, - submissionWindow: SUBMISSION_WINDOW, - }, - }, - }, - ); - - const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), - owner, - ); - const committeeSortition = CommitteeSortitionFactory.connect( - await committeeSortitionContract.committeeSortition.getAddress(), - owner, - ); - - const usdcToken = MockUSDCFactory.connect( - await usdcContract.mockUSDC.getAddress(), - owner, - ); - const ticketToken = EnclaveTicketTokenFactory.connect( - await ticketTokenContract.enclaveTicketToken.getAddress(), - owner, - ); - - // Deploy a mock ciphernode registry for testing - const mockRegistry = await ignition.deploy( - MockCiphernodeRegistryEmptyKeyModule, - ); - - // Set up cross-contract dependencies - await ticketToken.setRegistry(await bondingRegistry.getAddress()); - await bondingRegistry.setRegistry( - await mockRegistry.mockCiphernodeRegistryEmptyKey.getAddress(), - ); - await bondingRegistry.setSlashingManager( - await slashingManagerContract.slashingManager.getAddress(), - ); - await slashingManagerContract.slashingManager.setBondingRegistry( - await bondingRegistry.getAddress(), - ); - - // Set up licensed operators with ticket balances - const licenseToken = EnclaveTicketTokenFactory.connect( - await enclTokenContract.enclaveToken.getAddress(), - owner, - ); - const licenseAmount = ethers.parseEther("1000"); // Min license bond - - // Whitelist bonding registry for license token transfers - await enclTokenContract.enclaveToken.whitelistContracts( - await bondingRegistry.getAddress(), - ethers.ZeroAddress, - ); - - for (const node of [node1, node2, node3, node4]) { - const nodeTickets = - node === node1 ? 5 : node === node2 ? 3 : node === node3 ? 7 : 2; - const ticketAmount = TICKET_PRICE * BigInt(nodeTickets); - - // Bond license first - await enclTokenContract.enclaveToken.mintAllocation( - node.address, - licenseAmount, - "Test allocation", - ); - await licenseToken - .connect(node) - .approve(await bondingRegistry.getAddress(), licenseAmount); - await bondingRegistry.connect(node).bondLicense(licenseAmount); - - // Then register operator - await bondingRegistry.connect(node).registerOperator(); - - // Mint USDC to node and have them add ticket balance through bonding registry - await usdcToken.mint(node.address, ticketAmount); - - // Node approves ticket token to spend USDC (needed for depositFrom) - await usdcToken - .connect(node) - .approve(await ticketToken.getAddress(), ticketAmount); - - // Node adds ticket balance (this will call ticketToken.depositFrom internally) - await bondingRegistry.connect(node).addTicketBalance(ticketAmount); - } - - return { - committeeSortition, - bondingRegistry, - owner, - ciphernodeRegistry, - node1, - node2, - node3, - node4, - }; - } - - describe("Initialization", function () { - it("Should initialize sortition correctly", async function () { - const { committeeSortition, ciphernodeRegistry } = - await loadFixture(deployFixture); - const requestBlock = await ethers.provider.getBlockNumber(); - - await committeeSortition - .connect(ciphernodeRegistry) - .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - - const [threshold, seed, reqBlock, deadline, finalized] = - await committeeSortition.getSortitionInfo(E3_ID); - - expect(threshold).to.equal(THRESHOLD); - expect(seed).to.equal(SEED); - expect(reqBlock).to.equal(requestBlock); - expect(finalized).to.be.false; - expect(deadline).to.be.gt(0); - }); - - it("Should revert if not called by ciphernode registry", async function () { - const { committeeSortition, owner } = await loadFixture(deployFixture); - const requestBlock = await ethers.provider.getBlockNumber(); - - await expect( - committeeSortition - .connect(owner) - .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), - ).to.be.revertedWithCustomError( - committeeSortition, - "OnlyCiphernodeRegistry", - ); - }); - - it("Should revert if already initialized", async function () { - const { committeeSortition, ciphernodeRegistry } = - await loadFixture(deployFixture); - const requestBlock = await ethers.provider.getBlockNumber(); - - await committeeSortition - .connect(ciphernodeRegistry) - .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - - await expect( - committeeSortition - .connect(ciphernodeRegistry) - .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), - ).to.be.revertedWithCustomError( - committeeSortition, - "CommitteeAlreadyFinalized", - ); - }); - }); - - describe("Ticket Submission", function () { - async function initializeFixture() { - const fixture = await deployFixture(); - const requestBlock = await ethers.provider.getBlockNumber(); - await fixture.committeeSortition - .connect(fixture.ciphernodeRegistry) - .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - return { ...fixture, requestBlock }; - } - - it("Should submit ticket successfully", async function () { - const { committeeSortition, node1 } = - await loadFixture(initializeFixture); - const ticketNumber = 1; - - const tx = await committeeSortition - .connect(node1) - .submitTicket(E3_ID, ticketNumber); - await expect(tx).to.emit(committeeSortition, "TicketSubmitted"); - - const submission = await committeeSortition.getSubmission( - E3_ID, - node1.address, - ); - expect(submission.exists).to.be.true; - expect(submission.ticketNumber).to.equal(ticketNumber); - }); - - it("Should track top N nodes correctly", async function () { - const { committeeSortition, node1, node2, node3, node4 } = - await loadFixture(initializeFixture); - // Submit tickets from multiple nodes - await committeeSortition.connect(node1).submitTicket(E3_ID, 1); - await committeeSortition.connect(node2).submitTicket(E3_ID, 1); - await committeeSortition.connect(node3).submitTicket(E3_ID, 1); - await committeeSortition.connect(node4).submitTicket(E3_ID, 1); - - const topNodes = await committeeSortition.getTopNodes(E3_ID); - expect(topNodes.length).to.equal(THRESHOLD); - }); - - it("Should revert if ticket number is 0", async function () { - const { committeeSortition, node1 } = - await loadFixture(initializeFixture); - await expect( - committeeSortition.connect(node1).submitTicket(E3_ID, 0), - ).to.be.revertedWithCustomError( - committeeSortition, - "InvalidTicketNumber", - ); - }); - - it("Should revert if ticket number exceeds available tickets", async function () { - const { committeeSortition, node1 } = - await loadFixture(initializeFixture); - await expect( - committeeSortition.connect(node1).submitTicket(E3_ID, 100), - ).to.be.revertedWithCustomError( - committeeSortition, - "InvalidTicketNumber", - ); - }); - - it("Should revert if node already submitted", async function () { - const { committeeSortition, node1 } = - await loadFixture(initializeFixture); - await committeeSortition.connect(node1).submitTicket(E3_ID, 1); - - await expect( - committeeSortition.connect(node1).submitTicket(E3_ID, 2), - ).to.be.revertedWithCustomError( - committeeSortition, - "NodeAlreadySubmitted", - ); - }); - - it("Should revert if node has no tickets", async function () { - const { committeeSortition } = await loadFixture(initializeFixture); - // Create a completely fresh wallet - const nodeWithNoTickets = ethers.Wallet.createRandom().connect( - ethers.provider, - ); - - // Fund it with ETH for gas but don't set ticket balance - const [funder] = await ethers.getSigners(); - await funder.sendTransaction({ - to: nodeWithNoTickets.address, - value: ethers.parseEther("1"), - }); - - // When a node has 0 tickets and tries to submit ticket 1, - // it will revert with InvalidTicketNumber (since 1 > 0 available tickets) - await expect( - committeeSortition.connect(nodeWithNoTickets).submitTicket(E3_ID, 1), - ).to.be.revertedWithCustomError( - committeeSortition, - "InvalidTicketNumber", - ); - }); - - it("Should revert if submission window closed", async function () { - const { committeeSortition, node1 } = - await loadFixture(initializeFixture); - // Fast forward time beyond submission window - await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); - await ethers.provider.send("evm_mine", []); - - await expect( - committeeSortition.connect(node1).submitTicket(E3_ID, 1), - ).to.be.revertedWithCustomError( - committeeSortition, - "SubmissionWindowClosed", - ); - }); - - it("Should compute scores correctly", async function () { - const { committeeSortition, node1 } = - await loadFixture(initializeFixture); - const ticketNumber = 1; - - // Compute expected score off-chain - const expectedScore = await committeeSortition.computeTicketScore( - node1.address, - ticketNumber, - E3_ID, - SEED, - ); - - await committeeSortition.connect(node1).submitTicket(E3_ID, ticketNumber); - - const submission = await committeeSortition.getSubmission( - E3_ID, - node1.address, - ); - expect(submission.score).to.equal(expectedScore); - }); - }); - - describe("Committee Finalization", function () { - async function finalizeFixture() { - const fixture = await deployFixture(); - const requestBlock = await ethers.provider.getBlockNumber(); - await fixture.committeeSortition - .connect(fixture.ciphernodeRegistry) - .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - - // Submit tickets from nodes - await fixture.committeeSortition - .connect(fixture.node1) - .submitTicket(E3_ID, 1); - await fixture.committeeSortition - .connect(fixture.node2) - .submitTicket(E3_ID, 1); - await fixture.committeeSortition - .connect(fixture.node3) - .submitTicket(E3_ID, 1); - return { ...fixture, requestBlock }; - } - - it("Should finalize committee after deadline", async function () { - const { committeeSortition, owner } = await loadFixture(finalizeFixture); - // Fast forward time - await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); - await ethers.provider.send("evm_mine", []); - - const tx = await committeeSortition - .connect(owner) - .finalizeCommittee(E3_ID); - await expect(tx).to.emit(committeeSortition, "CommitteeFinalized"); - - const [, , , , finalized] = - await committeeSortition.getSortitionInfo(E3_ID); - expect(finalized).to.be.true; - }); - - it("Should revert if finalized before deadline", async function () { - const { committeeSortition, owner } = await loadFixture(finalizeFixture); - await expect( - committeeSortition.connect(owner).finalizeCommittee(E3_ID), - ).to.be.revertedWithCustomError( - committeeSortition, - "SubmissionWindowNotClosed", - ); - }); - - it("Should revert if already finalized", async function () { - const { committeeSortition, owner } = await loadFixture(finalizeFixture); - // Fast forward time - await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); - await ethers.provider.send("evm_mine", []); - - await committeeSortition.connect(owner).finalizeCommittee(E3_ID); - - await expect( - committeeSortition.connect(owner).finalizeCommittee(E3_ID), - ).to.be.revertedWithCustomError( - committeeSortition, - "CommitteeAlreadyFinalized", - ); - }); - - it("Should return correct committee", async function () { - const { committeeSortition, owner } = await loadFixture(finalizeFixture); - // Fast forward time - await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); - await ethers.provider.send("evm_mine", []); - - const committee = await committeeSortition - .connect(owner) - .finalizeCommittee.staticCall(E3_ID); - - expect(committee.length).to.equal(THRESHOLD); - }); - - it("Should prevent submissions after finalization", async function () { - const { committeeSortition, owner, node4 } = - await loadFixture(finalizeFixture); - // Fast forward time - await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); - await ethers.provider.send("evm_mine", []); - - await committeeSortition.connect(owner).finalizeCommittee(E3_ID); - - // Try to submit - should fail because submission window is closed - // Note: The contract checks submission window before checking if finalized - await expect( - committeeSortition.connect(node4).submitTicket(E3_ID, 1), - ).to.be.revertedWithCustomError( - committeeSortition, - "SubmissionWindowClosed", - ); - }); - }); - - describe("Score Sorting", function () { - async function scoreSortingFixture() { - const fixture = await deployFixture(); - const requestBlock = await ethers.provider.getBlockNumber(); - await fixture.committeeSortition - .connect(fixture.ciphernodeRegistry) - .initializeSortition(E3_ID, 2, SEED, requestBlock); // Threshold of 2 - return { ...fixture, requestBlock }; - } - - it("Should maintain sorted order (lowest scores first)", async function () { - const { committeeSortition, node1, node2, node3 } = - await loadFixture(scoreSortingFixture); - // Submit tickets - await committeeSortition.connect(node1).submitTicket(E3_ID, 1); - await committeeSortition.connect(node2).submitTicket(E3_ID, 1); - await committeeSortition.connect(node3).submitTicket(E3_ID, 1); - - const topNodes = await committeeSortition.getTopNodes(E3_ID); - expect(topNodes.length).to.equal(2); - - // Verify scores are in ascending order - const score1 = ( - await committeeSortition.getSubmission(E3_ID, topNodes[0]) - ).score; - const score2 = ( - await committeeSortition.getSubmission(E3_ID, topNodes[1]) - ).score; - - expect(score1).to.be.lte(score2); - }); - - it("Should replace worst node when better score arrives", async function () { - const { committeeSortition, node1, node2, node3 } = - await loadFixture(scoreSortingFixture); - await committeeSortition.connect(node1).submitTicket(E3_ID, 1); - await committeeSortition.connect(node2).submitTicket(E3_ID, 1); - - // const topNodesBefore = await committeeSortition.getTopNodes(E3_ID); - - // Submit from node3 - should replace worst if score is better - await committeeSortition.connect(node3).submitTicket(E3_ID, 1); - - const topNodesAfter = await committeeSortition.getTopNodes(E3_ID); - expect(topNodesAfter.length).to.equal(2); - }); - }); -}); +// // SPDX-License-Identifier: LGPL-3.0-only +// // +// // This file is provided WITHOUT ANY WARRANTY; +// // without even the implied warranty of MERCHANTABILITY +// // or FITNESS FOR A PARTICULAR PURPOSE. +// import { expect } from "chai"; +// import { network } from "hardhat"; + +// import BondingRegistryModule from "../ignition/modules/bondingRegistry"; +// import CommitteeSortitionModule from "../ignition/modules/committeeSortition"; +// import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; +// import EnclaveTokenModule from "../ignition/modules/enclaveToken"; +// import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; +// import MockStableTokenModule from "../ignition/modules/mockStableToken"; +// import SlashingManagerModule from "../ignition/modules/slashingManager"; +// import { +// BondingRegistry__factory as BondingRegistryFactory, +// CommitteeSortition__factory as CommitteeSortitionFactory, +// EnclaveTicketToken__factory as EnclaveTicketTokenFactory, +// MockUSDC__factory as MockUSDCFactory, +// } from "../types"; + +// const { ethers, networkHelpers, ignition } = await network.connect(); +// const { loadFixture } = networkHelpers; + +// describe("CommitteeSortition", function () { +// const SUBMISSION_WINDOW = 300; // 5 minutes +// const TICKET_PRICE = ethers.parseEther("10"); +// const E3_ID = 1; +// const THRESHOLD = 3; +// const SEED = 12345; +// const AddressOne = "0x0000000000000000000000000000000000000001"; + +// async function deployFixture() { +// const [owner, ciphernodeRegistry, node1, node2, node3, node4] = +// await ethers.getSigners(); + +// const ownerAddress = await owner.getAddress(); + +// // Deploy token contracts +// const usdcContract = await ignition.deploy(MockStableTokenModule, { +// parameters: { +// MockUSDC: { +// initialSupply: 1000000, +// }, +// }, +// }); + +// const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { +// parameters: { +// EnclaveToken: { +// owner: ownerAddress, +// }, +// }, +// }); + +// const ticketTokenContract = await ignition.deploy( +// EnclaveTicketTokenModule, +// { +// parameters: { +// EnclaveTicketToken: { +// baseToken: await usdcContract.mockUSDC.getAddress(), +// registry: AddressOne, +// owner: ownerAddress, +// }, +// }, +// }, +// ); + +// const slashingManagerContract = await ignition.deploy( +// SlashingManagerModule, +// { +// parameters: { +// SlashingManager: { +// admin: ownerAddress, +// bondingRegistry: AddressOne, +// }, +// }, +// }, +// ); + +// const bondingRegistryContract = await ignition.deploy( +// BondingRegistryModule, +// { +// parameters: { +// BondingRegistry: { +// owner: ownerAddress, +// ticketToken: +// await ticketTokenContract.enclaveTicketToken.getAddress(), +// licenseToken: await enclTokenContract.enclaveToken.getAddress(), +// registry: AddressOne, +// slashedFundsTreasury: ownerAddress, +// ticketPrice: TICKET_PRICE, +// licenseRequiredBond: ethers.parseEther("1000"), +// minTicketBalance: 1, +// exitDelay: 7 * 24 * 60 * 60, +// }, +// }, +// }, +// ); + +// const committeeSortitionContract = await ignition.deploy( +// CommitteeSortitionModule, +// { +// parameters: { +// CommitteeSortition: { +// bondingRegistry: +// await bondingRegistryContract.bondingRegistry.getAddress(), +// ciphernodeRegistry: ciphernodeRegistry.address, +// submissionWindow: SUBMISSION_WINDOW, +// }, +// }, +// }, +// ); + +// const bondingRegistry = BondingRegistryFactory.connect( +// await bondingRegistryContract.bondingRegistry.getAddress(), +// owner, +// ); +// const committeeSortition = CommitteeSortitionFactory.connect( +// await committeeSortitionContract.committeeSortition.getAddress(), +// owner, +// ); + +// const usdcToken = MockUSDCFactory.connect( +// await usdcContract.mockUSDC.getAddress(), +// owner, +// ); +// const ticketToken = EnclaveTicketTokenFactory.connect( +// await ticketTokenContract.enclaveTicketToken.getAddress(), +// owner, +// ); + +// // Deploy a mock ciphernode registry for testing +// const mockRegistry = await ignition.deploy( +// MockCiphernodeRegistryEmptyKeyModule, +// ); + +// // Set up cross-contract dependencies +// await ticketToken.setRegistry(await bondingRegistry.getAddress()); +// await bondingRegistry.setRegistry( +// await mockRegistry.mockCiphernodeRegistryEmptyKey.getAddress(), +// ); +// await bondingRegistry.setSlashingManager( +// await slashingManagerContract.slashingManager.getAddress(), +// ); +// await slashingManagerContract.slashingManager.setBondingRegistry( +// await bondingRegistry.getAddress(), +// ); + +// // Set up licensed operators with ticket balances +// const licenseToken = EnclaveTicketTokenFactory.connect( +// await enclTokenContract.enclaveToken.getAddress(), +// owner, +// ); +// const licenseAmount = ethers.parseEther("1000"); // Min license bond + +// // Whitelist bonding registry for license token transfers +// await enclTokenContract.enclaveToken.whitelistContracts( +// await bondingRegistry.getAddress(), +// ethers.ZeroAddress, +// ); + +// for (const node of [node1, node2, node3, node4]) { +// const nodeTickets = +// node === node1 ? 5 : node === node2 ? 3 : node === node3 ? 7 : 2; +// const ticketAmount = TICKET_PRICE * BigInt(nodeTickets); + +// // Bond license first +// await enclTokenContract.enclaveToken.mintAllocation( +// node.address, +// licenseAmount, +// "Test allocation", +// ); +// await licenseToken +// .connect(node) +// .approve(await bondingRegistry.getAddress(), licenseAmount); +// await bondingRegistry.connect(node).bondLicense(licenseAmount); + +// // Then register operator +// await bondingRegistry.connect(node).registerOperator(); + +// // Mint USDC to node and have them add ticket balance through bonding registry +// await usdcToken.mint(node.address, ticketAmount); + +// // Node approves ticket token to spend USDC (needed for depositFrom) +// await usdcToken +// .connect(node) +// .approve(await ticketToken.getAddress(), ticketAmount); + +// // Node adds ticket balance (this will call ticketToken.depositFrom internally) +// await bondingRegistry.connect(node).addTicketBalance(ticketAmount); +// } + +// return { +// committeeSortition, +// bondingRegistry, +// owner, +// ciphernodeRegistry, +// node1, +// node2, +// node3, +// node4, +// }; +// } + +// describe("Initialization", function () { +// it("Should initialize sortition correctly", async function () { +// const { committeeSortition, ciphernodeRegistry } = +// await loadFixture(deployFixture); +// const requestBlock = await ethers.provider.getBlockNumber(); + +// await committeeSortition +// .connect(ciphernodeRegistry) +// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + +// const [threshold, seed, reqBlock, deadline, finalized] = +// await committeeSortition.getSortitionInfo(E3_ID); + +// expect(threshold).to.equal(THRESHOLD); +// expect(seed).to.equal(SEED); +// expect(reqBlock).to.equal(requestBlock); +// expect(finalized).to.be.false; +// expect(deadline).to.be.gt(0); +// }); + +// it("Should revert if not called by ciphernode registry", async function () { +// const { committeeSortition, owner } = await loadFixture(deployFixture); +// const requestBlock = await ethers.provider.getBlockNumber(); + +// await expect( +// committeeSortition +// .connect(owner) +// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "OnlyCiphernodeRegistry", +// ); +// }); + +// it("Should revert if already initialized", async function () { +// const { committeeSortition, ciphernodeRegistry } = +// await loadFixture(deployFixture); +// const requestBlock = await ethers.provider.getBlockNumber(); + +// await committeeSortition +// .connect(ciphernodeRegistry) +// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + +// await expect( +// committeeSortition +// .connect(ciphernodeRegistry) +// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "CommitteeAlreadyFinalized", +// ); +// }); +// }); + +// describe("Ticket Submission", function () { +// async function initializeFixture() { +// const fixture = await deployFixture(); +// const requestBlock = await ethers.provider.getBlockNumber(); +// await fixture.committeeSortition +// .connect(fixture.ciphernodeRegistry) +// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); +// return { ...fixture, requestBlock }; +// } + +// it("Should submit ticket successfully", async function () { +// const { committeeSortition, node1 } = +// await loadFixture(initializeFixture); +// const ticketNumber = 1; + +// const tx = await committeeSortition +// .connect(node1) +// .submitTicket(E3_ID, ticketNumber); +// await expect(tx).to.emit(committeeSortition, "TicketSubmitted"); + +// const submission = await committeeSortition.getSubmission( +// E3_ID, +// node1.address, +// ); +// expect(submission.exists).to.be.true; +// expect(submission.ticketNumber).to.equal(ticketNumber); +// }); + +// it("Should track top N nodes correctly", async function () { +// const { committeeSortition, node1, node2, node3, node4 } = +// await loadFixture(initializeFixture); +// // Submit tickets from multiple nodes +// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); +// await committeeSortition.connect(node2).submitTicket(E3_ID, 1); +// await committeeSortition.connect(node3).submitTicket(E3_ID, 1); +// await committeeSortition.connect(node4).submitTicket(E3_ID, 1); + +// const topNodes = await committeeSortition.getTopNodes(E3_ID); +// expect(topNodes.length).to.equal(THRESHOLD); +// }); + +// it("Should revert if ticket number is 0", async function () { +// const { committeeSortition, node1 } = +// await loadFixture(initializeFixture); +// await expect( +// committeeSortition.connect(node1).submitTicket(E3_ID, 0), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "InvalidTicketNumber", +// ); +// }); + +// it("Should revert if ticket number exceeds available tickets", async function () { +// const { committeeSortition, node1 } = +// await loadFixture(initializeFixture); +// await expect( +// committeeSortition.connect(node1).submitTicket(E3_ID, 100), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "InvalidTicketNumber", +// ); +// }); + +// it("Should revert if node already submitted", async function () { +// const { committeeSortition, node1 } = +// await loadFixture(initializeFixture); +// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); + +// await expect( +// committeeSortition.connect(node1).submitTicket(E3_ID, 2), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "NodeAlreadySubmitted", +// ); +// }); + +// it("Should revert if node has no tickets", async function () { +// const { committeeSortition } = await loadFixture(initializeFixture); +// // Create a completely fresh wallet +// const nodeWithNoTickets = ethers.Wallet.createRandom().connect( +// ethers.provider, +// ); + +// // Fund it with ETH for gas but don't set ticket balance +// const [funder] = await ethers.getSigners(); +// await funder.sendTransaction({ +// to: nodeWithNoTickets.address, +// value: ethers.parseEther("1"), +// }); + +// // When a node has 0 tickets and tries to submit ticket 1, +// // it will revert with InvalidTicketNumber (since 1 > 0 available tickets) +// await expect( +// committeeSortition.connect(nodeWithNoTickets).submitTicket(E3_ID, 1), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "InvalidTicketNumber", +// ); +// }); + +// it("Should revert if submission window closed", async function () { +// const { committeeSortition, node1 } = +// await loadFixture(initializeFixture); +// // Fast forward time beyond submission window +// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); +// await ethers.provider.send("evm_mine", []); + +// await expect( +// committeeSortition.connect(node1).submitTicket(E3_ID, 1), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "SubmissionWindowClosed", +// ); +// }); + +// it("Should compute scores correctly", async function () { +// const { committeeSortition, node1 } = +// await loadFixture(initializeFixture); +// const ticketNumber = 1; + +// // Compute expected score off-chain +// const expectedScore = await committeeSortition.computeTicketScore( +// node1.address, +// ticketNumber, +// E3_ID, +// SEED, +// ); + +// await committeeSortition.connect(node1).submitTicket(E3_ID, ticketNumber); + +// const submission = await committeeSortition.getSubmission( +// E3_ID, +// node1.address, +// ); +// expect(submission.score).to.equal(expectedScore); +// }); +// }); + +// describe("Committee Finalization", function () { +// async function finalizeFixture() { +// const fixture = await deployFixture(); +// const requestBlock = await ethers.provider.getBlockNumber(); +// await fixture.committeeSortition +// .connect(fixture.ciphernodeRegistry) +// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); + +// // Submit tickets from nodes +// await fixture.committeeSortition +// .connect(fixture.node1) +// .submitTicket(E3_ID, 1); +// await fixture.committeeSortition +// .connect(fixture.node2) +// .submitTicket(E3_ID, 1); +// await fixture.committeeSortition +// .connect(fixture.node3) +// .submitTicket(E3_ID, 1); +// return { ...fixture, requestBlock }; +// } + +// it("Should finalize committee after deadline", async function () { +// const { committeeSortition, owner } = await loadFixture(finalizeFixture); +// // Fast forward time +// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); +// await ethers.provider.send("evm_mine", []); + +// const tx = await committeeSortition +// .connect(owner) +// .finalizeCommittee(E3_ID); +// await expect(tx).to.emit(committeeSortition, "CommitteeFinalized"); + +// const [, , , , finalized] = +// await committeeSortition.getSortitionInfo(E3_ID); +// expect(finalized).to.be.true; +// }); + +// it("Should revert if finalized before deadline", async function () { +// const { committeeSortition, owner } = await loadFixture(finalizeFixture); +// await expect( +// committeeSortition.connect(owner).finalizeCommittee(E3_ID), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "SubmissionWindowNotClosed", +// ); +// }); + +// it("Should revert if already finalized", async function () { +// const { committeeSortition, owner } = await loadFixture(finalizeFixture); +// // Fast forward time +// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); +// await ethers.provider.send("evm_mine", []); + +// await committeeSortition.connect(owner).finalizeCommittee(E3_ID); + +// await expect( +// committeeSortition.connect(owner).finalizeCommittee(E3_ID), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "CommitteeAlreadyFinalized", +// ); +// }); + +// it("Should return correct committee", async function () { +// const { committeeSortition, owner } = await loadFixture(finalizeFixture); +// // Fast forward time +// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); +// await ethers.provider.send("evm_mine", []); + +// const committee = await committeeSortition +// .connect(owner) +// .finalizeCommittee.staticCall(E3_ID); + +// expect(committee.length).to.equal(THRESHOLD); +// }); + +// it("Should prevent submissions after finalization", async function () { +// const { committeeSortition, owner, node4 } = +// await loadFixture(finalizeFixture); +// // Fast forward time +// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); +// await ethers.provider.send("evm_mine", []); + +// await committeeSortition.connect(owner).finalizeCommittee(E3_ID); + +// // Try to submit - should fail because submission window is closed +// // Note: The contract checks submission window before checking if finalized +// await expect( +// committeeSortition.connect(node4).submitTicket(E3_ID, 1), +// ).to.be.revertedWithCustomError( +// committeeSortition, +// "SubmissionWindowClosed", +// ); +// }); +// }); + +// describe("Score Sorting", function () { +// async function scoreSortingFixture() { +// const fixture = await deployFixture(); +// const requestBlock = await ethers.provider.getBlockNumber(); +// await fixture.committeeSortition +// .connect(fixture.ciphernodeRegistry) +// .initializeSortition(E3_ID, 2, SEED, requestBlock); // Threshold of 2 +// return { ...fixture, requestBlock }; +// } + +// it("Should maintain sorted order (lowest scores first)", async function () { +// const { committeeSortition, node1, node2, node3 } = +// await loadFixture(scoreSortingFixture); +// // Submit tickets +// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); +// await committeeSortition.connect(node2).submitTicket(E3_ID, 1); +// await committeeSortition.connect(node3).submitTicket(E3_ID, 1); + +// const topNodes = await committeeSortition.getTopNodes(E3_ID); +// expect(topNodes.length).to.equal(2); + +// // Verify scores are in ascending order +// const score1 = ( +// await committeeSortition.getSubmission(E3_ID, topNodes[0]) +// ).score; +// const score2 = ( +// await committeeSortition.getSubmission(E3_ID, topNodes[1]) +// ).score; + +// expect(score1).to.be.lte(score2); +// }); + +// it("Should replace worst node when better score arrives", async function () { +// const { committeeSortition, node1, node2, node3 } = +// await loadFixture(scoreSortingFixture); +// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); +// await committeeSortition.connect(node2).submitTicket(E3_ID, 1); + +// // const topNodesBefore = await committeeSortition.getTopNodes(E3_ID); + +// // Submit from node3 - should replace worst if score is better +// await committeeSortition.connect(node3).submitTicket(E3_ID, 1); + +// const topNodesAfter = await committeeSortition.getTopNodes(E3_ID); +// expect(topNodesAfter.length).to.equal(2); +// }); +// }); +// }); diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index b32d91486c..750d8ee7a4 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -182,29 +182,33 @@ describe("CiphernodeRegistryOwnable", function () { describe("requestCommittee()", function () { it("reverts if committee has already been requested for given e3Id", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); await expect( - registry.requestCommittee(request.e3Id, request.threshold), + registry.requestCommittee(request.e3Id, 0, request.threshold), ).to.be.revertedWithCustomError(registry, "CommitteeAlreadyRequested"); }); it("stores the root of the ciphernode registry at the time of the request", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); expect(await registry.rootAt(request.e3Id)).to.equal( await registry.root(), ); }); it("emits a CommitteeRequested event", async function () { const { registry, request } = await loadFixture(setup); - await expect(registry.requestCommittee(request.e3Id, request.threshold)) + const blockNumber = (await ethers.provider.getBlockNumber()) + 1; + await expect( + registry.requestCommittee(request.e3Id, 0, request.threshold), + ) .to.emit(registry, "CommitteeRequested") - .withArgs(request.e3Id, request.threshold); + .withArgs(request.e3Id, 0, request.threshold, blockNumber); }); it("returns true if the request is successful", async function () { const { registry, request } = await loadFixture(setup); expect( await registry.requestCommittee.staticCall( request.e3Id, + 0, request.threshold, ), ).to.be.true; @@ -214,7 +218,7 @@ describe("CiphernodeRegistryOwnable", function () { describe("publishCommittee()", function () { it("reverts if the caller is not the owner", async function () { const { registry, request, notTheOwner } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); await expect( registry @@ -224,7 +228,7 @@ describe("CiphernodeRegistryOwnable", function () { }); it("stores the public key of the committee", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); await registry.publishCommittee( request.e3Id, [AddressOne, AddressTwo], @@ -236,7 +240,7 @@ describe("CiphernodeRegistryOwnable", function () { }); it("emits a CommitteePublished event", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); await expect( await registry.publishCommittee( request.e3Id, @@ -349,7 +353,7 @@ describe("CiphernodeRegistryOwnable", function () { describe("committeePublicKey()", function () { it("returns the public key of the committee for the given e3Id", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); await registry.publishCommittee( request.e3Id, [AddressOne, AddressTwo], @@ -361,7 +365,7 @@ describe("CiphernodeRegistryOwnable", function () { }); it("reverts if the committee has not been published", async function () { const { registry, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); await expect( registry.committeePublicKey(request.e3Id), ).to.be.revertedWithCustomError(registry, "CommitteeNotPublished"); @@ -400,7 +404,7 @@ describe("CiphernodeRegistryOwnable", function () { describe("rootAt()", function () { it("returns the root of the ciphernode registry merkle tree at the given e3Id", async function () { const { registry, tree, request } = await loadFixture(setup); - await registry.requestCommittee(request.e3Id, request.threshold); + await registry.requestCommittee(request.e3Id, 0, request.threshold); expect(await registry.rootAt(request.e3Id)).to.equal(tree.root); }); }); From 8f41d253fb1ae3bad5648e42edacaf1d0938461e Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sun, 26 Oct 2025 21:42:42 +0500 Subject: [PATCH 45/88] chore: merge sortition to ciphernode contract --- packages/enclave-contracts/.gitignore | 2 - .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 109 +++- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../CommitteeSortition.json | 353 ------------ .../enclave-contracts/contracts/Enclave.sol | 39 +- .../interfaces/ICiphernodeRegistry.sol | 35 +- .../contracts/interfaces/IEnclave.sol | 11 - .../registry/CiphernodeRegistryOwnable.sol | 273 +++++++-- .../sortition/CommitteeSortition.sol | 216 ------- .../contracts/sortition/OldCS.sol | 392 ------------- .../contracts/test/MockCiphernodeRegistry.sol | 40 +- .../ignition/modules/ciphernodeRegistry.ts | 3 +- .../ignition/modules/committeeSortition.ts | 24 - .../ciphernodeRegistryOwnable.ts | 14 +- .../deployAndSave/committeeSortition.ts | 99 ---- .../scripts/deployEnclave.ts | 16 +- .../test/CommitteeSortition.spec.ts | 542 ------------------ .../enclave-contracts/test/Enclave.spec.ts | 18 +- .../CiphernodeRegistryOwnable.spec.ts | 52 +- 20 files changed, 436 insertions(+), 1806 deletions(-) delete mode 100644 packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json delete mode 100644 packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol delete mode 100644 packages/enclave-contracts/contracts/sortition/OldCS.sol delete mode 100644 packages/enclave-contracts/ignition/modules/committeeSortition.ts delete mode 100644 packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts delete mode 100644 packages/enclave-contracts/test/CommitteeSortition.spec.ts diff --git a/packages/enclave-contracts/.gitignore b/packages/enclave-contracts/.gitignore index bc722a45a3..d778ae5d7a 100644 --- a/packages/enclave-contracts/.gitignore +++ b/packages/enclave-contracts/.gitignore @@ -8,14 +8,12 @@ !/artifacts/contracts/ !/artifacts/contracts/interfaces/ !/artifacts/contracts/registry/ -!/artifacts/contracts/sortition/ !/artifacts/contracts/interfaces/IEnclave.sol/ !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ !/artifacts/contracts/interfaces/IBondingRegistry.sol/ !/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json !/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json -!/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json build cache coverage diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index 17ba2a6b65..a662c79788 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-332be8f9366934dd9b42983a9b2838dffc905ed0" + "buildInfoId": "solc-0_8_28-a254ab6ed640b952e36a352bed155c87f85018f2" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 11ce641987..6521c46b49 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -135,6 +135,12 @@ "internalType": "uint256", "name": "requestBlock", "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "submissionDeadline", + "type": "uint256" } ], "name": "CommitteeRequested", @@ -153,6 +159,19 @@ "name": "EnclaveSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sortitionSubmissionWindow", + "type": "uint256" + } + ], + "name": "SortitionSubmissionWindowSet", + "type": "event" + }, { "inputs": [ { @@ -185,6 +204,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "finalizeCommittee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "getBondingRegistry", @@ -206,29 +238,12 @@ "type": "uint256" } ], - "name": "getCommittee", + "name": "getCommitteeNodes", "outputs": [ { - "components": [ - { - "internalType": "address[]", - "name": "nodes", - "type": "address[]" - }, - { - "internalType": "uint32[2]", - "name": "threshold", - "type": "uint32[2]" - }, - { - "internalType": "bytes32", - "name": "publicKey", - "type": "bytes32" - } - ], - "internalType": "struct ICiphernodeRegistry.Committee", - "name": "committee", - "type": "tuple" + "internalType": "address[]", + "name": "committeeNodes", + "type": "address[]" } ], "stateMutability": "view", @@ -272,6 +287,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "isOpen", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -400,6 +434,37 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_sortitionSubmissionWindow", + "type": "uint256" + } + ], + "name": "setSortitionSubmissionWindow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ticketNumber", + "type": "uint256" + } + ], + "name": "submitTicket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "treeSize", @@ -420,5 +485,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-332be8f9366934dd9b42983a9b2838dffc905ed0" + "buildInfoId": "solc-0_8_28-a254ab6ed640b952e36a352bed155c87f85018f2" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 2453d06f4b..c45b3ff560 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-332be8f9366934dd9b42983a9b2838dffc905ed0" + "buildInfoId": "solc-0_8_28-a254ab6ed640b952e36a352bed155c87f85018f2" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json b/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json deleted file mode 100644 index 0c76208801..0000000000 --- a/packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json +++ /dev/null @@ -1,353 +0,0 @@ -{ - "_format": "hh3-artifact-1", - "contractName": "CommitteeSortition", - "sourceName": "contracts/sortition/CommitteeSortition.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "_ciphernodeRegistry", - "type": "address" - }, - { - "internalType": "address", - "name": "_bondingRegistry", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_submissionWindowSeconds", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "CommitteeAlreadyFinalized", - "type": "error" - }, - { - "inputs": [], - "name": "NodeAlreadySubmitted", - "type": "error" - }, - { - "inputs": [], - "name": "NodeNotEligible", - "type": "error" - }, - { - "inputs": [], - "name": "OnlyCiphernodeRegistry", - "type": "error" - }, - { - "inputs": [], - "name": "RoundAlreadyInitialized", - "type": "error" - }, - { - "inputs": [], - "name": "RoundNotInitialized", - "type": "error" - }, - { - "inputs": [], - "name": "SubmissionWindowClosed", - "type": "error" - }, - { - "inputs": [], - "name": "SubmissionWindowNotClosed", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address[]", - "name": "committee", - "type": "address[]" - } - ], - "name": "CommitteeFinalized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint32", - "name": "committeeSize", - "type": "uint32" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "seed", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "startTime", - "type": "uint64" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "endTime", - "type": "uint64" - } - ], - "name": "SortitionInitialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "address", - "name": "node", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "ticketId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "score", - "type": "uint256" - } - ], - "name": "TicketSubmitted", - "type": "event" - }, - { - "inputs": [], - "name": "bondingRegistry", - "outputs": [ - { - "internalType": "contract IBondingRegistry", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "ciphernodeRegistry", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "finalizeCommittee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "getCommittee", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "getTopSoFar", - "outputs": [ - { - "internalType": "address[]", - "name": "", - "type": "address[]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint32", - "name": "committeeSize", - "type": "uint32" - }, - { - "internalType": "uint256", - "name": "seed", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "initializeSortition", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "isOpen", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "submissionWindow", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "ticketId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "score", - "type": "uint256" - } - ], - "name": "submitTicket", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x60e060405234801561000f575f5ffd5b50604051610f1d380380610f1d83398101604081905261002e9161012e565b6001600160a01b0383166100785760405162461bcd60e51b815260206004820152600c60248201526b62616420726567697374727960a01b60448201526064015b60405180910390fd5b6001600160a01b0382166100bc5760405162461bcd60e51b815260206004820152600b60248201526a62616420626f6e64696e6760a81b604482015260640161006f565b5f81116100f85760405162461bcd60e51b815260206004820152600a6024820152696261642077696e646f7760b01b604482015260640161006f565b6001600160a01b0392831660c052911660a052608052610167565b80516001600160a01b0381168114610129575f5ffd5b919050565b5f5f5f60608486031215610140575f5ffd5b61014984610113565b925061015760208501610113565b9150604084015190509250925092565b60805160a05160c051610d786101a55f395f8181610132015261045b01525f818160f3015261031701525f818161019401526105330152610d785ff3fe608060405234801561000f575f5ffd5b506004361061009e575f3560e01c80638e6c157311610072578063da881e5a11610058578063da881e5a1461017c578063e621dbc71461018f578063ea1514a3146101c4575f5ffd5b80638e6c157314610154578063c88f60ae14610169575f5ffd5b806218449a146100a25780634d6861a6146100cb57806385814243146100ee5780638dcdd86b1461012d575b5f5ffd5b6100b56100b0366004610bed565b6101d7565b6040516100c29190610c04565b60405180910390f35b6100de6100d9366004610bed565b610241565b60405190151581526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b610167610162366004610c4f565b610294565b005b610167610177366004610c78565b610450565b61016761018a366004610bed565b6105f4565b6101b67f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020016100c2565b6100b56101d2366004610bed565b6107a4565b5f818152602081815260409182902060030180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610217575b50505050509050919050565b5f818152602081905260408120805460ff16158061026557508054610100900460ff165b1561027257505f92915050565b546a0100000000000000000000900467ffffffffffffffff1642111592915050565b5f838152602081905260409020805460ff166102dc576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6102e584610241565b610302576040516332999ab560e11b815260040160405180910390fd5b604051639f8a13d760e01b81523360048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690639f8a13d790602401602060405180830381865afa158015610364573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103889190610cba565b6103a55760405163149fbcfd60e11b815260040160405180910390fd5b335f90815260048201602052604090205460ff16156103d75760405163257309f160e11b815260040160405180910390fd5b335f8181526004830160209081526040808320805460ff1916600117905560058501909152902083905561040d9082908461080c565b6040805184815260208101849052339186917f52999628fb1cb05707e842278833b22e511f11746202cecdf221968b0b89e8bd910160405180910390a350505050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146104995760405163b56831db60e01b815260040160405180910390fd5b5f848152602081905260409020805460ff16156104c9576040516306e1765960e21b815260040160405180910390fd5b805460018083018590557fffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff0000909116600160901b63ffffffff871602171769ffffffffffffffff00001916620100004267ffffffffffffffff811691909102919091178255610558907f000000000000000000000000000000000000000000000000000000000000000090610cf4565b815471ffffffffffffffff0000000000000000000019166a010000000000000000000067ffffffffffffffff9283168102919091178084556040805163ffffffff8916815260208101889052620100008304851691810191909152919004909116606082015285907f298ce6cec139152f1e7b795aeee6b9e8c11c7cdf0650f99dc5315f822eb4bba99060800160405180910390a25050505050565b5f818152602081905260409020805460ff1661063c576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61064582610241565b1561066357604051632f021e8d60e11b815260040160405180910390fd5b8054610100900460ff161561068b57604051631860f69960e31b815260040160405180910390fd5b805461ff00191661010017815560028101545f8167ffffffffffffffff8111156106b7576106b7610d07565b6040519080825280602002602001820160405280156106e0578160200160208202803683370190505b5090505f5b8281101561074f5783600201818154811061070257610702610d1b565b905f5260205f20015f9054906101000a90046001600160a01b031682828151811061072f5761072f610d1b565b6001600160a01b03909216602092830291909101909101526001016106e5565b5080516107659060038501906020840190610b76565b50837fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132826040516107969190610c04565b60405180910390a250505050565b5f818152602081815260409182902060020180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f209081546001600160a01b031681526001909101906020018083116102175750505050509050919050565b60028301548354600160901b900463ffffffff1681101561094d575f6108328584610b02565b600286018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038a161790559154929350916108739190610d2f565b90505b81811115610904576002860161088d600183610d2f565b8154811061089d5761089d610d1b565b5f918252602090912001546002870180546001600160a01b0390921691839081106108ca576108ca610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b0392909216919091179055806108fc81610d42565b915050610876565b508385600201828154811061091b5761091b610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b5f6002850161095d600184610d2f565b8154811061096d5761096d610d1b565b5f9182526020808320909101546001600160a01b0316808352600588019091526040909120549091508084106109a557505050505050565b856002018054806109b8576109b8610d57565b5f8281526020812082015f1990810180546001600160a01b03191690559091019091556109e58786610b02565b600288018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038c16179055915492935091610a269190610d2f565b90505b81811115610ab75760028801610a40600183610d2f565b81548110610a5057610a50610d1b565b5f918252602090912001546002890180546001600160a01b039092169183908110610a7d57610a7d610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610aaf81610d42565b915050610a29565b5085876002018281548110610ace57610ace610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b0316021790555050505050505050565b60028201545f90815b81811015610b6c575f856002018281548110610b2957610b29610d1b565b5f9182526020808320909101546001600160a01b031680835260058901909152604090912054909150851015610b6357509150610b709050565b50600101610b0b565b5090505b92915050565b828054828255905f5260205f20908101928215610bc9579160200282015b82811115610bc957825182546001600160a01b0319166001600160a01b03909116178255602090920191600190910190610b94565b50610bd5929150610bd9565b5090565b5b80821115610bd5575f8155600101610bda565b5f60208284031215610bfd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610c445783516001600160a01b0316835260209384019390920191600101610c1d565b509095945050505050565b5f5f5f60608486031215610c61575f5ffd5b505081359360208301359350604090920135919050565b5f5f5f5f60808587031215610c8b575f5ffd5b84359350602085013563ffffffff81168114610ca5575f5ffd5b93969395505050506040820135916060013590565b5f60208284031215610cca575f5ffd5b81518015158114610cd9575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610b7057610b70610ce0565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b81810381811115610b7057610b70610ce0565b5f81610d5057610d50610ce0565b505f190190565b634e487b7160e01b5f52603160045260245ffdfea164736f6c634300081c000a", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b506004361061009e575f3560e01c80638e6c157311610072578063da881e5a11610058578063da881e5a1461017c578063e621dbc71461018f578063ea1514a3146101c4575f5ffd5b80638e6c157314610154578063c88f60ae14610169575f5ffd5b806218449a146100a25780634d6861a6146100cb57806385814243146100ee5780638dcdd86b1461012d575b5f5ffd5b6100b56100b0366004610bed565b6101d7565b6040516100c29190610c04565b60405180910390f35b6100de6100d9366004610bed565b610241565b60405190151581526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100c2565b6101157f000000000000000000000000000000000000000000000000000000000000000081565b610167610162366004610c4f565b610294565b005b610167610177366004610c78565b610450565b61016761018a366004610bed565b6105f4565b6101b67f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020016100c2565b6100b56101d2366004610bed565b6107a4565b5f818152602081815260409182902060030180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610217575b50505050509050919050565b5f818152602081905260408120805460ff16158061026557508054610100900460ff165b1561027257505f92915050565b546a0100000000000000000000900467ffffffffffffffff1642111592915050565b5f838152602081905260409020805460ff166102dc576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6102e584610241565b610302576040516332999ab560e11b815260040160405180910390fd5b604051639f8a13d760e01b81523360048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690639f8a13d790602401602060405180830381865afa158015610364573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103889190610cba565b6103a55760405163149fbcfd60e11b815260040160405180910390fd5b335f90815260048201602052604090205460ff16156103d75760405163257309f160e11b815260040160405180910390fd5b335f8181526004830160209081526040808320805460ff1916600117905560058501909152902083905561040d9082908461080c565b6040805184815260208101849052339186917f52999628fb1cb05707e842278833b22e511f11746202cecdf221968b0b89e8bd910160405180910390a350505050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146104995760405163b56831db60e01b815260040160405180910390fd5b5f848152602081905260409020805460ff16156104c9576040516306e1765960e21b815260040160405180910390fd5b805460018083018590557fffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff0000909116600160901b63ffffffff871602171769ffffffffffffffff00001916620100004267ffffffffffffffff811691909102919091178255610558907f000000000000000000000000000000000000000000000000000000000000000090610cf4565b815471ffffffffffffffff0000000000000000000019166a010000000000000000000067ffffffffffffffff9283168102919091178084556040805163ffffffff8916815260208101889052620100008304851691810191909152919004909116606082015285907f298ce6cec139152f1e7b795aeee6b9e8c11c7cdf0650f99dc5315f822eb4bba99060800160405180910390a25050505050565b5f818152602081905260409020805460ff1661063c576040517fffc9e8c500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61064582610241565b1561066357604051632f021e8d60e11b815260040160405180910390fd5b8054610100900460ff161561068b57604051631860f69960e31b815260040160405180910390fd5b805461ff00191661010017815560028101545f8167ffffffffffffffff8111156106b7576106b7610d07565b6040519080825280602002602001820160405280156106e0578160200160208202803683370190505b5090505f5b8281101561074f5783600201818154811061070257610702610d1b565b905f5260205f20015f9054906101000a90046001600160a01b031682828151811061072f5761072f610d1b565b6001600160a01b03909216602092830291909101909101526001016106e5565b5080516107659060038501906020840190610b76565b50837fed38c6266ebed7c01d311349a7fa67e0ef0f1d2f4760d60dbb34ca4799a2e132826040516107969190610c04565b60405180910390a250505050565b5f818152602081815260409182902060020180548351818402810184019094528084526060939283018282801561023557602002820191905f5260205f209081546001600160a01b031681526001909101906020018083116102175750505050509050919050565b60028301548354600160901b900463ffffffff1681101561094d575f6108328584610b02565b600286018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038a161790559154929350916108739190610d2f565b90505b81811115610904576002860161088d600183610d2f565b8154811061089d5761089d610d1b565b5f918252602090912001546002870180546001600160a01b0390921691839081106108ca576108ca610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b0392909216919091179055806108fc81610d42565b915050610876565b508385600201828154811061091b5761091b610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b031602179055505050505050565b5f6002850161095d600184610d2f565b8154811061096d5761096d610d1b565b5f9182526020808320909101546001600160a01b0316808352600588019091526040909120549091508084106109a557505050505050565b856002018054806109b8576109b8610d57565b5f8281526020812082015f1990810180546001600160a01b03191690559091019091556109e58786610b02565b600288018054600180820183555f8381526020812090920180546001600160a01b0319166001600160a01b038c16179055915492935091610a269190610d2f565b90505b81811115610ab75760028801610a40600183610d2f565b81548110610a5057610a50610d1b565b5f918252602090912001546002890180546001600160a01b039092169183908110610a7d57610a7d610d1b565b5f91825260209091200180546001600160a01b0319166001600160a01b039290921691909117905580610aaf81610d42565b915050610a29565b5085876002018281548110610ace57610ace610d1b565b905f5260205f20015f6101000a8154816001600160a01b0302191690836001600160a01b0316021790555050505050505050565b60028201545f90815b81811015610b6c575f856002018281548110610b2957610b29610d1b565b5f9182526020808320909101546001600160a01b031680835260058901909152604090912054909150851015610b6357509150610b709050565b50600101610b0b565b5090505b92915050565b828054828255905f5260205f20908101928215610bc9579160200282015b82811115610bc957825182546001600160a01b0319166001600160a01b03909116178255602090920191600190910190610b94565b50610bd5929150610bd9565b5090565b5b80821115610bd5575f8155600101610bda565b5f60208284031215610bfd575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b81811015610c445783516001600160a01b0316835260209384019390920191600101610c1d565b509095945050505050565b5f5f5f60608486031215610c61575f5ffd5b505081359360208301359350604090920135919050565b5f5f5f5f60808587031215610c8b575f5ffd5b84359350602085013563ffffffff81168114610ca5575f5ffd5b93969395505050506040820135916060013590565b5f60208284031215610cca575f5ffd5b81518015158114610cd9575f5ffd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b80820180821115610b7057610b70610ce0565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b81810381811115610b7057610b70610ce0565b5f81610d5057610d50610ce0565b505f190190565b634e487b7160e01b5f52603160045260245ffdfea164736f6c634300081c000a", - "linkReferences": {}, - "deployedLinkReferences": {}, - "immutableReferences": { - "13294": [ - { - "length": 32, - "start": 404 - }, - { - "length": 32, - "start": 1331 - } - ], - "13297": [ - { - "length": 32, - "start": 243 - }, - { - "length": 32, - "start": 791 - } - ], - "13299": [ - { - "length": 32, - "start": 306 - }, - { - "length": 32, - "start": 1115 - } - ] - }, - "inputSourceName": "project/contracts/sortition/CommitteeSortition.sol", - "buildInfoId": "solc-0_8_28-a9cacbeeb68df13e87656122d54fd5cb4ca7e178" -} \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index c1e87fe183..45376ae0e8 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -58,11 +58,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Incremented after each successful E3 request. uint256 public nexte3Id; - /// @notice Submission Window for an E3 Sortition. - /// @dev The submission window is the time period during which the ciphernodes can submit - /// their tickets to be a part of the committee. - uint256 public sortitionSubmissionWindow; - /// @notice Mapping of allowed E3 Programs. /// @dev Only enabled E3 Programs can be used in computation requests. mapping(IE3Program e3Program => bool allowed) public e3Programs; @@ -208,7 +203,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @param _bondingRegistry The address of the Bonding Registry contract. /// @param _feeToken The address of the ERC20 token used for E3 fees. /// @param _maxDuration The maximum duration of a computation in seconds. - /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). constructor( address _owner, @@ -216,7 +210,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { IBondingRegistry _bondingRegistry, IERC20 _feeToken, uint256 _maxDuration, - uint256 _sortitionSubmissionWindow, bytes[] memory _e3ProgramsParams ) { initialize( @@ -225,7 +218,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { _bondingRegistry, _feeToken, _maxDuration, - _sortitionSubmissionWindow, _e3ProgramsParams ); } @@ -237,7 +229,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @param _bondingRegistry The address of the Bonding Registry contract. /// @param _feeToken The address of the ERC20 token used for E3 fees. /// @param _maxDuration The maximum duration of a computation in seconds. - /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). function initialize( address _owner, @@ -245,7 +236,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { IBondingRegistry _bondingRegistry, IERC20 _feeToken, uint256 _maxDuration, - uint256 _sortitionSubmissionWindow, bytes[] memory _e3ProgramsParams ) public initializer { __Ownable_init(msg.sender); @@ -253,7 +243,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { setCiphernodeRegistry(_ciphernodeRegistry); setBondingRegistry(_bondingRegistry); setFeeToken(_feeToken); - setSortitionSubmissionWindow(_sortitionSubmissionWindow); setE3ProgramsParams(_e3ProgramsParams); if (_owner != owner()) transferOwnership(_owner); } @@ -343,8 +332,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { ciphernodeRegistry.requestCommittee( e3Id, seed, - requestParams.threshold, - sortitionSubmissionWindow + requestParams.threshold ), CommitteeSelectionFailed() ); @@ -481,14 +469,16 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Emits RewardsDistributed event upon successful distribution. /// @param e3Id The ID of the E3 for which to distribute rewards. function _distributeRewards(uint256 e3Id) internal { - ICiphernodeRegistry.Committee memory committee = ciphernodeRegistry - .getCommittee(e3Id); - uint256[] memory amounts = new uint256[](committee.nodes.length); + address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( + e3Id + ); + uint256 committeeLength = committeeNodes.length; + uint256[] memory amounts = new uint256[](committeeLength); // TODO: do we need to pay different amounts to different nodes? // For now, we'll pay the same amount to all nodes. - uint256 amount = e3Payments[e3Id] / committee.nodes.length; - for (uint256 i = 0; i < committee.nodes.length; i++) { + uint256 amount = e3Payments[e3Id] / committeeLength; + for (uint256 i = 0; i < committeeLength; i++) { amounts[i] = amount; } @@ -497,12 +487,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { feeToken.approve(address(bondingRegistry), totalAmount); - bondingRegistry.distributeRewards(feeToken, committee.nodes, amounts); + bondingRegistry.distributeRewards(feeToken, committeeNodes, amounts); // TODO: decide where does dust go? Treasury maybe? feeToken.approve(address(bondingRegistry), 0); - emit RewardsDistributed(e3Id, committee.nodes, amounts); + emit RewardsDistributed(e3Id, committeeNodes, amounts); } //////////////////////////////////////////////////////////// @@ -520,15 +510,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit MaxDurationSet(_maxDuration); } - /// @inheritdoc IEnclave - function setSortitionSubmissionWindow( - uint256 _sortitionSubmissionWindow - ) public onlyOwner returns (bool success) { - sortitionSubmissionWindow = _sortitionSubmissionWindow; - success = true; - emit SortitionSubmissionWindowSet(_sortitionSubmissionWindow); - } - /// @inheritdoc IEnclave function setCiphernodeRegistry( ICiphernodeRegistry _ciphernodeRegistry diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 942d0e586b..8d44328b8c 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -94,6 +94,10 @@ interface ICiphernodeRegistry { uint256 size ); + /// @notice This event MUST be emitted any time the `sortitionSubmissionWindow` is set. + /// @param sortitionSubmissionWindow The submission window for the E3 sortition in seconds. + event SortitionSubmissionWindowSet(uint256 sortitionSubmissionWindow); + /// @notice Check if a ciphernode is eligible for committee selection /// @dev A ciphernode is eligible if it is enabled in the registry and meets bonding requirements /// @param ciphernode Address of the ciphernode to check @@ -122,13 +126,11 @@ interface ICiphernodeRegistry { /// @param e3Id ID of the E3 for which to select the committee. /// @param seed Random seed for score computation. /// @param threshold The M/N threshold for the committee. - /// @param submissionWindow The submission window for the E3 sortition in seconds. /// @return success True if committee selection was successfully initiated. function requestCommittee( uint256 e3Id, uint256 seed, - uint32[2] calldata threshold, - uint256 submissionWindow + uint32[2] calldata threshold ) external returns (bool success); /// @notice Publishes the public key resulting from the committee selection process. @@ -154,10 +156,10 @@ interface ICiphernodeRegistry { /// @notice This function should be called by the Enclave contract to get the committee for a given E3. /// @dev This function MUST revert if no committee has been requested for the given E3. /// @param e3Id ID of the E3 for which to get the committee. - /// @return committee The committee for the given E3. - function getCommittee( + /// @return committeeNodes The nodes in the committee for the given E3. + function getCommitteeNodes( uint256 e3Id - ) external view returns (Committee memory committee); + ) external view returns (address[] memory committeeNodes); /// @notice Returns the current root of the ciphernode IMT /// @return Current IMT root @@ -185,4 +187,25 @@ interface ICiphernodeRegistry { /// @dev Only callable by owner /// @param _bondingRegistry Address of the bonding registry contract function setBondingRegistry(address _bondingRegistry) external; + + /// @notice This function should be called to set the submission window for the E3 sortition. + /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. + function setSortitionSubmissionWindow( + uint256 _sortitionSubmissionWindow + ) external; + + /// @notice Submit a ticket for sortition + /// @dev Validates ticket against node's balance at request block + /// @param e3Id ID of the E3 computation + /// @param ticketNumber The ticket number to submit + function submitTicket(uint256 e3Id, uint256 ticketNumber) external; + + /// @notice Finalize the committee after submission window closes + /// @param e3Id ID of the E3 computation + function finalizeCommittee(uint256 e3Id) external; + + /// @notice Check if submission window is still open for an E3 + /// @param e3Id ID of the E3 computation + /// @return Whether the submission window is open + function isOpen(uint256 e3Id) external view returns (bool); } diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 786f7e3295..e6f403b509 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -64,10 +64,6 @@ interface IEnclave { /// @param maxDuration The maximum duration of a computation in seconds. event MaxDurationSet(uint256 maxDuration); - /// @notice This event MUST be emitted any time the `sortitionSubmissionWindow` is set. - /// @param sortitionSubmissionWindow The submission window for the E3 sortition in seconds. - event SortitionSubmissionWindowSet(uint256 sortitionSubmissionWindow); - /// @notice This event MUST be emitted any time the CiphernodeRegistry is set. /// @param ciphernodeRegistry The address of the CiphernodeRegistry contract. event CiphernodeRegistrySet(address ciphernodeRegistry); @@ -210,13 +206,6 @@ interface IEnclave { uint256 _maxDuration ) external returns (bool success); - /// @notice This function should be called to set the submission window for the E3 sortition. - /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. - /// @return success True if the sortition submission window was successfully set. - function setSortitionSubmissionWindow( - uint256 _sortitionSubmissionWindow - ) external returns (bool success); - /// @notice Sets the Ciphernode Registry contract address. /// @dev This function MUST revert if the address is zero or the same as the current registry. /// @param _ciphernodeRegistry The address of the new Ciphernode Registry contract. diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index c5f03cf469..f30e085076 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -7,7 +7,6 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; -import { CommitteeSortition } from "../sortition/CommitteeSortition.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -34,9 +33,22 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @param bondingRegistry Address of the bonding registry contract event BondingRegistrySet(address indexed bondingRegistry); - /// @notice Emitted when the committee sortition address is set - /// @param committeeSortition Address of the committee sortition contract - event CommitteeSortitionSet(address indexed committeeSortition); + /// @notice Emitted when a ticket is submitted for sortition + /// @param e3Id ID of the E3 computation + /// @param node Address of the ciphernode submitting the ticket + /// @param ticketId The ticket number being submitted + /// @param score The computed score for the ticket + event TicketSubmitted( + uint256 indexed e3Id, + address indexed node, + uint256 ticketId, + uint256 score + ); + + /// @notice Emitted when a committee is finalized + /// @param e3Id ID of the E3 computation + /// @param committee Array of selected ciphernode addresses + event CommitteeFinalized(uint256 indexed e3Id, address[] committee); //////////////////////////////////////////////////////////// // // @@ -50,12 +62,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Address of the bonding registry for checking node eligibility address public bondingRegistry; - /// @notice Address of the committee sortition contract - address public committeeSortition; - /// @notice Current number of registered ciphernodes uint256 public numCiphernodes; + /// @notice Submission Window for an E3 Sortition. + /// @dev The submission window is the time period during which the ciphernodes can submit + /// their tickets to be a part of the committee. + uint256 public sortitionSubmissionWindow; + /// @notice Incremental Merkle Tree (IMT) containing all registered ciphernodes LeanIMTData public ciphernodes; @@ -66,7 +80,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; /// @notice Maps E3 ID to its committee data - mapping(uint256 e3Id => Committee committee) public committees; + mapping(uint256 e3Id => Committee committee) internal committees; //////////////////////////////////////////////////////////// // // @@ -86,8 +100,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Committee has not been requested yet for this E3 error CommitteeNotRequested(); - /// @notice Submission Window Not valid for this E3 - error SubmissionWindowNotValid(); + /// @notice Committee Not Initialized or Finalized + error CommitteeNotInitializedOrFinalized(); /// @notice Submission Window has been closed for this E3 error SubmissionWindowClosed(); @@ -133,8 +147,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Bonding registry has not been set error BondingRegistryNotSet(); - /// @notice Committee sortition has not been set - error CommitteeSortitionNotSet(); + /// @notice Invalid ticket number + error InvalidTicketNumber(); + + /// @notice Submission window not closed yet + error SubmissionWindowNotClosed(); /// @notice Caller is not authorized error Unauthorized(); @@ -175,20 +192,27 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Constructor that initializes the registry with owner and enclave /// @param _owner Address that will own the contract /// @param _enclave Address of the Enclave contract - constructor(address _owner, address _enclave) { - initialize(_owner, _enclave); + /// @param _submissionWindow The submission window for the E3 sortition in seconds + constructor(address _owner, address _enclave, uint256 _submissionWindow) { + initialize(_owner, _enclave, _submissionWindow); } /// @notice Initializes the registry contract /// @dev Can only be called once due to initializer modifier /// @param _owner Address that will own the contract /// @param _enclave Address of the Enclave contract - function initialize(address _owner, address _enclave) public initializer { + /// @param _submissionWindow The submission window for the E3 sortition in seconds + function initialize( + address _owner, + address _enclave, + uint256 _submissionWindow + ) public initializer { require(_owner != address(0), ZeroAddress()); require(_enclave != address(0), ZeroAddress()); __Ownable_init(msg.sender); setEnclave(_enclave); + setSortitionSubmissionWindow(_submissionWindow); if (_owner != owner()) transferOwnership(_owner); } @@ -202,8 +226,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { function requestCommittee( uint256 e3Id, uint256 seed, - uint32[2] calldata threshold, - uint256 submissionWindow + uint32[2] calldata threshold ) external onlyEnclave returns (bool success) { Committee storage c = committees[e3Id]; require(!c.initialized, CommitteeAlreadyRequested()); @@ -212,7 +235,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { c.finalized = false; c.seed = seed; c.requestBlock = block.number; - c.submissionDeadline = block.timestamp + submissionWindow; + c.submissionDeadline = block.timestamp + sortitionSubmissionWindow; c.threshold = threshold; roots[e3Id] = root(); @@ -236,11 +259,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { address[] calldata nodes, bytes calldata publicKey ) external onlyOwner { - ICiphernodeRegistry.Committee storage committee = committees[e3Id]; - require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); - committee.nodes = nodes; + Committee storage c = committees[e3Id]; + require(c.publicKey == bytes32(0), CommitteeAlreadyPublished()); + + // Store the nodes in the committee array + for (uint256 i = 0; i < nodes.length; i++) { + c.committee.push(nodes[i]); + } + bytes32 publicKeyHash = keccak256(publicKey); - committee.publicKey = publicKeyHash; + c.publicKey = publicKeyHash; publicKeyHashes[e3Id] = publicKeyHash; emit CommitteePublished(e3Id, nodes, publicKey); } @@ -282,31 +310,67 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// - /// @inheritdoc ICiphernodeRegistry - function submitTicket( - uint256 e3Id, - uint256 ticketId, - uint256 score - ) external { + /// @notice Submit a ticket for sortition + /// @dev Validates ticket against node's balance at request block and inserts into top-N + /// @param e3Id ID of the E3 computation + /// @param ticketNumber The ticket number to submit (1 to available tickets at snapshot) + function submitTicket(uint256 e3Id, uint256 ticketNumber) external { Committee storage c = committees[e3Id]; - require(!r.initialized || r.finalized, CommitteeNotRequested()); + require(c.initialized, CommitteeNotRequested()); + require(!c.finalized, CommitteeAlreadyFinalized()); require( block.timestamp <= c.submissionDeadline, SubmissionDeadlineReached() ); + require(!c.submitted[msg.sender], NodeAlreadySubmitted()); + + // Validate node eligibility and ticket number + _validateNodeEligibility(msg.sender, ticketNumber, e3Id); + + // Compute score + uint256 score = _computeTicketScore( + msg.sender, + ticketNumber, + e3Id, + c.seed + ); + + // Store submission + c.submitted[msg.sender] = true; + c.scoreOf[msg.sender] = score; + + // Insert into top-N (ascending score) + _insertTopN(c, msg.sender, score); + + emit TicketSubmitted(e3Id, msg.sender, ticketNumber, score); + } - if (!isOpen(e3Id)) revert SubmissionWindowClosed(); - if (!IBondingRegistry(bondingRegistry).isActive(msg.sender)) - revert NodeNotEligible(); - if (r.submitted[msg.sender]) revert NodeAlreadySubmitted(); + /// @notice Finalize the committee after submission window closes + /// @dev Can be called by anyone after the deadline. Reverts if not enough nodes submitted. + /// @param e3Id ID of the E3 computation + function finalizeCommittee(uint256 e3Id) external { + Committee storage c = committees[e3Id]; + require(c.initialized, CommitteeNotRequested()); + require(!c.finalized, CommitteeAlreadyFinalized()); + require( + block.timestamp > c.submissionDeadline, + SubmissionWindowNotClosed() + ); + require(c.topNodes.length >= c.threshold[0], NodeNotEligible()); - r.submitted[msg.sender] = true; - r.scoreOf[msg.sender] = score; + c.finalized = true; + c.committee = c.topNodes; - // insert into top-N (ascending score) - _insertTopN(r, msg.sender, score); + emit CommitteeFinalized(e3Id, c.topNodes); + } - emit TicketSubmitted(e3Id, msg.sender, ticketId, score); + /// @notice Check if submission window is still open for an E3 + /// @param e3Id ID of the E3 computation + /// @return Whether the submission window is open + function isOpen(uint256 e3Id) public view returns (bool) { + Committee storage c = committees[e3Id]; + if (!c.initialized || c.finalized) return false; + return block.timestamp <= c.submissionDeadline; } //////////////////////////////////////////////////////////// @@ -333,15 +397,12 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit BondingRegistrySet(_bondingRegistry); } - /// @notice Sets the committee sortition contract address - /// @dev Only callable by owner - /// @param _committeeSortition Address of the committee sortition contract - function setCommitteeSortition( - address _committeeSortition + /// @inheritdoc ICiphernodeRegistry + function setSortitionSubmissionWindow( + uint256 _sortitionSubmissionWindow ) public onlyOwner { - require(_committeeSortition != address(0), ZeroAddress()); - committeeSortition = _committeeSortition; - emit CommitteeSortitionSet(_committeeSortition); + sortitionSubmissionWindow = _sortitionSubmissionWindow; + emit SortitionSubmissionWindowSet(_sortitionSubmissionWindow); } //////////////////////////////////////////////////////////// @@ -385,11 +446,12 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @inheritdoc ICiphernodeRegistry - function getCommittee( + function getCommitteeNodes( uint256 e3Id - ) public view returns (ICiphernodeRegistry.Committee memory committee) { - committee = committees[e3Id]; - require(committee.publicKey != bytes32(0), CommitteeNotPublished()); + ) public view returns (address[] memory nodes) { + Committee storage c = committees[e3Id]; + require(c.publicKey != bytes32(0), CommitteeNotPublished()); + nodes = c.committee; } /// @notice Returns the current size of the ciphernode IMT @@ -403,4 +465,115 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { function getBondingRegistry() external view returns (address) { return bondingRegistry; } + + //////////////////////////////////////////////////////////// + // // + // Internal Functions // + // // + //////////////////////////////////////////////////////////// + + /// @notice Computes ticket score as keccak256(node || ticketNumber || e3Id || seed) + /// @param node Address of the ciphernode + /// @param ticketNumber The ticket number + /// @param e3Id ID of the E3 computation + /// @param seed Random seed for the E3 + /// @return score The computed score + function _computeTicketScore( + address node, + uint256 ticketNumber, + uint256 e3Id, + uint256 seed + ) internal pure returns (uint256) { + bytes32 hash = keccak256( + abi.encodePacked(node, ticketNumber, e3Id, seed) + ); + return uint256(hash); + } + + /// @notice Validates that a node is eligible to submit a ticket + /// @dev Uses snapshot of ticket balance at E3 request block for deterministic validation + /// @param node Address of the ciphernode + /// @param ticketNumber The ticket number being submitted + /// @param e3Id ID of the E3 computation + function _validateNodeEligibility( + address node, + uint256 ticketNumber, + uint256 e3Id + ) internal view { + require(ticketNumber > 0, InvalidTicketNumber()); + require(bondingRegistry != address(0), BondingRegistryNotSet()); + + Committee storage c = committees[e3Id]; + + // Get ticket balance at the time E3 was requested (snapshot) + uint256 ticketBalance = IBondingRegistry(bondingRegistry) + .getTicketBalanceAtBlock(node, c.requestBlock); + uint256 ticketPrice = IBondingRegistry(bondingRegistry).ticketPrice(); + + require(ticketPrice > 0, InvalidTicketNumber()); + + // Calculate available tickets at snapshot + uint256 availableTickets = ticketBalance / ticketPrice; + + // Check node is eligible (has tickets at snapshot) + require(availableTickets > 0, NodeNotEligible()); + + // Check ticket number is valid + require(ticketNumber <= availableTickets, InvalidTicketNumber()); + } + + /// @notice Inserts a node into the top-N sorted list by score + /// @dev Maintains sorted order (ascending by score) + /// @param c Committee storage reference + /// @param node Address of the ciphernode + /// @param score The computed score + function _insertTopN( + Committee storage c, + address node, + uint256 score + ) internal { + address[] storage topNodes = c.topNodes; + + // If list not full, insert in sorted order + if (topNodes.length < c.threshold[1]) { + _insertSorted(c, node, score); + return; + } + + // If list is full, only add if score is better than worst + uint256 worstScore = c.scoreOf[topNodes[topNodes.length - 1]]; + if (score < worstScore) { + topNodes.pop(); // Remove worst + _insertSorted(c, node, score); + } + } + + /// @notice Inserts a node at the correct sorted position (ascending by score) + /// @param c Committee storage reference + /// @param node Address of the ciphernode + /// @param score The computed score + function _insertSorted( + Committee storage c, + address node, + uint256 score + ) internal { + address[] storage topNodes = c.topNodes; + + // Find insertion position + uint256 insertPos = topNodes.length; + for (uint256 i = 0; i < topNodes.length; i++) { + uint256 existingScore = c.scoreOf[topNodes[i]]; + if (score < existingScore) { + insertPos = i; + break; + } + } + + // Insert at position + topNodes.push(address(0)); // Extend array + for (uint256 i = topNodes.length - 1; i > insertPos; i--) { + topNodes[i] = topNodes[i - 1]; + } + topNodes[insertPos] = node; + } } diff --git a/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol b/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol deleted file mode 100644 index 1b7715ce7e..0000000000 --- a/packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -pragma solidity >=0.8.27; - -import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; - -contract CommitteeSortition { - // ============ Config ============ - uint256 public immutable submissionWindow; // seconds - IBondingRegistry public immutable bondingRegistry; - address public immutable ciphernodeRegistry; // who opens rounds - - // ============ Types ============ - struct Round { - // lifecycle - bool initialized; - bool finalized; - uint64 startTime; - uint64 endTime; - // params - uint32 committeeSize; - uint256 seed; - // state - address[] topNodes; // sorted by ascending score - address[] committee; // frozen after finalize - mapping(address => bool) submitted; - mapping(address => uint256) scoreOf; // score for submitted node (lower is better) - } - - // e3Id => Round - mapping(uint256 => Round) private rounds; - - // ============ Errors ============ - error OnlyCiphernodeRegistry(); - error RoundNotInitialized(); - error RoundAlreadyInitialized(); - error SubmissionWindowClosed(); - error SubmissionWindowNotClosed(); - error NodeNotEligible(); - error NodeAlreadySubmitted(); - error CommitteeAlreadyFinalized(); - - // ============ Events ============ - event SortitionInitialized( - uint256 indexed e3Id, - uint32 committeeSize, - uint256 seed, - uint64 startTime, - uint64 endTime - ); - - event TicketSubmitted( - uint256 indexed e3Id, - address indexed node, - uint256 ticketId, - uint256 score - ); - - event CommitteeFinalized(uint256 indexed e3Id, address[] committee); - - // ============ Constructor ============ - constructor( - address _ciphernodeRegistry, - address _bondingRegistry, - uint256 _submissionWindowSeconds - ) { - require(_ciphernodeRegistry != address(0), "bad registry"); - require(_bondingRegistry != address(0), "bad bonding"); - require(_submissionWindowSeconds > 0, "bad window"); - ciphernodeRegistry = _ciphernodeRegistry; - bondingRegistry = IBondingRegistry(_bondingRegistry); - submissionWindow = _submissionWindowSeconds; - } - - // ============ View helpers ============ - function getCommittee( - uint256 e3Id - ) external view returns (address[] memory) { - return rounds[e3Id].committee; - } - - function getTopSoFar( - uint256 e3Id - ) external view returns (address[] memory) { - return rounds[e3Id].topNodes; - } - - function isOpen(uint256 e3Id) public view returns (bool) { - Round storage r = rounds[e3Id]; - if (!r.initialized || r.finalized) return false; - return block.timestamp <= r.endTime; - } - - // ============ Core ============ - - /// called by CiphernodeRegistry when Enclave.requestCommittee(...) happens - function initializeSortition( - uint256 e3Id, - uint32 committeeSize, - uint256 seed, - uint256 /* requestBlock */ // kept for compatibility / auditing, not used here - ) external { - if (msg.sender != ciphernodeRegistry) revert OnlyCiphernodeRegistry(); - Round storage r = rounds[e3Id]; - if (r.initialized) revert RoundAlreadyInitialized(); - - r.initialized = true; - r.finalized = false; - r.committeeSize = committeeSize; - r.seed = seed; - r.startTime = uint64(block.timestamp); - r.endTime = uint64(block.timestamp + submissionWindow); - - emit SortitionInitialized( - e3Id, - committeeSize, - seed, - r.startTime, - r.endTime - ); - } - - /// nodes submit their *best* ticket (id, score) if eligible - function submitTicket( - uint256 e3Id, - uint256 ticketId, - uint256 score - ) external { - Round storage r = rounds[e3Id]; - if (!r.initialized) revert RoundNotInitialized(); - if (!isOpen(e3Id)) revert SubmissionWindowClosed(); - if (!IBondingRegistry(bondingRegistry).isActive(msg.sender)) - revert NodeNotEligible(); - if (r.submitted[msg.sender]) revert NodeAlreadySubmitted(); - - r.submitted[msg.sender] = true; - r.scoreOf[msg.sender] = score; - - // insert into top-N (ascending score) - _insertTopN(r, msg.sender, score); - - emit TicketSubmitted(e3Id, msg.sender, ticketId, score); - } - - /// anyone can finalize after the window closes - function finalizeCommittee(uint256 e3Id) external { - Round storage r = rounds[e3Id]; - if (!r.initialized) revert RoundNotInitialized(); - if (isOpen(e3Id)) revert SubmissionWindowNotClosed(); - if (r.finalized) revert CommitteeAlreadyFinalized(); - - r.finalized = true; - - // freeze committee - uint256 n = r.topNodes.length; - address[] memory committee = new address[](n); - for (uint256 i = 0; i < n; i++) committee[i] = r.topNodes[i]; - r.committee = committee; - - emit CommitteeFinalized(e3Id, committee); - } - - // ============ Internal ============ - - function _insertTopN( - Round storage r, - address node, - uint256 score - ) internal { - uint256 n = r.topNodes.length; - - // if we still have room, just insert in sorted spot - if (n < r.committeeSize) { - uint256 pos = _findInsertPos(r, score); - r.topNodes.push(node); - for (uint256 i = r.topNodes.length - 1; i > pos; i--) { - r.topNodes[i] = r.topNodes[i - 1]; - } - r.topNodes[pos] = node; - return; - } - - // otherwise compare with worst current score - address worst = r.topNodes[n - 1]; - uint256 worstScore = r.scoreOf[worst]; - if (score >= worstScore) { - // not better than worst, ignore - return; - } - - // replace worst with node at correct position - r.topNodes.pop(); // drop worst - uint256 pos2 = _findInsertPos(r, score); - r.topNodes.push(node); - for (uint256 i = r.topNodes.length - 1; i > pos2; i--) { - r.topNodes[i] = r.topNodes[i - 1]; - } - r.topNodes[pos2] = node; - } - - function _findInsertPos( - Round storage r, - uint256 score - ) internal view returns (uint256) { - uint256 n = r.topNodes.length; - for (uint256 i = 0; i < n; i++) { - address a = r.topNodes[i]; - if (score < r.scoreOf[a]) return i; - } - return n; - } -} diff --git a/packages/enclave-contracts/contracts/sortition/OldCS.sol b/packages/enclave-contracts/contracts/sortition/OldCS.sol deleted file mode 100644 index 57816fb279..0000000000 --- a/packages/enclave-contracts/contracts/sortition/OldCS.sol +++ /dev/null @@ -1,392 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -pragma solidity >=0.8.27; - -import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; - -/** - * @title CommitteeSortition - * @notice Simple on-chain verification of ticket-based sortition - * @dev Validates ticket submissions and tracks committee members - * - * Flow: - * 1. Nodes perform sortition off-chain - * 2. Selected nodes submit their winning ticket via submitTicket() - * 3. Contract validates ticket against snapshot balance - * 4. Contract tracks top N nodes by score - */ -contract OldCS { - // ====================== - // Errors - // ====================== - - error InvalidTicketNumber(); - error NodeNotEligible(); - error NodeAlreadySubmitted(); - error SubmissionWindowClosed(); - error SubmissionWindowNotClosed(); - error CommitteeNotInitialized(); - error CommitteeAlreadyFinalized(); - error OnlyCiphernodeRegistry(); - - // ====================== - // Events - // ====================== - - event TicketSubmitted( - uint256 indexed e3Id, - address indexed node, - uint256 ticketNumber, - uint256 score, - bool addedToCommittee - ); - - event CommitteeFinalized(uint256 indexed e3Id, address[] committee); - - // ====================== - // Structs - // ====================== - - /// @notice Represents a node's ticket submission - struct TicketSubmission { - address node; - uint256 ticketNumber; - uint256 score; - bool exists; - } - - /// @notice Sortition state for an E3 - struct SortitionState { - uint256 threshold; // Number of nodes needed - uint256 seed; // Random seed for this E3 - uint256 requestBlock; // Block number when E3 was requested (for snapshot) - uint256 submissionDeadline; // Timestamp when submission window closes - bool finalized; // Whether committee has been finalized - address[] topNodes; // Current top N nodes (sorted by score) - mapping(address => TicketSubmission) submissions; - } - - // ====================== - // Storage - // ====================== - - /// @notice Bonding registry for checking ticket balances - IBondingRegistry public immutable bondingRegistry; - - /// @notice Ciphernode registry that can initialize sortitions - address public immutable ciphernodeRegistry; - - /// @notice Default submission window duration (in seconds) - uint256 public immutable submissionWindow; - - /// @notice Maps E3 ID to its sortition state - mapping(uint256 => SortitionState) public sortitions; - - // ====================== - // Constructor - // ====================== - - constructor( - address _bondingRegistry, - address _ciphernodeRegistry, - uint256 _submissionWindow - ) { - bondingRegistry = IBondingRegistry(_bondingRegistry); - ciphernodeRegistry = _ciphernodeRegistry; - submissionWindow = _submissionWindow; - } - - // ====================== - // Main Functions - // ====================== - - /** - * @notice Initialize sortition for an E3 - * @dev Only callable by ciphernode registry when committee is requested - * @param e3Id The E3 identifier - * @param threshold Number of committee members needed - * @param seed Random seed for score computation - * @param requestBlock Block number for snapshot validation - */ - function initializeSortition( - uint256 e3Id, - uint256 threshold, - uint256 seed, - uint256 requestBlock - ) external { - require(msg.sender == ciphernodeRegistry, OnlyCiphernodeRegistry()); - SortitionState storage state = sortitions[e3Id]; - require(state.threshold == 0, CommitteeAlreadyFinalized()); - - state.threshold = threshold; - state.seed = seed; - state.requestBlock = requestBlock; - state.submissionDeadline = block.timestamp + submissionWindow; - state.finalized = false; - } - - /** - * @notice Submit a ticket for sortition - * @dev Nodes call this to submit their best ticket. Score is computed and verified on-chain. - * @param e3Id The E3 identifier - * @param ticketNumber The ticket number to submit (1 to available_tickets at snapshot) - */ - function submitTicket(uint256 e3Id, uint256 ticketNumber) external { - SortitionState storage state = sortitions[e3Id]; - - // Check sortition is initialized - require(state.threshold > 0, CommitteeNotInitialized()); - - // Check submission window is still open - require( - block.timestamp <= state.submissionDeadline, - SubmissionWindowClosed() - ); - - // Check not finalized - require(!state.finalized, CommitteeAlreadyFinalized()); - - // Check node hasn't already submitted - if (state.submissions[msg.sender].exists) revert NodeAlreadySubmitted(); - - // Check node is eligible (has ticket balance at snapshot) - _validateNodeEligibility(msg.sender, ticketNumber, e3Id); - - // Compute score - uint256 score = _computeTicketScore( - msg.sender, - ticketNumber, - e3Id, - state.seed - ); - - // Store submission - state.submissions[msg.sender] = TicketSubmission({ - node: msg.sender, - ticketNumber: ticketNumber, - score: score, - exists: true - }); - - // Try to insert into top N - bool added = _tryInsertIntoTopN(state, msg.sender, score); - - emit TicketSubmitted(e3Id, msg.sender, ticketNumber, score, added); - } - - /** - * @notice Finalize the committee after submission window closes - * @dev Can be called by anyone after the deadline. Sets finalized flag. - * @param e3Id The E3 identifier - * @return committee The final committee addresses - */ - function finalizeCommittee( - uint256 e3Id - ) external returns (address[] memory committee) { - SortitionState storage state = sortitions[e3Id]; - - require(state.threshold > 0, CommitteeNotInitialized()); - require(!state.finalized, CommitteeAlreadyFinalized()); - require( - block.timestamp > state.submissionDeadline, - SubmissionWindowNotClosed() - ); - - state.finalized = true; - committee = state.topNodes; - - emit CommitteeFinalized(e3Id, committee); - } - - // ====================== - // View Functions - // ====================== - - /** - * @notice Get the current top N nodes for an E3 - * @param e3Id The E3 identifier - * @return Array of top N node addresses - */ - function getTopNodes( - uint256 e3Id - ) external view returns (address[] memory) { - return sortitions[e3Id].topNodes; - } - - /** - * @notice Get a node's submission for an E3 - * @param e3Id The E3 identifier - * @param node The node address - * @return The ticket submission - */ - function getSubmission( - uint256 e3Id, - address node - ) external view returns (TicketSubmission memory) { - return sortitions[e3Id].submissions[node]; - } - - /** - * @notice Compute the score for a ticket - * @dev Public function to allow off-chain computation verification - * @param node Node address - * @param ticketNumber Ticket number (1 to N) - * @param e3Id E3 identifier - * @param seed Random seed - * @return The computed score - */ - function computeTicketScore( - address node, - uint256 ticketNumber, - uint256 e3Id, - uint256 seed - ) external pure returns (uint256) { - return _computeTicketScore(node, ticketNumber, e3Id, seed); - } - - /** - * @notice Get sortition information for an E3 - * @param e3Id The E3 identifier - * @return threshold Number of committee members needed - * @return seed Random seed - * @return requestBlock Block number when E3 was requested - * @return submissionDeadline Timestamp when submission window closes - * @return finalized Whether committee has been finalized - */ - function getSortitionInfo( - uint256 e3Id - ) - external - view - returns ( - uint256 threshold, - uint256 seed, - uint256 requestBlock, - uint256 submissionDeadline, - bool finalized - ) - { - SortitionState storage state = sortitions[e3Id]; - return ( - state.threshold, - state.seed, - state.requestBlock, - state.submissionDeadline, - state.finalized - ); - } - - // ====================== - // Internal Functions - // ====================== - - /** - * @notice Computes score = keccak256(node || ticketNumber || e3Id || seed) - */ - function _computeTicketScore( - address node, - uint256 ticketNumber, - uint256 e3Id, - uint256 seed - ) internal pure returns (uint256) { - bytes32 hash = keccak256( - abi.encodePacked(node, ticketNumber, e3Id, seed) - ); - return uint256(hash); - } - - /** - * @notice Validates that a node is eligible to participate - * @dev Uses snapshot of ticket balance at E3 request block for deterministic validation - */ - function _validateNodeEligibility( - address node, - uint256 ticketNumber, - uint256 e3Id - ) internal view { - if (ticketNumber == 0) revert InvalidTicketNumber(); - - SortitionState storage state = sortitions[e3Id]; - - // Get ticket balance at the time E3 was requested (snapshot) - uint256 ticketBalance = bondingRegistry.getTicketBalanceAtBlock( - node, - state.requestBlock - ); - uint256 ticketPrice = bondingRegistry.ticketPrice(); - - if (ticketPrice == 0) revert InvalidTicketNumber(); - - // Calculate available tickets at snapshot - uint256 availableTickets = ticketBalance / ticketPrice; - - // Check ticket number is valid - if (ticketNumber > availableTickets) revert InvalidTicketNumber(); - - // Check node is eligible (has tickets at snapshot) - if (availableTickets == 0) revert NodeNotEligible(); - } - - /** - * @notice Try to insert node into top N sorted list - * @dev Maintains sorted order by score (lowest first) - * @return Whether node was added to top N - */ - function _tryInsertIntoTopN( - SortitionState storage state, - address node, - uint256 score - ) internal returns (bool) { - address[] storage topNodes = state.topNodes; - - // If list not full, insert in sorted order - if (topNodes.length < state.threshold) { - _insertSorted(state, node, score); - return true; - } - - // If list is full, only add if score is better than worst - uint256 worstScore = state - .submissions[topNodes[topNodes.length - 1]] - .score; - if (score < worstScore) { - topNodes.pop(); // Remove worst - _insertSorted(state, node, score); - return true; - } - - return false; - } - - /** - * @notice Insert node into sorted position (ascending by score) - */ - function _insertSorted( - SortitionState storage state, - address node, - uint256 score - ) internal { - address[] storage topNodes = state.topNodes; - - // Find insertion position - uint256 insertPos = topNodes.length; - for (uint256 i = 0; i < topNodes.length; i++) { - uint256 existingScore = state.submissions[topNodes[i]].score; - if (score < existingScore) { - insertPos = i; - break; - } - } - - // Insert at position - topNodes.push(address(0)); // Extend array - for (uint256 i = topNodes.length - 1; i > insertPos; i--) { - topNodes[i] = topNodes[i - 1]; - } - topNodes[insertPos] = node; - } -} diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index 3d02a7ddf6..a3b7695b4c 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -44,12 +44,11 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { bytes calldata ) external pure {} // solhint-disable-line no-empty-blocks - function getCommittee( + function getCommitteeNodes( uint256 - ) external pure returns (ICiphernodeRegistry.Committee memory) { + ) external pure returns (address[] memory) { address[] memory nodes = new address[](0); - uint32[2] memory threshold = [uint32(0), uint32(0)]; - return ICiphernodeRegistry.Committee(nodes, threshold, bytes32(0)); + return nodes; } function root() external pure returns (uint256) { @@ -73,6 +72,19 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { // solhint-disable-next-line no-empty-blocks function setBondingRegistry(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function submitTicket(uint256, uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function finalizeCommittee(uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setSortitionSubmissionWindow(uint256) external pure {} + + function isOpen(uint256) external pure returns (bool) { + return false; + } } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { @@ -108,12 +120,11 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { bytes calldata ) external pure {} // solhint-disable-line no-empty-blocks - function getCommittee( + function getCommitteeNodes( uint256 - ) external pure returns (ICiphernodeRegistry.Committee memory) { + ) external pure returns (address[] memory) { address[] memory nodes = new address[](0); - uint32[2] memory threshold = [uint32(0), uint32(0)]; - return ICiphernodeRegistry.Committee(nodes, threshold, bytes32(0)); + return nodes; } function root() external pure returns (uint256) { @@ -137,4 +148,17 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { // solhint-disable-next-line no-empty-blocks function setBondingRegistry(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setSortitionSubmissionWindow(uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function submitTicket(uint256, uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function finalizeCommittee(uint256) external pure {} + + function isOpen(uint256) external pure returns (bool) { + return false; + } } diff --git a/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts b/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts index e374e8f928..4819aa85d2 100644 --- a/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts +++ b/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts @@ -10,12 +10,13 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("CiphernodeRegistry", (m) => { const enclaveAddress = m.getParameter("enclaveAddress"); const owner = m.getParameter("owner"); + const submissionWindow = m.getParameter("submissionWindow"); const poseidonT3 = m.library("PoseidonT3"); const cipherNodeRegistry = m.contract( "CiphernodeRegistryOwnable", - [owner, enclaveAddress], + [owner, enclaveAddress, submissionWindow], { libraries: { PoseidonT3: poseidonT3, diff --git a/packages/enclave-contracts/ignition/modules/committeeSortition.ts b/packages/enclave-contracts/ignition/modules/committeeSortition.ts deleted file mode 100644 index 61d583eab5..0000000000 --- a/packages/enclave-contracts/ignition/modules/committeeSortition.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; - -export default buildModule("CommitteeSortition", (m) => { - const bondingRegistry = m.getParameter("bondingRegistry"); - const ciphernodeRegistry = m.getParameter("ciphernodeRegistry"); - - // TODO: 5 minutes is the default submission window - const submissionWindow = m.getParameter("submissionWindow", 300); - - const committeeSortition = m.contract("CommitteeSortition", [ - bondingRegistry, - ciphernodeRegistry, - submissionWindow, - ]); - - return { committeeSortition }; -}) as any; diff --git a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts index d15d332df6..eaebb43a7d 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts @@ -17,6 +17,7 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; export interface CiphernodeRegistryOwnableArgs { enclaveAddress?: string; owner?: string; + submissionWindow?: number; poseidonT3Address: string; hre: HardhatRuntimeEnvironment; } @@ -29,6 +30,7 @@ export interface CiphernodeRegistryOwnableArgs { export const deployAndSaveCiphernodeRegistryOwnable = async ({ enclaveAddress, owner, + submissionWindow, poseidonT3Address, hre, }: CiphernodeRegistryOwnableArgs): Promise<{ @@ -46,8 +48,11 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ if ( !enclaveAddress || !owner || + !submissionWindow || (preDeployedArgs?.constructorArgs?.enclaveAddress === enclaveAddress && - preDeployedArgs?.constructorArgs?.owner === owner) + preDeployedArgs?.constructorArgs?.owner === owner && + preDeployedArgs?.constructorArgs?.submissionWindow === + submissionWindow.toString()) ) { if (!preDeployedArgs?.address) { throw new Error( @@ -73,6 +78,7 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ const ciphernodeRegistry = await ciphernodeRegistryFactory.deploy( owner, enclaveAddress, + submissionWindow, ); await ciphernodeRegistry.waitForDeployment(); @@ -83,7 +89,11 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ storeDeploymentArgs( { - constructorArgs: { owner, enclaveAddress: enclaveAddress }, + constructorArgs: { + owner, + enclaveAddress: enclaveAddress, + submissionWindow: submissionWindow.toString(), + }, blockNumber, address: ciphernodeRegistryAddress, }, diff --git a/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts b/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts deleted file mode 100644 index dcee0c852d..0000000000 --- a/packages/enclave-contracts/scripts/deployAndSave/committeeSortition.ts +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; - -import { - CommitteeSortition, - CommitteeSortition__factory as CommitteeSortitionFactory, -} from "../../types"; -import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; - -/** - * The arguments for the deployAndSaveCommitteeSortition function - */ -export interface CommitteeSortitionArgs { - bondingRegistry?: string; - ciphernodeRegistry?: string; - submissionWindow?: number; - hre: HardhatRuntimeEnvironment; -} - -/** - * Deploys the CommitteeSortition contract and saves the deployment arguments - * @param param0 - The deployment arguments - * @returns The deployed CommitteeSortition contract - */ -export const deployAndSaveCommitteeSortition = async ({ - bondingRegistry, - ciphernodeRegistry, - submissionWindow = 300, // Default 5 minutes - hre, -}: CommitteeSortitionArgs): Promise<{ - committeeSortition: CommitteeSortition; -}> => { - const { ethers } = await hre.network.connect(); - const [signer] = await ethers.getSigners(); - const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; - - const preDeployedArgs = readDeploymentArgs("CommitteeSortition", chain); - - if ( - !bondingRegistry || - !ciphernodeRegistry || - (preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && - preDeployedArgs?.constructorArgs?.ciphernodeRegistry === - ciphernodeRegistry && - Number(preDeployedArgs?.constructorArgs?.submissionWindow) === - submissionWindow) - ) { - if (!preDeployedArgs?.address) { - throw new Error( - "CommitteeSortition address not found, it must be deployed first", - ); - } - const committeeSortitionContract = CommitteeSortitionFactory.connect( - preDeployedArgs.address, - signer, - ); - return { committeeSortition: committeeSortitionContract }; - } - - const committeeSortitionFactory = - await ethers.getContractFactory("CommitteeSortition"); - - const committeeSortition = await committeeSortitionFactory.deploy( - bondingRegistry, - ciphernodeRegistry, - submissionWindow, - ); - - await committeeSortition.waitForDeployment(); - - const blockNumber = await ethers.provider.getBlockNumber(); - - const committeeSortitionAddress = await committeeSortition.getAddress(); - - storeDeploymentArgs( - { - constructorArgs: { - bondingRegistry, - ciphernodeRegistry, - submissionWindow: submissionWindow.toString(), - }, - blockNumber, - address: committeeSortitionAddress, - }, - "CommitteeSortition", - chain, - ); - - const committeeSortitionContract = CommitteeSortitionFactory.connect( - committeeSortitionAddress, - signer, - ); - - return { committeeSortition: committeeSortitionContract }; -}; diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 43921657ba..0595fc440b 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -8,7 +8,6 @@ import hre from "hardhat"; import { autoCleanForLocalhost } from "./cleanIgnitionState"; import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; -import { deployAndSaveCommitteeSortition } from "./deployAndSave/committeeSortition"; import { deployAndSaveEnclave } from "./deployAndSave/enclave"; import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; @@ -41,6 +40,7 @@ export const deployEnclave = async (withMocks?: boolean) => { ); const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; + const SORTITION_SUBMISSION_WINDOW = 300; const addressOne = "0x0000000000000000000000000000000000000001"; const poseidonT3 = await deployAndSavePoseidonT3({ hre }); @@ -110,20 +110,12 @@ export const deployEnclave = async (withMocks?: boolean) => { poseidonT3Address: poseidonT3, enclaveAddress: addressOne, owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, hre, }); const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); console.log("CiphernodeRegistry deployed to:", ciphernodeRegistryAddress); - console.log("Deploying CommitteeSortition..."); - const { committeeSortition } = await deployAndSaveCommitteeSortition({ - bondingRegistry: bondingRegistryAddress, - ciphernodeRegistry: ciphernodeRegistryAddress, - hre, - }); - const committeeSortitionAddress = await committeeSortition.getAddress(); - console.log("CommitteeSortition deployed to:", committeeSortitionAddress); - console.log("Deploying Enclave..."); const { enclave } = await deployAndSaveEnclave({ params: [encoded], @@ -165,9 +157,6 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting Enclave as reward distributor in BondingRegistry..."); await bondingRegistry.setRewardDistributor(enclaveAddress); - console.log("Setting CommitteeSortition address in CiphernodeRegistry..."); - await ciphernodeRegistry.setCommitteeSortition(committeeSortitionAddress); - if (shouldDeployMocks) { const { decryptionVerifierAddress, e3ProgramAddress } = await deployMocks(); @@ -206,7 +195,6 @@ export const deployEnclave = async (withMocks?: boolean) => { EnclaveTicketToken: ${enclaveTicketTokenAddress} SlashingManager: ${slashingManagerAddress} BondingRegistry: ${bondingRegistryAddress} - CommitteeSortition: ${committeeSortitionAddress} CiphernodeRegistry: ${ciphernodeRegistryAddress} Enclave: ${enclaveAddress} ============================================ diff --git a/packages/enclave-contracts/test/CommitteeSortition.spec.ts b/packages/enclave-contracts/test/CommitteeSortition.spec.ts deleted file mode 100644 index 46f123c3f2..0000000000 --- a/packages/enclave-contracts/test/CommitteeSortition.spec.ts +++ /dev/null @@ -1,542 +0,0 @@ -// // SPDX-License-Identifier: LGPL-3.0-only -// // -// // This file is provided WITHOUT ANY WARRANTY; -// // without even the implied warranty of MERCHANTABILITY -// // or FITNESS FOR A PARTICULAR PURPOSE. -// import { expect } from "chai"; -// import { network } from "hardhat"; - -// import BondingRegistryModule from "../ignition/modules/bondingRegistry"; -// import CommitteeSortitionModule from "../ignition/modules/committeeSortition"; -// import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; -// import EnclaveTokenModule from "../ignition/modules/enclaveToken"; -// import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; -// import MockStableTokenModule from "../ignition/modules/mockStableToken"; -// import SlashingManagerModule from "../ignition/modules/slashingManager"; -// import { -// BondingRegistry__factory as BondingRegistryFactory, -// CommitteeSortition__factory as CommitteeSortitionFactory, -// EnclaveTicketToken__factory as EnclaveTicketTokenFactory, -// MockUSDC__factory as MockUSDCFactory, -// } from "../types"; - -// const { ethers, networkHelpers, ignition } = await network.connect(); -// const { loadFixture } = networkHelpers; - -// describe("CommitteeSortition", function () { -// const SUBMISSION_WINDOW = 300; // 5 minutes -// const TICKET_PRICE = ethers.parseEther("10"); -// const E3_ID = 1; -// const THRESHOLD = 3; -// const SEED = 12345; -// const AddressOne = "0x0000000000000000000000000000000000000001"; - -// async function deployFixture() { -// const [owner, ciphernodeRegistry, node1, node2, node3, node4] = -// await ethers.getSigners(); - -// const ownerAddress = await owner.getAddress(); - -// // Deploy token contracts -// const usdcContract = await ignition.deploy(MockStableTokenModule, { -// parameters: { -// MockUSDC: { -// initialSupply: 1000000, -// }, -// }, -// }); - -// const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { -// parameters: { -// EnclaveToken: { -// owner: ownerAddress, -// }, -// }, -// }); - -// const ticketTokenContract = await ignition.deploy( -// EnclaveTicketTokenModule, -// { -// parameters: { -// EnclaveTicketToken: { -// baseToken: await usdcContract.mockUSDC.getAddress(), -// registry: AddressOne, -// owner: ownerAddress, -// }, -// }, -// }, -// ); - -// const slashingManagerContract = await ignition.deploy( -// SlashingManagerModule, -// { -// parameters: { -// SlashingManager: { -// admin: ownerAddress, -// bondingRegistry: AddressOne, -// }, -// }, -// }, -// ); - -// const bondingRegistryContract = await ignition.deploy( -// BondingRegistryModule, -// { -// parameters: { -// BondingRegistry: { -// owner: ownerAddress, -// ticketToken: -// await ticketTokenContract.enclaveTicketToken.getAddress(), -// licenseToken: await enclTokenContract.enclaveToken.getAddress(), -// registry: AddressOne, -// slashedFundsTreasury: ownerAddress, -// ticketPrice: TICKET_PRICE, -// licenseRequiredBond: ethers.parseEther("1000"), -// minTicketBalance: 1, -// exitDelay: 7 * 24 * 60 * 60, -// }, -// }, -// }, -// ); - -// const committeeSortitionContract = await ignition.deploy( -// CommitteeSortitionModule, -// { -// parameters: { -// CommitteeSortition: { -// bondingRegistry: -// await bondingRegistryContract.bondingRegistry.getAddress(), -// ciphernodeRegistry: ciphernodeRegistry.address, -// submissionWindow: SUBMISSION_WINDOW, -// }, -// }, -// }, -// ); - -// const bondingRegistry = BondingRegistryFactory.connect( -// await bondingRegistryContract.bondingRegistry.getAddress(), -// owner, -// ); -// const committeeSortition = CommitteeSortitionFactory.connect( -// await committeeSortitionContract.committeeSortition.getAddress(), -// owner, -// ); - -// const usdcToken = MockUSDCFactory.connect( -// await usdcContract.mockUSDC.getAddress(), -// owner, -// ); -// const ticketToken = EnclaveTicketTokenFactory.connect( -// await ticketTokenContract.enclaveTicketToken.getAddress(), -// owner, -// ); - -// // Deploy a mock ciphernode registry for testing -// const mockRegistry = await ignition.deploy( -// MockCiphernodeRegistryEmptyKeyModule, -// ); - -// // Set up cross-contract dependencies -// await ticketToken.setRegistry(await bondingRegistry.getAddress()); -// await bondingRegistry.setRegistry( -// await mockRegistry.mockCiphernodeRegistryEmptyKey.getAddress(), -// ); -// await bondingRegistry.setSlashingManager( -// await slashingManagerContract.slashingManager.getAddress(), -// ); -// await slashingManagerContract.slashingManager.setBondingRegistry( -// await bondingRegistry.getAddress(), -// ); - -// // Set up licensed operators with ticket balances -// const licenseToken = EnclaveTicketTokenFactory.connect( -// await enclTokenContract.enclaveToken.getAddress(), -// owner, -// ); -// const licenseAmount = ethers.parseEther("1000"); // Min license bond - -// // Whitelist bonding registry for license token transfers -// await enclTokenContract.enclaveToken.whitelistContracts( -// await bondingRegistry.getAddress(), -// ethers.ZeroAddress, -// ); - -// for (const node of [node1, node2, node3, node4]) { -// const nodeTickets = -// node === node1 ? 5 : node === node2 ? 3 : node === node3 ? 7 : 2; -// const ticketAmount = TICKET_PRICE * BigInt(nodeTickets); - -// // Bond license first -// await enclTokenContract.enclaveToken.mintAllocation( -// node.address, -// licenseAmount, -// "Test allocation", -// ); -// await licenseToken -// .connect(node) -// .approve(await bondingRegistry.getAddress(), licenseAmount); -// await bondingRegistry.connect(node).bondLicense(licenseAmount); - -// // Then register operator -// await bondingRegistry.connect(node).registerOperator(); - -// // Mint USDC to node and have them add ticket balance through bonding registry -// await usdcToken.mint(node.address, ticketAmount); - -// // Node approves ticket token to spend USDC (needed for depositFrom) -// await usdcToken -// .connect(node) -// .approve(await ticketToken.getAddress(), ticketAmount); - -// // Node adds ticket balance (this will call ticketToken.depositFrom internally) -// await bondingRegistry.connect(node).addTicketBalance(ticketAmount); -// } - -// return { -// committeeSortition, -// bondingRegistry, -// owner, -// ciphernodeRegistry, -// node1, -// node2, -// node3, -// node4, -// }; -// } - -// describe("Initialization", function () { -// it("Should initialize sortition correctly", async function () { -// const { committeeSortition, ciphernodeRegistry } = -// await loadFixture(deployFixture); -// const requestBlock = await ethers.provider.getBlockNumber(); - -// await committeeSortition -// .connect(ciphernodeRegistry) -// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - -// const [threshold, seed, reqBlock, deadline, finalized] = -// await committeeSortition.getSortitionInfo(E3_ID); - -// expect(threshold).to.equal(THRESHOLD); -// expect(seed).to.equal(SEED); -// expect(reqBlock).to.equal(requestBlock); -// expect(finalized).to.be.false; -// expect(deadline).to.be.gt(0); -// }); - -// it("Should revert if not called by ciphernode registry", async function () { -// const { committeeSortition, owner } = await loadFixture(deployFixture); -// const requestBlock = await ethers.provider.getBlockNumber(); - -// await expect( -// committeeSortition -// .connect(owner) -// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "OnlyCiphernodeRegistry", -// ); -// }); - -// it("Should revert if already initialized", async function () { -// const { committeeSortition, ciphernodeRegistry } = -// await loadFixture(deployFixture); -// const requestBlock = await ethers.provider.getBlockNumber(); - -// await committeeSortition -// .connect(ciphernodeRegistry) -// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - -// await expect( -// committeeSortition -// .connect(ciphernodeRegistry) -// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "CommitteeAlreadyFinalized", -// ); -// }); -// }); - -// describe("Ticket Submission", function () { -// async function initializeFixture() { -// const fixture = await deployFixture(); -// const requestBlock = await ethers.provider.getBlockNumber(); -// await fixture.committeeSortition -// .connect(fixture.ciphernodeRegistry) -// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); -// return { ...fixture, requestBlock }; -// } - -// it("Should submit ticket successfully", async function () { -// const { committeeSortition, node1 } = -// await loadFixture(initializeFixture); -// const ticketNumber = 1; - -// const tx = await committeeSortition -// .connect(node1) -// .submitTicket(E3_ID, ticketNumber); -// await expect(tx).to.emit(committeeSortition, "TicketSubmitted"); - -// const submission = await committeeSortition.getSubmission( -// E3_ID, -// node1.address, -// ); -// expect(submission.exists).to.be.true; -// expect(submission.ticketNumber).to.equal(ticketNumber); -// }); - -// it("Should track top N nodes correctly", async function () { -// const { committeeSortition, node1, node2, node3, node4 } = -// await loadFixture(initializeFixture); -// // Submit tickets from multiple nodes -// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); -// await committeeSortition.connect(node2).submitTicket(E3_ID, 1); -// await committeeSortition.connect(node3).submitTicket(E3_ID, 1); -// await committeeSortition.connect(node4).submitTicket(E3_ID, 1); - -// const topNodes = await committeeSortition.getTopNodes(E3_ID); -// expect(topNodes.length).to.equal(THRESHOLD); -// }); - -// it("Should revert if ticket number is 0", async function () { -// const { committeeSortition, node1 } = -// await loadFixture(initializeFixture); -// await expect( -// committeeSortition.connect(node1).submitTicket(E3_ID, 0), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "InvalidTicketNumber", -// ); -// }); - -// it("Should revert if ticket number exceeds available tickets", async function () { -// const { committeeSortition, node1 } = -// await loadFixture(initializeFixture); -// await expect( -// committeeSortition.connect(node1).submitTicket(E3_ID, 100), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "InvalidTicketNumber", -// ); -// }); - -// it("Should revert if node already submitted", async function () { -// const { committeeSortition, node1 } = -// await loadFixture(initializeFixture); -// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); - -// await expect( -// committeeSortition.connect(node1).submitTicket(E3_ID, 2), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "NodeAlreadySubmitted", -// ); -// }); - -// it("Should revert if node has no tickets", async function () { -// const { committeeSortition } = await loadFixture(initializeFixture); -// // Create a completely fresh wallet -// const nodeWithNoTickets = ethers.Wallet.createRandom().connect( -// ethers.provider, -// ); - -// // Fund it with ETH for gas but don't set ticket balance -// const [funder] = await ethers.getSigners(); -// await funder.sendTransaction({ -// to: nodeWithNoTickets.address, -// value: ethers.parseEther("1"), -// }); - -// // When a node has 0 tickets and tries to submit ticket 1, -// // it will revert with InvalidTicketNumber (since 1 > 0 available tickets) -// await expect( -// committeeSortition.connect(nodeWithNoTickets).submitTicket(E3_ID, 1), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "InvalidTicketNumber", -// ); -// }); - -// it("Should revert if submission window closed", async function () { -// const { committeeSortition, node1 } = -// await loadFixture(initializeFixture); -// // Fast forward time beyond submission window -// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); -// await ethers.provider.send("evm_mine", []); - -// await expect( -// committeeSortition.connect(node1).submitTicket(E3_ID, 1), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "SubmissionWindowClosed", -// ); -// }); - -// it("Should compute scores correctly", async function () { -// const { committeeSortition, node1 } = -// await loadFixture(initializeFixture); -// const ticketNumber = 1; - -// // Compute expected score off-chain -// const expectedScore = await committeeSortition.computeTicketScore( -// node1.address, -// ticketNumber, -// E3_ID, -// SEED, -// ); - -// await committeeSortition.connect(node1).submitTicket(E3_ID, ticketNumber); - -// const submission = await committeeSortition.getSubmission( -// E3_ID, -// node1.address, -// ); -// expect(submission.score).to.equal(expectedScore); -// }); -// }); - -// describe("Committee Finalization", function () { -// async function finalizeFixture() { -// const fixture = await deployFixture(); -// const requestBlock = await ethers.provider.getBlockNumber(); -// await fixture.committeeSortition -// .connect(fixture.ciphernodeRegistry) -// .initializeSortition(E3_ID, THRESHOLD, SEED, requestBlock); - -// // Submit tickets from nodes -// await fixture.committeeSortition -// .connect(fixture.node1) -// .submitTicket(E3_ID, 1); -// await fixture.committeeSortition -// .connect(fixture.node2) -// .submitTicket(E3_ID, 1); -// await fixture.committeeSortition -// .connect(fixture.node3) -// .submitTicket(E3_ID, 1); -// return { ...fixture, requestBlock }; -// } - -// it("Should finalize committee after deadline", async function () { -// const { committeeSortition, owner } = await loadFixture(finalizeFixture); -// // Fast forward time -// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); -// await ethers.provider.send("evm_mine", []); - -// const tx = await committeeSortition -// .connect(owner) -// .finalizeCommittee(E3_ID); -// await expect(tx).to.emit(committeeSortition, "CommitteeFinalized"); - -// const [, , , , finalized] = -// await committeeSortition.getSortitionInfo(E3_ID); -// expect(finalized).to.be.true; -// }); - -// it("Should revert if finalized before deadline", async function () { -// const { committeeSortition, owner } = await loadFixture(finalizeFixture); -// await expect( -// committeeSortition.connect(owner).finalizeCommittee(E3_ID), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "SubmissionWindowNotClosed", -// ); -// }); - -// it("Should revert if already finalized", async function () { -// const { committeeSortition, owner } = await loadFixture(finalizeFixture); -// // Fast forward time -// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); -// await ethers.provider.send("evm_mine", []); - -// await committeeSortition.connect(owner).finalizeCommittee(E3_ID); - -// await expect( -// committeeSortition.connect(owner).finalizeCommittee(E3_ID), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "CommitteeAlreadyFinalized", -// ); -// }); - -// it("Should return correct committee", async function () { -// const { committeeSortition, owner } = await loadFixture(finalizeFixture); -// // Fast forward time -// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); -// await ethers.provider.send("evm_mine", []); - -// const committee = await committeeSortition -// .connect(owner) -// .finalizeCommittee.staticCall(E3_ID); - -// expect(committee.length).to.equal(THRESHOLD); -// }); - -// it("Should prevent submissions after finalization", async function () { -// const { committeeSortition, owner, node4 } = -// await loadFixture(finalizeFixture); -// // Fast forward time -// await ethers.provider.send("evm_increaseTime", [SUBMISSION_WINDOW + 1]); -// await ethers.provider.send("evm_mine", []); - -// await committeeSortition.connect(owner).finalizeCommittee(E3_ID); - -// // Try to submit - should fail because submission window is closed -// // Note: The contract checks submission window before checking if finalized -// await expect( -// committeeSortition.connect(node4).submitTicket(E3_ID, 1), -// ).to.be.revertedWithCustomError( -// committeeSortition, -// "SubmissionWindowClosed", -// ); -// }); -// }); - -// describe("Score Sorting", function () { -// async function scoreSortingFixture() { -// const fixture = await deployFixture(); -// const requestBlock = await ethers.provider.getBlockNumber(); -// await fixture.committeeSortition -// .connect(fixture.ciphernodeRegistry) -// .initializeSortition(E3_ID, 2, SEED, requestBlock); // Threshold of 2 -// return { ...fixture, requestBlock }; -// } - -// it("Should maintain sorted order (lowest scores first)", async function () { -// const { committeeSortition, node1, node2, node3 } = -// await loadFixture(scoreSortingFixture); -// // Submit tickets -// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); -// await committeeSortition.connect(node2).submitTicket(E3_ID, 1); -// await committeeSortition.connect(node3).submitTicket(E3_ID, 1); - -// const topNodes = await committeeSortition.getTopNodes(E3_ID); -// expect(topNodes.length).to.equal(2); - -// // Verify scores are in ascending order -// const score1 = ( -// await committeeSortition.getSubmission(E3_ID, topNodes[0]) -// ).score; -// const score2 = ( -// await committeeSortition.getSubmission(E3_ID, topNodes[1]) -// ).score; - -// expect(score1).to.be.lte(score2); -// }); - -// it("Should replace worst node when better score arrives", async function () { -// const { committeeSortition, node1, node2, node3 } = -// await loadFixture(scoreSortingFixture); -// await committeeSortition.connect(node1).submitTicket(E3_ID, 1); -// await committeeSortition.connect(node2).submitTicket(E3_ID, 1); - -// // const topNodesBefore = await committeeSortition.getTopNodes(E3_ID); - -// // Submit from node3 - should replace worst if score is better -// await committeeSortition.connect(node3).submitTicket(E3_ID, 1); - -// const topNodesAfter = await committeeSortition.getTopNodes(E3_ID); -// expect(topNodesAfter.length).to.equal(2); -// }); -// }); -// }); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index d8a0582cee..c3d10066b3 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -11,7 +11,6 @@ import { poseidon2 } from "poseidon-lite"; import BondingRegistryModule from "../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../ignition/modules/ciphernodeRegistry"; -import CommitteeSortitionModule from "../ignition/modules/committeeSortition"; import EnclaveModule from "../ignition/modules/enclave"; import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../ignition/modules/enclaveToken"; @@ -35,6 +34,7 @@ const { loadFixture, time, mine } = networkHelpers; describe("Enclave", function () { const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; + const SORTITION_SUBMISSION_WINDOW = 300; const addressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; @@ -168,6 +168,7 @@ describe("Enclave", function () { CiphernodeRegistry: { enclaveAddress: enclaveAddress, owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, }, }, }); @@ -175,17 +176,6 @@ describe("Enclave", function () { const ciphernodeRegistryAddress = await ciphernodeRegistry.cipherNodeRegistry.getAddress(); - const committeeSortition = await ignition.deploy(CommitteeSortitionModule, { - parameters: { - CommitteeSortition: { - bondingRegistry: - await bondingRegistryContract.bondingRegistry.getAddress(), - ciphernodeRegistry: ciphernodeRegistryAddress, - submissionWindow: 300, - }, - }, - }); - const enclave = EnclaveFactory.connect(enclaveAddress, owner); const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, @@ -197,8 +187,8 @@ describe("Enclave", function () { await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); } - await ciphernodeRegistryContract.setCommitteeSortition( - await committeeSortition.committeeSortition.getAddress(), + await ciphernodeRegistryContract.setBondingRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), ); await ticketTokenContract.enclaveTicketToken.setRegistry( diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 750d8ee7a4..5d8439ab36 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -7,10 +7,10 @@ import { LeanIMT } from "@zk-kit/lean-imt"; import { expect } from "chai"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; +import { toASCII } from "punycode"; import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; -import CommitteeSortitionModule from "../../ignition/modules/committeeSortition"; import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; @@ -26,6 +26,7 @@ const { loadFixture } = networkHelpers; const data = "0xda7a"; const dataHash = ethers.keccak256(data); +const SORTITION_SUBMISSION_WINDOW = 300; // Hash function used to compute the tree nodes. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); @@ -102,6 +103,7 @@ describe("CiphernodeRegistryOwnable", function () { CiphernodeRegistry: { enclaveAddress: ownerAddress, owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, }, }, }); @@ -109,17 +111,6 @@ describe("CiphernodeRegistryOwnable", function () { const registryAddress = await registryContract.cipherNodeRegistry.getAddress(); - const committeeSortition = await ignition.deploy(CommitteeSortitionModule, { - parameters: { - CommitteeSortition: { - bondingRegistry: - await bondingRegistryContract.bondingRegistry.getAddress(), - ciphernodeRegistry: registryAddress, - submissionWindow: 300, - }, - }, - }); - const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); // Set up cross-contract dependencies @@ -134,8 +125,8 @@ describe("CiphernodeRegistryOwnable", function () { await bondingRegistryContract.bondingRegistry.getAddress(), ); - await registry.setCommitteeSortition( - await committeeSortition.committeeSortition.getAddress(), + await registry.setBondingRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), ); const tree = new LeanIMT(hash); @@ -173,9 +164,13 @@ describe("CiphernodeRegistryOwnable", function () { const ciphernodeRegistry = await ciphernodeRegistryFactory.deploy( deployer.address, AddressTwo, + SORTITION_SUBMISSION_WINDOW, ); expect(await ciphernodeRegistry.owner()).to.equal(deployer.address); expect(await ciphernodeRegistry.enclave()).to.equal(AddressTwo); + expect(await ciphernodeRegistry.sortitionSubmissionWindow()).to.equal( + SORTITION_SUBMISSION_WINDOW, + ); }); }); @@ -196,12 +191,31 @@ describe("CiphernodeRegistryOwnable", function () { }); it("emits a CommitteeRequested event", async function () { const { registry, request } = await loadFixture(setup); - const blockNumber = (await ethers.provider.getBlockNumber()) + 1; - await expect( - registry.requestCommittee(request.e3Id, 0, request.threshold), - ) + + const tx = await registry.requestCommittee( + request.e3Id, + 0n, + request.threshold, + ); + const receipt = await tx.wait(); + if (!receipt) throw new Error("Transaction failed"); + + const sWindow = await registry.sortitionSubmissionWindow(); + const block = await ethers.provider.getBlock(receipt.blockNumber); + if (!block) throw new Error("Block not found"); + + const expectedBlockNumber = BigInt(receipt.blockNumber); + const expectedDeadline = BigInt(block.timestamp) + sWindow; + + await expect(tx) .to.emit(registry, "CommitteeRequested") - .withArgs(request.e3Id, 0, request.threshold, blockNumber); + .withArgs( + request.e3Id, + 0n, + request.threshold, + expectedBlockNumber, + expectedDeadline, + ); }); it("returns true if the request is successful", async function () { const { registry, request } = await loadFixture(setup); From 21bc5184883a188b4d378a89dd55c63ab46d16a7 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sun, 26 Oct 2025 22:24:54 +0500 Subject: [PATCH 46/88] feat: ciphernode contract methods in crates --- .../src/ciphernode_builder.rs | 45 +-- .../src/enclave_event/committee_requested.rs | 31 ++ crates/events/src/enclave_event/mod.rs | 10 + crates/evm/src/ciphernode_registry_sol.rs | 170 ++++++++- crates/evm/src/committee_sortition_sol.rs | 358 ------------------ crates/evm/src/lib.rs | 4 - crates/evm/src/repo.rs | 13 - crates/keyshare/src/ext.rs | 254 +++++++++---- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 52 ++- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../interfaces/ICiphernodeRegistry.sol | 17 + .../registry/CiphernodeRegistryOwnable.sol | 17 - .../enclave-contracts/deployed_contracts.json | 96 ++++- 14 files changed, 559 insertions(+), 512 deletions(-) create mode 100644 crates/events/src/enclave_event/committee_requested.rs delete mode 100644 crates/evm/src/committee_sortition_sol.rs diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 1f2e882765..19a34c66c7 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -23,8 +23,7 @@ use e3_evm::{ ProviderConfig, }, BondingRegistryReaderRepositoryFactory, BondingRegistrySol, - CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, - CommitteeSortitionReaderRepositoryFactory, CommitteeSortitionSol, EnclaveSol, EnclaveSolReader, + CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, EnclaveSol, EnclaveSolReader, EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, }; use e3_fhe::ext::FheExtension; @@ -371,48 +370,6 @@ impl CiphernodeBuilder { } } } - - if self.contract_components.committee_sortition { - if let Some(committee_sortition_contract) = &chain.contracts.committee_sortition { - match provider_cache - .ensure_write_provider(&repositories, chain, cipher) - .await - { - Ok(write_provider) => { - let enable_finalizer = self.pubkey_agg; - CommitteeSortitionSol::attach_with_finalizer( - &local_bus, - write_provider.clone(), - &committee_sortition_contract.address(), - &repositories.committee_sortition_reader(write_provider.chain_id()), - committee_sortition_contract.deploy_block(), - chain.rpc_url.clone(), - enable_finalizer, - ) - .await?; - } - Err(e) => { - return Err(anyhow::anyhow!( - "Score sortition enabled but no wallet configured for node. \ - All nodes must have wallets to submit tickets. Error: {}", - e - )); - } - } - } else { - info!( - "📍 DISTANCE SORTITION MODE (CommitteeSortition contract not configured)" - ); - if self.pubkey_agg { - info!(" Role: AGGREGATOR (will publish committees immediately)"); - } - } - } else { - info!("📍 DISTANCE SORTITION MODE"); - if self.pubkey_agg { - info!(" Role: AGGREGATOR (will publish committees immediately)"); - } - } } // E3 specific setup diff --git a/crates/events/src/enclave_event/committee_requested.rs b/crates/events/src/enclave_event/committee_requested.rs new file mode 100644 index 0000000000..ebe8b4a08c --- /dev/null +++ b/crates/events/src/enclave_event/committee_requested.rs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::{E3id, Seed}; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeRequested { + pub e3_id: E3id, + pub seed: Seed, + pub threshold: [usize; 2], + pub request_block: u64, + pub submission_deadline: u64, + pub chain_id: u64, +} + +impl Display for CommitteeRequested { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, seed: {:?}, threshold: [{}, {}], request_block: {}, submission_deadline: {}, chain_id: {}", + self.e3_id, self.seed, self.threshold[0], self.threshold[1], self.request_block, self.submission_deadline, self.chain_id + ) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 06b10753d6..98fcac9c4d 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -10,6 +10,7 @@ mod ciphernode_selected; mod ciphertext_output_published; mod committee_finalized; mod committee_published; +mod committee_requested; mod compute_request; mod configuration_updated; mod decryptionshare_created; @@ -35,6 +36,7 @@ pub use ciphernode_selected::*; pub use ciphertext_output_published::*; pub use committee_finalized::*; pub use committee_published::*; +pub use committee_requested::*; pub use compute_request::*; pub use configuration_updated::*; pub use decryptionshare_created::*; @@ -137,6 +139,10 @@ pub enum EnclaveEvent { id: EventId, data: CommitteePublished, }, + CommitteeRequested { + id: EventId, + data: CommitteeRequested, + }, CommitteeFinalized { id: EventId, data: CommitteeFinalized, @@ -235,6 +241,7 @@ impl From for EventId { EnclaveEvent::ConfigurationUpdated { id, .. } => id, EnclaveEvent::OperatorActivationChanged { id, .. } => id, EnclaveEvent::CommitteePublished { id, .. } => id, + EnclaveEvent::CommitteeRequested { id, .. } => id, EnclaveEvent::PlaintextOutputPublished { id, .. } => id, EnclaveEvent::EnclaveError { id, .. } => id, EnclaveEvent::E3RequestComplete { id, .. } => id, @@ -260,6 +267,7 @@ impl EnclaveEvent { EnclaveEvent::CiphernodeSelected { data, .. } => Some(data.e3_id), EnclaveEvent::ThresholdShareCreated { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteePublished { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteeRequested { data, .. } => Some(data.e3_id), EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteeFinalized { data, .. } => Some(data.e3_id), EnclaveEvent::TicketSubmitted { data, .. } => Some(data.e3_id), @@ -282,6 +290,7 @@ impl EnclaveEvent { EnclaveEvent::ConfigurationUpdated { data, .. } => format!("{:?}", data), EnclaveEvent::OperatorActivationChanged { data, .. } => format!("{:?}", data), EnclaveEvent::CommitteePublished { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteeRequested { data, .. } => format!("{:?}", data), EnclaveEvent::PlaintextOutputPublished { data, .. } => format!("{:?}", data), EnclaveEvent::E3RequestComplete { data, .. } => format!("{}", data), EnclaveEvent::EnclaveError { data, .. } => format!("{:?}", data), @@ -312,6 +321,7 @@ impl_from_event!( ConfigurationUpdated, OperatorActivationChanged, CommitteePublished, + CommitteeRequested, CommitteeFinalized, TicketSubmitted, PlaintextOutputPublished, diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index 9a3ecb460c..2b7470bdbc 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -16,8 +16,8 @@ use alloy::{ use anyhow::Result; use e3_data::Repository; use e3_events::{ - BusError, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, - PublicKeyAggregated, Shutdown, Subscribe, + BusError, CiphernodeSelected, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, + EventBus, OrderedSet, PublicKeyAggregated, Seed, Shutdown, Subscribe, }; use std::collections::HashMap; use tracing::{error, info, trace}; @@ -86,6 +86,73 @@ impl From for EnclaveEvent { } } +struct CommitteeRequestedWithChainId(pub ICiphernodeRegistry::CommitteeRequested, pub u64); + +impl From for e3_events::CommitteeRequested { + fn from(value: CommitteeRequestedWithChainId) -> Self { + e3_events::CommitteeRequested { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + seed: Seed(value.0.seed.to_be_bytes()), + threshold: [value.0.threshold[0] as usize, value.0.threshold[1] as usize], + request_block: value.0.requestBlock.to(), + submission_deadline: value.0.submissionDeadline.to(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: CommitteeRequestedWithChainId) -> Self { + let payload: e3_events::CommitteeRequested = value.into(); + EnclaveEvent::from(payload) + } +} + +struct CommitteeFinalizedWithChainId(pub ICiphernodeRegistry::CommitteeFinalized, pub u64); + +impl From for e3_events::CommitteeFinalized { + fn from(value: CommitteeFinalizedWithChainId) -> Self { + e3_events::CommitteeFinalized { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + committee: value + .0 + .committee + .iter() + .map(|addr| addr.to_string()) + .collect(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: CommitteeFinalizedWithChainId) -> Self { + let payload: e3_events::CommitteeFinalized = value.into(); + EnclaveEvent::from(payload) + } +} + +struct TicketSubmittedWithChainId(pub ICiphernodeRegistry::TicketSubmitted, pub u64); + +impl From for e3_events::TicketSubmitted { + fn from(value: TicketSubmittedWithChainId) -> Self { + e3_events::TicketSubmitted { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + node: value.0.node.to_string(), + ticket_id: value.0.ticketId.to(), + score: value.0.score.to_string(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: TicketSubmittedWithChainId) -> Self { + let payload: e3_events::TicketSubmitted = value.into(); + EnclaveEvent::from(payload) + } +} + pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { match topic { Some(&ICiphernodeRegistry::CiphernodeAdded::SIGNATURE_HASH) => { @@ -106,6 +173,33 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< event, chain_id, ))) } + Some(&ICiphernodeRegistry::CommitteeRequested::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CommitteeRequested::decode_log_data(data) else { + error!("Error parsing event CommitteeRequested after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(CommitteeRequestedWithChainId( + event, chain_id, + ))) + } + Some(&ICiphernodeRegistry::CommitteeFinalized::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CommitteeFinalized::decode_log_data(data) else { + error!("Error parsing event CommitteeFinalized after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(CommitteeFinalizedWithChainId( + event, chain_id, + ))) + } + Some(&ICiphernodeRegistry::TicketSubmitted::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::TicketSubmitted::decode_log_data(data) else { + error!("Error parsing event TicketSubmitted after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(TicketSubmittedWithChainId( + event, chain_id, + ))) + } _topic => { trace!( topic=?_topic, @@ -190,6 +284,11 @@ impl CiphernodeRegistrySolWriter .send(Subscribe::new("CommitteeFinalized", addr.clone().into())) .await; + // Subscribe to CiphernodeSelected for ticket submission + let _ = bus + .send(Subscribe::new("CiphernodeSelected", addr.clone().into())) + .await; + Ok(addr) } } @@ -217,6 +316,12 @@ impl Handler ctx.notify(data); } } + EnclaveEvent::CiphernodeSelected { data, .. } => { + // Submit ticket if chain matches and ticket_id is present + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), _ => (), } @@ -238,6 +343,45 @@ impl Handler } } +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: CiphernodeSelected, _: &mut Self::Context) -> Self::Result { + let e3_id = msg.e3_id.clone(); + let ticket_id = msg.ticket_id; + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + Box::pin(async move { + // Only submit if we have a ticket_id (score sortition) + let Some(ticket_id) = ticket_id else { + info!( + "No ticket_id for E3 {:?}, skipping ticket submission (distance sortition)", + e3_id + ); + return; + }; + + info!("Submitting ticket {} for E3 {:?}", ticket_id, e3_id); + + let result = + submit_ticket_to_registry(provider, contract_address, e3_id, ticket_id).await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Ticket submitted to registry"); + } + Err(err) => { + error!("Failed to submit ticket: {:?}", err); + bus.err(EnclaveErrorType::Evm, err); + } + } + }) + } +} + impl Handler for CiphernodeRegistrySolWriter

{ @@ -291,6 +435,28 @@ impl Handler } } +pub async fn submit_ticket_to_registry( + provider: EthProvider

, + contract_address: Address, + e3_id: E3id, + ticket_number: u64, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let ticket_number = U256::from(ticket_number); + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + let contract = ICiphernodeRegistry::new(contract_address, provider.provider()); + let builder = contract + .submitTicket(e3_id, ticket_number) + .nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + pub async fn publish_committee_to_registry( provider: EthProvider

, contract_address: Address, diff --git a/crates/evm/src/committee_sortition_sol.rs b/crates/evm/src/committee_sortition_sol.rs deleted file mode 100644 index d129b87ff8..0000000000 --- a/crates/evm/src/committee_sortition_sol.rs +++ /dev/null @@ -1,358 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use crate::{helpers::EthProvider, EvmEventReader, EvmEventReaderState}; -use actix::prelude::*; -use alloy::{ - primitives::{Address, LogData, B256, U256}, - providers::{Provider, WalletProvider}, - sol, - sol_types::SolEvent, -}; -use anyhow::Result; -use e3_data::Repository; -use e3_events::{BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, Shutdown, Subscribe}; -use tracing::{error, info, trace, warn}; - -sol!( - #[sol(rpc)] - #[derive(Debug)] - CommitteeSortition, - "../../packages/enclave-contracts/artifacts/contracts/sortition/CommitteeSortition.sol/CommitteeSortition.json" -); - -struct TicketSubmittedWithChainId(pub CommitteeSortition::TicketSubmitted, pub u64); - -impl From for e3_events::TicketSubmitted { - fn from(value: TicketSubmittedWithChainId) -> Self { - e3_events::TicketSubmitted { - e3_id: E3id::new(value.0.e3Id.to_string(), value.1), - node: value.0.node.to_string(), - ticket_id: value.0.ticketId.try_into().unwrap_or(0), - score: value.0.score.to_string(), - chain_id: value.1, - } - } -} - -impl From for EnclaveEvent { - fn from(value: TicketSubmittedWithChainId) -> Self { - let payload: e3_events::TicketSubmitted = value.into(); - EnclaveEvent::from(payload) - } -} - -struct CommitteeFinalizedWithChainId(pub CommitteeSortition::CommitteeFinalized, pub u64); - -impl From for e3_events::CommitteeFinalized { - fn from(value: CommitteeFinalizedWithChainId) -> Self { - e3_events::CommitteeFinalized { - e3_id: E3id::new(value.0.e3Id.to_string(), value.1), - committee: value - .0 - .committee - .iter() - .map(|addr| addr.to_string()) - .collect(), - chain_id: value.1, - } - } -} - -impl From for EnclaveEvent { - fn from(value: CommitteeFinalizedWithChainId) -> Self { - let payload: e3_events::CommitteeFinalized = value.into(); - EnclaveEvent::from(payload) - } -} - -pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { - match topic { - Some(&CommitteeSortition::TicketSubmitted::SIGNATURE_HASH) => { - let Ok(event) = CommitteeSortition::TicketSubmitted::decode_log_data(data) else { - error!("Error parsing event TicketSubmitted after topic was matched!"); - return None; - }; - Some(EnclaveEvent::from(TicketSubmittedWithChainId( - event, chain_id, - ))) - } - Some(&CommitteeSortition::CommitteeFinalized::SIGNATURE_HASH) => { - let Ok(event) = CommitteeSortition::CommitteeFinalized::decode_log_data(data) else { - error!("Error parsing event CommitteeFinalized after topic was matched!"); - return None; - }; - Some(EnclaveEvent::from(CommitteeFinalizedWithChainId( - event, chain_id, - ))) - } - _topic => { - trace!( - topic=?_topic, - "Unknown event was received by CommitteeSortition.sol parser but was ignored" - ); - None - } - } -} - -pub struct CommitteeSortitionSolReader; - -impl CommitteeSortitionSolReader { - pub async fn attach( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - repository: &Repository, - start_block: Option, - rpc_url: String, - ) -> Result>> { - let addr = EvmEventReader::attach( - provider, - extractor, - contract_address, - start_block, - &bus.clone().into(), - repository, - rpc_url, - ) - .await?; - - info!(address=%contract_address, "CommitteeSortitionSolReader is listening to address"); - - Ok(addr) - } -} - -/// Writer for CommitteeSortition contract -pub struct CommitteeSortitionSolWriter

{ - bus: Addr>, - provider: EthProvider

, - contract_address: Address, - is_aggregator: bool, -} - -impl CommitteeSortitionSolWriter

{ - pub async fn attach_with_finalizer( - bus: &Addr>, - provider: EthProvider

, - contract_address: Address, - is_aggregator: bool, - ) -> Result> { - let writer = Self { - bus: bus.clone(), - provider, - contract_address, - is_aggregator, - } - .start(); - - bus.send(Subscribe::new("E3Requested", writer.clone().recipient())) - .await?; - - Ok(writer) - } -} - -impl Handler - for CommitteeSortitionSolWriter

-{ - type Result = (); - - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { - match msg { - EnclaveEvent::CiphernodeSelected { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::E3Requested { data, .. } => { - if self.enable_finalizer { - ctx.notify(data); - } - } - EnclaveEvent::CommitteeFinalized { data, .. } => { - if self.enable_finalizer { - ctx.notify(data); - } - } - EnclaveEvent::Shutdown { data, .. } => { - ctx.notify(data); - } - _ => {} - } - } -} - -impl Handler - for CommitteeSortitionSolWriter

-{ - type Result = ResponseFuture<()>; - - fn handle( - &mut self, - data: e3_events::CiphernodeSelected, - _: &mut Self::Context, - ) -> Self::Result { - let e3_id = data.e3_id.clone(); - let provider = self.provider.clone(); - let contract_address = self.contract_address; - let bus = self.bus.clone(); - - let ticket_number = data.ticket_id; - - Box::pin(async move { - let Some(ticket) = ticket_number else { - info!( - "CiphernodeSelected: No ticket number (non-bonding backend), skipping ticket submission for E3 {:?}", - e3_id - ); - return; - }; - - info!( - "CiphernodeSelected: Submitting ticket {} for E3 {:?}", - ticket, e3_id - ); - - // Get the node's wallet address - let node_address = provider.provider().default_signer_address(); - - info!( - "Node {:?} submitting ticket {} for E3 {:?}", - node_address, ticket, e3_id - ); - - let writer = CommitteeSortitionSolWriter::new( - &bus, - provider.clone(), - contract_address, - false, - 60, - ) - .expect("Failed to create writer"); - - match writer.submit_ticket(e3_id.clone(), ticket).await { - Ok(receipt) => { - info!( - "Successfully submitted ticket for E3 {:?}, tx: {:?}", - e3_id, receipt.transaction_hash - ); - } - Err(e) => { - error!("Failed to submit ticket for E3 {:?}: {:?}", e3_id, e); - bus.err(EnclaveErrorType::Evm, e); - } - } - }) - } -} - -impl Handler - for CommitteeSortitionSolWriter

-{ - type Result = (); - - fn handle(&mut self, data: e3_events::E3Requested, ctx: &mut Self::Context) -> Self::Result { - info!( - "E3Requested for E3 {:?}, scheduling committee finalization", - data.e3_id - ); - self.schedule_finalization(data.e3_id, ctx); - } -} - -impl Handler - for CommitteeSortitionSolWriter

-{ - type Result = (); - - fn handle( - &mut self, - data: e3_events::CommitteeFinalized, - _: &mut Self::Context, - ) -> Self::Result { - // Remove from pending tracking since it's already finalized - if self.pending_e3s.remove(&data.e3_id).is_some() { - info!( - "CommitteeFinalized received for E3 {:?}, removed from pending", - data.e3_id - ); - } - } -} - -impl Handler - for CommitteeSortitionSolWriter

-{ - type Result = (); - - fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { - ctx.stop(); - } -} - -/// Wrapper for reader and writer -pub struct CommitteeSortitionSol; - -impl CommitteeSortitionSol { - /// Attach reader and writer (no automatic finalization) - pub async fn attach

( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - repository: &Repository, - start_block: Option, - rpc_url: String, - ) -> Result>> - where - P: Provider + WalletProvider + Clone + 'static, - { - Self::attach_with_finalizer( - bus, - provider, - contract_address, - repository, - start_block, - rpc_url, - false, - ) - .await - } - - /// Attach reader and writer with optional automatic committee finalization - /// The submission window is automatically fetched from the contract - pub async fn attach_with_finalizer

( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - repository: &Repository, - start_block: Option, - rpc_url: String, - enable_finalizer: bool, - ) -> Result>> - where - P: Provider + WalletProvider + Clone + 'static, - { - CommitteeSortitionSolReader::attach( - bus, - provider.clone(), - contract_address, - repository, - start_block, - rpc_url, - ) - .await?; - - let writer = CommitteeSortitionSolWriter::attach_with_finalizer( - bus, - provider, - contract_address, - enable_finalizer, - ) - .await?; - - Ok(writer) - } -} diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index c473568502..477e20798c 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -6,7 +6,6 @@ mod bonding_registry_sol; mod ciphernode_registry_sol; -mod committee_sortition_sol; mod enclave_sol; mod enclave_sol_reader; mod enclave_sol_writer; @@ -18,9 +17,6 @@ pub use bonding_registry_sol::{BondingRegistrySol, BondingRegistrySolReader}; pub use ciphernode_registry_sol::{ CiphernodeRegistrySol, CiphernodeRegistrySolReader, CiphernodeRegistrySolWriter, }; -pub use committee_sortition_sol::{ - CommitteeSortitionSol, CommitteeSortitionSolReader, CommitteeSortitionSolWriter, -}; pub use enclave_sol::EnclaveSol; pub use enclave_sol_reader::EnclaveSolReader; pub use enclave_sol_writer::EnclaveSolWriter; diff --git a/crates/evm/src/repo.rs b/crates/evm/src/repo.rs index c21f21a920..0455c09f04 100644 --- a/crates/evm/src/repo.rs +++ b/crates/evm/src/repo.rs @@ -54,16 +54,3 @@ impl BondingRegistryReaderRepositoryFactory for Repositories { ) } } - -pub trait CommitteeSortitionReaderRepositoryFactory { - fn committee_sortition_reader(&self, chain_id: u64) -> Repository; -} - -impl CommitteeSortitionReaderRepositoryFactory for Repositories { - fn committee_sortition_reader(&self, chain_id: u64) -> Repository { - Repository::new( - self.store - .scope(StoreKeys::committee_sortition_reader(chain_id)), - ) - } -} diff --git a/crates/keyshare/src/ext.rs b/crates/keyshare/src/ext.rs index 134c664e23..3a218f5945 100644 --- a/crates/keyshare/src/ext.rs +++ b/crates/keyshare/src/ext.rs @@ -13,12 +13,14 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use e3_crypto::Cipher; use e3_data::{AutoPersist, RepositoriesFactory}; -use e3_events::{BusError, EnclaveErrorType, EnclaveEvent, EventBus}; +use e3_events::{BusError, CiphernodeSelected, EnclaveErrorType, EnclaveEvent, EventBus}; use e3_fhe::ext::FHE_KEY; use e3_multithread::Multithread; -use e3_request::{E3Context, E3ContextSnapshot, E3Extension, META_KEY}; +use e3_request::{E3Context, E3ContextSnapshot, E3Extension, TypedKey, META_KEY}; use std::sync::Arc; +const CIPHERNODE_SELECTED_KEY: TypedKey = TypedKey::new("ciphernode_selected"); + pub struct KeyshareExtension { bus: Addr>, address: String, @@ -45,38 +47,91 @@ const ERROR_KEYSHARE_FHE_MISSING: &str = #[async_trait] impl E3Extension for KeyshareExtension { fn on_event(&self, ctx: &mut E3Context, evt: &EnclaveEvent) { - // if this is NOT a CiphernodeSelected event then ignore - let EnclaveEvent::CiphernodeSelected { data, .. } = evt else { - return; - }; + match evt { + // Store CiphernodeSelected data for later use + EnclaveEvent::CiphernodeSelected { data, .. } => { + // For score sortition, CiphernodeSelected just means we might be selected + // We need to wait for CommitteeFinalized to confirm we're actually in the committee + // Store the selection data for when CommitteeFinalized arrives + if data.ticket_id.is_some() { + // Store selection data - we'll start keyshare generation after CommitteeFinalized + ctx.set_dependency(CIPHERNODE_SELECTED_KEY, data.clone()); + return; + } - // Has the FHE dependency been already setup? (hint: it should have) - let Some(fhe) = ctx.get_dependency(FHE_KEY) else { - self.bus.err( - EnclaveErrorType::KeyGeneration, - anyhow!(ERROR_KEYSHARE_FHE_MISSING), - ); - return; - }; + // For distance sortition (no ticket_id), proceed immediately as before + // Has the FHE dependency been already setup? (hint: it should have) + let Some(fhe) = ctx.get_dependency(FHE_KEY) else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!(ERROR_KEYSHARE_FHE_MISSING), + ); + return; + }; + + let e3_id = data.clone().e3_id; + let repo = ctx.repositories().keyshare(&e3_id); + let container = repo.send(None); // New container with None + + ctx.set_event_recipient( + "keyshare", + Some( + Keyshare::new(KeyshareParams { + bus: self.bus.clone(), + secret: container, + fhe: fhe.clone(), + address: self.address.clone(), + cipher: self.cipher.clone(), + }) + .start() + .into(), + ), + ); + } + // For score sortition, start keyshare generation after CommitteeFinalized + EnclaveEvent::CommitteeFinalized { data, .. } => { + // Check if we have stored CiphernodeSelected data (score sortition) + let Some(selected_data) = ctx.get_dependency(CIPHERNODE_SELECTED_KEY) else { + // No stored data means this was distance sortition or we weren't selected + return; + }; + + // Verify this node is in the finalized committee + if !data.committee.contains(&self.address) { + // We submitted a ticket but didn't make it into the final committee + return; + } + + // Has the FHE dependency been already setup? (hint: it should have) + let Some(fhe) = ctx.get_dependency(FHE_KEY) else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!(ERROR_KEYSHARE_FHE_MISSING), + ); + return; + }; - let e3_id = data.clone().e3_id; - let repo = ctx.repositories().keyshare(&e3_id); - let container = repo.send(None); // New container with None - - ctx.set_event_recipient( - "keyshare", - Some( - Keyshare::new(KeyshareParams { - bus: self.bus.clone(), - secret: container, - fhe: fhe.clone(), - address: self.address.clone(), - cipher: self.cipher.clone(), - }) - .start() - .into(), - ), - ); + let e3_id = selected_data.e3_id.clone(); + let repo = ctx.repositories().keyshare(&e3_id); + let container = repo.send(None); // New container with None + + ctx.set_event_recipient( + "keyshare", + Some( + Keyshare::new(KeyshareParams { + bus: self.bus.clone(), + secret: container, + fhe: fhe.clone(), + address: self.address.clone(), + cipher: self.cipher.clone(), + }) + .start() + .into(), + ), + ); + } + _ => {} + } } async fn hydrate(&self, ctx: &mut E3Context, snapshot: &E3ContextSnapshot) -> Result<()> { @@ -146,45 +201,104 @@ impl ThresholdKeyshareExtension { #[async_trait] impl E3Extension for ThresholdKeyshareExtension { fn on_event(&self, ctx: &mut E3Context, evt: &EnclaveEvent) { - // if this is NOT a CiphernodeSelected event then ignore - let EnclaveEvent::CiphernodeSelected { data, .. } = evt else { - return; - }; + match evt { + // Store CiphernodeSelected data for later use + EnclaveEvent::CiphernodeSelected { data, .. } => { + // For score sortition, CiphernodeSelected just means we might be selected + // We need to wait for CommitteeFinalized to confirm we're actually in the committee + // Store the selection data for when CommitteeFinalized arrives + if data.ticket_id.is_some() { + // Store selection data - we'll start keyshare generation after CommitteeFinalized + ctx.set_dependency(CIPHERNODE_SELECTED_KEY, data.clone()); + return; + } - let e3_id = data.clone().e3_id; - let party_id = data.clone().party_id; - let Some(meta) = ctx.get_dependency(META_KEY) else { - self.bus.err( - EnclaveErrorType::KeyGeneration, - anyhow!(ERROR_KEYSHARE_FHE_MISSING), - ); - return; - }; - let repo = ctx.repositories().threshold_keyshare(&e3_id); - let container = repo.send(Some(ThresholdKeyshareState::new( - e3_id.clone(), - party_id, - KeyshareState::Init, - meta.threshold_m as u64, - meta.threshold_n as u64, - meta.params.clone(), - self.address.clone(), - ))); - - // New container with None - ctx.set_event_recipient( - "threshold_keyshare", - Some( - ThresholdKeyshare::new(ThresholdKeyshareParams { - bus: self.bus.clone(), - cipher: self.cipher.clone(), - multithread: self.multithread.clone(), - state: container, - }) - .start() - .into(), - ), - ); + // For distance sortition (no ticket_id), proceed immediately as before + let e3_id = data.clone().e3_id; + let party_id = data.clone().party_id; + let Some(meta) = ctx.get_dependency(META_KEY) else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!(ERROR_KEYSHARE_FHE_MISSING), + ); + return; + }; + let repo = ctx.repositories().threshold_keyshare(&e3_id); + let container = repo.send(Some(ThresholdKeyshareState::new( + e3_id.clone(), + party_id, + KeyshareState::Init, + meta.threshold_m as u64, + meta.threshold_n as u64, + meta.params.clone(), + self.address.clone(), + ))); + + ctx.set_event_recipient( + "threshold_keyshare", + Some( + ThresholdKeyshare::new(ThresholdKeyshareParams { + bus: self.bus.clone(), + cipher: self.cipher.clone(), + multithread: self.multithread.clone(), + state: container, + }) + .start() + .into(), + ), + ); + } + // For score sortition, start keyshare generation after CommitteeFinalized + EnclaveEvent::CommitteeFinalized { data, .. } => { + // Check if we have stored CiphernodeSelected data (score sortition) + let Some(selected_data) = ctx.get_dependency(CIPHERNODE_SELECTED_KEY) else { + // No stored data means this was distance sortition or we weren't selected + return; + }; + + // Verify this node is in the finalized committee + if !data.committee.contains(&self.address) { + // We submitted a ticket but didn't make it into the final committee + return; + } + + let e3_id = selected_data.e3_id.clone(); + let party_id = selected_data.party_id; + let Some(meta) = ctx.get_dependency(META_KEY) else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!(ERROR_KEYSHARE_FHE_MISSING), + ); + return; + }; + + let repo = ctx.repositories().threshold_keyshare(&e3_id); + let container = repo.send(Some(ThresholdKeyshareState::new( + e3_id.clone(), + party_id, + KeyshareState::Init, + meta.threshold_m as u64, + meta.threshold_n as u64, + meta.params.clone(), + self.address.clone(), + ))); + + ctx.set_event_recipient( + "threshold_keyshare", + Some( + ThresholdKeyshare::new(ThresholdKeyshareParams { + bus: self.bus.clone(), + cipher: self.cipher.clone(), + multithread: self.multithread.clone(), + state: container, + }) + .start() + .into(), + ), + ); + } + _ => {} + } } async fn hydrate(&self, ctx: &mut E3Context, snapshot: &E3ContextSnapshot) -> Result<()> { diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index a662c79788..b2f0565602 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-a254ab6ed640b952e36a352bed155c87f85018f2" + "buildInfoId": "solc-0_8_28-10b2b688a0db0c7a1bf549986929dcd9bb192f79" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 6521c46b49..12cbcbae39 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -84,6 +84,25 @@ "name": "CommitteeActivationChanged", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "committee", + "type": "address[]" + } + ], + "name": "CommitteeFinalized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -172,6 +191,37 @@ "name": "SortitionSubmissionWindowSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "score", + "type": "uint256" + } + ], + "name": "TicketSubmitted", + "type": "event" + }, { "inputs": [ { @@ -485,5 +535,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-a254ab6ed640b952e36a352bed155c87f85018f2" + "buildInfoId": "solc-0_8_28-10b2b688a0db0c7a1bf549986929dcd9bb192f79" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index c45b3ff560..ffd6abca4f 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-a254ab6ed640b952e36a352bed155c87f85018f2" + "buildInfoId": "solc-0_8_28-10b2b688a0db0c7a1bf549986929dcd9bb192f79" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 8d44328b8c..f294ff4ec3 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -52,6 +52,23 @@ interface ICiphernodeRegistry { uint256 submissionDeadline ); + /// @notice This event MUST be emitted when a ticket is submitted for sortition + /// @param e3Id ID of the E3 computation + /// @param node Address of the ciphernode submitting the ticket + /// @param ticketId The ticket number being submitted + /// @param score The computed score for the ticket + event TicketSubmitted( + uint256 indexed e3Id, + address indexed node, + uint256 ticketId, + uint256 score + ); + + /// @notice This event MUST be emitted when a committee is finalized + /// @param e3Id ID of the E3 computation + /// @param committee Array of selected ciphernode addresses + event CommitteeFinalized(uint256 indexed e3Id, address[] committee); + /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. /// @param publicKey Public key of the committee. diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index f30e085076..639e856278 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -33,23 +33,6 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @param bondingRegistry Address of the bonding registry contract event BondingRegistrySet(address indexed bondingRegistry); - /// @notice Emitted when a ticket is submitted for sortition - /// @param e3Id ID of the E3 computation - /// @param node Address of the ciphernode submitting the ticket - /// @param ticketId The ticket number being submitted - /// @param score The computed score for the ticket - event TicketSubmitted( - uint256 indexed e3Id, - address indexed node, - uint256 ticketId, - uint256 score - ); - - /// @notice Emitted when a committee is finalized - /// @param e3Id ID of the E3 computation - /// @param committee Array of selected ciphernode addresses - event CommitteeFinalized(uint256 indexed e3Id, address[] committee); - //////////////////////////////////////////////////////////// // // // Storage Variables // diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 49540e3a24..d308f5532a 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -116,5 +116,99 @@ "blockNumber": 1, "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 1, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 3, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 6, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "blockNumber": 7, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "300" + }, + "blockNumber": 8, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "maxDuration": "2592000", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] + }, + "blockNumber": 9, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "MockComputeProvider": { + "blockNumber": 17, + "address": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1" + }, + "MockDecryptionVerifier": { + "blockNumber": 18, + "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + }, + "MockInputValidator": { + "blockNumber": 19, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + }, + "MockE3Program": { + "constructorArgs": { + "mockInputValidator": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + }, + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + } } -} +} \ No newline at end of file From fcd2bcaafb77d91ff53a64b6c4343e9e629c4bc2 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sun, 26 Oct 2025 23:35:46 +0500 Subject: [PATCH 47/88] feat: integrate committee finalizer --- Cargo.lock | 2 + crates/aggregator/Cargo.toml | 1 + crates/aggregator/src/committee_finalizer.rs | 127 +++++++++ crates/aggregator/src/lib.rs | 2 + .../src/ciphernode_builder.rs | 13 +- .../src/enclave_event/ciphernode_selected.rs | 2 - crates/events/src/enclave_event/mod.rs | 10 + .../src/enclave_event/ticket_generated.rs | 28 ++ crates/evm/src/ciphernode_registry_sol.rs | 84 ++++-- crates/evm/src/lib.rs | 1 + crates/keyshare/src/ext.rs | 254 +++++------------- crates/request/src/meta.rs | 6 + crates/sortition/Cargo.toml | 3 +- crates/sortition/src/ciphernode_selector.rs | 111 +++++++- .../CiphernodeRegistryOwnable.spec.ts | 1 - 15 files changed, 421 insertions(+), 224 deletions(-) create mode 100644 crates/aggregator/src/committee_finalizer.rs create mode 100644 crates/events/src/enclave_event/ticket_generated.rs diff --git a/Cargo.lock b/Cargo.lock index 8f6d3519b9..315a98c961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2594,6 +2594,7 @@ dependencies = [ "e3-config", "e3-data", "e3-events", + "e3-evm", "e3-fhe", "e3-multithread", "e3-request", @@ -3077,6 +3078,7 @@ dependencies = [ "e3-config", "e3-data", "e3-events", + "e3-request", "num", "num-bigint", "rand 0.8.5", diff --git a/crates/aggregator/Cargo.toml b/crates/aggregator/Cargo.toml index 6ac89ac0e0..f41f43110a 100644 --- a/crates/aggregator/Cargo.toml +++ b/crates/aggregator/Cargo.toml @@ -14,6 +14,7 @@ bincode = { workspace = true } e3-config = { workspace = true } e3-data = { workspace = true } e3-events = { workspace = true } +e3-evm = { workspace = true } e3-fhe = { workspace = true } e3-multithread = { workspace = true } e3-trbfv = { workspace = true } diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs new file mode 100644 index 0000000000..8f6230d46b --- /dev/null +++ b/crates/aggregator/src/committee_finalizer.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use actix::prelude::*; +use e3_events::{CommitteeRequested, EnclaveEvent, EventBus, Shutdown, Subscribe}; +use e3_evm::FinalizeCommittee; +use std::collections::HashMap; +use std::time::Duration; +use tracing::info; + +/// CommitteeFinalizer is an actor that listens to CommitteeRequested events and calls +/// finalizeCommittee on the registry after the submission deadline has passed. +pub struct CommitteeFinalizer { + bus: Addr>, + registry_writer: Recipient, + pending_committees: HashMap, +} + +impl CommitteeFinalizer { + pub fn new( + bus: &Addr>, + registry_writer: Recipient, + ) -> Self { + Self { + bus: bus.clone(), + registry_writer, + pending_committees: HashMap::new(), + } + } + + pub fn attach( + bus: &Addr>, + registry_writer: Recipient, + ) -> Addr { + let addr = CommitteeFinalizer::new(bus, registry_writer).start(); + + bus.do_send(Subscribe::new( + "CommitteeRequested", + addr.clone().recipient(), + )); + bus.do_send(Subscribe::new("Shutdown", addr.clone().recipient())); + + addr + } +} + +impl Actor for CommitteeFinalizer { + type Context = Context; +} + +impl Handler for CommitteeFinalizer { + type Result = (); + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::CommitteeRequested { data, .. } => ctx.notify(data), + EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), + _ => (), + } + } +} + +impl Handler for CommitteeFinalizer { + type Result = (); + + fn handle(&mut self, msg: CommitteeRequested, ctx: &mut Self::Context) -> Self::Result { + let e3_id = msg.e3_id.clone(); + let submission_deadline = msg.submission_deadline; + + let current_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let seconds_until_deadline = if submission_deadline > current_timestamp { + submission_deadline - current_timestamp + } else { + info!( + e3_id = %e3_id, + submission_deadline = submission_deadline, + current_timestamp = current_timestamp, + "Submission deadline already passed, finalizing immediately" + ); + 0 + }; + + info!( + e3_id = %e3_id, + submission_deadline = submission_deadline, + current_timestamp = current_timestamp, + seconds_to_wait = seconds_until_deadline, + "Scheduling committee finalization" + ); + + let registry_writer = self.registry_writer.clone(); + let e3_id_clone = e3_id.clone(); + + let handle = ctx.run_later( + Duration::from_secs(seconds_until_deadline), + move |act, _ctx| { + info!(e3_id = %e3_id_clone, "Calling finalizeCommittee"); + + registry_writer.do_send(FinalizeCommittee { + e3_id: e3_id_clone.clone(), + }); + + act.pending_committees.remove(&e3_id_clone.to_string()); + }, + ); + + self.pending_committees.insert(e3_id.to_string(), handle); + } +} + +impl Handler for CommitteeFinalizer { + type Result = (); + fn handle(&mut self, _msg: Shutdown, ctx: &mut Self::Context) -> Self::Result { + info!("Killing CommitteeFinalizer"); + // Cancel all pending finalization tasks + for (_, handle) in self.pending_committees.drain() { + ctx.cancel_future(handle); + } + ctx.stop(); + } +} diff --git a/crates/aggregator/src/lib.rs b/crates/aggregator/src/lib.rs index f05f1558d7..1b25bee22c 100644 --- a/crates/aggregator/src/lib.rs +++ b/crates/aggregator/src/lib.rs @@ -4,11 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod committee_finalizer; pub mod ext; mod plaintext_aggregator; mod publickey_aggregator; mod repo; mod threshold_plaintext_aggregator; +pub use committee_finalizer::CommitteeFinalizer; pub use plaintext_aggregator::{ PlaintextAggregator, PlaintextAggregatorParams, PlaintextAggregatorState, }; diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 19a34c66c7..390b4985b2 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -285,7 +285,7 @@ impl CiphernodeBuilder { let sortition = Sortition::attach(&local_bus, repositories.sortition()).await?; // Ciphernode Selector - CiphernodeSelector::attach(&local_bus, &sortition, &addr); + CiphernodeSelector::attach(&local_bus, &sortition, &addr, &store); let mut provider_cache = ProviderCaches::new(); let cipher = &self.cipher; @@ -357,13 +357,22 @@ impl CiphernodeBuilder { .await { Ok(write_provider) => { - CiphernodeRegistrySol::attach_writer( + let writer = CiphernodeRegistrySol::attach_writer( &local_bus, write_provider.clone(), &chain.contracts.ciphernode_registry.address(), ) .await?; info!("CiphernodeRegistrySolWriter attached for publishing committees"); + + // Attach CommitteeFinalizer if aggregator mode is enabled + if self.pubkey_agg { + info!("Attaching CommitteeFinalizer for score sortition"); + e3_aggregator::CommitteeFinalizer::attach( + &local_bus, + writer.recipient(), + ); + } } Err(_) => { info!("No wallet configured for this node, skipping writer attachment"); diff --git a/crates/events/src/enclave_event/ciphernode_selected.rs b/crates/events/src/enclave_event/ciphernode_selected.rs index 281cb12d9e..96626eeb8d 100644 --- a/crates/events/src/enclave_event/ciphernode_selected.rs +++ b/crates/events/src/enclave_event/ciphernode_selected.rs @@ -21,7 +21,6 @@ pub struct CiphernodeSelected { pub esi_per_ct: usize, pub params: ArcBytes, pub party_id: u64, - pub ticket_id: Option, } impl Default for CiphernodeSelected { @@ -35,7 +34,6 @@ impl Default for CiphernodeSelected { seed: Seed([0u8; 32]), threshold_m: 0, threshold_n: 0, - ticket_id: None, } } } diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 98fcac9c4d..8f799588dd 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -28,6 +28,7 @@ mod shutdown; mod test_event; mod threshold_share_created; mod ticket_balance_updated; +mod ticket_generated; mod ticket_submitted; pub use ciphernode_added::*; @@ -54,6 +55,7 @@ pub use shutdown::*; pub use test_event::*; pub use threshold_share_created::*; pub use ticket_balance_updated::*; +pub use ticket_generated::*; pub use ticket_submitted::*; use crate::{E3id, ErrorEvent, Event, EventId}; @@ -147,6 +149,10 @@ pub enum EnclaveEvent { id: EventId, data: CommitteeFinalized, }, + TicketGenerated { + id: EventId, + data: TicketGenerated, + }, TicketSubmitted { id: EventId, data: TicketSubmitted, @@ -250,6 +256,7 @@ impl From for EventId { EnclaveEvent::DocumentReceived { id, .. } => id, EnclaveEvent::ThresholdShareCreated { id, .. } => id, EnclaveEvent::CommitteeFinalized { id, .. } => id, + EnclaveEvent::TicketGenerated { id, .. } => id, EnclaveEvent::TicketSubmitted { id, .. } => id, } } @@ -270,6 +277,7 @@ impl EnclaveEvent { EnclaveEvent::CommitteeRequested { data, .. } => Some(data.e3_id), EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteeFinalized { data, .. } => Some(data.e3_id), + EnclaveEvent::TicketGenerated { data, .. } => Some(data.e3_id), EnclaveEvent::TicketSubmitted { data, .. } => Some(data.e3_id), _ => None, } @@ -299,6 +307,7 @@ impl EnclaveEvent { EnclaveEvent::TestEvent { data, .. } => format!("{:?}", data), EnclaveEvent::DocumentReceived { data, .. } => format!("{:?}", data), EnclaveEvent::CommitteeFinalized { data, .. } => format!("{:?}", data), + EnclaveEvent::TicketGenerated { data, .. } => format!("{:?}", data), EnclaveEvent::TicketSubmitted { data, .. } => format!("{:?}", data), // _ => "".to_string(), } @@ -323,6 +332,7 @@ impl_from_event!( CommitteePublished, CommitteeRequested, CommitteeFinalized, + TicketGenerated, TicketSubmitted, PlaintextOutputPublished, EnclaveError, diff --git a/crates/events/src/enclave_event/ticket_generated.rs b/crates/events/src/enclave_event/ticket_generated.rs new file mode 100644 index 0000000000..113d099a55 --- /dev/null +++ b/crates/events/src/enclave_event/ticket_generated.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct TicketGenerated { + pub e3_id: E3id, + pub ticket_id: u64, + pub node: String, +} + +impl Display for TicketGenerated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, ticket_id: {}, node: {}", + self.e3_id, self.ticket_id, self.node + ) + } +} diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index 2b7470bdbc..0e477f7728 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -16,8 +16,8 @@ use alloy::{ use anyhow::Result; use e3_data::Repository; use e3_events::{ - BusError, CiphernodeSelected, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, - EventBus, OrderedSet, PublicKeyAggregated, Seed, Shutdown, Subscribe, + BusError, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, + PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, }; use std::collections::HashMap; use tracing::{error, info, trace}; @@ -284,9 +284,9 @@ impl CiphernodeRegistrySolWriter .send(Subscribe::new("CommitteeFinalized", addr.clone().into())) .await; - // Subscribe to CiphernodeSelected for ticket submission + // Subscribe to TicketGenerated for ticket submission let _ = bus - .send(Subscribe::new("CiphernodeSelected", addr.clone().into())) + .send(Subscribe::new("TicketGenerated", addr.clone().into())) .await; Ok(addr) @@ -316,8 +316,8 @@ impl Handler ctx.notify(data); } } - EnclaveEvent::CiphernodeSelected { data, .. } => { - // Submit ticket if chain matches and ticket_id is present + EnclaveEvent::TicketGenerated { data, .. } => { + // Submit ticket if chain matches if self.provider.chain_id() == data.e3_id.chain_id() { ctx.notify(data); } @@ -343,12 +343,12 @@ impl Handler } } -impl Handler +impl Handler for CiphernodeRegistrySolWriter

{ type Result = ResponseFuture<()>; - fn handle(&mut self, msg: CiphernodeSelected, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: TicketGenerated, _: &mut Self::Context) -> Self::Result { let e3_id = msg.e3_id.clone(); let ticket_id = msg.ticket_id; let contract_address = self.contract_address; @@ -356,15 +356,6 @@ impl Handler let bus = self.bus.clone(); Box::pin(async move { - // Only submit if we have a ticket_id (score sortition) - let Some(ticket_id) = ticket_id else { - info!( - "No ticket_id for E3 {:?}, skipping ticket submission (distance sortition)", - e3_id - ); - return; - }; - info!("Submitting ticket {} for E3 {:?}", ticket_id, e3_id); let result = @@ -382,6 +373,41 @@ impl Handler } } +/// Message to trigger committee finalization (called by aggregator) +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct FinalizeCommittee { + pub e3_id: E3id, +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: FinalizeCommittee, _: &mut Self::Context) -> Self::Result { + let e3_id = msg.e3_id.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + Box::pin(async move { + info!("Finalizing committee for E3 {:?}", e3_id); + + let result = finalize_committee_on_registry(provider, contract_address, e3_id).await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Committee finalized on registry"); + } + Err(err) => { + error!("Failed to finalize committee: {:?}", err); + bus.err(EnclaveErrorType::Evm, err); + } + } + }) + } +} + impl Handler for CiphernodeRegistrySolWriter

{ @@ -457,6 +483,24 @@ pub async fn submit_ticket_to_registry( Ok(receipt) } +pub async fn finalize_committee_on_registry( + provider: EthProvider

, + contract_address: Address, + e3_id: E3id, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + let contract = ICiphernodeRegistry::new(contract_address, provider.provider()); + let builder = contract.finalizeCommittee(e3_id).nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + pub async fn publish_committee_to_registry( provider: EthProvider

, contract_address: Address, @@ -515,11 +559,11 @@ impl CiphernodeRegistrySol { bus: &Addr>, provider: EthProvider

, contract_address: &str, - ) -> Result<()> + ) -> Result>> where P: Provider + WalletProvider + Clone + 'static, { - CiphernodeRegistrySolWriter::attach(bus, provider, contract_address).await?; - Ok(()) + let writer = CiphernodeRegistrySolWriter::attach(bus, provider, contract_address).await?; + Ok(writer) } } diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 477e20798c..60b462d8d3 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -16,6 +16,7 @@ mod repo; pub use bonding_registry_sol::{BondingRegistrySol, BondingRegistrySolReader}; pub use ciphernode_registry_sol::{ CiphernodeRegistrySol, CiphernodeRegistrySolReader, CiphernodeRegistrySolWriter, + FinalizeCommittee, }; pub use enclave_sol::EnclaveSol; pub use enclave_sol_reader::EnclaveSolReader; diff --git a/crates/keyshare/src/ext.rs b/crates/keyshare/src/ext.rs index 3a218f5945..134c664e23 100644 --- a/crates/keyshare/src/ext.rs +++ b/crates/keyshare/src/ext.rs @@ -13,14 +13,12 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use e3_crypto::Cipher; use e3_data::{AutoPersist, RepositoriesFactory}; -use e3_events::{BusError, CiphernodeSelected, EnclaveErrorType, EnclaveEvent, EventBus}; +use e3_events::{BusError, EnclaveErrorType, EnclaveEvent, EventBus}; use e3_fhe::ext::FHE_KEY; use e3_multithread::Multithread; -use e3_request::{E3Context, E3ContextSnapshot, E3Extension, TypedKey, META_KEY}; +use e3_request::{E3Context, E3ContextSnapshot, E3Extension, META_KEY}; use std::sync::Arc; -const CIPHERNODE_SELECTED_KEY: TypedKey = TypedKey::new("ciphernode_selected"); - pub struct KeyshareExtension { bus: Addr>, address: String, @@ -47,91 +45,38 @@ const ERROR_KEYSHARE_FHE_MISSING: &str = #[async_trait] impl E3Extension for KeyshareExtension { fn on_event(&self, ctx: &mut E3Context, evt: &EnclaveEvent) { - match evt { - // Store CiphernodeSelected data for later use - EnclaveEvent::CiphernodeSelected { data, .. } => { - // For score sortition, CiphernodeSelected just means we might be selected - // We need to wait for CommitteeFinalized to confirm we're actually in the committee - // Store the selection data for when CommitteeFinalized arrives - if data.ticket_id.is_some() { - // Store selection data - we'll start keyshare generation after CommitteeFinalized - ctx.set_dependency(CIPHERNODE_SELECTED_KEY, data.clone()); - return; - } - - // For distance sortition (no ticket_id), proceed immediately as before - // Has the FHE dependency been already setup? (hint: it should have) - let Some(fhe) = ctx.get_dependency(FHE_KEY) else { - self.bus.err( - EnclaveErrorType::KeyGeneration, - anyhow!(ERROR_KEYSHARE_FHE_MISSING), - ); - return; - }; - - let e3_id = data.clone().e3_id; - let repo = ctx.repositories().keyshare(&e3_id); - let container = repo.send(None); // New container with None - - ctx.set_event_recipient( - "keyshare", - Some( - Keyshare::new(KeyshareParams { - bus: self.bus.clone(), - secret: container, - fhe: fhe.clone(), - address: self.address.clone(), - cipher: self.cipher.clone(), - }) - .start() - .into(), - ), - ); - } - // For score sortition, start keyshare generation after CommitteeFinalized - EnclaveEvent::CommitteeFinalized { data, .. } => { - // Check if we have stored CiphernodeSelected data (score sortition) - let Some(selected_data) = ctx.get_dependency(CIPHERNODE_SELECTED_KEY) else { - // No stored data means this was distance sortition or we weren't selected - return; - }; - - // Verify this node is in the finalized committee - if !data.committee.contains(&self.address) { - // We submitted a ticket but didn't make it into the final committee - return; - } - - // Has the FHE dependency been already setup? (hint: it should have) - let Some(fhe) = ctx.get_dependency(FHE_KEY) else { - self.bus.err( - EnclaveErrorType::KeyGeneration, - anyhow!(ERROR_KEYSHARE_FHE_MISSING), - ); - return; - }; + // if this is NOT a CiphernodeSelected event then ignore + let EnclaveEvent::CiphernodeSelected { data, .. } = evt else { + return; + }; - let e3_id = selected_data.e3_id.clone(); - let repo = ctx.repositories().keyshare(&e3_id); - let container = repo.send(None); // New container with None + // Has the FHE dependency been already setup? (hint: it should have) + let Some(fhe) = ctx.get_dependency(FHE_KEY) else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!(ERROR_KEYSHARE_FHE_MISSING), + ); + return; + }; - ctx.set_event_recipient( - "keyshare", - Some( - Keyshare::new(KeyshareParams { - bus: self.bus.clone(), - secret: container, - fhe: fhe.clone(), - address: self.address.clone(), - cipher: self.cipher.clone(), - }) - .start() - .into(), - ), - ); - } - _ => {} - } + let e3_id = data.clone().e3_id; + let repo = ctx.repositories().keyshare(&e3_id); + let container = repo.send(None); // New container with None + + ctx.set_event_recipient( + "keyshare", + Some( + Keyshare::new(KeyshareParams { + bus: self.bus.clone(), + secret: container, + fhe: fhe.clone(), + address: self.address.clone(), + cipher: self.cipher.clone(), + }) + .start() + .into(), + ), + ); } async fn hydrate(&self, ctx: &mut E3Context, snapshot: &E3ContextSnapshot) -> Result<()> { @@ -201,104 +146,45 @@ impl ThresholdKeyshareExtension { #[async_trait] impl E3Extension for ThresholdKeyshareExtension { fn on_event(&self, ctx: &mut E3Context, evt: &EnclaveEvent) { - match evt { - // Store CiphernodeSelected data for later use - EnclaveEvent::CiphernodeSelected { data, .. } => { - // For score sortition, CiphernodeSelected just means we might be selected - // We need to wait for CommitteeFinalized to confirm we're actually in the committee - // Store the selection data for when CommitteeFinalized arrives - if data.ticket_id.is_some() { - // Store selection data - we'll start keyshare generation after CommitteeFinalized - ctx.set_dependency(CIPHERNODE_SELECTED_KEY, data.clone()); - return; - } - - // For distance sortition (no ticket_id), proceed immediately as before - let e3_id = data.clone().e3_id; - let party_id = data.clone().party_id; - let Some(meta) = ctx.get_dependency(META_KEY) else { - self.bus.err( - EnclaveErrorType::KeyGeneration, - anyhow!(ERROR_KEYSHARE_FHE_MISSING), - ); - return; - }; - let repo = ctx.repositories().threshold_keyshare(&e3_id); - let container = repo.send(Some(ThresholdKeyshareState::new( - e3_id.clone(), - party_id, - KeyshareState::Init, - meta.threshold_m as u64, - meta.threshold_n as u64, - meta.params.clone(), - self.address.clone(), - ))); - - ctx.set_event_recipient( - "threshold_keyshare", - Some( - ThresholdKeyshare::new(ThresholdKeyshareParams { - bus: self.bus.clone(), - cipher: self.cipher.clone(), - multithread: self.multithread.clone(), - state: container, - }) - .start() - .into(), - ), - ); - } - // For score sortition, start keyshare generation after CommitteeFinalized - EnclaveEvent::CommitteeFinalized { data, .. } => { - // Check if we have stored CiphernodeSelected data (score sortition) - let Some(selected_data) = ctx.get_dependency(CIPHERNODE_SELECTED_KEY) else { - // No stored data means this was distance sortition or we weren't selected - return; - }; - - // Verify this node is in the finalized committee - if !data.committee.contains(&self.address) { - // We submitted a ticket but didn't make it into the final committee - return; - } - - let e3_id = selected_data.e3_id.clone(); - let party_id = selected_data.party_id; - let Some(meta) = ctx.get_dependency(META_KEY) else { - self.bus.err( - EnclaveErrorType::KeyGeneration, - anyhow!(ERROR_KEYSHARE_FHE_MISSING), - ); - return; - }; - - let repo = ctx.repositories().threshold_keyshare(&e3_id); - let container = repo.send(Some(ThresholdKeyshareState::new( - e3_id.clone(), - party_id, - KeyshareState::Init, - meta.threshold_m as u64, - meta.threshold_n as u64, - meta.params.clone(), - self.address.clone(), - ))); + // if this is NOT a CiphernodeSelected event then ignore + let EnclaveEvent::CiphernodeSelected { data, .. } = evt else { + return; + }; - ctx.set_event_recipient( - "threshold_keyshare", - Some( - ThresholdKeyshare::new(ThresholdKeyshareParams { - bus: self.bus.clone(), - cipher: self.cipher.clone(), - multithread: self.multithread.clone(), - state: container, - }) - .start() - .into(), - ), - ); - } - _ => {} - } + let e3_id = data.clone().e3_id; + let party_id = data.clone().party_id; + let Some(meta) = ctx.get_dependency(META_KEY) else { + self.bus.err( + EnclaveErrorType::KeyGeneration, + anyhow!(ERROR_KEYSHARE_FHE_MISSING), + ); + return; + }; + let repo = ctx.repositories().threshold_keyshare(&e3_id); + let container = repo.send(Some(ThresholdKeyshareState::new( + e3_id.clone(), + party_id, + KeyshareState::Init, + meta.threshold_m as u64, + meta.threshold_n as u64, + meta.params.clone(), + self.address.clone(), + ))); + + // New container with None + ctx.set_event_recipient( + "threshold_keyshare", + Some( + ThresholdKeyshare::new(ThresholdKeyshareParams { + bus: self.bus.clone(), + cipher: self.cipher.clone(), + multithread: self.multithread.clone(), + state: container, + }) + .start() + .into(), + ), + ); } async fn hydrate(&self, ctx: &mut E3Context, snapshot: &E3ContextSnapshot) -> Result<()> { diff --git a/crates/request/src/meta.rs b/crates/request/src/meta.rs index 7c60447e91..209ad63da2 100644 --- a/crates/request/src/meta.rs +++ b/crates/request/src/meta.rs @@ -19,6 +19,8 @@ pub struct E3Meta { pub threshold_n: usize, pub seed: Seed, pub params: ArcBytes, + pub esi_per_ct: usize, + pub error_size: ArcBytes, } pub struct E3MetaExtension; @@ -41,6 +43,8 @@ impl E3Extension for E3MetaExtension { seed, e3_id, params, + esi_per_ct, + error_size, .. } = data.clone(); @@ -50,6 +54,8 @@ impl E3Extension for E3MetaExtension { threshold_n, seed, params, + esi_per_ct, + error_size, }; ctx.repositories().meta(&e3_id).write(&meta); let _ = ctx.set_dependency(META_KEY, meta); diff --git a/crates/sortition/Cargo.toml b/crates/sortition/Cargo.toml index b16f0883f1..096f450ec1 100644 --- a/crates/sortition/Cargo.toml +++ b/crates/sortition/Cargo.toml @@ -16,7 +16,8 @@ async-trait = { workspace = true } e3-config = { workspace = true } e3-data = { workspace = true } e3-events = { workspace = true } -num = { workspace = true } +e3-request = { workspace = true } +num = { workspace = true } rand = { workspace = true } serde = { workspace = true } tracing = { workspace = true } diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index 387dc41c51..1e61769fb1 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -6,15 +6,21 @@ use crate::{GetNodeIndex, Sortition}; /// CiphernodeSelector is an actor that determines if a ciphernode is part of a committee and if so -/// forwards a CiphernodeSelected event to the event bus +/// forwards a CiphernodeSelected event (distance sortition) or TicketGenerated event (score sortition) to the event bus use actix::prelude::*; -use e3_events::{CiphernodeSelected, E3Requested, EnclaveEvent, EventBus, Shutdown, Subscribe}; +use e3_data::{DataStore, RepositoriesFactory}; +use e3_events::{ + CiphernodeSelected, CommitteeFinalized, E3Requested, EnclaveEvent, EventBus, Shutdown, + Subscribe, TicketGenerated, +}; +use e3_request::MetaRepositoryFactory; use tracing::info; pub struct CiphernodeSelector { bus: Addr>, sortition: Addr, address: String, + data_store: DataStore, } impl Actor for CiphernodeSelector { @@ -26,11 +32,13 @@ impl CiphernodeSelector { bus: &Addr>, sortition: &Addr, address: &str, + data_store: &DataStore, ) -> Self { Self { bus: bus.clone(), sortition: sortition.clone(), address: address.to_owned(), + data_store: data_store.clone(), } } @@ -38,10 +46,15 @@ impl CiphernodeSelector { bus: &Addr>, sortition: &Addr, address: &str, + data_store: &DataStore, ) -> Addr { - let addr = CiphernodeSelector::new(bus, sortition, address).start(); + let addr = CiphernodeSelector::new(bus, sortition, address, data_store).start(); bus.do_send(Subscribe::new("E3Requested", addr.clone().recipient())); + bus.do_send(Subscribe::new( + "CommitteeFinalized", + addr.clone().recipient(), + )); bus.do_send(Subscribe::new("Shutdown", addr.clone().recipient())); addr @@ -53,6 +66,7 @@ impl Handler for CiphernodeSelector { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { EnclaveEvent::E3Requested { data, .. } => ctx.notify(data), + EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data), EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), _ => (), } @@ -62,7 +76,7 @@ impl Handler for CiphernodeSelector { impl Handler for CiphernodeSelector { type Result = ResponseFuture<()>; - fn handle(&mut self, data: E3Requested, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, data: E3Requested, ctx: &mut Self::Context) -> Self::Result { let address = self.address.clone(); let sortition = self.sortition.clone(); let bus = self.bus.clone(); @@ -90,19 +104,88 @@ impl Handler for CiphernodeSelector { info!(node = address, "Ciphernode was not selected"); return; }; + + if let Some(tid) = ticket_id { + info!( + node = address, + ticket_id = tid, + "Ticket generated for score sortition" + ); + bus.do_send(EnclaveEvent::from(TicketGenerated { + e3_id: data.e3_id.clone(), + ticket_id: tid, + node: address.clone(), + })); + } else { + info!(node = address, "Ciphernode selected via distance sortition"); + bus.do_send(EnclaveEvent::from(CiphernodeSelected { + party_id, + e3_id: data.e3_id, + threshold_m: data.threshold_m, + threshold_n: data.threshold_n, + esi_per_ct: data.esi_per_ct, + error_size: data.error_size, + params: data.params.clone(), + seed: data.seed.clone(), + })); + } + } else { + info!("This node is not selected"); + } + }) + } +} + +impl Handler for CiphernodeSelector { + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { + let address = self.address.clone(); + let bus = self.bus.clone(); + let sortition = self.sortition.clone(); + let repositories = self.data_store.repositories(); + let e3_id = msg.e3_id.clone(); + + // Check if this node is in the finalized committee + if !msg.committee.contains(&address) { + info!(node = address, "Node not in finalized committee"); + return Box::pin(async {}); + } + + Box::pin(async move { + // Retrieve E3 metadata from repository + let meta_repo = repositories.meta(&e3_id); + let Some(e3_meta) = meta_repo.read().await.ok().flatten() else { + info!( + node = address, + "No stored E3 metadata for {:?}, skipping", e3_id + ); + return; + }; + + if let Ok(Some((party_id, _ticket_id))) = sortition + .send(GetNodeIndex { + chain_id: e3_id.chain_id(), + seed: e3_meta.seed, + address: address.clone(), + size: e3_meta.threshold_n, + }) + .await + { + info!( + node = address, + "Node is in finalized committee, emitting CiphernodeSelected" + ); bus.do_send(EnclaveEvent::from(CiphernodeSelected { party_id, - ticket_id, - e3_id: data.e3_id, - threshold_m: data.threshold_m, - threshold_n: data.threshold_n, - esi_per_ct: data.esi_per_ct, - error_size: data.error_size, - params: data.params.clone(), - seed: data.seed.clone(), + e3_id, + threshold_m: e3_meta.threshold_m, + threshold_n: e3_meta.threshold_n, + esi_per_ct: e3_meta.esi_per_ct, + error_size: e3_meta.error_size, + params: e3_meta.params, + seed: e3_meta.seed, })); - } else { - info!("This node is not selected"); } }) } diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 5d8439ab36..93abc80e90 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -7,7 +7,6 @@ import { LeanIMT } from "@zk-kit/lean-imt"; import { expect } from "chai"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; -import { toASCII } from "punycode"; import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; From ac2f7b245a125e4815b02bd57c787d9a2645a827 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 00:05:35 +0500 Subject: [PATCH 48/88] chore: merge dev --- deploy/local/contracts.sh | 8 +- examples/CRISP/package.json | 1 - .../crisp-contracts/deployed_contracts.json | 87 ++++++++++++------- .../crisp-contracts/hardhat.config.ts | 1 - .../packages/crisp-contracts/package.json | 2 +- .../enclave-contracts/deployed_contracts.json | 60 ++++++------- 6 files changed, 90 insertions(+), 69 deletions(-) diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index 061acb2ba8..13ea010027 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -4,7 +4,7 @@ # cargo install --locked --path ./crates/cli --bin enclave -f # Deploy CRISP Contracts -(cd examples/CRISP && pnpm deploy:contracts:full --network localhost) +(cd examples/CRISP/packages/crisp-contracts && pnpm deploy:contracts:full --network localhost) # Add Ciphernodes to Enclave sleep 2 # wait for enclave to start @@ -15,9 +15,9 @@ CN2=0xdD2FD4581271e230360230F9337D5c0430Bf44C0 CN3=0x2546BcD3c84621e976D8185a91A922aE77ECEc30 # Add the ciphernodes to the enclave -(cd examples/CRISP && pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost") -(cd examples/CRISP && pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost") -(cd examples/CRISP && pnpm ciphernode:add --ciphernode-address "$CN3" --network "localhost") +(cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost") +(cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost") +(cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN3" --network "localhost") # Delete local DB diff --git a/examples/CRISP/package.json b/examples/CRISP/package.json index 08482b2a10..de994b13da 100644 --- a/examples/CRISP/package.json +++ b/examples/CRISP/package.json @@ -9,7 +9,6 @@ }, "scripts": { "compile": "forge compile", - "ciphernode:add": "hardhat ciphernode:admin-add", "cli": "bash ./scripts/cli.sh", "dev:setup": "bash ./scripts/setup.sh", "dev:build": "bash ./scripts/build.sh", diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index df2fe3cb92..2e50db12c6 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -82,6 +82,34 @@ } } }, + "undefined": { + "RiscZeroGroth16Verifier": { + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "CRISPInputValidator": { + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + } + }, + "default": { + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + } + }, "localhost": { "PoseidonT3": { "blockNumber": 1, @@ -136,20 +164,12 @@ "CiphernodeRegistryOwnable": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "enclaveAddress": "0x0000000000000000000000000000000000000001" + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "300" }, "blockNumber": 8, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, - "CommitteeSortition": { - "constructorArgs": { - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "ciphernodeRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "submissionWindow": "300" - }, - "blockNumber": 9, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", @@ -161,49 +181,52 @@ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 10, - "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + "blockNumber": 9, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "MockComputeProvider": { - "blockNumber": 19, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "blockNumber": 17, + "address": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1" }, "MockDecryptionVerifier": { - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 18, + "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" }, "MockInputValidator": { - "blockNumber": 21, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "blockNumber": 19, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "mockInputValidator": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, - "blockNumber": 22, - "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, - "MockRISC0Verifier": { - "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "RiscZeroGroth16Verifier": { + "address": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" + }, + "CRISPInputValidator": { + "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" }, "CRISPInputValidatorFactory": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", "constructorArgs": { - "inputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "inputValidator": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" } }, "HonkVerifier": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" }, "CRISPProgram": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "constructorArgs": { - "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", - "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - "inputValidatorAddress": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "enclave": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "verifierAddress": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1", + "inputValidatorAddress": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", + "honkVerifierAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } } -} +} \ No newline at end of file diff --git a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts index fa3d7fc064..8bc4e18077 100644 --- a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts +++ b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts @@ -147,7 +147,6 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", - "@enclave-e3/contracts/contracts/sortition/CommitteeSortition.sol", "@enclave-e3/contracts/contracts/token/EnclaveToken.sol", "@enclave-e3/contracts/contracts/token/EnclaveTicketToken.sol", "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", diff --git a/examples/CRISP/packages/crisp-contracts/package.json b/examples/CRISP/packages/crisp-contracts/package.json index aadc206fa8..f29f703175 100644 --- a/examples/CRISP/packages/crisp-contracts/package.json +++ b/examples/CRISP/packages/crisp-contracts/package.json @@ -11,7 +11,7 @@ }, "scripts": { "compile": "hardhat compile", - "ciphernode:add": "hardhat ciphernode:add", + "ciphernode:add": "hardhat ciphernode:admin-add", "clean:deployments": "hardhat utils:clean-deployments", "deploy:contracts": "hardhat run deploy/deploy.ts", "deploy:contracts:full": "export DEPLOY_ENCLAVE=true && pnpm deploy:contracts", diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index d308f5532a..48fb0f2e3a 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -119,45 +119,45 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 1, + "blockNumber": 94, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 95, + "address": "0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 96, + "address": "0x172076E0166D1F9Cc711C77Adf8488051744980C" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "baseToken": "0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 5, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 98, + "address": "0xBEc49fA140aCaA83533fB00A2BB19bDdd0290f25" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 6, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 99, + "address": "0xD84379CEae14AA33C123Af12424A37803F885889" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "ticketToken": "0xBEc49fA140aCaA83533fB00A2BB19bDdd0290f25", + "licenseToken": "0x172076E0166D1F9Cc711C77Adf8488051744980C", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -165,8 +165,8 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 7, - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + "blockNumber": 100, + "address": "0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5" }, "CiphernodeRegistryOwnable": { "constructorArgs": { @@ -174,41 +174,41 @@ "enclaveAddress": "0x0000000000000000000000000000000000000001", "submissionWindow": "300" }, - "blockNumber": 8, - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + "blockNumber": 101, + "address": "0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d", + "bondingRegistry": "0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5", + "feeToken": "0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 9, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + "blockNumber": 102, + "address": "0x46b142DD1E924FAb83eCc3c08e4D46E82f005e0E" }, "MockComputeProvider": { - "blockNumber": 17, - "address": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1" + "blockNumber": 110, + "address": "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D" }, "MockDecryptionVerifier": { - "blockNumber": 18, - "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + "blockNumber": 111, + "address": "0xA4899D35897033b927acFCf422bc745916139776" }, "MockInputValidator": { - "blockNumber": 19, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "blockNumber": 112, + "address": "0xf953b3A269d80e3eB0F2947630Da976B896A8C5b" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "mockInputValidator": "0xf953b3A269d80e3eB0F2947630Da976B896A8C5b" }, - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 113, + "address": "0xAA292E8611aDF267e563f334Ee42320aC96D0463" } } } \ No newline at end of file From 31764ca4e49668c2d99b8a05e0ac64af5ec31618 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 00:16:25 +0500 Subject: [PATCH 49/88] chore: remove unused contracts --- .../src/ciphernode_builder.rs | 8 - crates/config/src/contract.rs | 1 - crates/config/src/store_keys.rs | 4 - .../entrypoint/src/start/aggregator_start.rs | 1 - crates/entrypoint/src/start/start.rs | 1 - docs/score-sortition-flow.md | 377 ------------------ examples/CRISP/client/.env.example | 2 +- examples/CRISP/enclave.config.yaml | 7 +- examples/CRISP/server/.env.example | 4 +- .../enclave-contracts/deployed_contracts.json | 11 +- templates/default/enclave.config.yaml | 7 +- templates/default/hardhat.config.ts | 4 +- tests/integration/enclave.config.yaml | 7 +- 13 files changed, 15 insertions(+), 419 deletions(-) delete mode 100644 docs/score-sortition-flow.md diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 390b4985b2..c7164d6c36 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -67,7 +67,6 @@ pub struct ContractComponents { enclave: bool, ciphernode_registry: bool, bonding_registry: bool, - committee_sortition: bool, } #[derive(Clone, Debug)] @@ -217,13 +216,6 @@ impl CiphernodeBuilder { self.contract_components.bonding_registry = true; self } - - /// Setup a writable CommitteeSortition for every evm chain provided - pub fn with_contract_committee_sortition(mut self) -> Self { - self.contract_components.committee_sortition = true; - self - } - /// Setup a CiphernodeRegistry listener for every evm chain provided pub fn with_contract_ciphernode_registry(mut self) -> Self { self.contract_components.ciphernode_registry = true; diff --git a/crates/config/src/contract.rs b/crates/config/src/contract.rs index ec6070f85a..17968c2a5f 100644 --- a/crates/config/src/contract.rs +++ b/crates/config/src/contract.rs @@ -39,6 +39,5 @@ pub struct ContractAddresses { pub enclave: Contract, pub ciphernode_registry: Contract, pub bonding_registry: Contract, - pub committee_sortition: Option, pub e3_program: Option, } diff --git a/crates/config/src/store_keys.rs b/crates/config/src/store_keys.rs index 111f2a2831..a85dba71fc 100644 --- a/crates/config/src/store_keys.rs +++ b/crates/config/src/store_keys.rs @@ -65,10 +65,6 @@ impl StoreKeys { format!("//evm_readers/bonding_registry/{chain_id}") } - pub fn committee_sortition_reader(chain_id: u64) -> String { - format!("//evm_readers/committee_sortition/{chain_id}") - } - pub fn node_state() -> String { String::from("//node_state") } diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index e6797df854..857dc9e57c 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -41,7 +41,6 @@ pub async fn execute( .with_contract_enclave_full() .with_contract_bonding_registry() .with_contract_ciphernode_registry() - .with_contract_committee_sortition() .with_plaintext_aggregation() .with_pubkey_aggregation() .build() diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index 1537ad7743..c88d0d026c 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -42,7 +42,6 @@ pub async fn execute( .with_chains(&config.chains()) .with_contract_enclave_reader() .with_contract_ciphernode_registry() - .with_contract_committee_sortition() .build() .await?; diff --git a/docs/score-sortition-flow.md b/docs/score-sortition-flow.md deleted file mode 100644 index 499e71339d..0000000000 --- a/docs/score-sortition-flow.md +++ /dev/null @@ -1,377 +0,0 @@ -# Complete Score Sortition Flow - End to End - -This document explains the entire score sortition process, from when an E3 is requested to when the committee and public key are published. - -## Overview - -**Score sortition** is an alternative to distance sortition where: -- ALL eligible nodes can participate (not just closest in merkle tree) -- Nodes submit "lottery tickets" with computed scores -- Contract selects top N nodes with lowest scores -- More decentralized and fair - ---- - -## Step-by-Step Flow - -### 1. **E3 Requested** (User creates computation request) - -**Contract: Enclave.sol** -```solidity -requestCompute() → emits E3Requested(e3Id, threshold, seed, ...) -``` - -**All Ciphernodes:** -- `EnclaveSolReader` picks up `E3Requested` event -- Converts to `EnclaveEvent::E3Requested` and broadcasts on event bus - -**CiphernodeRegistry.sol** (if score sortition enabled): -```solidity -requestCommittee() is called by Enclave - → Calls CommitteeSortition.initializeSortition(e3Id, threshold, seed, block.number) - → Sets submission deadline = now + submissionWindow (e.g., 60 seconds) -``` - -**All Ciphernodes:** -- `CiphernodeSelector` receives `E3Requested` -- Checks eligibility (bonding, ticket balance) -- Performs ticket sortition locally (computes scores for all owned tickets) -- Finds best ticket (lowest score) -- Emits `CiphernodeSelected` event with ticket_id - -**Aggregator:** -- `CommitteeSortitionSolWriter` (with `enable_finalizer=true`) receives `E3Requested` -- Calls `schedule_finalization(e3_id)`: - - Sets deadline = now + submission_window (fetched from contract) - - Stores in `pending_e3s` HashMap - - Schedules timer to check after submission_window expires - ---- - -### 2. **Ticket Submission Window** (Selected nodes submit tickets) - -**Selected Ciphernodes:** -- `CommitteeSortitionSolWriter` receives `CiphernodeSelected` event -- Calls `submitTicket(e3Id, ticketNumber)` on contract - -**Contract: CommitteeSortition.sol** -```solidity -submitTicket(e3Id, ticketNumber) - 1. Validates submission window still open (block.timestamp <= deadline) - 2. Validates node hasn't submitted before - 3. Validates node has ticket balance at snapshot block - 4. Computes score = keccak256(node || ticketNumber || e3Id || seed) - 5. Tries to insert into topNodes sorted array (size = threshold) - 6. Emits TicketSubmitted(e3Id, node, ticketNumber, score, addedToCommittee) -``` - -**All Ciphernodes:** -- `CommitteeSortitionSolReader` reads `TicketSubmitted` events -- Broadcasts as `EnclaveEvent::TicketSubmitted` -- Nodes can see who submitted and whether they made it into top N - ---- - -### 3. **Submission Window Closes** (After 60 seconds) - -**Aggregator:** -- `CommitteeSortitionSolWriter` has a timer running (10-second interval checks) -- `CheckDeadlines` handler finds expired E3s in `pending_e3s` -- Calls `finalize_committee(e3_id)` - -**Contract: CommitteeSortition.sol** -```solidity -finalizeCommittee(e3Id) - 1. Validates submission window has closed (block.timestamp > deadline) - 2. Validates not already finalized - 3. Sets finalized = true - 4. Returns topNodes array - 5. Emits CommitteeFinalized(e3Id, topNodes[]) -``` - -**All Ciphernodes:** -- `CommitteeSortitionSolReader` picks up `CommitteeFinalized` event -- Broadcasts as `EnclaveEvent::CommitteeFinalized(e3_id, committee[])` - -**Aggregator:** -- `CommitteeSortitionSolWriter` receives `CommitteeFinalized` -- Removes e3_id from `pending_e3s` (cleanup) - ---- - -### 4. **Keygen Starts** (Committee members generate key shares) - -**Committee Members Only:** -- Receive `CommitteeFinalized` event -- Check if their address is in the committee array -- `ThresholdKeyshareExtension` starts DKG (Distributed Key Generation): - - Generates local secret share - - Broadcasts commitments to other committee members - - Receives and validates shares from others - - Computes public key share - -**All Committee Members:** -- Complete DKG protocol -- Each member now has: - - Their secret share (stored locally) - - The aggregated public key (same for all) - ---- - -### 5. **Public Key Aggregation** (Aggregator collects and publishes) - -**Committee Members:** -- `ThresholdKeyshareExtension` emits `PublicKeyGenerated` event locally -- Contains their computed public key - -**Aggregator:** -- `PublicKeyAggregatorExtension` receives multiple `PublicKeyGenerated` events -- Waits for threshold M nodes to report same public key -- Once threshold reached, emits `PublicKeyAggregated` event with: - - `e3_id` - - `publicKey` (aggregated) - ---- - -### 6. **Committee Published to Registry** (Aggregator writes to blockchain) - -**Aggregator:** -- `CiphernodeRegistrySolWriter` receives `PublicKeyAggregated` event -- Calls `publishCommittee(e3Id, nodes[], publicKey)` on contract - -> **Note:** For score sortition, this needs an update (still TODO): -> - Should track committee from earlier `CommitteeFinalized` event -> - Use those stored nodes when publishing -> - Current code works for distance sortition - -**Contract: CiphernodeRegistryOwnable.sol** -```solidity -publishCommittee(e3Id, nodes[], publicKey) - 1. Validates not already published - 2. Stores committee data - 3. Stores publicKey hash - 4. Emits CommitteePublished(e3Id, nodes[], publicKey) -``` - -**All Ciphernodes:** -- `CiphernodeRegistrySolReader` picks up `CommitteePublished` event -- Broadcasts as `EnclaveEvent::CommitteePublished` -- Committee is now officially registered and ready for computations - ---- - -## Architecture Diagram - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. E3 REQUESTED │ -│ Enclave.sol → E3Requested event │ -│ ↓ │ -│ All Nodes: CiphernodeSelector checks eligibility │ -│ ↓ │ -│ Selected Nodes: Emit CiphernodeSelected(ticket_id) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. TICKET SUBMISSION (60 second window) │ -│ Selected Nodes → CommitteeSortition.submitTicket() │ -│ ↓ │ -│ Contract: Validates, computes score, inserts into topN │ -│ ↓ │ -│ Contract: Emits TicketSubmitted for each submission │ -│ │ -│ [Meanwhile: Aggregator schedules finalization timer] │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. FINALIZATION (after 60s) │ -│ Aggregator → CommitteeSortition.finalizeCommittee() │ -│ ↓ │ -│ Contract: Returns topN nodes, emits CommitteeFinalized │ -│ ↓ │ -│ All Nodes: Receive CommitteeFinalized(e3Id, nodes[]) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. KEYGEN (Committee members only) │ -│ Committee Members: Run DKG protocol │ -│ ↓ │ -│ Each generates secret share + aggregated public key │ -│ ↓ │ -│ Emit PublicKeyGenerated locally │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. PUBLIC KEY AGGREGATION │ -│ Aggregator: Collects PublicKeyGenerated from M+ nodes │ -│ ↓ │ -│ Emits PublicKeyAggregated(e3Id, publicKey) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 6. COMMITTEE PUBLISHED │ -│ Aggregator → CiphernodeRegistry.publishCommittee() │ -│ ↓ │ -│ Contract: Stores committee + pubkey, emits CommitteePublished│ -│ ↓ │ -│ ✅ Committee is now registered and ready! │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Key Components & Their Roles - -### **CommitteeSortitionSolWriter** (`crates/evm/src/committee_sortition_sol.rs`) -- **All Nodes:** Submits tickets when `CiphernodeSelected` -- **Aggregator Only** (if `enable_finalizer=true`): - - Tracks submission deadlines - - Auto-calls `finalizeCommittee()` after window - - Cleans up after `CommitteeFinalized` - -### **CommitteeSortition.sol** (Solidity contract) -- Validates ticket submissions -- Maintains sorted topN array (by score) -- Enforces submission window -- Finalizes committee selection - -### **CiphernodeSelector** (`crates/sortition/`) -- Checks if node is eligible (bonding, tickets) -- Computes scores for all owned tickets -- Finds best ticket to submit -- Emits `CiphernodeSelected` if should participate - -### **ThresholdKeyshareExtension** (`crates/keyshare/`) -- Waits for `CommitteeFinalized` event -- Runs DKG with other committee members -- Generates secret shares and public key - -### **PublicKeyAggregatorExtension** (`crates/aggregator/`) -- Collects public keys from committee members -- Validates threshold reached -- Emits `PublicKeyAggregated` - -### **CiphernodeRegistrySolWriter** (`crates/evm/`) -- Receives `PublicKeyAggregated` -- Publishes committee + pubkey to blockchain -- Makes committee official - ---- - -## What Makes Score Sortition Different from Distance Sortition? - -| Aspect | Distance Sortition | Score Sortition | -|--------|-------------------|-----------------| -| **Participation** | Only closest N nodes in merkle tree | ALL eligible nodes can try | -| **Selection** | Aggregator computes distances | Nodes self-select via lottery | -| **Submission** | Aggregator publishes immediately | 60s window for submissions | -| **Fairness** | Depends on tree position | Equal chance based on tickets | -| **Finalization** | Immediate | After submission window | -| **Contract** | CiphernodeRegistry only | CiphernodeRegistry + CommitteeSortition | - ---- - -## Event Flow Summary - -``` -E3Requested - ↓ -CiphernodeSelected (local, selected nodes only) - ↓ -TicketSubmitted (on-chain, for each submission) - ↓ -CommitteeFinalized (on-chain, after window closes) - ↓ -PublicKeyGenerated (local, committee members) - ↓ -PublicKeyAggregated (local, aggregator) - ↓ -CommitteePublished (on-chain, final registration) -``` - ---- - -## Configuration - -To enable score sortition in your builder: -```rust -CiphernodeBuilder::new() - .with_contract_committee_sortition() // ← Enable score sortition - .with_pubkey_agg() // ← Makes this node an aggregator - .build() -``` - -- **Regular nodes:** Submit tickets only -- **Aggregator nodes:** Submit tickets + auto-finalize committees - -The submission window is automatically fetched from the contract's `submissionWindow` immutable variable (typically 60 seconds). - ---- - -## Implementation Files - -### Core Implementation -- `crates/evm/src/committee_sortition_sol.rs` - Contract interaction and finalization logic -- `crates/ciphernode-builder/src/ciphernode_builder.rs:375-410` - Integration and attachment -- `packages/enclave-contracts/contracts/sortition/CommitteeSortition.sol` - On-chain sortition logic - -### Events -- `crates/events/src/enclave_event/committee_finalized.rs` - CommitteeFinalized event -- `crates/events/src/enclave_event/committee_published.rs` - CommitteePublished event - -### Related Components -- `crates/sortition/` - Node selection and ticket computation -- `crates/keyshare/` - DKG and key generation -- `crates/aggregator/` - Public key aggregation - ---- - -## TODOs for Complete Score Sortition Support - -1. **Update CiphernodeRegistrySolWriter** to track finalized committees: - - Store committee nodes from `CommitteeFinalized` event - - Use stored nodes when publishing (not aggregated nodes) - -2. **Make ThresholdKeyshareExtension wait for CommitteeFinalized**: - - Currently starts on `CiphernodeSelected` - - Should wait for `CommitteeFinalized` in score sortition - - Add mode detection or configuration - -3. **Make submission window configurable**: - - Currently fetched from contract (good!) - - Consider adding override in config for testing - ---- - -## Testing Score Sortition - -1. Deploy contracts with `CommitteeSortition` enabled -2. Start aggregator node with `with_contract_committee_sortition()` and `with_pubkey_agg()` -3. Start multiple regular nodes with `with_contract_committee_sortition()` -4. Request E3 computation -5. Watch logs for: - - Ticket submissions - - Finalization after 60s - - Committee members starting keygen - - Public key aggregation - - Final committee publication - ---- - -## Advantages of Score Sortition - -1. **Fair Participation**: All bonded nodes have equal chance based on tickets owned -2. **Decentralized Selection**: No single node controls selection -3. **Transparent**: All submissions on-chain, verifiable -4. **Secure**: Uses cryptographic randomness (seed + ticket numbers) -5. **Flexible**: Can adjust committee size via threshold parameter - ---- - -## Security Considerations - -1. **Submission Window**: Must be long enough for honest nodes but short enough to prevent attacks -2. **Ticket Balance Snapshot**: Uses block number at E3 request to prevent manipulation -3. **One Submission per Node**: Prevents spam and ensures fair distribution -4. **Score Verification**: Contract recomputes score on-chain (not trusted input) -5. **Finalization Permission**: Anyone can finalize (no central authority) diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 75fa09b0cc..0de92b7779 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,4 +1,4 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0xc6e7DF5E7b4f2A278906862b61205850344D4e7d # Default E3 program address from hardhat +VITE_E3_PROGRAM_ADDRESS=0x7a2088a1bFc9d81c55368AE168C2C02570cB814F # Default E3 program address from hardhat diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 0becf549c1..609596eba1 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,10 +3,10 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0xc5a5C42992dECbae36851359345FE25997F5C42d" + address: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" deploy_block: 1 # Set to actual deploy block enclave: - address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block ciphernode_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" @@ -14,9 +14,6 @@ chains: bonding_registry: address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" deploy_block: 1 # Set to actual deploy block - committee_sortition: - address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - deploy_block: 1 # Set to actual deploy block program: dev: true diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 3bc1ee0fee..a593ba8a6e 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -13,9 +13,9 @@ BITQUERY_API_KEY="" CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +ENCLAVE_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # E3 Config diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 48fb0f2e3a..1003650c82 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -90,15 +90,6 @@ }, "blockNumber": 9479398, "address": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61" - }, - "CommitteeSortition": { - "constructorArgs": { - "bondingRegistry": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61", - "ciphernodeRegistry": "0xEC98074C1F64f820f897842d266e1091A0f47Ad8", - "submissionWindow": "300" - }, - "blockNumber": 9479400, - "address": "0xB48207B7faAf3504025552ba2db43d3b4aD74E04" } }, "hardhat": { @@ -211,4 +202,4 @@ "address": "0xAA292E8611aDF267e563f334Ee42320aC96D0463" } } -} \ No newline at end of file +} diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 02ced93e30..37ff7e92ea 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,11 +2,10 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" - enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - ciphernode_registry: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + e3_program: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + enclave: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" - committee_sortition: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" program: dev: true diff --git a/templates/default/hardhat.config.ts b/templates/default/hardhat.config.ts index 9f2a2025ff..d0eed4631f 100644 --- a/templates/default/hardhat.config.ts +++ b/templates/default/hardhat.config.ts @@ -118,12 +118,14 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", + "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/sortition/CommitteeSortition.sol", + "@enclave-e3/contracts/contracts/test/MockStableToken.sol", + "@enclave-e3/contracts/contracts/test/MockSlashingVerifier.sol", ], compilers: [ { diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index ed92ac63cc..bbbdbe497b 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,11 +2,10 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" - enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - ciphernode_registry: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + e3_program: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + enclave: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" - committee_sortition: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" nodes: cn1: From 5028edc1f548f056395adda9afe10ac5a58d933b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 00:27:03 +0500 Subject: [PATCH 50/88] fix: coderabbit review fixes --- README.md | 2 +- .../src/ciphernode_builder.rs | 9 ++++---- .../operator_activation_changed.rs | 1 + crates/evm/src/bonding_registry_sol.rs | 22 +++++++++++++------ crates/evm/src/ciphernode_registry_sol.rs | 5 +++++ 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a88bbcd5c2..1347c23a8d 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ sequenceDiagram E3Program-->>Enclave: inputValidator Enclave->>ComputeProvider: validate(computeProviderParams) ComputeProvider-->>Enclave: decryptionVerifier - Enclave->>CiphernodeRegistry: requestCommittee(e3Id, threshold) + Enclave->>CiphernodeRegistry: requestCommittee(e3Id, seed, threshold) CiphernodeRegistry-->>Enclave: success Enclave-->>Users: e3Id, E3 struct diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index c7164d6c36..016eeaf711 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -33,7 +33,7 @@ use e3_request::E3Router; use e3_sortition::{CiphernodeSelector, Sortition, SortitionRepositoryFactory}; use e3_utils::{rand_eth_addr, SharedRng}; use std::{collections::HashMap, sync::Arc}; -use tracing::info; +use tracing::{error, info}; /// Build a ciphernode configuration. // NOTE: We could use a typestate pattern here to separate production and testing methods. I hummed @@ -366,9 +366,10 @@ impl CiphernodeBuilder { ); } } - Err(_) => { - info!("No wallet configured for this node, skipping writer attachment"); - } + Err(e) => error!( + "Failed to create write provider (likely no wallet configured), skipping writer attachment: {}", + e + ), } } } diff --git a/crates/events/src/enclave_event/operator_activation_changed.rs b/crates/events/src/enclave_event/operator_activation_changed.rs index 2d528f9499..c979b5e4c4 100644 --- a/crates/events/src/enclave_event/operator_activation_changed.rs +++ b/crates/events/src/enclave_event/operator_activation_changed.rs @@ -12,4 +12,5 @@ use serde::{Deserialize, Serialize}; pub struct OperatorActivationChanged { pub operator: String, pub active: bool, + pub chain_id: u64, } diff --git a/crates/evm/src/bonding_registry_sol.rs b/crates/evm/src/bonding_registry_sol.rs index f860910e40..f5b95a2a71 100644 --- a/crates/evm/src/bonding_registry_sol.rs +++ b/crates/evm/src/bonding_registry_sol.rs @@ -58,17 +58,23 @@ impl From for e3_events::ConfigurationUpdated { } } -impl From for e3_events::OperatorActivationChanged { - fn from(value: IBondingRegistry::OperatorActivationChanged) -> Self { +struct OperatorActivationChangedWithChainId( + pub IBondingRegistry::OperatorActivationChanged, + pub u64, +); + +impl From for e3_events::OperatorActivationChanged { + fn from(value: OperatorActivationChangedWithChainId) -> Self { e3_events::OperatorActivationChanged { - operator: value.operator.to_string(), - active: value.active, + operator: value.0.operator.to_string(), + active: value.0.active, + chain_id: value.1, } } } -impl From for EnclaveEvent { - fn from(value: IBondingRegistry::OperatorActivationChanged) -> Self { +impl From for EnclaveEvent { + fn from(value: OperatorActivationChangedWithChainId) -> Self { let payload: e3_events::OperatorActivationChanged = value.into(); EnclaveEvent::from(payload) } @@ -98,7 +104,9 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< error!("Error parsing event OperatorActivationChanged after topic was matched!"); return None; }; - Some(EnclaveEvent::from(event)) + Some(EnclaveEvent::from(OperatorActivationChangedWithChainId( + event, chain_id, + ))) } Some(&IBondingRegistry::ConfigurationUpdated::SIGNATURE_HASH) => { let Ok(event) = IBondingRegistry::ConfigurationUpdated::decode_log_data(data) else { diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index 0e477f7728..1e44fb6fd5 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -289,6 +289,11 @@ impl CiphernodeRegistrySolWriter .send(Subscribe::new("TicketGenerated", addr.clone().into())) .await; + // Stop gracefully on shutdown + let _ = bus + .send(Subscribe::new("Shutdown", addr.clone().into())) + .await; + Ok(addr) } } From 7d7789731337272b2fa54787a6966ecef8ca880d Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 00:28:04 +0500 Subject: [PATCH 51/88] fix: error handling for committee finalizer --- crates/aggregator/src/committee_finalizer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 8f6230d46b..1cfa9ccade 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -71,7 +71,7 @@ impl Handler for CommitteeFinalizer { let current_timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .expect("System time should be after UNIX_EPOCH") .as_secs(); let seconds_until_deadline = if submission_deadline > current_timestamp { From 0805bd94b6be5a672201dbcde785112eb17a9212 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 02:19:26 +0500 Subject: [PATCH 52/88] fix: update nodes addresses --- .../src/ciphernode_builder.rs | 15 ++- crates/docs/user_guide.md | 2 +- crates/entrypoint/src/start/start.rs | 1 + crates/evm/src/bonding_registry_sol.rs | 12 +- crates/sortition/src/node_state.rs | 13 +- crates/sortition/src/sortition.rs | 125 ++++++++++++------ deploy/docker-compose.yml | 16 +-- deploy/local/contracts.sh | 6 +- deploy/local/start.sh | 6 +- docs/pages/CRISP/running-e3.mdx | 23 +++- docs/pages/hello-world-tutorial.mdx | 22 +-- examples/CRISP/enclave.config.yaml | 8 +- .../enclave-contracts/tasks/ciphernode.ts | 2 +- templates/default/enclave.config.yaml | 8 +- tests/integration/enclave.config.yaml | 8 +- tests/integration/fns.sh | 8 +- 16 files changed, 185 insertions(+), 90 deletions(-) diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 016eeaf711..9e491df74d 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -30,7 +30,9 @@ use e3_fhe::ext::FheExtension; use e3_keyshare::ext::{KeyshareExtension, ThresholdKeyshareExtension}; use e3_multithread::Multithread; use e3_request::E3Router; -use e3_sortition::{CiphernodeSelector, Sortition, SortitionRepositoryFactory}; +use e3_sortition::{ + CiphernodeSelector, NodeStateRepositoryFactory, Sortition, SortitionRepositoryFactory, +}; use e3_utils::{rand_eth_addr, SharedRng}; use std::{collections::HashMap, sync::Arc}; use tracing::{error, info}; @@ -274,9 +276,16 @@ impl CiphernodeBuilder { .unwrap_or_else(|| (&InMemStore::new(self.logging).start()).into()); let repositories = store.repositories(); - let sortition = Sortition::attach(&local_bus, repositories.sortition()).await?; - // Ciphernode Selector + let node_state_manager = + e3_sortition::NodeStateManager::attach(&local_bus, &repositories.node_state()).await?; + let sortition = Sortition::attach_with_node_state( + &local_bus, + repositories.sortition(), + node_state_manager, + ) + .await?; + CiphernodeSelector::attach(&local_bus, &sortition, &addr, &store); let mut provider_cache = ProviderCaches::new(); diff --git a/crates/docs/user_guide.md b/crates/docs/user_guide.md index dc9d451fb9..bb179ec488 100644 --- a/crates/docs/user_guide.md +++ b/crates/docs/user_guide.md @@ -99,7 +99,7 @@ Ciphernodes need a registration address to identify themselves within a committe ``` # ~/.config/enclave/config.yaml -address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" +address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" ``` ## Setting your encryption password diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index c88d0d026c..dbb4fe0b82 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -41,6 +41,7 @@ pub async fn execute( .with_datastore(store) .with_chains(&config.chains()) .with_contract_enclave_reader() + .with_contract_bonding_registry() .with_contract_ciphernode_registry() .build() .await?; diff --git a/crates/evm/src/bonding_registry_sol.rs b/crates/evm/src/bonding_registry_sol.rs index f5b95a2a71..0cec4ca360 100644 --- a/crates/evm/src/bonding_registry_sol.rs +++ b/crates/evm/src/bonding_registry_sol.rs @@ -49,8 +49,18 @@ struct ConfigurationUpdatedWithChainId(pub IBondingRegistry::ConfigurationUpdate impl From for e3_events::ConfigurationUpdated { fn from(value: ConfigurationUpdatedWithChainId) -> Self { + let param_bytes = value.0.parameter.as_slice(); + let param_str = String::from_utf8( + param_bytes + .iter() + .copied() + .take_while(|&b| b != 0) + .collect(), + ) + .unwrap_or_else(|_| value.0.parameter.to_string()); + e3_events::ConfigurationUpdated { - parameter: value.0.parameter.to_string(), + parameter: param_str, old_value: value.0.oldValue, new_value: value.0.newValue, chain_id: value.1, diff --git a/crates/sortition/src/node_state.rs b/crates/sortition/src/node_state.rs index 8e3481323a..af0b320905 100644 --- a/crates/sortition/src/node_state.rs +++ b/crates/sortition/src/node_state.rs @@ -87,7 +87,10 @@ impl NodeStateStore { } } -/// Actor that manages node state +#[derive(Message, Clone, Debug)] +#[rtype(result = "Option")] +pub struct GetNodeState; + pub struct NodeStateManager { state: Persistable, bus: Addr>, @@ -135,6 +138,14 @@ impl Actor for NodeStateManager { type Context = Context; } +impl Handler for NodeStateManager { + type Result = Option; + + fn handle(&mut self, _msg: GetNodeState, _: &mut Self::Context) -> Self::Result { + self.state.get() + } +} + impl Handler for NodeStateManager { type Result = (); diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index bf9ea7701b..3af0eaf749 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::distance::DistanceSortition; -use crate::node_state::NodeStateStore; +use crate::node_state::{GetNodeState, NodeStateManager, NodeStateStore}; use crate::ticket::{RegisteredNode, Ticket}; use crate::ticket_sortition::ScoreSortition; use actix::prelude::*; @@ -222,23 +222,61 @@ impl ScoreBackend { chain_id: u64, node_state: &NodeStateStore, ) -> Vec { + info!( + chain_id = chain_id, + registered_count = self.registered.len(), + node_state_count = node_state.nodes.len(), + "Building nodes from state for score sortition" + ); + self.registered .iter() .filter_map(|n| { let addr_str = n.address.to_string(); let key = (chain_id, addr_str.clone()); let Some(ns) = node_state.nodes.get(&key) else { + info!( + address = %addr_str, + chain_id = chain_id, + "Node not found in NodeStateStore" + ); return None; }; if !ns.active { + info!( + address = %addr_str, + "Node is not active" + ); return None; } let count = node_state.available_tickets(chain_id, &addr_str) as u64; + let ticket_price = node_state + .ticket_prices + .get(&chain_id) + .copied() + .unwrap_or(alloy::primitives::U256::from(1)); + let total_tickets = (ns.ticket_balance / ticket_price) + .try_into() + .unwrap_or(0u64); + if count == 0 { + info!( + address = %addr_str, + ticket_balance = ?ns.ticket_balance, + ticket_price = ?ticket_price, + total_tickets = total_tickets, + active_jobs = ns.active_jobs, + "Node has no available tickets" + ); return None; } + info!( + address = %addr_str, + available_tickets = count, + "Node eligible for score sortition" + ); let tickets = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); Some(RegisteredNode { address: n.address, @@ -353,9 +391,9 @@ impl SortitionList for ScoreBackend { /// Enum wrapper around the supported backends. /// -/// New chains should default to `Distance`. If a chain is intended to -/// use score selection, construct it as `SortitionBackend::Score(ScoreBackend::default())` -/// and then populate tickets explicitly. +/// New chains default to `Score` sortition. If a chain is intended to +/// use distance selection, construct it as `SortitionBackend::Distance(DistanceBackend::default())` +/// explicitly. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum SortitionBackend { /// Distance-based selection (stores a simple set of addresses). @@ -365,9 +403,9 @@ pub enum SortitionBackend { } impl SortitionBackend { - /// Construct a backend preconfigured with a default `DistanceBackend`. + /// Construct a backend preconfigured with a default `ScoreBackend`. pub fn default() -> Self { - SortitionBackend::Distance(DistanceBackend::default()) + SortitionBackend::Score(ScoreBackend::default()) } } @@ -435,8 +473,8 @@ pub struct Sortition { list: Persistable>, /// Event bus for error reporting and enclave event subscription. bus: Addr>, - /// Optional reference to node state for score-based sortition - node_state: Option>, + /// Optional reference to NodeStateManager for score-based sortition + node_state_manager: Option>, } /// Parameters for constructing a `Sortition` actor. @@ -446,8 +484,8 @@ pub struct SortitionParams { pub bus: Addr>, /// Persisted per-chain backend map. pub list: Persistable>, - /// Optional node state for score-based sortition - pub node_state: Option>, + /// Optional NodeStateManager for score-based sortition + pub node_state_manager: Option>, } impl Sortition { @@ -456,7 +494,7 @@ impl Sortition { Self { list: params.list, bus: params.bus, - node_state: params.node_state, + node_state_manager: params.node_state_manager, } } @@ -472,7 +510,7 @@ impl Sortition { let addr = Sortition::new(SortitionParams { bus: bus.clone(), list, - node_state: None, // Legacy attach without node state + node_state_manager: None, // Legacy attach without node state }) .start(); bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); @@ -487,16 +525,13 @@ impl Sortition { pub async fn attach_with_node_state( bus: &Addr>, store: Repository>, - node_state_store: Repository, + node_state_manager: Addr, ) -> Result> { let list = store.load_or_default(HashMap::new()).await?; - let node_state = node_state_store - .load_or_default(NodeStateStore::default()) - .await?; let addr = Sortition::new(SortitionParams { bus: bus.clone(), list, - node_state: Some(node_state), + node_state_manager: Some(node_state_manager), }) .start(); bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); @@ -542,9 +577,9 @@ impl Handler for Sortition { /// Add a node to the target chain. /// - /// If the chain does not exist yet, its backend is initialized to `Distance`. - /// For score-based chains, switch construction time to `SortitionBackend::Score` - /// and call the ticket setters separately (this handler only adds the address). + /// If the chain does not exist yet, its backend is initialized to `Score` (default). + /// For distance-based chains, initialize explicitly with `SortitionBackend::Distance` + /// before any nodes are added. #[instrument(name = "sortition_add_node", skip_all)] fn handle(&mut self, msg: CiphernodeAdded, _ctx: &mut Self::Context) -> Self::Result { trace!("Adding node: {}", msg.address); @@ -587,30 +622,46 @@ impl Handler for Sortition { } impl Handler for Sortition { - type Result = Option<(u64, Option)>; + type Result = ResponseFuture)>>; fn handle(&mut self, msg: GetNodeIndex, _ctx: &mut Self::Context) -> Self::Result { - let node_state_snapshot = self.node_state.as_ref().and_then(|p| p.get()); - let node_state_ref = node_state_snapshot.as_ref(); + let node_state_manager = self.node_state_manager.clone(); + let bus = self.bus.clone(); + + // Get the sortition backends synchronously + let backends_snapshot = self.list.get(); + + Box::pin(async move { + // Query NodeStateManager for fresh state + let node_state_snapshot = if let Some(manager) = node_state_manager { + manager.send(GetNodeState).await.ok().flatten() + } else { + None + }; + let node_state_ref = node_state_snapshot.as_ref(); - self.list - .try_with(|map| { + // Use the backends snapshot + if let Some(map) = backends_snapshot { if let Some(backend) = map.get(&msg.chain_id) { - backend.get_index( - msg.seed, - msg.size, - msg.address.clone(), - node_state_ref, - msg.chain_id, - ) + backend + .get_index( + msg.seed, + msg.size, + msg.address.clone(), + node_state_ref, + msg.chain_id, + ) + .unwrap_or_else(|err| { + bus.err(EnclaveErrorType::Sortition, err); + None + }) } else { - Ok(None) + None } - }) - .unwrap_or_else(|err| { - self.bus.err(EnclaveErrorType::Sortition, err); + } else { None - }) + } + }) } } impl Handler for Sortition { diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 8f64733321..a96bc32c7f 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,6 +1,6 @@ services: cn1: - image: {{IMAGE}} + image: { { IMAGE } } volumes: - ./cn1.yaml:/home/ciphernode/.config/enclave/config.yaml:ro - cn1-data:/home/ciphernode/.local/share/enclave @@ -10,7 +10,7 @@ services: env_file: .env environment: AGGREGATOR: "false" - ADDRESS: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + ADDRESS: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" QUIC_PORT: 9091 deploy: replicas: 1 @@ -19,7 +19,7 @@ services: - global-network cn2: - image: {{IMAGE}} + image: { { IMAGE } } volumes: - ./cn2.yaml:/home/ciphernode/.config/enclave/config.yaml:ro - cn2-data:/home/ciphernode/.local/share/enclave @@ -29,7 +29,7 @@ services: env_file: .env environment: AGGREGATOR: "false" - ADDRESS: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + ADDRESS: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" QUIC_PORT: 9092 deploy: replicas: 1 @@ -38,7 +38,7 @@ services: - global-network cn3: - image: {{IMAGE}} + image: { { IMAGE } } volumes: - ./cn3.yaml:/home/ciphernode/.config/enclave/config.yaml:ro - cn3-data:/home/ciphernode/.local/share/enclave @@ -48,7 +48,7 @@ services: env_file: .env environment: AGGREGATOR: "false" - ADDRESS: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + ADDRESS: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" QUIC_PORT: 9093 deploy: replicas: 1 @@ -57,7 +57,7 @@ services: - global-network aggregator: - image: {{IMAGE}} + image: { { IMAGE } } depends_on: - cn1 volumes: @@ -85,7 +85,7 @@ secrets: secrets_cn3: file: cn3.secrets.json secrets_agg: - file: agg.secrets.json + file: agg.secrets.json volumes: cn1-data: diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index 13ea010027..356e1f08bf 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -10,9 +10,9 @@ sleep 2 # wait for enclave to start # Get the addresses of the ciphernodes -CN1=0xbDA5747bFD65F08deb54cb465eB87D40e51B197E -CN2=0xdD2FD4581271e230360230F9337D5c0430Bf44C0 -CN3=0x2546BcD3c84621e976D8185a91A922aE77ECEc30 +CN1=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +CN2=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +CN3=0x90F79bf6EB2c4f870365E785982E1f101E93b906 # Add the ciphernodes to the enclave (cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost") diff --git a/deploy/local/start.sh b/deploy/local/start.sh index 708268b8f9..20beab7df5 100755 --- a/deploy/local/start.sh +++ b/deploy/local/start.sh @@ -79,9 +79,9 @@ deploy_contracts() { # Add ciphernodes to the registry echo " Adding ciphernodes to registry..." - CN1=0xbDA5747bFD65F08deb54cb465eB87D40e51B197E - CN2=0xdD2FD4581271e230360230F9337D5c0430Bf44C0 - CN3=0x2546BcD3c84621e976D8185a91A922aE77ECEc30 + CN1=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + CN2=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC + CN3=0x90F79bf6EB2c4f870365E785982E1f101E93b906 pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost" pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost" diff --git a/docs/pages/CRISP/running-e3.mdx b/docs/pages/CRISP/running-e3.mdx index aa593a6fc5..64e8001183 100644 --- a/docs/pages/CRISP/running-e3.mdx +++ b/docs/pages/CRISP/running-e3.mdx @@ -7,7 +7,8 @@ import { Steps } from 'nextra/components' # Running an E3 Program -In this section, we will go through all the steps to run an E3 Program using CRISP. We will run a complete voting round of CRISP and do the following: +In this section, we will go through all the steps to run an E3 Program using CRISP. We will run a +complete voting round of CRISP and do the following: - Start the infrastructure (nodes and contracts) - Start the CRISP applications (client, server, program) @@ -24,17 +25,20 @@ Please make sure you have followed the [CRISP Setup](/CRISP/setup) guide before First, ensure you have the infrastructure running. If you haven't already, complete the setup: **Terminal 1: Start Anvil** + ```sh anvil ``` **Terminal 2: Start Ciphernodes** + ```sh cd examples/CRISP enclave nodes up -v ``` -Make sure contracts are deployed and ciphernodes are added to the registry as described in the setup guide. +Make sure contracts are deployed and ciphernodes are added to the registry as described in the setup +guide. ### Start the Client Application @@ -70,7 +74,7 @@ Navigate to the program directory and start the program server: ```sh cd examples/CRISP/ -enclave program start +enclave program start ``` This runs the RISC Zero program server that handles secure computations. @@ -82,7 +86,7 @@ cd examples/CRISP/ enclave program start --dev true ``` -In this case, Risc0 will not be used to generate proofs, and instead these will be mocked. +In this case, Risc0 will not be used to generate proofs, and instead these will be mocked. ### Initialize a New Voting Round @@ -101,6 +105,7 @@ Follow these steps in the CLI: 2. Choose `Initialize new E3 round` to start a new voting round You should see output similar to: + ```sh [2024-10-22 11:56:11] [commands.rs:42] - Starting new CRISP round! [2024-10-22 11:56:11] [commands.rs:46] - Enabling E3 Program... @@ -135,11 +140,13 @@ To interact with the client application, you need to configure MetaMask: You can monitor the entire process through the various terminal outputs: **Server logs will show:** + - Vote submissions being received - Computation starting when the voting period ends - Results being computed and published **Example server output:** + ```sh [2024-10-22 11:59:12] [handlers.rs:95] - Vote Count: 1 [2024-10-22 11:59:12] [handlers.rs:101] - Starting computation for E3: 0 @@ -150,16 +157,18 @@ Prove function execution time: 2 minutes and 37 seconds ``` **Ciphernode logs will show:** + ```sh INFO Extracted log from evm sending now. INFO evt=CiphertextOutputPublished(e3_id: 0) e3_id=0 -INFO evt=DecryptionshareCreated(e3_id: 0, node: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30) e3_id=0 +INFO evt=DecryptionshareCreated(e3_id: 0, node: 0x90F79bf6EB2c4f870365E785982E1f101E93b906) e3_id=0 INFO evt=PlaintextAggregated(e3_id: 0, src_chain_id: 31337) e3_id=0 INFO evt=E3RequestComplete(e3_id: 0) INFO Plaintext published. tx=0x320dd95358cc86c2a709b6fec0c6865b43fa063cb61dfcb8a748005d4886f040 ``` **Final result logs:** + ```sh [2024-10-22 12:01:49] [handlers.rs:171] - Handling PlaintextOutputPublished event... [2024-10-22 12:01:49] [handlers.rs:181] - Vote Count: 1 @@ -183,7 +192,8 @@ The CRISP voting process involves several key steps: ## Troubleshooting - **Ensure all terminals remain open** during the voting process -- **MetaMask connection issues**: Check that you're connected to the correct network (Chain ID: 31337) +- **MetaMask connection issues**: Check that you're connected to the correct network (Chain + ID: 31337) - **Transaction failures**: Verify you have sufficient ETH balance from the Anvil faucet - **Server errors**: Monitor the server logs for detailed error messages - **Ciphernode issues**: Ensure all ciphernode processes are running and connected @@ -198,4 +208,3 @@ Once you've successfully run a voting round, you can: - **Deploy to testnet**: Move beyond local development to public testnets ![Result](/poll-result.png) - diff --git a/docs/pages/hello-world-tutorial.mdx b/docs/pages/hello-world-tutorial.mdx index dd6b7c3701..3db59eac73 100644 --- a/docs/pages/hello-world-tutorial.mdx +++ b/docs/pages/hello-world-tutorial.mdx @@ -5,13 +5,16 @@ description: 'Build your first E3 program from scratch with step-by-step explana # Hello World Tutorial -This tutorial walks you through building your first E3 program from scratch. You'll learn how each component works and how they interact to create a secure, encrypted computation. +This tutorial walks you through building your first E3 program from scratch. You'll learn how each +component works and how they interact to create a secure, encrypted computation. -> Make sure to complete the [Quick Start](/quick-start) guide first to get familiar with the basic workflow before diving into this detailed tutorial. +> Make sure to complete the [Quick Start](/quick-start) guide first to get familiar with the basic +> workflow before diving into this detailed tutorial. ## What We're Building We'll create a simple E3 program that: + 1. **Accepts** two encrypted numbers from users 2. **Computes** their sum using Fully Homomorphic Encryption 3. **Returns** the encrypted result without ever decrypting the inputs @@ -19,6 +22,7 @@ We'll create a simple E3 program that: ## Prerequisites Before starting, ensure you have: + - [Enclave CLI installed](/installation) - Basic knowledge of Rust and TypeScript - Rust, Docker, Node.js, and pnpm installed @@ -61,7 +65,7 @@ pub fn fhe_processor(fhe_inputs: &FHEInputs) -> Vec { // Start with zero (encrypted) let mut sum = Ciphertext::zero(¶ms); - + // Add each encrypted input to the sum for ciphertext_bytes in &fhe_inputs.ciphertexts { let ciphertext = Ciphertext::from_bytes(&ciphertext_bytes.0, ¶ms).unwrap(); @@ -116,15 +120,15 @@ The `enclave.config.yaml` file configures your development environment: ```yaml chains: - - name: "hardhat" - rpc_url: "ws://localhost:8545" + - name: 'hardhat' + rpc_url: 'ws://localhost:8545' contracts: - e3_program: "0x9A676e781A523b5d0C0e43731313A708CB607508" + e3_program: '0x9A676e781A523b5d0C0e43731313A708CB607508' # ... other contract addresses nodes: - cn1: # Ciphernode 1 - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + cn1: # Ciphernode 1 + address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' quic_port: 9201 autonetkey: true autopassword: true @@ -189,7 +193,7 @@ Modify your program to accept 3 or more encrypted inputs. Customize the client application in `./client/src/` to match your computation. -Happy building with Enclave! 🚀 +Happy building with Enclave! 🚀 ## Next Steps diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 609596eba1..c8d154561d 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -24,22 +24,22 @@ program: nodes: cn1: - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" quic_port: 9201 autonetkey: true autopassword: true cn2: - address: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" quic_port: 9202 autonetkey: true autopassword: true cn3: - address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" quic_port: 9203 autonetkey: true autopassword: true ag: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" + address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" quic_port: 9094 autonetkey: true autopassword: true diff --git a/packages/enclave-contracts/tasks/ciphernode.ts b/packages/enclave-contracts/tasks/ciphernode.ts index af0e10f207..8c85500b76 100644 --- a/packages/enclave-contracts/tasks/ciphernode.ts +++ b/packages/enclave-contracts/tasks/ciphernode.ts @@ -21,7 +21,7 @@ export const ciphernodeAdd = task( .addOption({ name: "ticketAmount", description: - "amount of USDC to deposit for tickets (in wei, e.g., 1000000000 for 1000 USDC)", + "amount of USDC to deposit for tickets (in wei, e.g., 1,000,000,000 for 1000 USDC)", defaultValue: "1000000000", }) .setAction(async () => ({ diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 37ff7e92ea..c3d49da27c 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -16,22 +16,22 @@ program: nodes: cn1: - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" quic_port: 9201 autonetkey: true autopassword: true cn2: - address: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" quic_port: 9202 autonetkey: true autopassword: true cn3: - address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" quic_port: 9203 autonetkey: true autopassword: true ag: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" + address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" quic_port: 9094 autonetkey: true autopassword: true diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index bbbdbe497b..191999a097 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -9,22 +9,22 @@ chains: nodes: cn1: - address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" quic_port: 9091 autonetkey: true autopassword: true cn2: - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" quic_port: 9092 autonetkey: true autopassword: true cn3: - address: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" quic_port: 9093 autonetkey: true autopassword: true cn4: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" + address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" quic_port: 9094 autonetkey: true autopassword: true diff --git a/tests/integration/fns.sh b/tests/integration/fns.sh index 8e799ce03e..81fed65715 100644 --- a/tests/integration/fns.sh +++ b/tests/integration/fns.sh @@ -20,10 +20,10 @@ NETWORK_PRIVATE_KEY_AG="0x51a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85 CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams." # These are random addresses for now -CIPHERNODE_ADDRESS_1="0x2546BcD3c84621e976D8185a91A922aE77ECEc30" -CIPHERNODE_ADDRESS_2="0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" -CIPHERNODE_ADDRESS_3="0xdD2FD4581271e230360230F9337D5c0430Bf44C0" -CIPHERNODE_ADDRESS_4="0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" +CIPHERNODE_ADDRESS_1="0x90F79bf6EB2c4f870365E785982E1f101E93b906" +CIPHERNODE_ADDRESS_2="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +CIPHERNODE_ADDRESS_3="0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" +CIPHERNODE_ADDRESS_4="0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" # These are the network private keys for the ciphernodes NETWORK_PRIVATE_KEY_1="0x11a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" From c5658c68c2392ada415d7b7476078a4609b47738 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 15:15:06 +0500 Subject: [PATCH 53/88] feat(sync): implement persistent node state, finalized committees, and historical event replay [skip ci] --- crates/aggregator/src/committee_finalizer.rs | 8 +- crates/aggregator/src/publickey_aggregator.rs | 6 +- .../src/ciphernode_builder.rs | 5 +- crates/config/src/store_keys.rs | 4 + crates/evm/src/ciphernode_registry_sol.rs | 63 +++------------ crates/request/src/context.rs | 10 ++- crates/request/src/router.rs | 15 +++- crates/sortition/src/ciphernode_selector.rs | 2 +- crates/sortition/src/node_state.rs | 60 +++++++++++++- crates/sortition/src/repo.rs | 11 +++ crates/sortition/src/sortition.rs | 59 +++++++++++++- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../registry/CiphernodeRegistryOwnable.sol | 2 +- .../enclave-contracts/deployed_contracts.json | 80 ++++++++----------- .../scripts/deployEnclave.ts | 7 +- .../enclave-contracts/test/Enclave.spec.ts | 2 +- .../CiphernodeRegistryOwnable.spec.ts | 2 +- 19 files changed, 218 insertions(+), 124 deletions(-) diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 1cfa9ccade..1be605d142 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -74,16 +74,18 @@ impl Handler for CommitteeFinalizer { .expect("System time should be after UNIX_EPOCH") .as_secs(); + const FINALIZATION_BUFFER_SECONDS: u64 = 5; + let seconds_until_deadline = if submission_deadline > current_timestamp { - submission_deadline - current_timestamp + (submission_deadline - current_timestamp) + FINALIZATION_BUFFER_SECONDS } else { info!( e3_id = %e3_id, submission_deadline = submission_deadline, current_timestamp = current_timestamp, - "Submission deadline already passed, finalizing immediately" + "Submission deadline already passed, finalizing with buffer" ); - 0 + FINALIZATION_BUFFER_SECONDS }; info!( diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index 88b31d24ac..e4b7b53349 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -11,7 +11,7 @@ use e3_events::{ Die, E3id, EnclaveEvent, EventBus, KeyshareCreated, OrderedSet, PublicKeyAggregated, Seed, }; use e3_fhe::{Fhe, GetAggregatePublicKey}; -use e3_sortition::{GetNodeIndex, GetNodes, Sortition}; +use e3_sortition::{GetNodeIndex, GetNodesForE3, Sortition}; use e3_utils::ArcBytes; use std::sync::Arc; use tracing::{error, trace}; @@ -228,8 +228,8 @@ impl Handler for PublicKeyAggregator { fn handle(&mut self, msg: NotifyNetwork, _: &mut Self::Context) -> Self::Result { Box::pin( self.sortition - .send(GetNodes { - chain_id: msg.e3_id.chain_id(), + .send(GetNodesForE3 { + e3_id: msg.e3_id.clone(), }) .into_actor(self) .map(move |res, act, _| { diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 9e491df74d..678adc8dca 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -31,7 +31,8 @@ use e3_keyshare::ext::{KeyshareExtension, ThresholdKeyshareExtension}; use e3_multithread::Multithread; use e3_request::E3Router; use e3_sortition::{ - CiphernodeSelector, NodeStateRepositoryFactory, Sortition, SortitionRepositoryFactory, + CiphernodeSelector, FinalizedCommitteesRepositoryFactory, NodeStateRepositoryFactory, + Sortition, SortitionRepositoryFactory, }; use e3_utils::{rand_eth_addr, SharedRng}; use std::{collections::HashMap, sync::Arc}; @@ -282,6 +283,7 @@ impl CiphernodeBuilder { let sortition = Sortition::attach_with_node_state( &local_bus, repositories.sortition(), + repositories.finalized_committees(), node_state_manager, ) .await?; @@ -362,6 +364,7 @@ impl CiphernodeBuilder { &local_bus, write_provider.clone(), &chain.contracts.ciphernode_registry.address(), + self.pubkey_agg, // is_aggregator flag ) .await?; info!("CiphernodeRegistrySolWriter attached for publishing committees"); diff --git a/crates/config/src/store_keys.rs b/crates/config/src/store_keys.rs index a85dba71fc..5cdc6ee26f 100644 --- a/crates/config/src/store_keys.rs +++ b/crates/config/src/store_keys.rs @@ -68,4 +68,8 @@ impl StoreKeys { pub fn node_state() -> String { String::from("//node_state") } + + pub fn finalized_committees() -> String { + String::from("//finalized_committees") + } } diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index 1e44fb6fd5..da9dba9d2e 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -19,7 +19,6 @@ use e3_events::{ BusError, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, }; -use std::collections::HashMap; use tracing::{error, info, trace}; sol!( @@ -247,9 +246,6 @@ pub struct CiphernodeRegistrySolWriter

{ provider: EthProvider

, contract_address: Address, bus: Addr>, - /// Store finalized committees for score sortition - /// Maps E3id to the finalized committee nodes - finalized_committees: HashMap>, } impl CiphernodeRegistrySolWriter

{ @@ -262,7 +258,6 @@ impl CiphernodeRegistrySolWriter provider, contract_address, bus: bus.clone(), - finalized_committees: HashMap::new(), }) } @@ -270,19 +265,17 @@ impl CiphernodeRegistrySolWriter bus: &Addr>, provider: EthProvider

, contract_address: &str, + is_aggregator: bool, ) -> Result>> { let addr = CiphernodeRegistrySolWriter::new(bus, provider, contract_address.parse()?) .await? .start(); - let _ = bus - .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) - .await; - - // Subscribe to CommitteeFinalized for score sortition - let _ = bus - .send(Subscribe::new("CommitteeFinalized", addr.clone().into())) - .await; + if is_aggregator { + let _ = bus + .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) + .await; + } // Subscribe to TicketGenerated for ticket submission let _ = bus @@ -315,12 +308,6 @@ impl Handler ctx.notify(data); } } - EnclaveEvent::CommitteeFinalized { data, .. } => { - // Only store if chain matches - if self.provider.chain_id() == data.e3_id.chain_id() { - ctx.notify(data); - } - } EnclaveEvent::TicketGenerated { data, .. } => { // Submit ticket if chain matches if self.provider.chain_id() == data.e3_id.chain_id() { @@ -333,21 +320,6 @@ impl Handler } } -impl Handler - for CiphernodeRegistrySolWriter

-{ - type Result = (); - - fn handle(&mut self, msg: CommitteeFinalized, _: &mut Self::Context) -> Self::Result { - info!( - "Storing finalized committee for E3 {:?} (score sortition)", - msg.e3_id - ); - self.finalized_committees - .insert(msg.e3_id.clone(), msg.committee); - } -} - impl Handler for CiphernodeRegistrySolWriter

{ @@ -421,27 +393,11 @@ impl Handler Self::Result { let e3_id = msg.e3_id.clone(); let pubkey = msg.pubkey.clone(); + let nodes = msg.nodes.clone(); let contract_address = self.contract_address; let provider = self.provider.clone(); let bus = self.bus.clone(); - // Check if we have a finalized committee for this E3 (score sortition) - // Otherwise use the nodes from PublicKeyAggregated (distance sortition) - let nodes = if let Some(finalized_nodes) = self.finalized_committees.remove(&e3_id) { - info!( - "Using finalized committee nodes for E3 {:?} (score sortition)", - e3_id - ); - // Convert Vec to OrderedSet - OrderedSet::from_iter(finalized_nodes) - } else { - info!( - "Using aggregated nodes for E3 {:?} (distance sortition)", - e3_id - ); - msg.nodes.clone() - }; - Box::pin(async move { let result = publish_committee_to_registry(provider, contract_address, e3_id, nodes, pubkey) @@ -564,11 +520,14 @@ impl CiphernodeRegistrySol { bus: &Addr>, provider: EthProvider

, contract_address: &str, + is_aggregator: bool, ) -> Result>> where P: Provider + WalletProvider + Clone + 'static, { - let writer = CiphernodeRegistrySolWriter::attach(bus, provider, contract_address).await?; + let writer = + CiphernodeRegistrySolWriter::attach(bus, provider, contract_address, is_aggregator) + .await?; Ok(writer) } } diff --git a/crates/request/src/context.rs b/crates/request/src/context.rs index d45d3b44a0..6a024e4b81 100644 --- a/crates/request/src/context.rs +++ b/crates/request/src/context.rs @@ -9,7 +9,8 @@ use actix::Recipient; use anyhow::Result; use async_trait::async_trait; use e3_data::{ - Checkpoint, FromSnapshotWithParams, Repositories, RepositoriesFactory, Repository, Snapshot, + Checkpoint, DataStore, FromSnapshotWithParams, Repositories, RepositoriesFactory, Repository, + Snapshot, }; use e3_events::{E3id, EnclaveEvent}; use serde::{Deserialize, Serialize}; @@ -38,6 +39,8 @@ pub struct E3Context { pub dependencies: HetrogenousMap, /// A Repository for storing this context's data snapshot pub repository: Repository, + /// Root data store for accessing global repositories + root_store: DataStore, } #[derive(Serialize, Deserialize)] @@ -57,6 +60,7 @@ pub struct E3ContextParams { pub repository: Repository, pub e3_id: E3id, pub extensions: Arc>>, + pub root_store: DataStore, } impl E3Context { @@ -66,6 +70,7 @@ impl E3Context { repository: params.repository, recipients: init_recipients(), dependencies: HetrogenousMap::new(), + root_store: params.root_store, } } @@ -132,7 +137,7 @@ impl E3Context { impl RepositoriesFactory for E3Context { fn repositories(&self) -> Repositories { - self.repository().clone().into() + self.root_store.repositories() } } @@ -158,6 +163,7 @@ impl FromSnapshotWithParams for E3Context { repository: params.repository, recipients: init_recipients(), dependencies: HetrogenousMap::new(), + root_store: params.root_store, }; for extension in params.extensions.iter() { diff --git a/crates/request/src/router.rs b/crates/request/src/router.rs index 865e21bfae..3af74a99a6 100644 --- a/crates/request/src/router.rs +++ b/crates/request/src/router.rs @@ -105,12 +105,15 @@ pub struct E3Router { bus: Addr>, /// A repository for storing snapshots store: Repository, + /// Root data store for creating E3 contexts + root_store: DataStore, } pub struct E3RouterParams { extensions: Arc>>, bus: Addr>, store: Repository, + root_store: DataStore, } impl E3Router { @@ -120,6 +123,7 @@ impl E3Router { bus: bus.clone(), extensions: vec![], store: repositories.router(), + root_store: store, }; // Everything needs the committe meta factory so adding it here by default @@ -131,6 +135,7 @@ impl E3Router { extensions: params.extensions, bus: params.bus.clone(), store: params.store.clone(), + root_store: params.root_store.clone(), completed: HashSet::new(), contexts: HashMap::new(), buffer: EventBuffer { @@ -164,12 +169,13 @@ impl Handler for E3Router { return; } - let repositories = self.repository().repositories(); + let repositories = self.root_store.repositories(); let context = self.contexts.entry(e3_id.clone()).or_insert_with(|| { E3Context::from_params(E3ContextParams { e3_id: e3_id.clone(), repository: repositories.context(&e3_id), extensions: self.extensions.clone(), + root_store: self.root_store.clone(), }) }); @@ -246,7 +252,7 @@ impl FromSnapshotWithParams for E3Router { async fn from_snapshot(params: Self::Params, snapshot: Self::Snapshot) -> Result { let mut contexts = HashMap::new(); - let repositories = params.store.repositories(); + let repositories = params.root_store.repositories(); for e3_id in snapshot.contexts { let Some(ctx_snapshot) = repositories.context(&e3_id).read().await? else { continue; @@ -259,6 +265,7 @@ impl FromSnapshotWithParams for E3Router { repository: repositories.context(&e3_id), e3_id: e3_id.clone(), extensions: params.extensions.clone(), + root_store: params.root_store.clone(), }, ctx_snapshot, ) @@ -273,6 +280,7 @@ impl FromSnapshotWithParams for E3Router { buffer: EventBuffer::default(), bus: params.bus, store: repositories.router(), + root_store: params.root_store, }) } } @@ -282,6 +290,7 @@ pub struct E3RouterBuilder { pub bus: Addr>, pub extensions: Vec>, pub store: Repository, + pub root_store: DataStore, } impl E3RouterBuilder { @@ -297,8 +306,8 @@ impl E3RouterBuilder { let params = E3RouterParams { extensions: self.extensions.into(), bus: self.bus.clone(), - store: router_repo, + root_store: self.root_store, }; let e3r = match snapshot { diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index 1e61769fb1..5c528e836a 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -76,7 +76,7 @@ impl Handler for CiphernodeSelector { impl Handler for CiphernodeSelector { type Result = ResponseFuture<()>; - fn handle(&mut self, data: E3Requested, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, data: E3Requested, _ctx: &mut Self::Context) -> Self::Result { let address = self.address.clone(); let sortition = self.sortition.clone(); let bus = self.bus.clone(); diff --git a/crates/sortition/src/node_state.rs b/crates/sortition/src/node_state.rs index af0b320905..da3501ec3f 100644 --- a/crates/sortition/src/node_state.rs +++ b/crates/sortition/src/node_state.rs @@ -9,8 +9,9 @@ use alloy::primitives::U256; use anyhow::Result; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ - BusError, CommitteePublished, ConfigurationUpdated, EnclaveErrorType, EnclaveEvent, EventBus, - OperatorActivationChanged, PlaintextOutputPublished, Subscribe, TicketBalanceUpdated, + BusError, CiphernodeAdded, CiphernodeRemoved, CommitteePublished, ConfigurationUpdated, + EnclaveErrorType, EnclaveEvent, EventBus, OperatorActivationChanged, PlaintextOutputPublished, + Subscribe, TicketBalanceUpdated, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -112,6 +113,10 @@ impl NodeStateManager { let addr = NodeStateManager::new(state, bus.clone()).start(); + bus.send(Subscribe::new("CiphernodeAdded", addr.clone().into())) + .await?; + bus.send(Subscribe::new("CiphernodeRemoved", addr.clone().into())) + .await?; bus.send(Subscribe::new("TicketBalanceUpdated", addr.clone().into())) .await?; bus.send(Subscribe::new( @@ -151,6 +156,12 @@ impl Handler for NodeStateManager { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { + EnclaveEvent::CiphernodeAdded { data, .. } => { + ctx.notify(data); + } + EnclaveEvent::CiphernodeRemoved { data, .. } => { + ctx.notify(data); + } EnclaveEvent::TicketBalanceUpdated { data, .. } => { ctx.notify(data); } @@ -325,3 +336,48 @@ impl Handler for NodeStateManager { } } } + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: CiphernodeAdded, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + let key = (msg.chain_id, msg.address.clone()); + // Only create entry if it doesn't exist - preserve existing state + state.nodes.entry(key).or_insert_with(NodeState::default); + + info!( + operator = %msg.address, + chain_id = msg.chain_id, + "Node registered in NodeStateManager" + ); + + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} + +impl Handler for NodeStateManager { + type Result = (); + + fn handle(&mut self, msg: CiphernodeRemoved, _: &mut Self::Context) -> Self::Result { + match self.state.try_mutate(|mut state| { + let key = (msg.chain_id, msg.address.clone()); + state.nodes.remove(&key); + + info!( + operator = %msg.address, + chain_id = msg.chain_id, + "Node removed from NodeStateManager" + ); + + Ok(state) + }) { + Ok(_) => (), + Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), + } + } +} diff --git a/crates/sortition/src/repo.rs b/crates/sortition/src/repo.rs index 3f48aea8a2..ab21448fc8 100644 --- a/crates/sortition/src/repo.rs +++ b/crates/sortition/src/repo.rs @@ -7,6 +7,7 @@ use crate::{NodeStateStore, SortitionBackend}; use e3_config::StoreKeys; use e3_data::{Repositories, Repository}; +use e3_events::E3id; use std::collections::HashMap; pub trait SortitionRepositoryFactory { @@ -28,3 +29,13 @@ impl NodeStateRepositoryFactory for Repositories { Repository::new(self.store.scope(StoreKeys::node_state())) } } + +pub trait FinalizedCommitteesRepositoryFactory { + fn finalized_committees(&self) -> Repository>>; +} + +impl FinalizedCommitteesRepositoryFactory for Repositories { + fn finalized_committees(&self) -> Repository>> { + Repository::new(self.store.scope(StoreKeys::finalized_committees())) + } +} diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index 3af0eaf749..b4b149424f 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -13,8 +13,8 @@ use alloy::primitives::Address; use anyhow::{anyhow, Context, Result}; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ - BusError, CiphernodeAdded, CiphernodeRemoved, EnclaveErrorType, EnclaveEvent, EventBus, Seed, - Subscribe, + BusError, CiphernodeAdded, CiphernodeRemoved, CommitteeFinalized, EnclaveErrorType, + EnclaveEvent, EventBus, Seed, Subscribe, }; use num::BigInt; use serde::{Deserialize, Serialize}; @@ -49,6 +49,14 @@ pub struct GetNodes { pub chain_id: u64, } +/// Message to get the finalized committee nodes for a specific E3. +#[derive(Message, Clone, Debug)] +#[rtype(result = "Vec")] +pub struct GetNodesForE3 { + /// E3 ID to get nodes for. + pub e3_id: e3_events::E3id, +} + /// Minimal interface that all sortition backends must implement. /// /// Backends can store their own shapes (e.g., a `HashSet` of addresses @@ -475,6 +483,8 @@ pub struct Sortition { bus: Addr>, /// Optional reference to NodeStateManager for score-based sortition node_state_manager: Option>, + /// Persistent map of finalized committees per E3 + finalized_committees: Persistable>>, } /// Parameters for constructing a `Sortition` actor. @@ -486,6 +496,8 @@ pub struct SortitionParams { pub list: Persistable>, /// Optional NodeStateManager for score-based sortition pub node_state_manager: Option>, + /// Persistent map of finalized committees per E3 + pub finalized_committees: Persistable>>, } impl Sortition { @@ -495,6 +507,7 @@ impl Sortition { list: params.list, bus: params.bus, node_state_manager: params.node_state_manager, + finalized_committees: params.finalized_committees, } } @@ -505,16 +518,20 @@ impl Sortition { pub async fn attach( bus: &Addr>, store: Repository>, + committees_store: Repository>>, ) -> Result> { let list = store.load_or_default(HashMap::new()).await?; + let finalized_committees = committees_store.load_or_default(HashMap::new()).await?; let addr = Sortition::new(SortitionParams { bus: bus.clone(), list, node_state_manager: None, // Legacy attach without node state + finalized_committees, }) .start(); bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); bus.do_send(Subscribe::new("CiphernodeRemoved", addr.clone().into())); + bus.do_send(Subscribe::new("CommitteeFinalized", addr.clone().into())); Ok(addr) } @@ -525,17 +542,21 @@ impl Sortition { pub async fn attach_with_node_state( bus: &Addr>, store: Repository>, + committees_store: Repository>>, node_state_manager: Addr, ) -> Result> { let list = store.load_or_default(HashMap::new()).await?; + let finalized_committees = committees_store.load_or_default(HashMap::new()).await?; let addr = Sortition::new(SortitionParams { bus: bus.clone(), list, node_state_manager: Some(node_state_manager), + finalized_committees, }) .start(); bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); bus.do_send(Subscribe::new("CiphernodeRemoved", addr.clone().into())); + bus.do_send(Subscribe::new("CommitteeFinalized", addr.clone().into())); Ok(addr) } @@ -567,6 +588,7 @@ impl Handler for Sortition { match msg { EnclaveEvent::CiphernodeAdded { data, .. } => ctx.notify(data.clone()), EnclaveEvent::CiphernodeRemoved { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data.clone()), _ => (), } } @@ -675,3 +697,36 @@ impl Handler for Sortition { }) } } + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { + info!( + e3_id = %msg.e3_id, + committee_size = msg.committee.len(), + "Storing finalized committee" + ); + + if let Err(err) = self.finalized_committees.try_mutate(|mut committees| { + committees.insert(msg.e3_id.clone(), msg.committee.clone()); + Ok(committees) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = Vec; + + fn handle(&mut self, msg: GetNodesForE3, _ctx: &mut Self::Context) -> Self::Result { + self.finalized_committees + .get() + .and_then(|committees| committees.get(&msg.e3_id).cloned()) + .unwrap_or_else(|| { + tracing::warn!("No finalized committee found for E3 {}", msg.e3_id); + Vec::new() + }) + } +} diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index b2f0565602..da0ed3208f 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -851,5 +851,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-10b2b688a0db0c7a1bf549986929dcd9bb192f79" + "buildInfoId": "solc-0_8_28-a2f64967aeae699bd499cc90bbcf76e2314a0651" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 12cbcbae39..b4749a16db 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -535,5 +535,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-10b2b688a0db0c7a1bf549986929dcd9bb192f79" + "buildInfoId": "solc-0_8_28-a2f64967aeae699bd499cc90bbcf76e2314a0651" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index ffd6abca4f..af06d236fa 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -958,5 +958,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-10b2b688a0db0c7a1bf549986929dcd9bb192f79" + "buildInfoId": "solc-0_8_28-a2f64967aeae699bd499cc90bbcf76e2314a0651" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 639e856278..766f0e0cad 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -336,7 +336,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(c.initialized, CommitteeNotRequested()); require(!c.finalized, CommitteeAlreadyFinalized()); require( - block.timestamp > c.submissionDeadline, + block.timestamp >= c.submissionDeadline, SubmissionWindowNotClosed() ); require(c.topNodes.length >= c.threshold[0], NodeNotEligible()); diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 1003650c82..32f535107b 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -92,63 +92,47 @@ "address": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61" } }, - "hardhat": { - "MockUSDC": { - "constructorArgs": { - "initialSupply": "1000000" - }, - "blockNumber": 1, - "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" - }, - "EnclaveToken": { - "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - }, - "blockNumber": 1, - "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" - } - }, "localhost": { "PoseidonT3": { - "blockNumber": 94, + "blockNumber": 1, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 95, - "address": "0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8" + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 96, - "address": "0x172076E0166D1F9Cc711C77Adf8488051744980C" + "blockNumber": 3, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8", + "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 98, - "address": "0xBEc49fA140aCaA83533fB00A2BB19bDdd0290f25" + "blockNumber": 5, + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 99, - "address": "0xD84379CEae14AA33C123Af12424A37803F885889" + "blockNumber": 6, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xBEc49fA140aCaA83533fB00A2BB19bDdd0290f25", - "licenseToken": "0x172076E0166D1F9Cc711C77Adf8488051744980C", + "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -156,50 +140,50 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 100, - "address": "0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5" + "blockNumber": 7, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, "CiphernodeRegistryOwnable": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "enclaveAddress": "0x0000000000000000000000000000000000000001", - "submissionWindow": "300" + "submissionWindow": "10" }, - "blockNumber": 101, - "address": "0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d" + "blockNumber": 8, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d", - "bondingRegistry": "0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5", - "feeToken": "0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8", + "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 102, - "address": "0x46b142DD1E924FAb83eCc3c08e4D46E82f005e0E" + "blockNumber": 9, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "MockComputeProvider": { - "blockNumber": 110, - "address": "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D" + "blockNumber": 18, + "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" }, "MockDecryptionVerifier": { - "blockNumber": 111, - "address": "0xA4899D35897033b927acFCf422bc745916139776" + "blockNumber": 19, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockInputValidator": { - "blockNumber": 112, - "address": "0xf953b3A269d80e3eB0F2947630Da976B896A8C5b" + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0xf953b3A269d80e3eB0F2947630Da976B896A8C5b" + "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, - "blockNumber": 113, - "address": "0xAA292E8611aDF267e563f334Ee42320aC96D0463" + "blockNumber": 21, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" } } -} +} \ No newline at end of file diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 0595fc440b..112a5aa649 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -40,7 +40,7 @@ export const deployEnclave = async (withMocks?: boolean) => { ); const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; - const SORTITION_SUBMISSION_WINDOW = 300; + const SORTITION_SUBMISSION_WINDOW = 10; const addressOne = "0x0000000000000000000000000000000000000001"; const poseidonT3 = await deployAndSavePoseidonT3({ hre }); @@ -142,6 +142,11 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting BondingRegistry address in CiphernodeRegistry..."); await ciphernodeRegistry.setBondingRegistry(bondingRegistryAddress); + console.log("Setting Submission Window in CiphernodeRegistry..."); + await ciphernodeRegistry.setSortitionSubmissionWindow( + SORTITION_SUBMISSION_WINDOW, + ); + console.log("Setting BondingRegistry address in EnclaveTicketToken..."); await enclaveTicketToken.setRegistry(bondingRegistryAddress); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index c3d10066b3..79f7a22867 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -34,7 +34,7 @@ const { loadFixture, time, mine } = networkHelpers; describe("Enclave", function () { const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; - const SORTITION_SUBMISSION_WINDOW = 300; + const SORTITION_SUBMISSION_WINDOW = 10; const addressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 93abc80e90..711cc24d0d 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -25,7 +25,7 @@ const { loadFixture } = networkHelpers; const data = "0xda7a"; const dataHash = ethers.keccak256(data); -const SORTITION_SUBMISSION_WINDOW = 300; +const SORTITION_SUBMISSION_WINDOW = 10; // Hash function used to compute the tree nodes. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); From d15361a397d3501cdd2155a5d5798efda54ed66d Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 16:17:52 +0500 Subject: [PATCH 54/88] fix: add score sorition to integration test --- crates/tests/tests/integration.rs | 70 +++++++++++++++++-- examples/CRISP/client/.env.example | 2 +- examples/CRISP/enclave.config.yaml | 2 +- .../crisp-contracts/deployed_contracts.json | 36 +++++----- examples/CRISP/server/.env.example | 2 +- templates/default/enclave.config.yaml | 2 +- tests/integration/enclave.config.yaml | 2 +- 7 files changed, 87 insertions(+), 29 deletions(-) diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 69b69317e2..5def61b7f6 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -5,12 +5,14 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::Actor; +use alloy::primitives::{FixedBytes, I256, U256}; use anyhow::{bail, Context, Result}; use e3_ciphernode_builder::CiphernodeBuilder; use e3_crypto::Cipher; use e3_events::{ - CiphertextOutputPublished, E3Requested, E3id, EnclaveEvent, EventBus, EventBusConfig, - PlaintextAggregated, + CiphertextOutputPublished, CommitteeFinalized, ConfigurationUpdated, E3Requested, E3id, + EnclaveEvent, EventBus, EventBusConfig, OperatorActivationChanged, PlaintextAggregated, + TicketBalanceUpdated, }; use e3_multithread::Multithread; use e3_sdk::bfv_helpers::{build_bfv_params_arc, encode_bfv_params}; @@ -138,8 +140,35 @@ async fn test_trbfv_actor() -> Result<()> { .build() .await?; + let chain_id = 1u64; + bus.send(EnclaveEvent::from(ConfigurationUpdated { + parameter: "ticketPrice".to_string(), + old_value: U256::ZERO, + new_value: U256::from(10_000_000u64), + chain_id, + })) + .await?; + for node in nodes.iter() { - adder.add(&node.address()).await?; + let addr = node.address(); + adder.add(&addr).await?; + + // TODO: is a 100 tickets worth of tokens enough? + bus.send(EnclaveEvent::from(TicketBalanceUpdated { + operator: addr.clone(), + delta: I256::try_from(1_000_000_000u64).unwrap(), + new_balance: U256::from(1_000_000_000u64), + reason: FixedBytes::ZERO, + chain_id, + })) + .await?; + + bus.send(EnclaveEvent::from(OperatorActivationChanged { + operator: addr.clone(), + active: true, + chain_id, + })) + .await?; } // Flush all events @@ -173,10 +202,40 @@ async fn test_trbfv_actor() -> Result<()> { bus.do_send(event); - // NOTE: We are using node 0 as the aggregator but it is not selected in this seed which is why - // there is no CiphernodeSelected event + // For score sortition, we need to wait for nodes to process E3Requested and run sortition + // Since TicketGenerated is a local-only event (not shared across network), we can't collect it + // we need to manually construct the committee that sortition would select + println!("Waiting for nodes to process E3Requested..."); + tokio::time::sleep(Duration::from_millis(200)).await; + + // For seed=123, these 5 nodes get selected by sortition: + // 0x8f32E487328F04927f20c4B14399e4F3123763df (ticket 6) + // 0x95b8a2b9b93aE9e0F13e215A49b8C53172c4f4ba (ticket 68) + // 0x8966a013047aef67Cac52Bc96eB77bC11B5D2572 (ticket 95) + // 0x2B1eD59AC30f668B5b9EcF3D8718A44C15E0E479 (ticket 15) + // 0x83A06c5Ac9E4207526C3eFA79812808428Dd5FaB (ticket 12) + let committee: Vec = vec![ + "0x8f32E487328F04927f20c4B14399e4F3123763df".to_string(), + "0x95b8a2b9b93aE9e0F13e215A49b8C53172c4f4ba".to_string(), + "0x8966a013047aef67Cac52Bc96eB77bC11B5D2572".to_string(), + "0x2B1eD59AC30f668B5b9EcF3D8718A44C15E0E479".to_string(), + "0x83A06c5Ac9E4207526C3eFA79812808428Dd5FaB".to_string(), + ]; + + println!("Emitting CommitteeFinalized with {} nodes", committee.len()); + + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: e3_id.clone(), + committee, + chain_id, + })) + .await?; + + tokio::time::sleep(Duration::from_millis(200)).await; + let expected = vec![ "E3Requested", + "CommitteeFinalized", "ThresholdShareCreated", "ThresholdShareCreated", "ThresholdShareCreated", @@ -195,7 +254,6 @@ async fn test_trbfv_actor() -> Result<()> { .await?; assert_eq!(h.event_types(), expected); - // Aggregate decryption // First we get the public key diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 0de92b7779..9f37fa99dd 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,4 +1,4 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0x7a2088a1bFc9d81c55368AE168C2C02570cB814F # Default E3 program address from hardhat +VITE_E3_PROGRAM_ADDRESS=0x09635F643e140090A9A8Dcd712eD6285858ceBef # Default E3 program address from hardhat diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index c8d154561d..810e936f4f 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + address: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" deploy_block: 1 # Set to actual deploy block enclave: address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 2e50db12c6..80f34f0c83 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -165,7 +165,7 @@ "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "enclaveAddress": "0x0000000000000000000000000000000000000001", - "submissionWindow": "300" + "submissionWindow": "10" }, "blockNumber": 8, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" @@ -185,46 +185,46 @@ "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "MockComputeProvider": { - "blockNumber": 17, - "address": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1" - }, - "MockDecryptionVerifier": { "blockNumber": 18, "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" }, - "MockInputValidator": { + "MockDecryptionVerifier": { "blockNumber": 19, "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, + "MockInputValidator": { + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 21, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, "RiscZeroGroth16Verifier": { - "address": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" + "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" }, "CRISPInputValidator": { - "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidatorFactory": { - "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", "constructorArgs": { - "inputValidator": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" + "inputValidator": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" } }, "HonkVerifier": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" }, "CRISPProgram": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "constructorArgs": { "enclave": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", - "verifierAddress": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1", - "inputValidatorAddress": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", - "honkVerifierAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "verifierAddress": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", + "inputValidatorAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index a593ba8a6e..e20aebbb44 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -15,7 +15,7 @@ CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) ENCLAVE_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -E3_PROGRAM_ADDRESS="0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # E3 Config diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index c3d49da27c..b30d5caabc 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,7 +2,7 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + e3_program: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" enclave: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 191999a097..8bde926bb1 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,7 +2,7 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + e3_program: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" enclave: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" From c6c4437cf09c0eb5a5d4a5786c458de241339628 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 17:29:28 +0500 Subject: [PATCH 55/88] fix: integration test --- crates/aggregator/src/committee_finalizer.rs | 2 +- crates/sortition/src/sortition.rs | 5 -- examples/CRISP/enclave.config.yaml | 2 +- .../enclave-contracts/deployed_contracts.json | 62 +++++++++---------- .../scripts/deployEnclave.ts | 3 +- tests/integration/base.sh | 11 ++-- tests/integration/enclave.config.yaml | 30 ++++++--- tests/integration/fns.sh | 7 ++- tests/integration/persist.sh | 8 +-- 9 files changed, 69 insertions(+), 61 deletions(-) diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 1be605d142..7ca043407e 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -74,7 +74,7 @@ impl Handler for CommitteeFinalizer { .expect("System time should be after UNIX_EPOCH") .as_secs(); - const FINALIZATION_BUFFER_SECONDS: u64 = 5; + const FINALIZATION_BUFFER_SECONDS: u64 = 3; let seconds_until_deadline = if submission_deadline > current_timestamp { (submission_deadline - current_timestamp) + FINALIZATION_BUFFER_SECONDS diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index b4b149424f..2a7a9d9f4b 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -280,11 +280,6 @@ impl ScoreBackend { return None; } - info!( - address = %addr_str, - available_tickets = count, - "Node eligible for score sortition" - ); let tickets = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); Some(RegisteredNode { address: n.address, diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 810e936f4f..0d5834078f 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -40,7 +40,7 @@ nodes: autopassword: true ag: address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" - quic_port: 9094 + quic_port: 9204 autonetkey: true autopassword: true role: diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 32f535107b..28d3c4814f 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -94,45 +94,45 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 1, + "blockNumber": 55, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 5, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 6, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -140,50 +140,50 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 7, - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + "blockNumber": 9, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "CiphernodeRegistryOwnable": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "enclaveAddress": "0x0000000000000000000000000000000000000001", - "submissionWindow": "10" + "submissionWindow": "3" }, - "blockNumber": 8, - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + "blockNumber": 10, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 9, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + "blockNumber": 11, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "MockComputeProvider": { - "blockNumber": 18, - "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + "blockNumber": 20, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockDecryptionVerifier": { - "blockNumber": 19, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "blockNumber": 21, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, "MockInputValidator": { - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 22, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, - "blockNumber": 21, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "blockNumber": 23, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" } } } \ No newline at end of file diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 112a5aa649..d9461ae78a 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -40,7 +40,7 @@ export const deployEnclave = async (withMocks?: boolean) => { ); const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; - const SORTITION_SUBMISSION_WINDOW = 10; + const SORTITION_SUBMISSION_WINDOW = 3; const addressOne = "0x0000000000000000000000000000000000000001"; const poseidonT3 = await deployAndSavePoseidonT3({ hre }); @@ -143,6 +143,7 @@ export const deployEnclave = async (withMocks?: boolean) => { await ciphernodeRegistry.setBondingRegistry(bondingRegistryAddress); console.log("Setting Submission Window in CiphernodeRegistry..."); + console.log("SORTITION_SUBMISSION_WINDOW:", SORTITION_SUBMISSION_WINDOW); await ciphernodeRegistry.setSortitionSubmissionWindow( SORTITION_SUBMISSION_WINDOW, ); diff --git a/tests/integration/base.sh b/tests/integration/base.sh index c112954358..7b1f9ecb4e 100755 --- a/tests/integration/base.sh +++ b/tests/integration/base.sh @@ -20,7 +20,10 @@ pnpm evm:clean pnpm evm:deploy --network localhost # set wallet to ag specifically -enclave_wallet_set ag "$PRIVATE_KEY" +enclave_wallet_set ag "$PRIVATE_KEY_AG" +enclave_wallet_set cn1 "$PRIVATE_KEY_CN1" +enclave_wallet_set cn2 "$PRIVATE_KEY_CN2" +enclave_wallet_set cn3 "$PRIVATE_KEY_CN3" # start swarm enclave_nodes_up @@ -40,9 +43,6 @@ pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_2 --network localho heading "Add ciphernode $CIPHERNODE_ADDRESS_3" pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_3 --network localhost -heading "Add ciphernode $CIPHERNODE_ADDRESS_4" -pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_4 --network localhost - heading "Request Committee" ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh --moduli 0x3FFFFFFF000001 --degree 2048 --plaintext-modulus 1032193) @@ -50,8 +50,9 @@ ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh --moduli 0x3FFFFFFF000001 - sleep 4 pnpm committee:new --network localhost --duration 4 --e3-params "$ENCODED_PARAMS" - +echo "Waiting for pubkey.bin" waiton "$SCRIPT_DIR/output/pubkey.bin" +echo "Pubkey.bin found" PUBLIC_KEY=$(xxd -p -c 10000000 "$SCRIPT_DIR/output/pubkey.bin") heading "Mock encrypted plaintext" diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 8bde926bb1..3298dc2ac3 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,10 +2,25 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" - enclave: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + e3_program: + address: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + deploy_block: 1 # Set to actual deploy block + enclave: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 1 # Set to actual deploy block + ciphernode_registry: + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + deploy_block: 1 # Set to actual deploy block + bonding_registry: + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 1 # Set to actual deploy block + +program: + dev: true + # risc0: + # risc0_dev_mode: 1 # 0 = real groth16 proofs, 1 = fake proofs (dev mode) + # bonsai_api_key: xxxxxxxxxxxxxxxx + # bonsai_api_url: xxxxxxxxxxxxxxxx nodes: cn1: @@ -23,16 +38,11 @@ nodes: quic_port: 9093 autonetkey: true autopassword: true - cn4: + ag: address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" quic_port: 9094 autonetkey: true autopassword: true - ag: - address: "0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097" - quic_port: 9095 - autonetkey: true - autopassword: true role: type: aggregator pubkey_write_path: "./output/pubkey.bin" diff --git a/tests/integration/fns.sh b/tests/integration/fns.sh index 81fed65715..2251c9f200 100644 --- a/tests/integration/fns.sh +++ b/tests/integration/fns.sh @@ -15,7 +15,10 @@ fi # Environment variables RPC_URL="ws://localhost:8545" -PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_AG="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" NETWORK_PRIVATE_KEY_AG="0x51a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams." @@ -23,13 +26,11 @@ CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams CIPHERNODE_ADDRESS_1="0x90F79bf6EB2c4f870365E785982E1f101E93b906" CIPHERNODE_ADDRESS_2="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" CIPHERNODE_ADDRESS_3="0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" -CIPHERNODE_ADDRESS_4="0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" # These are the network private keys for the ciphernodes NETWORK_PRIVATE_KEY_1="0x11a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" NETWORK_PRIVATE_KEY_2="0x21a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" NETWORK_PRIVATE_KEY_3="0x31a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" -NETWORK_PRIVATE_KEY_4="0x41a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" if command -v enclave >/dev/null 2>&1; then ENCLAVE_BIN="enclave" diff --git a/tests/integration/persist.sh b/tests/integration/persist.sh index c29f6386c1..480805f3f8 100755 --- a/tests/integration/persist.sh +++ b/tests/integration/persist.sh @@ -20,7 +20,10 @@ pnpm evm:clean pnpm evm:deploy --network localhost # set wallet to ag specifically -enclave_wallet_set ag "$PRIVATE_KEY" +enclave_wallet_set ag "$PRIVATE_KEY_AG" +enclave_wallet_set cn1 "$PRIVATE_KEY_CN1" +enclave_wallet_set cn2 "$PRIVATE_KEY_CN2" +enclave_wallet_set cn3 "$PRIVATE_KEY_CN3" # start swarm enclave_nodes_up @@ -36,9 +39,6 @@ pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_2 --network localho heading "Add ciphernode $CIPHERNODE_ADDRESS_3" pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_3 --network localhost -heading "Add ciphernode $CIPHERNODE_ADDRESS_4" -pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_4 --network localhost - heading "Request Committee" ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh --moduli 0x3FFFFFFF000001 --degree 2048 --plaintext-modulus 1032193) From 96ef2ef8fb020a7f0a831c5fe23b38b9303ef9f7 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 18:19:34 +0500 Subject: [PATCH 56/88] fix: dont call sortition on Committee Finalized --- crates/sortition/src/ciphernode_selector.rs | 44 ++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index 5c528e836a..f31e7304ad 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -142,7 +142,6 @@ impl Handler for CiphernodeSelector { fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { let address = self.address.clone(); let bus = self.bus.clone(); - let sortition = self.sortition.clone(); let repositories = self.data_store.repositories(); let e3_id = msg.e3_id.clone(); @@ -163,30 +162,31 @@ impl Handler for CiphernodeSelector { return; }; - if let Ok(Some((party_id, _ticket_id))) = sortition - .send(GetNodeIndex { - chain_id: e3_id.chain_id(), - seed: e3_meta.seed, - address: address.clone(), - size: e3_meta.threshold_n, - }) - .await - { + let Some(party_id) = msg.committee.iter().position(|addr| addr == &address) else { info!( node = address, - "Node is in finalized committee, emitting CiphernodeSelected" + "Node address not found in committee list (should not happen)" ); - bus.do_send(EnclaveEvent::from(CiphernodeSelected { - party_id, - e3_id, - threshold_m: e3_meta.threshold_m, - threshold_n: e3_meta.threshold_n, - esi_per_ct: e3_meta.esi_per_ct, - error_size: e3_meta.error_size, - params: e3_meta.params, - seed: e3_meta.seed, - })); - } + return; + }; + + let party_id = (party_id + 1) as u64; + + info!( + node = address, + party_id = party_id, + "Node is in finalized committee, emitting CiphernodeSelected" + ); + bus.do_send(EnclaveEvent::from(CiphernodeSelected { + party_id, + e3_id, + threshold_m: e3_meta.threshold_m, + threshold_n: e3_meta.threshold_n, + esi_per_ct: e3_meta.esi_per_ct, + error_size: e3_meta.error_size, + params: e3_meta.params, + seed: e3_meta.seed, + })); }) } } From 77f81ff04c665ac98295dd1fdf2fe531cc047f44 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 18:19:55 +0500 Subject: [PATCH 57/88] fix: dont call sortition on Committee Finalized --- .../crisp-contracts/deployed_contracts.json | 2 +- .../crisp-contracts/hardhat.config.ts | 4 +- templates/default/deployed_contracts.json | 112 ++++++++++++++++++ templates/default/enclave.config.yaml | 18 ++- templates/default/hardhat.config.ts | 13 +- templates/default/package.json | 2 +- templates/default/scripts/dev_ciphernodes.sh | 16 ++- tests/integration/base.sh | 3 +- 8 files changed, 150 insertions(+), 20 deletions(-) diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 80f34f0c83..b5940bc130 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -165,7 +165,7 @@ "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "enclaveAddress": "0x0000000000000000000000000000000000000001", - "submissionWindow": "10" + "submissionWindow": "3" }, "blockNumber": 8, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" diff --git a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts index 8bc4e18077..bced15cd93 100644 --- a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts +++ b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts @@ -149,13 +149,13 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", "@enclave-e3/contracts/contracts/token/EnclaveToken.sol", "@enclave-e3/contracts/contracts/token/EnclaveTicketToken.sol", - "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/test/MockStableToken.sol", + "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockSlashingVerifier.sol", + "@enclave-e3/contracts/contracts/test/MockStableToken.sol", ], settings: { optimizer: { diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index 0fe03e4dbe..2ca9796e1d 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -26,5 +26,117 @@ "blockNumber": 9181753, "address": "0x58708A1bf1AEdf8e75755FDa1882F8dc46985009" } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "blockNumber": 9, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "3" + }, + "blockNumber": 10, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] + }, + "blockNumber": 11, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "MockComputeProvider": { + "blockNumber": 20, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + }, + "MockDecryptionVerifier": { + "blockNumber": 21, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, + "MockInputValidator": { + "blockNumber": 22, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "MockE3Program": { + "constructorArgs": { + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "blockNumber": 23, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + }, + "MockRISC0Verifier": { + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + }, + "ImageID": { + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + }, + "InputValidator": { + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + }, + "MyProgram": { + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "constructorArgs": { + "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "verifier": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "programId": "0xaf928ebf39fec4696c3f41f473a1a9473b67d723c6373149c6ab99ba4c1a76ef", + "inputValidator": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + } + } } } \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index b30d5caabc..c099519d7e 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,10 +2,18 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" - enclave: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - ciphernode_registry: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - bonding_registry: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + e3_program: + address: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + deploy_block: 1 # Set to actual deploy block + enclave: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 1 # Set to actual deploy block + ciphernode_registry: + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + deploy_block: 1 # Set to actual deploy block + bonding_registry: + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 1 # Set to actual deploy block program: dev: true @@ -32,7 +40,7 @@ nodes: autopassword: true ag: address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" - quic_port: 9094 + quic_port: 9204 autonetkey: true autopassword: true role: diff --git a/templates/default/hardhat.config.ts b/templates/default/hardhat.config.ts index d0eed4631f..739e7f671f 100644 --- a/templates/default/hardhat.config.ts +++ b/templates/default/hardhat.config.ts @@ -4,7 +4,10 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { ciphernodeAdd } from "@enclave-e3/contracts/tasks/ciphernode"; +import { + ciphernodeAdd, + ciphernodeAdminAdd, +} from "@enclave-e3/contracts/tasks/ciphernode"; import { cleanDeploymentsTask } from "@enclave-e3/contracts/tasks/utils"; import dotenv from "dotenv"; @@ -65,7 +68,7 @@ function getChainConfig(chain: keyof typeof chainIds, apiUrl: string) { } const config: HardhatUserConfig = { - tasks: [ciphernodeAdd, cleanDeploymentsTask], + tasks: [ciphernodeAdd, ciphernodeAdminAdd, cleanDeploymentsTask], plugins: [ hardhatTypechainPlugin, hardhatEthersChaiMatchers, @@ -119,13 +122,15 @@ const config: HardhatUserConfig = { "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", - "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", + "@enclave-e3/contracts/contracts/token/EnclaveToken.sol", + "@enclave-e3/contracts/contracts/token/EnclaveTicketToken.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/test/MockStableToken.sol", + "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", "@enclave-e3/contracts/contracts/test/MockSlashingVerifier.sol", + "@enclave-e3/contracts/contracts/test/MockStableToken.sol", ], compilers: [ { diff --git a/templates/default/package.json b/templates/default/package.json index 57dc05ef89..0d374e889b 100644 --- a/templates/default/package.json +++ b/templates/default/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "ciphernode:add": "hardhat run scripts/ciphernode-add.ts -- ", + "ciphernode:admin-add": "hardhat run scripts/ciphernode-add.ts -- ", "compile": "hardhat compile", "clean:deployments": "hardhat utils:clean-deployments", "deploy": "pnpm clean:deployments && hardhat run scripts/deploy-local.ts --network localhost", diff --git a/templates/default/scripts/dev_ciphernodes.sh b/templates/default/scripts/dev_ciphernodes.sh index 3f0f406adb..e6f982b859 100755 --- a/templates/default/scripts/dev_ciphernodes.sh +++ b/templates/default/scripts/dev_ciphernodes.sh @@ -24,9 +24,15 @@ pnpm wait-on http://localhost:8545 rm -rf .enclave/data rm -rf .enclave/config -PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_AG="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" -enclave wallet set --name ag --private-key "$PRIVATE_KEY" +enclave wallet set --name ag --private-key "$PRIVATE_KEY_AG" +enclave wallet set --name cn1 --private-key "$PRIVATE_KEY_CN1" +enclave wallet set --name cn2 --private-key "$PRIVATE_KEY_CN2" +enclave wallet set --name cn3 --private-key "$PRIVATE_KEY_CN3" # using & instead of -d so that wait works below enclave nodes up -v & @@ -39,9 +45,9 @@ CN3=$(grep -A 1 'cn3:' enclave.config.yaml | grep 'address:' | sed 's/.*address: # Add ciphernodes using variables from config.sh pnpm run deploy && sleep 2 -pnpm hardhat ciphernode:add --ciphernode-address $CN1 --network localhost -pnpm hardhat ciphernode:add --ciphernode-address $CN2 --network localhost -pnpm hardhat ciphernode:add --ciphernode-address $CN3 --network localhost +pnpm hardhat ciphernode:admin-add --ciphernode-address $CN1 --network localhost +pnpm hardhat ciphernode:admin-add --ciphernode-address $CN2 --network localhost +pnpm hardhat ciphernode:admin-add --ciphernode-address $CN3 --network localhost # Function to send RPC request. send_rpc() { diff --git a/tests/integration/base.sh b/tests/integration/base.sh index 7b1f9ecb4e..7bc1e979c8 100755 --- a/tests/integration/base.sh +++ b/tests/integration/base.sh @@ -50,9 +50,8 @@ ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh --moduli 0x3FFFFFFF000001 - sleep 4 pnpm committee:new --network localhost --duration 4 --e3-params "$ENCODED_PARAMS" -echo "Waiting for pubkey.bin" + waiton "$SCRIPT_DIR/output/pubkey.bin" -echo "Pubkey.bin found" PUBLIC_KEY=$(xxd -p -c 10000000 "$SCRIPT_DIR/output/pubkey.bin") heading "Mock encrypted plaintext" From 64b60e500645b3ba36e3e37c613f578fe57d328c Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 18:39:07 +0500 Subject: [PATCH 58/88] fix: integration test passing --- examples/CRISP/enclave.config.yaml | 8 +- .../crisp-contracts/deployed_contracts.json | 80 +++++++++---------- .../enclave-contracts/deployed_contracts.json | 2 +- tests/integration/enclave.config.yaml | 14 ++-- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 0d5834078f..5c7391d75d 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,16 +3,16 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + address: "0xc5a5C42992dECbae36851359345FE25997F5C42d" deploy_block: 1 # Set to actual deploy block enclave: - address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" deploy_block: 1 # Set to actual deploy block ciphernode_registry: - address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block bonding_registry: - address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" deploy_block: 1 # Set to actual deploy block program: diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index b5940bc130..22c64fe348 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -112,45 +112,45 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 1, + "blockNumber": 3, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 5, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 6, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -158,8 +158,8 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 7, - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + "blockNumber": 9, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "CiphernodeRegistryOwnable": { "constructorArgs": { @@ -167,64 +167,64 @@ "enclaveAddress": "0x0000000000000000000000000000000000000001", "submissionWindow": "3" }, - "blockNumber": 8, - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + "blockNumber": 10, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 9, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + "blockNumber": 11, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "MockComputeProvider": { - "blockNumber": 18, - "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + "blockNumber": 20, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockDecryptionVerifier": { - "blockNumber": 19, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "blockNumber": 21, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, "MockInputValidator": { - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 22, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, - "blockNumber": 21, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "blockNumber": 23, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, "RiscZeroGroth16Verifier": { - "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidator": { - "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" }, "CRISPInputValidatorFactory": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "constructorArgs": { - "inputValidator": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "inputValidator": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" } }, "HonkVerifier": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" }, "CRISPProgram": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", "constructorArgs": { - "enclave": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", - "verifierAddress": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", - "inputValidatorAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "inputValidatorAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "honkVerifierAddress": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 28d3c4814f..aec12f3073 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -94,7 +94,7 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 55, + "blockNumber": 63, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 3298dc2ac3..61e8a16b23 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -24,23 +24,23 @@ program: nodes: cn1: - address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" - quic_port: 9091 + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + quic_port: 9201 autonetkey: true autopassword: true cn2: - address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" - quic_port: 9092 + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + quic_port: 9202 autonetkey: true autopassword: true cn3: - address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" - quic_port: 9093 + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" + quic_port: 9203 autonetkey: true autopassword: true ag: address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" - quic_port: 9094 + quic_port: 9204 autonetkey: true autopassword: true role: From 1b6e85ca762ca7f54d97278dc763c5755e64d257 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 21:59:14 +0500 Subject: [PATCH 59/88] chore(sdk): approve fee token for e3 request --- crates/cli/src/print_env.rs | 6 ++ crates/config/src/contract.rs | 1 + examples/CRISP/enclave.config.yaml | 3 + .../crisp-contracts/deployed_contracts.json | 80 +++++++++---------- .../enclave-contracts/deployed_contracts.json | 24 +++++- packages/enclave-react/src/useEnclaveSDK.ts | 18 +++-- packages/enclave-sdk/README.md | 1 + packages/enclave-sdk/src/contract-client.ts | 55 +++++++++++++ packages/enclave-sdk/src/enclave-sdk.ts | 49 +++++++++--- packages/enclave-sdk/src/index.ts | 8 +- packages/enclave-sdk/src/types.ts | 49 ++++++++---- packages/enclave-sdk/src/utils.ts | 14 +++- .../src/pages/steps/RequestComputation.tsx | 2 - .../default/client/src/utils/env-config.ts | 3 + templates/default/deployed_contracts.json | 8 -- templates/default/enclave.config.yaml | 5 +- templates/default/package.json | 1 - templates/default/server/index.ts | 2 + templates/default/server/utils.ts | 1 + templates/default/tests/integration.spec.ts | 15 +++- tests/integration/enclave.config.yaml | 3 + 21 files changed, 251 insertions(+), 97 deletions(-) diff --git a/crates/cli/src/print_env.rs b/crates/cli/src/print_env.rs index 072c5d932d..0a6ffa25f5 100644 --- a/crates/cli/src/print_env.rs +++ b/crates/cli/src/print_env.rs @@ -25,6 +25,9 @@ pub fn extract_env_vars_vite(config: &AppConfig, chain: &str) -> String { if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("VITE_E3_PROGRAM_ADDRESS={}", e3_program.address())); } + if let Some(fee_token) = &chain.contracts.fee_token { + env_vars.push(format!("VITE_FEE_TOKEN_ADDRESS={}", fee_token.address())); + } } env_vars.join(" ") @@ -48,6 +51,9 @@ pub fn extract_env_vars(config: &AppConfig, chain: &str) -> String { if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("E3_PROGRAM_ADDRESS={}", e3_program.address())); } + if let Some(fee_token) = &chain.contracts.fee_token { + env_vars.push(format!("FEE_TOKEN_ADDRESS={}", fee_token.address())); + } } env_vars.join(" ") diff --git a/crates/config/src/contract.rs b/crates/config/src/contract.rs index 17968c2a5f..cf8115d978 100644 --- a/crates/config/src/contract.rs +++ b/crates/config/src/contract.rs @@ -40,4 +40,5 @@ pub struct ContractAddresses { pub ciphernode_registry: Contract, pub bonding_registry: Contract, pub e3_program: Option, + pub fee_token: Option, } diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 5c7391d75d..6119452800 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -14,6 +14,9 @@ chains: bonding_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" deploy_block: 1 # Set to actual deploy block + fee_token: + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + deploy_block: 1 # Set to actual deploy block program: dev: true diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 22c64fe348..b5940bc130 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -112,45 +112,45 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 3, + "blockNumber": 1, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 4, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 2, + "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 5, - "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + "blockNumber": 3, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 7, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 5, + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 8, - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + "blockNumber": 6, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -158,8 +158,8 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 9, - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + "blockNumber": 7, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, "CiphernodeRegistryOwnable": { "constructorArgs": { @@ -167,64 +167,64 @@ "enclaveAddress": "0x0000000000000000000000000000000000000001", "submissionWindow": "3" }, - "blockNumber": 10, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + "blockNumber": 8, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", - "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 11, - "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + "blockNumber": 9, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "MockComputeProvider": { - "blockNumber": 20, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "blockNumber": 18, + "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" }, "MockDecryptionVerifier": { - "blockNumber": 21, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 19, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockInputValidator": { - "blockNumber": 22, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "blockNumber": 20, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, - "blockNumber": 23, - "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + "blockNumber": 21, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, "RiscZeroGroth16Verifier": { - "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" }, "CRISPInputValidator": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidatorFactory": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", "constructorArgs": { - "inputValidator": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + "inputValidator": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" } }, "HonkVerifier": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" }, "CRISPProgram": { - "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "constructorArgs": { - "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", - "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - "inputValidatorAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", - "honkVerifierAddress": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "enclave": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "verifierAddress": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", + "inputValidatorAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index aec12f3073..d78a7c4c85 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -92,9 +92,31 @@ "address": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61" } }, + "undefined": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + } + }, + "default": { + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + } + }, "localhost": { "PoseidonT3": { - "blockNumber": 63, + "blockNumber": 3, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { diff --git a/packages/enclave-react/src/useEnclaveSDK.ts b/packages/enclave-react/src/useEnclaveSDK.ts index 172875cfce..fadd197fa4 100644 --- a/packages/enclave-react/src/useEnclaveSDK.ts +++ b/packages/enclave-react/src/useEnclaveSDK.ts @@ -22,6 +22,7 @@ export interface UseEnclaveSDKConfig { contracts?: { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; + feeToken: `0x${string}`; }; chainId?: number; autoConnect?: boolean; @@ -40,11 +41,11 @@ export interface UseEnclaveSDKReturn { // Event handling onEnclaveEvent: ( eventType: T, - callback: EventCallback, + callback: EventCallback ) => void; off: ( eventType: T, - callback: EventCallback, + callback: EventCallback ) => void; // Event types for convenience EnclaveEventType: typeof EnclaveEventType; @@ -87,7 +88,7 @@ export interface UseEnclaveSDKReturn { * ``` */ export const useEnclaveSDK = ( - config: UseEnclaveSDKConfig, + config: UseEnclaveSDKConfig ): UseEnclaveSDKReturn => { const [sdk, setSdk] = useState(null); const [isInitialized, setIsInitialized] = useState(false); @@ -115,6 +116,7 @@ export const useEnclaveSDK = ( contracts: config.contracts || { enclave: "0x0000000000000000000000000000000000000000", ciphernodeRegistry: "0x0000000000000000000000000000000000000000", + feeToken: "0x0000000000000000000000000000000000000000", }, chainId: config.chainId, protocol: config.protocol, @@ -165,7 +167,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.requestE3(...args); }, - [sdk], + [sdk] ); const activateE3 = useCallback( @@ -173,7 +175,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.activateE3(...args); }, - [sdk], + [sdk] ); const publishInput = useCallback( @@ -181,7 +183,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.publishInput(...args); }, - [sdk], + [sdk] ); // Event handling methods @@ -190,7 +192,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.onEnclaveEvent(eventType, callback); }, - [sdk], + [sdk] ); const off = useCallback( @@ -198,7 +200,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.off(eventType, callback); }, - [sdk], + [sdk] ); return { diff --git a/packages/enclave-sdk/README.md b/packages/enclave-sdk/README.md index dff910667f..f947d7e7b0 100644 --- a/packages/enclave-sdk/README.md +++ b/packages/enclave-sdk/README.md @@ -135,6 +135,7 @@ enum RegistryEventType { CIPHERNODE_REMOVED = "CiphernodeRemoved", COMMITTEE_REQUESTED = "CommitteeRequested", COMMITTEE_PUBLISHED = "CommitteePublished", + COMMITTEE_FINALIZED = "CommitteeFinalized", ENCLAVE_SET = "EnclaveSet", // ... more events } diff --git a/packages/enclave-sdk/src/contract-client.ts b/packages/enclave-sdk/src/contract-client.ts index 3fcc93b576..b6cb03e190 100644 --- a/packages/enclave-sdk/src/contract-client.ts +++ b/packages/enclave-sdk/src/contract-client.ts @@ -15,6 +15,7 @@ import { import { CiphernodeRegistryOwnable__factory, Enclave__factory, + EnclaveToken__factory, } from "@enclave-e3/contracts/types"; import { type E3 } from "./types"; import { SDKError, isValidAddress } from "./utils"; @@ -23,6 +24,7 @@ export class ContractClient { private contractInfo: { enclave: { address: `0x${string}`; abi: Abi }; ciphernodeRegistry: { address: `0x${string}`; abi: Abi }; + feeToken: { address: `0x${string}`; abi: Abi }; } | null = null; constructor( @@ -31,9 +33,11 @@ export class ContractClient { private addresses: { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; + feeToken: `0x${string}`; } = { enclave: "0x0000000000000000000000000000000000000000", ciphernodeRegistry: "0x0000000000000000000000000000000000000000", + feeToken: "0x0000000000000000000000000000000000000000", } ) { if (!isValidAddress(addresses.enclave)) { @@ -45,6 +49,12 @@ export class ContractClient { "INVALID_ADDRESS" ); } + if (!isValidAddress(addresses.feeToken)) { + throw new SDKError( + "Invalid FeeToken contract address", + "INVALID_ADDRESS" + ); + } } /** @@ -61,6 +71,10 @@ export class ContractClient { address: this.addresses.ciphernodeRegistry, abi: CiphernodeRegistryOwnable__factory.abi, }, + feeToken: { + address: this.addresses.feeToken, + abi: EnclaveToken__factory.abi, + }, }; } catch (error) { throw new SDKError( @@ -70,6 +84,47 @@ export class ContractClient { } } + /** + * Approve the fee token for the Enclave + * approve(address spender, uint256 amount) + */ + public async approveFeeToken(amount: bigint): Promise { + if (!this.walletClient) { + throw new SDKError( + "Wallet client required for write operations", + "NO_WALLET" + ); + } + + if (!this.contractInfo) { + await this.initialize(); + } + + try { + const account = this.walletClient.account; + if (!account) { + throw new SDKError("No account connected", "NO_ACCOUNT"); + } + + const { request } = await this.publicClient.simulateContract({ + address: this.addresses.feeToken, + abi: EnclaveToken__factory.abi, + functionName: "approve", + args: [this.addresses.enclave, amount], + account, + }); + + const hash = await this.walletClient.writeContract(request); + + return hash; + } catch (error) { + throw new SDKError( + `Failed to approve fee token: ${error}`, + "APPROVE_FEE_TOKEN_FAILED" + ); + } + } + /** * Request a new E3 computation * request(address filter, uint32[2] threshold, uint256[2] startWindow, uint256 duration, IE3Program e3Program, bytes e3ProgramParams, bytes computeProviderParams, bytes customParams) diff --git a/packages/enclave-sdk/src/enclave-sdk.ts b/packages/enclave-sdk/src/enclave-sdk.ts index 82871be4a8..9401a8ebb3 100644 --- a/packages/enclave-sdk/src/enclave-sdk.ts +++ b/packages/enclave-sdk/src/enclave-sdk.ts @@ -75,6 +75,13 @@ export class EnclaveSDK { ); } + if (!isValidAddress(config.contracts.feeToken)) { + throw new SDKError( + "Invalid FeeToken contract address", + "INVALID_ADDRESS" + ); + } + this.eventListener = new EventListener(config.publicClient); this.contractClient = new ContractClient( config.publicClient, @@ -166,7 +173,7 @@ export class EnclaveSDK { } /** - * This function encrypts a number using the configured FHE protocol + * This function encrypts a number using the configured FHE protocol * and generates the necessary public inputs for a zk-SNARK proof. * @param data The number to encrypt * @param publicKey The public key to use for encryption @@ -174,7 +181,7 @@ export class EnclaveSDK { */ public async encryptNumberAndGenInputs( data: bigint, - publicKey: Uint8Array, + publicKey: Uint8Array ): Promise { await initializeWasm(); switch (this.protocol) { @@ -190,7 +197,7 @@ export class EnclaveSDK { const publicInputs = JSON.parse(circuitInputs); return { encryptedData, - publicInputs + publicInputs, }; default: throw new Error("Protocol not supported"); @@ -209,13 +216,14 @@ export class EnclaveSDK { publicKey: Uint8Array, circuit: CompiledCircuit ): Promise { - const { publicInputs, encryptedData } = await this.encryptNumberAndGenInputs(data, publicKey); - const proof = await generateProof(publicInputs, circuit); + const { publicInputs, encryptedData } = + await this.encryptNumberAndGenInputs(data, publicKey); + const proof = await generateProof(publicInputs, circuit); - return { - encryptedData, - proof, - }; + return { + encryptedData, + proof, + }; } /** @@ -226,7 +234,7 @@ export class EnclaveSDK { */ public async encryptVectorAndGenInputs( data: BigUint64Array, - publicKey: Uint8Array, + publicKey: Uint8Array ): Promise { await initializeWasm(); switch (this.protocol) { @@ -242,7 +250,7 @@ export class EnclaveSDK { const publicInputs = JSON.parse(circuitInputs); return { encryptedData, - publicInputs + publicInputs, }; default: throw new Error("Protocol not supported"); @@ -261,7 +269,8 @@ export class EnclaveSDK { publicKey: Uint8Array, circuit: CompiledCircuit ): Promise { - const { publicInputs, encryptedData } = await this.encryptVectorAndGenInputs(data, publicKey); + const { publicInputs, encryptedData } = + await this.encryptVectorAndGenInputs(data, publicKey); const proof = await generateProof(publicInputs, circuit); @@ -271,6 +280,21 @@ export class EnclaveSDK { }; } + /** + * Approve the fee token for the Enclave + * @param amount - The amount to approve + * @returns The approval transaction hash + */ + public async approveFeeToken(amount: bigint): Promise { + console.log(">>> APPROVE FEE TOKEN"); + + if (!this.initialized) { + await this.initialize(); + } + + return this.contractClient.approveFeeToken(amount); + } + /** * Request a new E3 computation */ @@ -547,6 +571,7 @@ export class EnclaveSDK { contracts: { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; + feeToken: `0x${string}`; }; privateKey?: `0x${string}`; chainId: keyof typeof EnclaveSDK.chains; diff --git a/packages/enclave-sdk/src/index.ts b/packages/enclave-sdk/src/index.ts index dc63dd2c54..31017513a5 100644 --- a/packages/enclave-sdk/src/index.ts +++ b/packages/enclave-sdk/src/index.ts @@ -32,6 +32,7 @@ export type { CiphernodeRemovedData, CommitteeRequestedData, CommitteePublishedData, + CommitteeFinalizedData, EnclaveEventData, RegistryEventData, ProtocolParams, @@ -71,4 +72,9 @@ export { type ComputeProviderParams, } from "./utils"; -export { generateProof, type Polynomial, convertToPolynomial, convertToPolynomialArray } from "./greco"; +export { + generateProof, + type Polynomial, + convertToPolynomial, + convertToPolynomialArray, +} from "./greco"; diff --git a/packages/enclave-sdk/src/types.ts b/packages/enclave-sdk/src/types.ts index 01b7475f41..0f21717cef 100644 --- a/packages/enclave-sdk/src/types.ts +++ b/packages/enclave-sdk/src/types.ts @@ -10,6 +10,8 @@ import type { CiphernodeRegistryOwnable, Enclave, MockCiphernodeRegistry, + MockUSDC, + EnclaveToken, } from "@enclave-e3/contracts/types"; import type { CircuitInputs } from "./greco"; @@ -29,7 +31,7 @@ export interface SDKConfig { walletClient?: WalletClient; /** - * The Enclave contracts + * The Enclave contracts */ contracts: { /** @@ -41,6 +43,11 @@ export interface SDKConfig { * The CiphernodeRegistry contract address */ ciphernodeRegistry: `0x${string}`; + + /** + * The FeeToken contract address + */ + feeToken: `0x${string}`; }; /** @@ -69,6 +76,7 @@ export interface EventListenerConfig { export interface ContractInstances { enclave: Enclave; ciphernodeRegistry: CiphernodeRegistryOwnable | MockCiphernodeRegistry; + feeToken: EnclaveToken | MockUSDC; } // Unified Event System @@ -102,6 +110,7 @@ export enum RegistryEventType { // Committee Management COMMITTEE_REQUESTED = "CommitteeRequested", COMMITTEE_PUBLISHED = "CommitteePublished", + COMMITTEE_FINALIZED = "CommitteeFinalized", // Configuration ENCLAVE_SET = "EnclaveSet", @@ -178,8 +187,10 @@ export interface CiphernodeRemovedData { export interface CommitteeRequestedData { e3Id: bigint; - filter: string; + seed: bigint; threshold: [bigint, bigint]; + requestBlock: bigint; + submissionDeadline: bigint; } export interface CommitteePublishedData { @@ -187,6 +198,11 @@ export interface CommitteePublishedData { publicKey: string; } +export interface CommitteeFinalizedData { + e3Id: bigint; + nodes: string[]; +} + // Event data mapping export interface EnclaveEventData { [EnclaveEventType.E3_REQUESTED]: E3RequestedData; @@ -213,6 +229,7 @@ export interface EnclaveEventData { export interface RegistryEventData { [RegistryEventType.COMMITTEE_REQUESTED]: CommitteeRequestedData; [RegistryEventType.COMMITTEE_PUBLISHED]: CommitteePublishedData; + [RegistryEventType.COMMITTEE_FINALIZED]: CommitteeFinalizedData; [RegistryEventType.ENCLAVE_SET]: { enclave: string }; [RegistryEventType.OWNERSHIP_TRANSFERRED]: { previousOwner: string; @@ -225,10 +242,10 @@ export interface RegistryEventData { export interface EnclaveEvent { type: T; data: T extends EnclaveEventType - ? EnclaveEventData[T] - : T extends RegistryEventType - ? RegistryEventData[T] - : unknown; + ? EnclaveEventData[T] + : T extends RegistryEventType + ? RegistryEventData[T] + : unknown; log: Log; timestamp: Date; blockNumber: bigint; @@ -236,7 +253,7 @@ export interface EnclaveEvent { } export type EventCallback = ( - event: EnclaveEvent, + event: EnclaveEvent ) => void | Promise; export interface EventFilter { @@ -287,7 +304,7 @@ export interface ProtocolParams { /** * The degree of the polynomial */ - degree: number; + degree: number; /** * The plaintext modulus */ @@ -299,21 +316,21 @@ export interface ProtocolParams { } /** - * Parameters for the BFV protocol + * Parameters for the BFV protocol */ export const BfvProtocolParams = { /** - * Recommended parameters for BFV protocol - * - Degree: 2048 - * - Plaintext modulus: 1032193 - * - Moduli:0x3FFFFFFF000001 - */ + * Recommended parameters for BFV protocol + * - Degree: 2048 + * - Plaintext modulus: 1032193 + * - Moduli:0x3FFFFFFF000001 + */ BFV_NORMAL: { degree: 2048, plaintextModulus: 1032193n, - moduli: 0x3FFFFFFF000001n, + moduli: 0x3fffffff000001n, } as const satisfies ProtocolParams, -} +}; /** * The result of encrypting a value and generating a proof diff --git a/packages/enclave-sdk/src/utils.ts b/packages/enclave-sdk/src/utils.ts index 1c0040a71b..a0c6ae416e 100644 --- a/packages/enclave-sdk/src/utils.ts +++ b/packages/enclave-sdk/src/utils.ts @@ -81,8 +81,8 @@ export const DEFAULT_COMPUTE_PROVIDER_PARAMS: ComputeProviderParams = { // Default E3 configuration export const DEFAULT_E3_CONFIG = { - threshold_min: 2, - threshold_max: 3, + threshold_min: 1, + threshold_max: 2, window_size: 120, // 2 minutes in seconds duration: 1800, // 30 minutes in seconds payment_amount: "0", // 0 ETH in wei @@ -120,11 +120,17 @@ export function encodeBfvParams( } /** - * Encode compute provider parameters for the smart contract + * Encode compute provider parameters for the smart contract' + * If mock is true, the compute provider parameters will return 32 bytes of 0x00 */ export function encodeComputeProviderParams( - params: ComputeProviderParams + params: ComputeProviderParams, + mock: boolean = false ): `0x${string}` { + if (mock) { + return `0x${"0".repeat(32)}` as `0x${string}`; + } + const jsonString = JSON.stringify(params); const encoder = new TextEncoder(); const bytes = encoder.encode(jsonString); diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx index 0916fe0bd1..e24314469e 100644 --- a/templates/default/client/src/pages/steps/RequestComputation.tsx +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -111,14 +111,12 @@ const RequestComputation: React.FC = () => { console.log('requestE3') const hash = await requestE3({ - filter: contracts.filterRegistry, threshold, startWindow, duration, e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, - value: BigInt('1000000000000000'), // 0.001 ETH }) setLocalTransactionHash(hash) diff --git a/templates/default/client/src/utils/env-config.ts b/templates/default/client/src/utils/env-config.ts index 05b414b313..ec681fa2b5 100644 --- a/templates/default/client/src/utils/env-config.ts +++ b/templates/default/client/src/utils/env-config.ts @@ -8,6 +8,7 @@ export const ENCLAVE_ADDRESS = import.meta.env.VITE_ENCLAVE_ADDRESS export const E3_PROGRAM_ADDRESS = import.meta.env.VITE_E3_PROGRAM_ADDRESS export const REGISTRY_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS export const BONDING_REGISTRY_ADDRESS = import.meta.env.VITE_BONDING_REGISTRY_ADDRESS +export const FEE_TOKEN_ADDRESS = import.meta.env.VITE_FEE_TOKEN_ADDRESS export const RPC_URL = import.meta.env.VITE_RPC_URL || 'http://localhost:8545' const requiredEnvVars = { @@ -15,6 +16,7 @@ const requiredEnvVars = { VITE_E3_PROGRAM_ADDRESS: E3_PROGRAM_ADDRESS, VITE_REGISTRY_ADDRESS: REGISTRY_ADDRESS, VITE_BONDING_REGISTRY_ADDRESS: BONDING_REGISTRY_ADDRESS, + VITE_FEE_TOKEN_ADDRESS: FEE_TOKEN_ADDRESS, } export const MISSING_ENV_VARS = Object.entries(requiredEnvVars) @@ -45,5 +47,6 @@ export function getContractAddresses() { ciphernodeRegistry: REGISTRY_ADDRESS as `0x${string}`, bondingRegistry: BONDING_REGISTRY_ADDRESS as `0x${string}`, e3Program: E3_PROGRAM_ADDRESS as `0x${string}`, + feeToken: FEE_TOKEN_ADDRESS as `0x${string}`, } } diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index 2ca9796e1d..67404b228a 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -17,14 +17,6 @@ }, "blockNumber": 9181748, "address": "0x5ABDfCbA0366ABF2893D3f2465F4C97908488A6d" - }, - "NaiveRegistryFilter": { - "constructorArgs": { - "ciphernodeRegistryAddress": "0x5ABDfCbA0366ABF2893D3f2465F4C97908488A6d", - "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676" - }, - "blockNumber": 9181753, - "address": "0x58708A1bf1AEdf8e75755FDa1882F8dc46985009" } }, "localhost": { diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index c099519d7e..8df8049404 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + address: "0x09635f643e140090a9a8dcd712ed6285858cebef" deploy_block: 1 # Set to actual deploy block enclave: address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" @@ -14,6 +14,9 @@ chains: bonding_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" deploy_block: 1 # Set to actual deploy block + fee_token: + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + deploy_block: 1 # Set to actual deploy block program: dev: true diff --git a/templates/default/package.json b/templates/default/package.json index 0d374e889b..d94bce430b 100644 --- a/templates/default/package.json +++ b/templates/default/package.json @@ -4,7 +4,6 @@ "private": true, "type": "module", "scripts": { - "ciphernode:admin-add": "hardhat run scripts/ciphernode-add.ts -- ", "compile": "hardhat compile", "clean:deployments": "hardhat utils:clean-deployments", "deploy": "pnpm clean:deployments && hardhat run scripts/deploy-local.ts --network localhost", diff --git a/templates/default/server/index.ts b/templates/default/server/index.ts index 6a4a2aefdb..c681cfdf1d 100644 --- a/templates/default/server/index.ts +++ b/templates/default/server/index.ts @@ -33,6 +33,7 @@ async function createPrivateSDK() { PRIVATE_KEY, CIPHERNODE_REGISTRY_CONTRACT, ENCLAVE_CONTRACT, + FEE_TOKEN_CONTRACT, RPC_URL, } = getCheckedEnvVars(); @@ -46,6 +47,7 @@ async function createPrivateSDK() { contracts: { enclave: ENCLAVE_CONTRACT as `0x${string}`, ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`, + feeToken: FEE_TOKEN_CONTRACT as `0x${string}`, }, chainId: CHAIN_ID, protocol: FheProtocol.BFV, diff --git a/templates/default/server/utils.ts b/templates/default/server/utils.ts index dba3efbbd3..b216e76bec 100644 --- a/templates/default/server/utils.ts +++ b/templates/default/server/utils.ts @@ -17,6 +17,7 @@ export function getCheckedEnvVars() { RPC_URL: ensureEnv("RPC_URL"), ENCLAVE_CONTRACT: ensureEnv("ENCLAVE_ADDRESS"), CIPHERNODE_REGISTRY_CONTRACT: ensureEnv("REGISTRY_ADDRESS"), + FEE_TOKEN_CONTRACT: ensureEnv("FEE_TOKEN_ADDRESS"), PRIVATE_KEY: ensureEnv("PRIVATE_KEY"), CHAIN_ID: parseInt(ensureEnv("CHAIN_ID")), PROGRAM_RUNNER_URL: diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index ab6e65769e..f8d27af0a2 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -29,6 +29,7 @@ export function getContractAddresses() { ciphernodeRegistry: process.env.REGISTRY_ADDRESS as `0x${string}`, bondingRegistry: process.env.BONDING_REGISTRY_ADDRESS as `0x${string}`, e3Program: process.env.E3_PROGRAM_ADDRESS as `0x${string}`, + feeToken: process.env.FEE_TOKEN_ADDRESS as `0x${string}`, }; } @@ -42,7 +43,6 @@ type E3Shared = { e3Id: bigint; e3Program: string; e3: E3; - filter: string; }; type E3StateRequested = E3Shared & { @@ -171,6 +171,7 @@ describe("Integration", () => { contracts: { enclave: contracts.enclave, ciphernodeRegistry: contracts.ciphernodeRegistry, + feeToken: contracts.feeToken, }, rpcUrl: "ws://localhost:8545", privateKey: @@ -185,16 +186,24 @@ describe("Integration", () => { DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max, ]; - const startWindow = calculateStartWindow(60); + const startWindow = calculateStartWindow(100); const duration = BigInt(10); const e3ProgramParams = encodeBfvParams(); const computeProviderParams = encodeComputeProviderParams( - DEFAULT_COMPUTE_PROVIDER_PARAMS + DEFAULT_COMPUTE_PROVIDER_PARAMS, + true // Mock the compute provider parameters, return 32 bytes of 0x00 ); let state; let event; + // Approve fee token + console.log("Approving fee token..."); + const hash = await sdk.approveFeeToken(100000000000n); + console.log("Fee token approved:", hash); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + // REQUEST phase await waitForEvent(EnclaveEventType.E3_REQUESTED, async () => { console.log("Requested E3..."); diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 61e8a16b23..fcb16eae42 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -14,6 +14,9 @@ chains: bonding_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" deploy_block: 1 # Set to actual deploy block + fee_token: + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + deploy_block: 1 # Set to actual deploy block program: dev: true From a0e9675f5ec4002f320af376d73f4cba753f84e4 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 27 Oct 2025 22:22:32 +0500 Subject: [PATCH 60/88] fix: fetch time from RPC --- Cargo.lock | 1 + crates/aggregator/Cargo.toml | 1 + crates/aggregator/src/committee_finalizer.rs | 127 +++++++++++------- .../src/ciphernode_builder.rs | 4 +- crates/evm/src/helpers.rs | 12 +- .../crisp-contracts/deployed_contracts.json | 80 +++++------ templates/default/tests/integration.spec.ts | 2 +- 7 files changed, 136 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 315a98c961..42279c58e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2588,6 +2588,7 @@ name = "e3-aggregator" version = "0.1.5" dependencies = [ "actix", + "alloy", "anyhow", "async-trait", "bincode", diff --git a/crates/aggregator/Cargo.toml b/crates/aggregator/Cargo.toml index f41f43110a..3412fd266d 100644 --- a/crates/aggregator/Cargo.toml +++ b/crates/aggregator/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/gnosisguild/enclave/crates/aggregator" [dependencies] actix = { workspace = true } +alloy = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } bincode = { workspace = true } diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 7ca043407e..7d3b00e003 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -5,28 +5,33 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::prelude::*; +use alloy::providers::Provider; use e3_events::{CommitteeRequested, EnclaveEvent, EventBus, Shutdown, Subscribe}; use e3_evm::FinalizeCommittee; use std::collections::HashMap; use std::time::Duration; -use tracing::info; +use tracing::{error, info}; /// CommitteeFinalizer is an actor that listens to CommitteeRequested events and calls /// finalizeCommittee on the registry after the submission deadline has passed. -pub struct CommitteeFinalizer { +pub struct CommitteeFinalizer { + #[allow(dead_code)] bus: Addr>, registry_writer: Recipient, + provider: P, pending_committees: HashMap, } -impl CommitteeFinalizer { +impl CommitteeFinalizer

{ pub fn new( bus: &Addr>, registry_writer: Recipient, + provider: P, ) -> Self { Self { bus: bus.clone(), registry_writer, + provider, pending_committees: HashMap::new(), } } @@ -34,8 +39,9 @@ impl CommitteeFinalizer { pub fn attach( bus: &Addr>, registry_writer: Recipient, + provider: P, ) -> Addr { - let addr = CommitteeFinalizer::new(bus, registry_writer).start(); + let addr = CommitteeFinalizer::new(bus, registry_writer, provider).start(); bus.do_send(Subscribe::new( "CommitteeRequested", @@ -47,11 +53,11 @@ impl CommitteeFinalizer { } } -impl Actor for CommitteeFinalizer { +impl Actor for CommitteeFinalizer

{ type Context = Context; } -impl Handler for CommitteeFinalizer { +impl Handler for CommitteeFinalizer

{ type Result = (); fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { @@ -62,61 +68,88 @@ impl Handler for CommitteeFinalizer { } } -impl Handler for CommitteeFinalizer { +impl Handler for CommitteeFinalizer

{ type Result = (); fn handle(&mut self, msg: CommitteeRequested, ctx: &mut Self::Context) -> Self::Result { let e3_id = msg.e3_id.clone(); let submission_deadline = msg.submission_deadline; - - let current_timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("System time should be after UNIX_EPOCH") - .as_secs(); + let provider = self.provider.clone(); const FINALIZATION_BUFFER_SECONDS: u64 = 3; - let seconds_until_deadline = if submission_deadline > current_timestamp { - (submission_deadline - current_timestamp) + FINALIZATION_BUFFER_SECONDS - } else { - info!( - e3_id = %e3_id, - submission_deadline = submission_deadline, - current_timestamp = current_timestamp, - "Submission deadline already passed, finalizing with buffer" - ); - FINALIZATION_BUFFER_SECONDS + let e3_id_for_log = e3_id.clone(); + let fut = async move { + match e3_evm::helpers::get_current_timestamp(&provider).await { + Ok(timestamp) => Some(timestamp), + Err(e) => { + error!( + e3_id = %e3_id_for_log, + error = %e, + "Failed to get current timestamp from RPC" + ); + None + } + } }; - info!( - e3_id = %e3_id, - submission_deadline = submission_deadline, - current_timestamp = current_timestamp, - seconds_to_wait = seconds_until_deadline, - "Scheduling committee finalization" + let e3_id_for_async = e3_id; + ctx.spawn( + fut.into_actor(self) + .then(move |current_timestamp, act, ctx| { + if let Some(current_timestamp) = current_timestamp { + let seconds_until_deadline = if submission_deadline > current_timestamp { + (submission_deadline - current_timestamp) + FINALIZATION_BUFFER_SECONDS + } else { + info!( + e3_id = %e3_id_for_async, + submission_deadline = submission_deadline, + current_timestamp = current_timestamp, + "Submission deadline already passed, finalizing with buffer" + ); + FINALIZATION_BUFFER_SECONDS + }; + + info!( + e3_id = %e3_id_for_async, + submission_deadline = submission_deadline, + current_timestamp = current_timestamp, + seconds_to_wait = seconds_until_deadline, + "Scheduling committee finalization" + ); + + let registry_writer = act.registry_writer.clone(); + let e3_id_clone = e3_id_for_async.clone(); + + let handle = ctx.run_later( + Duration::from_secs(seconds_until_deadline), + move |act, _ctx| { + info!(e3_id = %e3_id_clone, "Calling finalizeCommittee"); + + registry_writer.do_send(FinalizeCommittee { + e3_id: e3_id_clone.clone(), + }); + + act.pending_committees.remove(&e3_id_clone.to_string()); + }, + ); + + act.pending_committees + .insert(e3_id_for_async.to_string(), handle); + } else { + error!( + e3_id = %e3_id_for_async, + "Skipping committee finalization due to timestamp fetch failure" + ); + } + + async {}.into_actor(act) + }), ); - - let registry_writer = self.registry_writer.clone(); - let e3_id_clone = e3_id.clone(); - - let handle = ctx.run_later( - Duration::from_secs(seconds_until_deadline), - move |act, _ctx| { - info!(e3_id = %e3_id_clone, "Calling finalizeCommittee"); - - registry_writer.do_send(FinalizeCommittee { - e3_id: e3_id_clone.clone(), - }); - - act.pending_committees.remove(&e3_id_clone.to_string()); - }, - ); - - self.pending_committees.insert(e3_id.to_string(), handle); } } -impl Handler for CommitteeFinalizer { +impl Handler for CommitteeFinalizer

{ type Result = (); fn handle(&mut self, _msg: Shutdown, ctx: &mut Self::Context) -> Self::Result { info!("Killing CommitteeFinalizer"); diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 678adc8dca..4e7d6ea3bd 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -364,17 +364,17 @@ impl CiphernodeBuilder { &local_bus, write_provider.clone(), &chain.contracts.ciphernode_registry.address(), - self.pubkey_agg, // is_aggregator flag + self.pubkey_agg, ) .await?; info!("CiphernodeRegistrySolWriter attached for publishing committees"); - // Attach CommitteeFinalizer if aggregator mode is enabled if self.pubkey_agg { info!("Attaching CommitteeFinalizer for score sortition"); e3_aggregator::CommitteeFinalizer::attach( &local_bus, writer.recipient(), + read_provider.provider().clone(), ); } } diff --git a/crates/evm/src/helpers.rs b/crates/evm/src/helpers.rs index 0dc86d3e5d..437a2493de 100644 --- a/crates/evm/src/helpers.rs +++ b/crates/evm/src/helpers.rs @@ -11,7 +11,7 @@ use alloy::{ BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, }, - Identity, Provider, ProviderBuilder, RootProvider, WalletProvider, + Identity, Provider, ProviderBuilder, RootProvider, }, signers::local::PrivateKeySigner, transports::{ @@ -197,6 +197,16 @@ pub async fn load_signer_from_repository( private_key.parse().map_err(Into::into) } +pub async fn get_current_timestamp(provider: &P) -> Result { + let block = provider + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await + .context("Failed to get latest block")? + .ok_or_else(|| anyhow::anyhow!("Latest block not found"))?; + + Ok(block.header.timestamp) +} + #[cfg(test)] mod tests { use super::*; diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index b5940bc130..22c64fe348 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -112,45 +112,45 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 1, + "blockNumber": 3, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 2, - "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 3, - "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" }, "EnclaveTicketToken": { "constructorArgs": { - "baseToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 5, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 6, - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, "BondingRegistry": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ticketToken": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - "licenseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", "registry": "0x0000000000000000000000000000000000000001", "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "ticketPrice": "10000000", @@ -158,8 +158,8 @@ "minTicketBalance": "1", "exitDelay": "604800" }, - "blockNumber": 7, - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + "blockNumber": 9, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "CiphernodeRegistryOwnable": { "constructorArgs": { @@ -167,64 +167,64 @@ "enclaveAddress": "0x0000000000000000000000000000000000000001", "submissionWindow": "3" }, - "blockNumber": 8, - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + "blockNumber": 10, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, "Enclave": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "bondingRegistry": "0x0165878A594ca255338adfa4d48449f69242Eb8F", - "feeToken": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 9, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + "blockNumber": 11, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "MockComputeProvider": { - "blockNumber": 18, - "address": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE" + "blockNumber": 20, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" }, "MockDecryptionVerifier": { - "blockNumber": 19, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + "blockNumber": 21, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" }, "MockInputValidator": { - "blockNumber": 20, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "blockNumber": 22, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" }, - "blockNumber": 21, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + "blockNumber": 23, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, "RiscZeroGroth16Verifier": { - "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidator": { - "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" }, "CRISPInputValidatorFactory": { - "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "constructorArgs": { - "inputValidator": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + "inputValidator": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" } }, "HonkVerifier": { - "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" }, "CRISPProgram": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", "constructorArgs": { - "enclave": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", - "verifierAddress": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", - "inputValidatorAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - "honkVerifierAddress": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "inputValidatorAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "honkVerifierAddress": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index f8d27af0a2..6d18d74be1 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -187,7 +187,7 @@ describe("Integration", () => { DEFAULT_E3_CONFIG.threshold_max, ]; const startWindow = calculateStartWindow(100); - const duration = BigInt(10); + const duration = BigInt(15); const e3ProgramParams = encodeBfvParams(); const computeProviderParams = encodeComputeProviderParams( DEFAULT_COMPUTE_PROVIDER_PARAMS, From 80c56893355cfe4ba92e9115691c11040b60bf6d Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 01:00:10 +0500 Subject: [PATCH 61/88] fix: trust committee finalized event for party ids --- .../src/threshold_plaintext_aggregator.rs | 93 ++++++------------- crates/sortition/src/ciphernode_selector.rs | 4 +- .../enclave-contracts/deployed_contracts.json | 2 +- 3 files changed, 31 insertions(+), 68 deletions(-) diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 773e8d0ced..870c0bdd4e 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -14,7 +14,7 @@ use e3_events::{ Seed, }; use e3_multithread::Multithread; -use e3_sortition::{GetNodeIndex, Sortition}; +use e3_sortition::Sortition; use e3_trbfv::{ calculate_threshold_decryption::{ CalculateThresholdDecryptionRequest, CalculateThresholdDecryptionResponse, @@ -244,81 +244,46 @@ impl Handler for ThresholdPlaintextAggregator { } impl Handler for ThresholdPlaintextAggregator { - type Result = ResponseActFuture>; + type Result = Result<()>; - fn handle(&mut self, event: DecryptionshareCreated, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, event: DecryptionshareCreated, ctx: &mut Self::Context) -> Self::Result { info!(event=?event, "Processing DecryptionShareCreated..."); - let Some(ThresholdPlaintextAggregatorState::Collecting(Collecting { - threshold_n, - seed, - .. - })) = self.state.get() + let Some(ThresholdPlaintextAggregatorState::Collecting(Collecting { threshold_n, .. })) = + self.state.get() else { error!(state=?self.state, "Aggregator has been closed for collecting."); - return Box::pin(fut::ready(Ok(()))); + return Ok(()); }; - let size = threshold_n as usize; - let address = event.node; let party_id = event.party_id; - let chain_id = event.e3_id.chain_id(); let e3_id = event.e3_id.clone(); let decryption_share = event.decryption_share.clone(); - // Why do we need to get the node index when the event contains the party_id? I guess we - // don't trust the event. Maybe that is fine. - Box::pin( - self.sortition - .send(GetNodeIndex { - chain_id, - address: address.clone(), - size, - seed, - }) - .into_actor(self) - .map(move |res, act, ctx| { - let maybe_found_index = res?; - let Some((party, _ticket_number)) = maybe_found_index else { - error!("Attempting to aggregate share but party not found in committee"); - return Ok(()); - }; + // Trust the party_id from the event - it's based on CommitteeFinalized order + // which is the authoritative source of truth for party IDs + if e3_id != self.e3_id { + error!("Wrong e3_id sent to aggregator. This should not happen."); + return Ok(()); + } + self.add_share(party_id, decryption_share)?; - if party != party_id { - error!( - "Bad aggregation state! Address {} not found at index {} instead it was found at {}", - address, party_id, party - ); - return Ok(()); - } - - if e3_id != act.e3_id { - error!("Wrong e3_id sent to aggregator. This should not happen."); - return Ok(()); - } - - // add the keyshare and - act.add_share(party_id, decryption_share)?; - - // Check the state and if it has changed to the computing - if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { - threshold_m, - threshold_n, - shares, - ciphertext_output, - .. - })) = act.state.get() - { - ctx.notify(ComputeAggregate { - shares: shares.clone(), - ciphertext_output: ciphertext_output.clone(), - threshold_m, - threshold_n, - }) - } + if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { + threshold_m, + threshold_n, + shares, + ciphertext_output, + .. + })) = self.state.get() + { + ctx.notify(ComputeAggregate { + shares: shares.clone(), + ciphertext_output: ciphertext_output.clone(), + threshold_m, + threshold_n, + }) + } - Ok(()) - }), - ) + Ok(()) } } diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index f31e7304ad..c23154e408 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -170,15 +170,13 @@ impl Handler for CiphernodeSelector { return; }; - let party_id = (party_id + 1) as u64; - info!( node = address, party_id = party_id, "Node is in finalized committee, emitting CiphernodeSelected" ); bus.do_send(EnclaveEvent::from(CiphernodeSelected { - party_id, + party_id: party_id as u64, e3_id, threshold_m: e3_meta.threshold_m, threshold_n: e3_meta.threshold_n, diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index d78a7c4c85..cce5afa279 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -116,7 +116,7 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 3, + "blockNumber": 55, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { From 7ef399d1a072eafcdab77216531fdf0d81534127 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 01:06:42 +0500 Subject: [PATCH 62/88] fix: sdk test --- packages/enclave-sdk/tests/sdk.test.ts | 34 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/enclave-sdk/tests/sdk.test.ts b/packages/enclave-sdk/tests/sdk.test.ts index af7f614125..56cca7d7d3 100644 --- a/packages/enclave-sdk/tests/sdk.test.ts +++ b/packages/enclave-sdk/tests/sdk.test.ts @@ -22,6 +22,7 @@ describe("encryptNumber", () => { contracts: { enclave: zeroAddress, ciphernodeRegistry: zeroAddress, + feeToken: zeroAddress, }, rpcUrl: "", privateKey: @@ -31,7 +32,7 @@ describe("encryptNumber", () => { it("should encrypt a number without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") ); const value = await sdk.encryptNumber(10n, Uint8Array.from(buffer)); expect(value).to.be.an.instanceof(Uint8Array); @@ -40,35 +41,46 @@ describe("encryptNumber", () => { }); it("should encrypt a number and generate a proof without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") + ); + + const value = await sdk.encryptNumberAndGenProof( + 1n, + Uint8Array.from(buffer), + demoCircuit as unknown as CompiledCircuit ); - const value = await sdk.encryptNumberAndGenProof(1n, Uint8Array.from(buffer), demoCircuit as unknown as CompiledCircuit); - expect(value).to.be.an.instanceof(Object); expect(value.encryptedData).to.be.an.instanceof(Uint8Array); - expect(value.proof).to.be.an.instanceOf(Object) + expect(value.proof).to.be.an.instanceOf(Object); }, 9999999); it("should encrypt a vecor of numbers without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") + ); + const value = await sdk.encryptVector( + new BigUint64Array([1n, 2n]), + Uint8Array.from(buffer) ); - const value = await sdk.encryptVector(new BigUint64Array([1n, 2n]), Uint8Array.from(buffer)); expect(value).to.be.an.instanceof(Uint8Array); expect(value.length).to.equal(27_674); }); it("should encrypt a vector and generate a proof without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") + ); + + const value = await sdk.encryptVectorAndGenProof( + new BigUint64Array([1n, 2n]), + Uint8Array.from(buffer), + demoCircuit as unknown as CompiledCircuit ); - const value = await sdk.encryptVectorAndGenProof(new BigUint64Array([1n, 2n]), Uint8Array.from(buffer), demoCircuit as unknown as CompiledCircuit); - expect(value).to.be.an.instanceof(Object); expect(value.encryptedData).to.be.an.instanceof(Uint8Array); - expect(value.proof).to.be.an.instanceOf(Object) + expect(value.proof).to.be.an.instanceOf(Object); }, 9999999); }); }); From 083c09f87ee0f64e2d19bb15a0443045ff342189 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 15:10:09 +0500 Subject: [PATCH 63/88] feat(sortition): add sortition to ciphernode builder and make distance sorition deprecated --- crates/aggregator/src/committee_finalizer.rs | 2 +- crates/aggregator/src/publickey_aggregator.rs | 36 ++-- .../src/threshold_plaintext_aggregator.rs | 82 +++++---- .../src/ciphernode_builder.rs | 32 +++- .../entrypoint/src/start/aggregator_start.rs | 1 + crates/entrypoint/src/start/start.rs | 1 + .../src/enclave_event/ticket_generated.rs | 10 +- crates/evm/src/ciphernode_registry_sol.rs | 57 +++--- crates/sortition/src/ciphernode_selector.rs | 49 +++-- crates/sortition/src/sortition.rs | 174 +++++++++++++++++- crates/tests/tests/integration.rs | 2 + crates/tests/tests/integration_legacy.rs | 17 +- 12 files changed, 349 insertions(+), 114 deletions(-) diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 7d3b00e003..4a3522df1e 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -76,7 +76,7 @@ impl Handler for Comm let submission_deadline = msg.submission_deadline; let provider = self.provider.clone(); - const FINALIZATION_BUFFER_SECONDS: u64 = 3; + const FINALIZATION_BUFFER_SECONDS: u64 = 1; let e3_id_for_log = e3_id.clone(); let fut = async move { diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index e4b7b53349..af7664c5f4 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -11,7 +11,7 @@ use e3_events::{ Die, E3id, EnclaveEvent, EventBus, KeyshareCreated, OrderedSet, PublicKeyAggregated, Seed, }; use e3_fhe::{Fhe, GetAggregatePublicKey}; -use e3_sortition::{GetNodeIndex, GetNodesForE3, Sortition}; +use e3_sortition::{GetNodesForE3, Sortition}; use e3_utils::ArcBytes; use std::sync::Arc; use tracing::{error, trace}; @@ -148,47 +148,32 @@ impl Handler for PublicKeyAggregator { type Result = ResponseActFuture>; fn handle(&mut self, event: KeyshareCreated, _: &mut Self::Context) -> Self::Result { - let Some(PublicKeyAggregatorState::Collecting { - threshold_n, seed, .. - }) = self.state.get() - else { - error!(state=?self.state, "Aggregator has been closed for collecting keyshares."); - return Box::pin(fut::ready(Ok(()))); - }; - - let size = threshold_n; - let address = event.node; - let chain_id = event.e3_id.chain_id(); + let address = event.node.clone(); let e3_id = event.e3_id.clone(); let pubkey = event.pubkey.clone(); Box::pin( self.sortition - .send(GetNodeIndex { - chain_id, - address, - size, - seed, + .send(GetNodesForE3 { + e3_id: e3_id.clone(), + chain_id: e3_id.chain_id(), }) .into_actor(self) .map(move |res, act, ctx| { - // NOTE: Returning Ok(()) on errors as we probably dont need a result type here since - // we will not be doing a send - let maybe_found_index = res?; - let Some(_) = maybe_found_index else { - trace!("Node not found in committee"); + let nodes = res?; + + if !nodes.contains(&address) { + trace!("Node {} not found in finalized committee", address); return Ok(()); - }; + } if e3_id != act.e3_id { error!("Wrong e3_id sent to aggregator. This should not happen."); return Ok(()); } - // add the keyshare and act.add_keyshare(pubkey)?; - // Check the state and if it has changed to the computing if let Some(PublicKeyAggregatorState::Computing { keyshares }) = &act.state.get() { @@ -230,6 +215,7 @@ impl Handler for PublicKeyAggregator { self.sortition .send(GetNodesForE3 { e3_id: msg.e3_id.clone(), + chain_id: msg.e3_id.chain_id(), }) .into_actor(self) .map(move |res, act, _| { diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 870c0bdd4e..9beb053e77 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -14,7 +14,7 @@ use e3_events::{ Seed, }; use e3_multithread::Multithread; -use e3_sortition::Sortition; +use e3_sortition::{GetNodesForE3, Sortition}; use e3_trbfv::{ calculate_threshold_decryption::{ CalculateThresholdDecryptionRequest, CalculateThresholdDecryptionResponse, @@ -22,7 +22,7 @@ use e3_trbfv::{ TrBFVConfig, TrBFVRequest, }; use e3_utils::utility_types::ArcBytes; -use tracing::{error, info}; +use tracing::{error, info, trace}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Collecting { @@ -244,46 +244,58 @@ impl Handler for ThresholdPlaintextAggregator { } impl Handler for ThresholdPlaintextAggregator { - type Result = Result<()>; + type Result = ResponseActFuture>; - fn handle(&mut self, event: DecryptionshareCreated, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, event: DecryptionshareCreated, _: &mut Self::Context) -> Self::Result { info!(event=?event, "Processing DecryptionShareCreated..."); - let Some(ThresholdPlaintextAggregatorState::Collecting(Collecting { threshold_n, .. })) = - self.state.get() - else { - error!(state=?self.state, "Aggregator has been closed for collecting."); - return Ok(()); - }; - + let address = event.node.clone(); let party_id = event.party_id; let e3_id = event.e3_id.clone(); let decryption_share = event.decryption_share.clone(); - // Trust the party_id from the event - it's based on CommitteeFinalized order - // which is the authoritative source of truth for party IDs - if e3_id != self.e3_id { - error!("Wrong e3_id sent to aggregator. This should not happen."); - return Ok(()); - } - self.add_share(party_id, decryption_share)?; - - if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { - threshold_m, - threshold_n, - shares, - ciphertext_output, - .. - })) = self.state.get() - { - ctx.notify(ComputeAggregate { - shares: shares.clone(), - ciphertext_output: ciphertext_output.clone(), - threshold_m, - threshold_n, - }) - } + Box::pin( + self.sortition + .send(GetNodesForE3 { + e3_id: e3_id.clone(), + chain_id: e3_id.chain_id(), + }) + .into_actor(self) + .map(move |res, act, ctx| { + let nodes = res?; + + if !nodes.contains(&address) { + trace!("Node {} not found in finalized committee", address); + return Ok(()); + } + + if e3_id != act.e3_id { + error!("Wrong e3_id sent to aggregator. This should not happen."); + return Ok(()); + } + + // Trust the party_id from the event - it's based on CommitteeFinalized order + // which is the authoritative source of truth for party IDs + act.add_share(party_id, decryption_share)?; + + if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { + threshold_m, + threshold_n, + shares, + ciphertext_output, + .. + })) = act.state.get() + { + ctx.notify(ComputeAggregate { + shares: shares.clone(), + ciphertext_output: ciphertext_output.clone(), + threshold_m, + threshold_n, + }) + } - Ok(()) + Ok(()) + }), + ) } } diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 4e7d6ea3bd..a46ce11d56 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -32,7 +32,7 @@ use e3_multithread::Multithread; use e3_request::E3Router; use e3_sortition::{ CiphernodeSelector, FinalizedCommitteesRepositoryFactory, NodeStateRepositoryFactory, - Sortition, SortitionRepositoryFactory, + Sortition, SortitionBackend, SortitionRepositoryFactory, }; use e3_utils::{rand_eth_addr, SharedRng}; use std::{collections::HashMap, sync::Arc}; @@ -58,6 +58,7 @@ pub struct CiphernodeBuilder { pubkey_agg: bool, rng: SharedRng, source_bus: Option>>>, + sortition_backend: SortitionBackend, testmode_errors: bool, testmode_history: bool, threads: Option, @@ -99,6 +100,7 @@ impl CiphernodeBuilder { pubkey_agg: false, rng, source_bus: None, + sortition_backend: SortitionBackend::score(), testmode_errors: false, testmode_history: false, threads: None, @@ -202,6 +204,25 @@ impl CiphernodeBuilder { self } + /// Use distance-based sortition (DEPRECATED) + /// + /// # Deprecation Notice + /// Distance sortition is deprecated and does not work with on-chain contracts. + /// Use `with_sortition_score()` instead. + #[deprecated( + note = "Distance sortition is deprecated and does not work with on-chain contracts. Use with_sortition_score() instead." + )] + pub fn with_sortition_distance(mut self) -> Self { + self.sortition_backend = SortitionBackend::distance(); + self + } + + /// Use score-based sortition (recommended) + pub fn with_sortition_score(mut self) -> Self { + self.sortition_backend = SortitionBackend::score(); + self + } + /// Setup an Enclave contract reader for every evm chain provided pub fn with_contract_enclave_reader(mut self) -> Self { self.contract_components.enclave_reader = true; @@ -280,11 +301,16 @@ impl CiphernodeBuilder { let node_state_manager = e3_sortition::NodeStateManager::attach(&local_bus, &repositories.node_state()).await?; - let sortition = Sortition::attach_with_node_state( + + // Use the configured backend directly + let default_backend = self.sortition_backend.clone(); + + let sortition = Sortition::attach_with_backend( &local_bus, repositories.sortition(), repositories.finalized_committees(), node_state_manager, + default_backend, ) .await?; @@ -369,7 +395,7 @@ impl CiphernodeBuilder { .await?; info!("CiphernodeRegistrySolWriter attached for publishing committees"); - if self.pubkey_agg { + if self.pubkey_agg && matches!(self.sortition_backend, SortitionBackend::Score(_)) { info!("Attaching CommitteeFinalizer for score sortition"); e3_aggregator::CommitteeFinalizer::attach( &local_bus, diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 857dc9e57c..d0cfd37d1e 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -38,6 +38,7 @@ pub async fn execute( .with_source_bus(&bus) .with_datastore(store) .with_chains(&config.chains()) + .with_sortition_score() .with_contract_enclave_full() .with_contract_bonding_registry() .with_contract_ciphernode_registry() diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index dbb4fe0b82..1fa874a5d6 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -39,6 +39,7 @@ pub async fn execute( .with_keyshare() .with_source_bus(&bus) .with_datastore(store) + .with_sortition_score() .with_chains(&config.chains()) .with_contract_enclave_reader() .with_contract_bonding_registry() diff --git a/crates/events/src/enclave_event/ticket_generated.rs b/crates/events/src/enclave_event/ticket_generated.rs index 113d099a55..c71297a932 100644 --- a/crates/events/src/enclave_event/ticket_generated.rs +++ b/crates/events/src/enclave_event/ticket_generated.rs @@ -9,11 +9,17 @@ use actix::Message; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TicketId { + Distance, + Score(u64), +} + #[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[rtype(result = "()")] pub struct TicketGenerated { pub e3_id: E3id, - pub ticket_id: u64, + pub ticket_id: TicketId, pub node: String, } @@ -21,7 +27,7 @@ impl Display for TicketGenerated { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "e3_id: {}, ticket_id: {}, node: {}", + "e3_id: {}, ticket_id: {:?}, node: {}", self.e3_id, self.ticket_id, self.node ) } diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index da9dba9d2e..110e3640d4 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -17,7 +17,7 @@ use anyhow::Result; use e3_data::Repository; use e3_events::{ BusError, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, - PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, + PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, TicketId, }; use tracing::{error, info, trace}; @@ -109,7 +109,7 @@ impl From for EnclaveEvent { struct CommitteeFinalizedWithChainId(pub ICiphernodeRegistry::CommitteeFinalized, pub u64); -impl From for e3_events::CommitteeFinalized { +impl From for CommitteeFinalized { fn from(value: CommitteeFinalizedWithChainId) -> Self { e3_events::CommitteeFinalized { e3_id: E3id::new(value.0.e3Id.to_string(), value.1), @@ -326,27 +326,40 @@ impl Handler type Result = ResponseFuture<()>; fn handle(&mut self, msg: TicketGenerated, _: &mut Self::Context) -> Self::Result { - let e3_id = msg.e3_id.clone(); - let ticket_id = msg.ticket_id; - let contract_address = self.contract_address; - let provider = self.provider.clone(); - let bus = self.bus.clone(); - - Box::pin(async move { - info!("Submitting ticket {} for E3 {:?}", ticket_id, e3_id); - - let result = - submit_ticket_to_registry(provider, contract_address, e3_id, ticket_id).await; - match result { - Ok(receipt) => { - info!(tx=%receipt.transaction_hash, "Ticket submitted to registry"); - } - Err(err) => { - error!("Failed to submit ticket: {:?}", err); - bus.err(EnclaveErrorType::Evm, err); - } + match msg.ticket_id { + TicketId::Distance => { + info!("Distance sortition ticket generated for E3 {:?}, no contract submission needed", msg.e3_id); + return Box::pin(async move {}); } - }) + TicketId::Score(ticket_id) => { + info!( + "Score sortition ticket generated for E3 {:?}, submitting to contract", + msg.e3_id + ); + + let e3_id = msg.e3_id.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + Box::pin(async move { + info!("Submitting ticket {} for E3 {:?}", ticket_id, e3_id); + + let result = + submit_ticket_to_registry(provider, contract_address, e3_id, ticket_id) + .await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Ticket submitted to registry"); + } + Err(err) => { + error!("Failed to submit ticket: {:?}", err); + bus.err(EnclaveErrorType::Evm, err); + } + } + }) + } + } } } diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index c23154e408..7bdaf376c5 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -4,14 +4,14 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{GetNodeIndex, Sortition}; +use crate::{GetCommittee, GetNodeIndex, Sortition}; /// CiphernodeSelector is an actor that determines if a ciphernode is part of a committee and if so /// forwards a CiphernodeSelected event (distance sortition) or TicketGenerated event (score sortition) to the event bus use actix::prelude::*; use e3_data::{DataStore, RepositoriesFactory}; use e3_events::{ CiphernodeSelected, CommitteeFinalized, E3Requested, EnclaveEvent, EventBus, Shutdown, - Subscribe, TicketGenerated, + Subscribe, TicketGenerated, TicketId, }; use e3_request::MetaRepositoryFactory; use tracing::info; @@ -100,7 +100,7 @@ impl Handler for CiphernodeSelector { }) .await { - let Some((party_id, ticket_id)) = found_result else { + let Some((_party_id, ticket_id)) = found_result else { info!(node = address, "Ciphernode was not selected"); return; }; @@ -113,21 +113,44 @@ impl Handler for CiphernodeSelector { ); bus.do_send(EnclaveEvent::from(TicketGenerated { e3_id: data.e3_id.clone(), - ticket_id: tid, + ticket_id: TicketId::Score(tid), node: address.clone(), })); } else { info!(node = address, "Ciphernode selected via distance sortition"); - bus.do_send(EnclaveEvent::from(CiphernodeSelected { - party_id, - e3_id: data.e3_id, - threshold_m: data.threshold_m, - threshold_n: data.threshold_n, - esi_per_ct: data.esi_per_ct, - error_size: data.error_size, - params: data.params.clone(), - seed: data.seed.clone(), + // This is a quick workaround to make distance sortition work until we remove it? + bus.do_send(EnclaveEvent::from(TicketGenerated { + e3_id: data.e3_id.clone(), + ticket_id: TicketId::Distance, + node: address.clone(), })); + + let committee_fut = sortition.send(GetCommittee { + seed, + size, + chain_id, + }); + + match committee_fut.await { + Ok(Ok(committee)) => { + info!( + node = address, + committee_size = committee.len(), + "Emitting CommitteeFinalized for distance sortition" + ); + bus.do_send(EnclaveEvent::from(CommitteeFinalized { + e3_id: data.e3_id.clone(), + committee, + chain_id: data.e3_id.chain_id(), + })); + } + Ok(Err(e)) => { + info!("Failed to get committee: {}", e); + } + Err(e) => { + info!("Failed to send GetCommittee: {}", e); + } + } } } else { info!("This node is not selected"); diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index 2a7a9d9f4b..949da89346 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -55,6 +55,20 @@ pub struct GetNodes { pub struct GetNodesForE3 { /// E3 ID to get nodes for. pub e3_id: e3_events::E3id, + /// Chain ID + pub chain_id: u64, +} + +/// Message to get the full committee for a specific sortition. +#[derive(Message, Clone, Debug)] +#[rtype(result = "anyhow::Result>")] +pub struct GetCommittee { + /// Round seed / randomness used by the sortition algorithm + pub seed: Seed, + /// Committee size (top-N) + pub size: usize, + /// Target chain + pub chain_id: u64, } /// Minimal interface that all sortition backends must implement. @@ -99,6 +113,19 @@ pub trait SortitionList { /// Return all registered node addresses as hex strings. fn nodes(&self) -> Vec; + + /// Return the full committee for a specific sortition. + /// + /// Implementations should return an error if the backend has no nodes + /// or if `size == 0`. For backends that don't support this operation, + /// they should return an appropriate error. + fn get_committee( + &self, + seed: Seed, + size: usize, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result>; } /// Distance-sortition backend. @@ -187,6 +214,25 @@ impl SortitionList for DistanceBackend { fn nodes(&self) -> Vec { self.nodes.iter().cloned().collect() } + + /// Return the full committee for distance sortition. + fn get_committee( + &self, + seed: Seed, + size: usize, + _node_state: Option<&NodeStateStore>, + _chain_id: u64, + ) -> anyhow::Result> { + if size == 0 { + return Err(anyhow!("Size cannot be 0")); + } + if self.nodes.len() == 0 { + return Err(anyhow!("No nodes registered!")); + } + + let committee = get_committee(seed, size, self.nodes.clone())?; + Ok(committee.iter().map(|(_, addr)| addr.to_string()).collect()) + } } fn get_committee( @@ -390,6 +436,22 @@ impl SortitionList for ScoreBackend { .map(|n| n.address.to_string()) .collect() } + + /// Return the full committee for score sortition. + /// + /// Note: This is not supported for score sortition as the committee + /// is determined by the contract after ticket submission. + fn get_committee( + &self, + _seed: Seed, + _size: usize, + _node_state: Option<&NodeStateStore>, + _chain_id: u64, + ) -> anyhow::Result> { + Err(anyhow!( + "get_committee not supported for ScoreBackend - committee is determined by contract" + )) + } } /// Enum wrapper around the supported backends. @@ -397,19 +459,45 @@ impl SortitionList for ScoreBackend { /// New chains default to `Score` sortition. If a chain is intended to /// use distance selection, construct it as `SortitionBackend::Distance(DistanceBackend::default())` /// explicitly. +/// +/// # Deprecation Notice +/// Distance sortition is deprecated and does not work with on-chain contracts. +/// Use Score sortition for all new implementations. +/// Distance sortition will be removed in a future release. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum SortitionBackend { /// Distance-based selection (stores a simple set of addresses). + #[deprecated( + note = "Distance sortition is deprecated and does not work with on-chain contracts. Use Score sortition instead." + )] Distance(DistanceBackend), /// Score-based selection (stores `RegisteredNode`s with tickets). Score(ScoreBackend), } +impl Default for SortitionBackend { + fn default() -> Self { + SortitionBackend::Distance(DistanceBackend::default()) + } +} + impl SortitionBackend { - /// Construct a backend preconfigured with a default `ScoreBackend`. - pub fn default() -> Self { + /// Use score-based sortition (recommended) + pub fn score() -> Self { SortitionBackend::Score(ScoreBackend::default()) } + + /// Use distance-based sortition (DEPRECATED) + /// + /// # Deprecation Notice + /// Distance sortition is deprecated and does not work with on-chain contracts. + /// Use `SortitionBackend::score()` instead. + #[deprecated( + note = "Distance sortition is deprecated and does not work with on-chain contracts. Use score() instead." + )] + pub fn distance() -> Self { + SortitionBackend::Distance(DistanceBackend::default()) + } } impl SortitionList for SortitionBackend { @@ -461,6 +549,23 @@ impl SortitionList for SortitionBackend { SortitionBackend::Score(backend) => backend.nodes(), } } + + fn get_committee( + &self, + seed: Seed, + size: usize, + node_state: Option<&NodeStateStore>, + chain_id: u64, + ) -> anyhow::Result> { + match self { + SortitionBackend::Distance(backend) => { + backend.get_committee(seed, size, node_state, chain_id) + } + SortitionBackend::Score(backend) => { + backend.get_committee(seed, size, node_state, chain_id) + } + } + } } /// `Sortition` is an Actix actor that owns per-chain backends and exposes @@ -530,18 +635,26 @@ impl Sortition { Ok(addr) } - /// Load persisted state with node state support for score-based sortition. + /// Load persisted state with node state support and configurable default backend. /// - /// This version allows score-based backends to query ticket availability. - #[instrument(name = "sortition_attach_with_node_state", skip_all)] - pub async fn attach_with_node_state( + /// This version allows score-based backends to query ticket availability and + /// configures the default backend type for new chains. + #[instrument(name = "sortition_attach_with_backend", skip_all)] + pub async fn attach_with_backend( bus: &Addr>, store: Repository>, committees_store: Repository>>, node_state_manager: Addr, + default_backend: SortitionBackend, ) -> Result> { - let list = store.load_or_default(HashMap::new()).await?; + let mut list = store.load_or_default(HashMap::new()).await?; let finalized_committees = committees_store.load_or_default(HashMap::new()).await?; + + list.try_mutate(|mut list| { + list.insert(u64::MAX, default_backend); + Ok(list) + })?; + let addr = Sortition::new(SortitionParams { bus: bus.clone(), list, @@ -604,9 +717,15 @@ impl Handler for Sortition { let addr = msg.address.clone(); if let Err(err) = self.list.try_mutate(move |mut list_map| { + // Use the configured default backend if available, otherwise fall back to Distance + let default_backend = list_map + .get(&u64::MAX) + .cloned() + .unwrap_or_else(|| SortitionBackend::distance()); + list_map .entry(chain_id) - .or_insert_with(SortitionBackend::default) + .or_insert_with(|| default_backend) .add(addr); Ok(list_map) }) { @@ -716,6 +835,15 @@ impl Handler for Sortition { type Result = Vec; fn handle(&mut self, msg: GetNodesForE3, _ctx: &mut Self::Context) -> Self::Result { + if msg.e3_id.chain_id() != msg.chain_id { + tracing::warn!( + "Chain ID mismatch: e3_id has chain_id {}, but requested chain_id {}", + msg.e3_id.chain_id(), + msg.chain_id + ); + return Vec::new(); + } + self.finalized_committees .get() .and_then(|committees| committees.get(&msg.e3_id).cloned()) @@ -725,3 +853,33 @@ impl Handler for Sortition { }) } } + +impl Handler for Sortition { + type Result = ResponseFuture>>; + + fn handle(&mut self, msg: GetCommittee, _ctx: &mut Self::Context) -> Self::Result { + let backends_snapshot = self.list.get(); + + Box::pin(async move { + if let Some(map) = backends_snapshot { + if let Some(backend) = map.get(&msg.chain_id) { + // Get node state for score backend + let node_state = if matches!(backend, SortitionBackend::Score(_)) { + // For score backend, we need node state + // This is a limitation - we'd need to pass node_state_manager + // For now, we'll return an error for score backend + return Err(anyhow!("GetCommittee not supported for ScoreBackend - use GetNodesForE3 instead")); + } else { + None + }; + + backend.get_committee(msg.seed, msg.size, node_state, msg.chain_id) + } else { + Err(anyhow!("No backend found for chain_id {}", msg.chain_id)) + } + } else { + Err(anyhow!("Could not get sortition's list cache")) + } + }) + } +} diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 5def61b7f6..629b78e53a 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -118,6 +118,7 @@ async fn test_trbfv_actor() -> Result<()> { .testmode_with_history() .with_trbfv() .with_pubkey_aggregation() + .with_sortition_score() .with_threshold_plaintext_aggregation() .testmode_with_forked_bus(&bus) .with_logging() @@ -131,6 +132,7 @@ async fn test_trbfv_actor() -> Result<()> { .with_address(&addr) .with_injected_multithread(multithread.clone()) .with_trbfv() + .with_sortition_score() .testmode_with_forked_bus(&bus) .with_logging() .build() diff --git a/crates/tests/tests/integration_legacy.rs b/crates/tests/tests/integration_legacy.rs index 29a9f04cc7..5c97a96456 100644 --- a/crates/tests/tests/integration_legacy.rs +++ b/crates/tests/tests/integration_legacy.rs @@ -54,7 +54,8 @@ async fn setup_local_ciphernode( .testmode_with_history() .testmode_with_errors() .with_pubkey_aggregation() - .with_plaintext_aggregation(); + .with_plaintext_aggregation() + .with_sortition_distance(); // Using deprecated distance sortition for legacy tests if let Some(data) = data { builder = builder.with_datastore((&data).into()); @@ -137,6 +138,7 @@ fn aggregate_public_key(shares: &Vec) -> Result { } #[actix::test] +#[ignore] async fn test_public_key_aggregation_and_decryption() -> Result<()> { // Setup let (bus, rng, seed, params, crpoly, _, _) = get_common_setup(None)?; @@ -151,6 +153,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { .collect::>(); println!("Adding ciphernodes..."); + add_ciphernodes(&bus, ð_addrs, 1).await?; let e3_request_event = EnclaveEvent::from(E3Requested { @@ -182,7 +185,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { let history_collector = ciphernodes.get(2).unwrap().history().unwrap(); let history = history_collector - .send(TakeEvents::::new(9)) + .send(TakeEvents::::new(15)) .await?; let aggregated_event: Vec<_> = history @@ -233,6 +236,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { } #[actix::test] +#[ignore] async fn test_stopped_keyshares_retain_state() -> Result<()> { let e3_id = E3id::new("1234", 1); let (rng, cn1_address, cn1_data, cn2_address, cn2_data, cipher, history, params, crpoly) = { @@ -260,10 +264,11 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { .clone(), ) .await?; + let history_collector = cn1.history().unwrap(); let error_collector = cn1.errors().unwrap(); let history = history_collector - .send(TakeEvents::::new(7)) + .send(TakeEvents::::new(10)) .await?; let errors = error_collector.send(GetEvents::new()).await?; @@ -442,6 +447,7 @@ async fn test_p2p_actor_forwards_events_to_network() -> Result<()> { } #[actix::test] +#[ignore] async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { // Setup let (bus, rng, seed, params, crpoly, _, _) = get_common_setup(None)?; @@ -450,6 +456,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { // Setup actual ciphernodes and dispatch add events let ciphernodes = create_local_ciphernodes(&bus, &rng, 3, &cipher).await?; let eth_addrs = ciphernodes.iter().map(|tup| tup.address()).collect(); + add_ciphernodes(&bus, ð_addrs, 1).await?; add_ciphernodes(&bus, ð_addrs, 2).await?; @@ -472,7 +479,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { let history_collector = ciphernodes.last().unwrap().history().unwrap(); let history = history_collector - .send(TakeEvents::::new(12)) + .send(TakeEvents::::new(15)) .await?; assert_eq!( @@ -500,7 +507,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { )?)?; let history = history_collector - .send(TakeEvents::::new(6)) + .send(TakeEvents::::new(9)) .await?; assert_eq!( From 233bcd4ca49c448bee8aae2cb742cfe566cf1216 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 15:10:38 +0500 Subject: [PATCH 64/88] fix: contract addresses for crisp e2e test --- .../crisp-contracts/deployed_contracts.json | 2 +- examples/CRISP/scripts/dev_cipher.sh | 12 +++-- examples/CRISP/server/.env.example | 8 ++-- .../registry/CiphernodeRegistryOwnable.sol | 15 ++++--- .../enclave-contracts/deployed_contracts.json | 44 +------------------ 5 files changed, 23 insertions(+), 58 deletions(-) diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 3317e6ee58..22c64fe348 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -229,4 +229,4 @@ } } } -} +} \ No newline at end of file diff --git a/examples/CRISP/scripts/dev_cipher.sh b/examples/CRISP/scripts/dev_cipher.sh index 65631343e6..0f950a514d 100755 --- a/examples/CRISP/scripts/dev_cipher.sh +++ b/examples/CRISP/scripts/dev_cipher.sh @@ -6,9 +6,15 @@ set -euo pipefail rm -rf ./enclave/data rm -rf ./enclave/config -PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -enclave wallet set --name ag --private-key "$PRIVATE_KEY" +PRIVATE_KEY_AG="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" + +enclave wallet set --name ag --private-key "$PRIVATE_KEY" +enclave wallet set --name cn1 --private-key "$PRIVATE_KEY_CN1" +enclave wallet set --name cn2 --private-key "$PRIVATE_KEY_CN2" +enclave wallet set --name cn3 --private-key "$PRIVATE_KEY_CN3" # using & instead of -d so that wait works below enclave nodes up -v & diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index e20aebbb44..75e0932a93 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -13,10 +13,10 @@ BITQUERY_API_KEY="" CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) -ENCLAVE_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" -CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" -E3_PROGRAM_ADDRESS="0x09635F643e140090A9A8Dcd712eD6285858ceBef" # CRISPProgram Contract Address -FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" +ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +CIPHERNODE_REGISTRY_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" +E3_PROGRAM_ADDRESS="0xc5a5C42992dECbae36851359345FE25997F5C42d" # CRISPProgram Contract Address +FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config E3_WINDOW_SIZE=40 diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 766f0e0cad..e0e01432c9 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -233,7 +233,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @notice Publishes a committee for an E3 computation - /// @dev Only callable by owner. Stores committee data and emits event + /// @dev Only callable by owner. Verifies committee is finalized and matches provided nodes. /// @param e3Id ID of the E3 computation /// @param nodes Array of ciphernode addresses selected for the committee /// @param publicKey Aggregated public key of the committee @@ -243,13 +243,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { bytes calldata publicKey ) external onlyOwner { Committee storage c = committees[e3Id]; - require(c.publicKey == bytes32(0), CommitteeAlreadyPublished()); - - // Store the nodes in the committee array - for (uint256 i = 0; i < nodes.length; i++) { - c.committee.push(nodes[i]); - } + require(c.initialized, CommitteeNotRequested()); + require(c.finalized, CommitteeNotFinalized()); + require(c.publicKey == bytes32(0), CommitteeAlreadyPublished()); + require(nodes.length == c.committee.length, "Node count mismatch"); + + // TODO: Currently we trust the owner to publish the correct committee. + // TODO: Need a Proof that the public key is generated from the committee bytes32 publicKeyHash = keccak256(publicKey); c.publicKey = publicKeyHash; publicKeyHashes[e3Id] = publicKeyHash; diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index cce5afa279..cf2ffe77d8 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -116,7 +116,7 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 55, + "blockNumber": 3, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { @@ -164,48 +164,6 @@ }, "blockNumber": 9, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - }, - "CiphernodeRegistryOwnable": { - "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "enclaveAddress": "0x0000000000000000000000000000000000000001", - "submissionWindow": "3" - }, - "blockNumber": 10, - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - }, - "Enclave": { - "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", - "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - "maxDuration": "2592000", - "params": [ - "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" - ] - }, - "blockNumber": 11, - "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" - }, - "MockComputeProvider": { - "blockNumber": 20, - "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" - }, - "MockDecryptionVerifier": { - "blockNumber": 21, - "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" - }, - "MockInputValidator": { - "blockNumber": 22, - "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" - }, - "MockE3Program": { - "constructorArgs": { - "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" - }, - "blockNumber": 23, - "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" } } } \ No newline at end of file From 3c975830f32ce1ec2f6bcb7fd7499b579ee55ad8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 16:03:29 +0500 Subject: [PATCH 65/88] feat: tests for new sortition changes --- .../registry/CiphernodeRegistryOwnable.sol | 7 +- .../enclave-contracts/test/Enclave.spec.ts | 531 ++++++++++++++---- .../CiphernodeRegistryOwnable.spec.ts | 198 +++++-- 3 files changed, 568 insertions(+), 168 deletions(-) diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index e0e01432c9..a366c1ad7f 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -136,6 +136,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Submission window not closed yet error SubmissionWindowNotClosed(); + /// @notice Threshold not met for this E3 + error ThresholdNotMet(); + /// @notice Caller is not authorized error Unauthorized(); @@ -248,7 +251,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(c.finalized, CommitteeNotFinalized()); require(c.publicKey == bytes32(0), CommitteeAlreadyPublished()); require(nodes.length == c.committee.length, "Node count mismatch"); - + // TODO: Currently we trust the owner to publish the correct committee. // TODO: Need a Proof that the public key is generated from the committee bytes32 publicKeyHash = keccak256(publicKey); @@ -340,7 +343,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { block.timestamp >= c.submissionDeadline, SubmissionWindowNotClosed() ); - require(c.topNodes.length >= c.threshold[0], NodeNotEligible()); + require(c.topNodes.length >= c.threshold[0], ThresholdNotMet()); c.finalized = true; c.committee = c.topNodes; diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 79f7a22867..de23eca218 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -61,6 +61,21 @@ describe("Enclave", function () { // Hash function used to compute the tree nodes. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); + const setupAndPublishCommittee = async ( + registry: any, + e3Id: number, + nodes: string[], + publicKey: string, + operator1: any, + operator2: any, + ): Promise => { + await registry.connect(operator1).submitTicket(e3Id, 1); + await registry.connect(operator2).submitTicket(e3Id, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(e3Id); + await registry.publishCommittee(e3Id, nodes, publicKey); + }; + // Helper function to approve USDC and make request const makeRequest = async ( enclave: Enclave, @@ -76,8 +91,43 @@ describe("Enclave", function () { return enclaveContract.request(requestParams); }; + async function setupOperatorForSortition( + operator: any, + bondingRegistry: any, + licenseToken: any, + usdcToken: any, + ticketToken: any, + registry: any, + ): Promise { + const operatorAddress = await operator.getAddress(); + + await licenseToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await licenseToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + + await registry.addCiphernode(operatorAddress); + } + const setup = async () => { - const [owner, notTheOwner] = await ethers.getSigners(); + const [owner, notTheOwner, operator1, operator2] = + await ethers.getSigners(); const ownerAddress = await owner.getAddress(); @@ -138,7 +188,7 @@ describe("Enclave", function () { licenseToken: await enclTokenContract.enclaveToken.getAddress(), registry: addressOne, slashedFundsTreasury: ownerAddress, - ticketPrice: ethers.parseEther("10"), + ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), minTicketBalance: 5, exitDelay: 7 * 24 * 60 * 60, @@ -210,11 +260,32 @@ describe("Enclave", function () { const tree = new LeanIMT(hash); - const testCiphernodes = [addressOne, AddressTwo]; - for (const ciphernodeAddress of testCiphernodes) { - await ciphernodeRegistryContract.addCiphernode(ciphernodeAddress); - tree.insert(BigInt(ciphernodeAddress)); - } + const licenseToken = enclTokenContract.enclaveToken; + const ticketToken = ticketTokenContract.enclaveTicketToken; + + await licenseToken.setTransferRestriction(false); + + await setupOperatorForSortition( + operator1, + bondingRegistryContract.bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + ciphernodeRegistryContract, + ); + tree.insert(BigInt(await operator1.getAddress())); + + await setupOperatorForSortition( + operator2, + bondingRegistryContract.bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + ciphernodeRegistryContract, + ); + tree.insert(BigInt(await operator2.getAddress())); + + await mine(1); const mockComputeProvider = await ignition.deploy( mockComputeProviderModule, @@ -266,23 +337,13 @@ describe("Enclave", function () { await notTheOwner.getAddress(), ethers.parseUnits("1000000", 6), ); - await enclTokenContract.enclaveToken.mintAllocation( - ownerAddress, - ethers.parseEther("10000"), - "Test allocation", - ); - await enclTokenContract.enclaveToken.mintAllocation( - await notTheOwner.getAddress(), - ethers.parseEther("10000"), - "Test allocation", - ); return { enclave, ciphernodeRegistryContract, bondingRegistry: bondingRegistryContract.bondingRegistry, ticketToken: ticketTokenContract.enclaveTicketToken, - licenseToken: enclTokenContract.enclaveToken, + licenseToken: licenseToken, usdcToken, slashingManager: slashingManagerContract.slashingManager, tree, @@ -295,6 +356,8 @@ describe("Enclave", function () { request, owner, notTheOwner, + operator1, + operator2, }; }; @@ -894,8 +957,14 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if E3 has already been activated", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -907,10 +976,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 0, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await expect(enclave.getE3(0)).to.not.be.revert(ethers); @@ -941,8 +1013,14 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; const currentTime = await time.latest(); const startTime = [currentTime + 10, currentTime + 100] as [ @@ -955,10 +1033,13 @@ describe("Enclave", function () { startWindow: startTime, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await mine(2, { interval: 2000 }); @@ -990,8 +1071,14 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; const currentTime = await time.latest(); const startTime = [currentTime + 5, currentTime + 50] as [number, number]; @@ -1001,10 +1088,13 @@ describe("Enclave", function () { startWindow: startTime, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await time.increaseTo(currentTime + request.duration + 100); @@ -1015,8 +1105,14 @@ describe("Enclave", function () { ); }); it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { + enclave, + request, + ciphernodeRegistryContract, + usdcToken, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, request); @@ -1034,18 +1130,27 @@ describe("Enclave", function () { await enclave.setCiphernodeRegistry(prevRegistry); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 0, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await expect(enclave.activate(0, data)).not.to.be.revert(ethers); }); it("sets committeePublicKey correctly", async () => { - const { enclave, request, ciphernodeRegistryContract, usdcToken } = - await loadFixture(setup); + const { + enclave, + request, + ciphernodeRegistryContract, + usdcToken, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -1059,10 +1164,13 @@ describe("Enclave", function () { const e3Id = 0; - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); const publicKey = @@ -1077,8 +1185,14 @@ describe("Enclave", function () { expect(e3.committeePublicKey).to.equal(publicKey); }); it("returns true if E3 is activated successfully", async () => { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -1092,17 +1206,26 @@ describe("Enclave", function () { const e3Id = 0; - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); expect(await enclave.activate.staticCall(e3Id, data)).to.be.equal(true); }); it("emits E3Activated event", async () => { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -1116,10 +1239,13 @@ describe("Enclave", function () { const e3Id = 0; - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await expect(enclave.activate(e3Id, data)).to.emit( @@ -1160,8 +1286,14 @@ describe("Enclave", function () { }); it("reverts if input is not valid", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -1173,10 +1305,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 0, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(0, data); await expect( @@ -1185,8 +1320,14 @@ describe("Enclave", function () { }); it("reverts if outside of input window", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -1198,10 +1339,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 0, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(0, data); @@ -1215,8 +1359,14 @@ describe("Enclave", function () { it("it allows publishing input to different requests", async function () { const fixtureSetup = () => setup(); - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(fixtureSetup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(fixtureSetup); const inputData = "0x12345678"; await makeRequest(enclave, usdcToken, { @@ -1229,10 +1379,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 0, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(0, data); await enclave.publishInput(0, inputData); @@ -1247,17 +1400,26 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 1, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(1, data); await enclave.publishInput(1, inputData); }); it("returns true if input is published successfully", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const inputData = "0x12345678"; await makeRequest(enclave, usdcToken, { @@ -1270,10 +1432,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, 0, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(0, data); @@ -1283,8 +1448,14 @@ describe("Enclave", function () { }); it("adds inputHash to merkle tree", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); const tree = new LeanIMT(hash); @@ -1301,10 +1472,13 @@ describe("Enclave", function () { const e3Id = 0; - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); @@ -1319,8 +1493,14 @@ describe("Enclave", function () { expect(await enclave.getInputRoot(e3Id)).to.equal(tree.root); }); it("emits InputPublished event", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); await makeRequest(enclave, usdcToken, { threshold: request.threshold, @@ -1335,10 +1515,13 @@ describe("Enclave", function () { const e3Id = 0; const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); const expectedHash = hash(BigInt(ethers.keccak256(inputData)), BigInt(0)); @@ -1375,8 +1558,14 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if input deadline has not passed", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const currentTime = await time.latest(); await makeRequest(enclave, usdcToken, { ...request, @@ -1384,10 +1573,13 @@ describe("Enclave", function () { }); const e3Id = 0; - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); @@ -1396,8 +1588,14 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "InputDeadlineNotPassed"); }); it("reverts if output has already been published", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1410,10 +1608,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1426,8 +1627,14 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1440,10 +1647,13 @@ describe("Enclave", function () { customParams: request.customParams, }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1452,8 +1662,14 @@ describe("Enclave", function () { ).to.be.revertedWithCustomError(enclave, "InvalidOutput"); }); it("sets ciphertextOutput correctly", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1461,10 +1677,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1473,8 +1692,14 @@ describe("Enclave", function () { expect(e3.ciphertextOutput).to.equal(dataHash); }); it("returns true if output is published successfully", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1482,10 +1707,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1494,8 +1722,14 @@ describe("Enclave", function () { ).to.equal(true); }); it("emits CiphertextOutputPublished event", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1503,10 +1737,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1538,8 +1775,14 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if ciphertextOutput has not been published", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1547,10 +1790,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) @@ -1558,8 +1804,14 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if plaintextOutput has already been published", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1567,10 +1819,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1584,8 +1839,14 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1593,10 +1854,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1606,8 +1870,14 @@ describe("Enclave", function () { .withArgs(data); }); it("sets plaintextOutput correctly", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1615,10 +1885,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1629,8 +1902,14 @@ describe("Enclave", function () { expect(e3.plaintextOutput).to.equal(data); }); it("returns true if output is published successfully", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1638,10 +1917,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); @@ -1651,8 +1933,14 @@ describe("Enclave", function () { ).to.equal(true); }); it("emits PlaintextOutputPublished event", async function () { - const { enclave, request, usdcToken, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; await makeRequest(enclave, usdcToken, { @@ -1660,10 +1948,13 @@ describe("Enclave", function () { startWindow: [await time.latest(), (await time.latest()) + 100], }); - await ciphernodeRegistryContract.publishCommittee( + await setupAndPublishCommittee( + ciphernodeRegistryContract, e3Id, - [addressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, + operator1, + operator2, ); await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 711cc24d0d..25de8d45df 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -25,14 +25,57 @@ const { loadFixture } = networkHelpers; const data = "0xda7a"; const dataHash = ethers.keccak256(data); -const SORTITION_SUBMISSION_WINDOW = 10; +const SORTITION_SUBMISSION_WINDOW = 3; // Hash function used to compute the tree nodes. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); describe("CiphernodeRegistryOwnable", function () { + async function finalizeCommitteeAfterWindow( + registry: any, + e3Id: number, + ): Promise { + await networkHelpers.time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(e3Id); + } + + async function setupOperatorForSortition( + operator: any, + bondingRegistry: any, + licenseToken: any, + usdcToken: any, + ticketToken: any, + registry: any, + ): Promise { + const operatorAddress = await operator.getAddress(); + + await licenseToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await licenseToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + + await registry.addCiphernode(operatorAddress); + } + async function setup() { - const [owner, notTheOwner] = await ethers.getSigners(); + const [owner, notTheOwner, operator1, operator2] = + await ethers.getSigners(); const ownerAddress = await owner.getAddress(); // Deploy token contracts @@ -88,7 +131,7 @@ describe("CiphernodeRegistryOwnable", function () { licenseToken: await enclTokenContract.enclaveToken.getAddress(), registry: AddressOne, slashedFundsTreasury: ownerAddress, - ticketPrice: ethers.parseEther("10"), + ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), minTicketBalance: 5, exitDelay: 7 * 24 * 60 * 60, @@ -111,33 +154,58 @@ describe("CiphernodeRegistryOwnable", function () { await registryContract.cipherNodeRegistry.getAddress(); const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); + const bondingRegistry = bondingRegistryContract.bondingRegistry; - // Set up cross-contract dependencies await ticketTokenContract.enclaveTicketToken.setRegistry( - await bondingRegistryContract.bondingRegistry.getAddress(), + await bondingRegistry.getAddress(), ); - await bondingRegistryContract.bondingRegistry.setRegistry(registryAddress); - await bondingRegistryContract.bondingRegistry.setSlashingManager( + await bondingRegistry.setRegistry(registryAddress); + await bondingRegistry.setSlashingManager( await slashingManagerContract.slashingManager.getAddress(), ); await slashingManagerContract.slashingManager.setBondingRegistry( - await bondingRegistryContract.bondingRegistry.getAddress(), + await bondingRegistry.getAddress(), ); - await registry.setBondingRegistry( - await bondingRegistryContract.bondingRegistry.getAddress(), - ); + await registry.setBondingRegistry(await bondingRegistry.getAddress()); const tree = new LeanIMT(hash); - await registry.addCiphernode(AddressOne); - tree.insert(BigInt(AddressOne)); - await registry.addCiphernode(AddressTwo); - tree.insert(BigInt(AddressTwo)); + const licenseToken = enclTokenContract.enclaveToken; + const ticketToken = ticketTokenContract.enclaveTicketToken; + const usdcToken = usdcContract.mockUSDC; + + await licenseToken.setTransferRestriction(false); + await setupOperatorForSortition( + operator1, + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + registry, + ); + tree.insert(BigInt(await operator1.getAddress())); + + await setupOperatorForSortition( + operator2, + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + registry, + ); + tree.insert(BigInt(await operator2.getAddress())); + await networkHelpers.mine(1); return { owner, notTheOwner, + operator1, + operator2, registry, + bondingRegistry, + licenseToken, + ticketToken, + usdcToken, tree, request: { e3Id: 1, @@ -230,21 +298,38 @@ describe("CiphernodeRegistryOwnable", function () { describe("publishCommittee()", function () { it("reverts if the caller is not the owner", async function () { - const { registry, request, notTheOwner } = await loadFixture(setup); + const { registry, request, notTheOwner, operator1, operator2 } = + await loadFixture(setup); await registry.requestCommittee(request.e3Id, 0, request.threshold); + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + await expect( registry .connect(notTheOwner) - .publishCommittee(request.e3Id, [AddressOne, AddressTwo], data), + .publishCommittee( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ), ).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount"); }); it("stores the public key of the committee", async function () { - const { registry, request } = await loadFixture(setup); + const { registry, request, operator1, operator2 } = + await loadFixture(setup); await registry.requestCommittee(request.e3Id, 0, request.threshold); + + await networkHelpers.mine(1); + + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + await registry.publishCommittee( request.e3Id, - [AddressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, ); expect(await registry.committeePublicKey(request.e3Id)).to.equal( @@ -252,17 +337,28 @@ describe("CiphernodeRegistryOwnable", function () { ); }); it("emits a CommitteePublished event", async function () { - const { registry, request } = await loadFixture(setup); + const { registry, request, operator1, operator2 } = + await loadFixture(setup); await registry.requestCommittee(request.e3Id, 0, request.threshold); + + // Submit tickets from both operators and finalize + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + await expect( await registry.publishCommittee( request.e3Id, - [AddressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, ), ) .to.emit(registry, "CommitteePublished") - .withArgs(request.e3Id, [AddressOne, AddressTwo], data); + .withArgs( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ); }); }); @@ -309,37 +405,41 @@ describe("CiphernodeRegistryOwnable", function () { ).to.be.revertedWithCustomError(registry, "NotOwnerOrBondingRegistry"); }); it("removes the ciphernode from the registry", async function () { - const { registry } = await loadFixture(setup); - const tree = new LeanIMT(hash); - tree.insert(BigInt(AddressOne)); - tree.insert(BigInt(AddressTwo)); - const index = tree.indexOf(BigInt(AddressOne)); - const proof = tree.generateProof(index); - tree.update(index, BigInt(0)); - expect(await registry.isEnabled(AddressOne)).to.be.true; - expect(await registry.removeCiphernode(AddressOne, proof.siblings)); - expect(await registry.isEnabled(AddressOne)).to.be.false; - expect(await registry.root()).to.equal(tree.root); + const { registry, operator1, tree } = await loadFixture(setup); + const operator1Address = await operator1.getAddress(); + const localTree = new LeanIMT(hash); + for (let i = 0; i < tree.size; i++) { + localTree.insert(tree.leaves[i]); + } + const index = localTree.indexOf(BigInt(operator1Address)); + const proof = localTree.generateProof(index); + localTree.update(index, BigInt(0)); + expect(await registry.isEnabled(operator1Address)).to.be.true; + expect(await registry.removeCiphernode(operator1Address, proof.siblings)); + expect(await registry.isEnabled(operator1Address)).to.be.false; + expect(await registry.root()).to.equal(localTree.root); }); it("decrements numCiphernodes", async function () { - const { registry, tree } = await loadFixture(setup); + const { registry, operator1, tree } = await loadFixture(setup); + const operator1Address = await operator1.getAddress(); const numCiphernodes = await registry.numCiphernodes(); - const index = tree.indexOf(BigInt(AddressOne)); + const index = tree.indexOf(BigInt(operator1Address)); const proof = tree.generateProof(index); - expect(await registry.removeCiphernode(AddressOne, proof.siblings)); + expect(await registry.removeCiphernode(operator1Address, proof.siblings)); expect(await registry.numCiphernodes()).to.equal( numCiphernodes - BigInt(1), ); }); it("emits a CiphernodeRemoved event", async function () { - const { registry, tree } = await loadFixture(setup); + const { registry, operator1, tree } = await loadFixture(setup); + const operator1Address = await operator1.getAddress(); const numCiphernodes = await registry.numCiphernodes(); const size = await registry.treeSize(); - const index = tree.indexOf(BigInt(AddressOne)); + const index = tree.indexOf(BigInt(operator1Address)); const proof = tree.generateProof(index); - await expect(registry.removeCiphernode(AddressOne, proof.siblings)) + await expect(registry.removeCiphernode(operator1Address, proof.siblings)) .to.emit(registry, "CiphernodeRemoved") - .withArgs(AddressOne, index, numCiphernodes - BigInt(1), size); + .withArgs(operator1Address, index, numCiphernodes - BigInt(1), size); }); }); @@ -365,11 +465,17 @@ describe("CiphernodeRegistryOwnable", function () { describe("committeePublicKey()", function () { it("returns the public key of the committee for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); + const { registry, request, operator1, operator2 } = + await loadFixture(setup); await registry.requestCommittee(request.e3Id, 0, request.threshold); + + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + await registry.publishCommittee( request.e3Id, - [AddressOne, AddressTwo], + [await operator1.getAddress(), await operator2.getAddress()], data, ); expect(await registry.committeePublicKey(request.e3Id)).to.equal( @@ -387,8 +493,8 @@ describe("CiphernodeRegistryOwnable", function () { describe("isCiphernodeEligible()", function () { it("returns true if the ciphernode is in the registry", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.isEnabled(AddressOne)).to.be.true; + const { registry, operator1 } = await loadFixture(setup); + expect(await registry.isEnabled(await operator1.getAddress())).to.be.true; }); it("returns false if the ciphernode is not in the registry", async function () { const { registry } = await loadFixture(setup); @@ -398,8 +504,8 @@ describe("CiphernodeRegistryOwnable", function () { describe("isEnabled()", function () { it("returns true if the ciphernode is currently enabled", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.isEnabled(AddressOne)).to.be.true; + const { registry, operator1 } = await loadFixture(setup); + expect(await registry.isEnabled(await operator1.getAddress())).to.be.true; }); it("returns false if the ciphernode is not currently enabled", async function () { const { registry } = await loadFixture(setup); From 0cec52a308d73208a62d133d0a446079d53889b1 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 16:13:02 +0500 Subject: [PATCH 66/88] chore: linter fixes --- packages/enclave-contracts/test/Enclave.spec.ts | 14 ++++++++++---- .../Registry/CiphernodeRegistryOwnable.spec.ts | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index de23eca218..fca6e51886 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -22,6 +22,7 @@ import MockInputValidatorModule from "../ignition/modules/mockInputValidator"; import MockStableTokenModule from "../ignition/modules/mockStableToken"; import SlashingManagerModule from "../ignition/modules/slashingManager"; import { + CiphernodeRegistryOwnable, CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, Enclave__factory as EnclaveFactory, MockUSDC__factory as MockUSDCFactory, @@ -62,12 +63,12 @@ describe("Enclave", function () { const hash = (a: bigint, b: bigint) => poseidon2([a, b]); const setupAndPublishCommittee = async ( - registry: any, + registry: CiphernodeRegistryOwnable, e3Id: number, nodes: string[], publicKey: string, - operator1: any, - operator2: any, + operator1: Signer, + operator2: Signer, ): Promise => { await registry.connect(operator1).submitTicket(e3Id, 1); await registry.connect(operator2).submitTicket(e3Id, 1); @@ -92,11 +93,16 @@ describe("Enclave", function () { }; async function setupOperatorForSortition( - operator: any, + operator: Signer, + // eslint-disable-next-line @typescript-eslint/no-explicit-any bondingRegistry: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any licenseToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any usdcToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any ticketToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any registry: any, ): Promise { const operatorAddress = await operator.getAddress(); diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 25de8d45df..7203769320 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { LeanIMT } from "@zk-kit/lean-imt"; import { expect } from "chai"; +import type { Signer } from "ethers"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; @@ -14,7 +15,10 @@ import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken" import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import SlashingManagerModule from "../../ignition/modules/slashingManager"; -import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory } from "../../types"; +import { + CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, + CiphernodeRegistryOwnable, +} from "../../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; @@ -32,7 +36,7 @@ const hash = (a: bigint, b: bigint) => poseidon2([a, b]); describe("CiphernodeRegistryOwnable", function () { async function finalizeCommitteeAfterWindow( - registry: any, + registry: CiphernodeRegistryOwnable, e3Id: number, ): Promise { await networkHelpers.time.increase(SORTITION_SUBMISSION_WINDOW + 1); @@ -40,11 +44,16 @@ describe("CiphernodeRegistryOwnable", function () { } async function setupOperatorForSortition( - operator: any, + operator: Signer, + // eslint-disable-next-line @typescript-eslint/no-explicit-any bondingRegistry: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any licenseToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any usdcToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any ticketToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any registry: any, ): Promise { const operatorAddress = await operator.getAddress(); @@ -78,7 +87,6 @@ describe("CiphernodeRegistryOwnable", function () { await ethers.getSigners(); const ownerAddress = await owner.getAddress(); - // Deploy token contracts const usdcContract = await ignition.deploy(MockStableTokenModule, { parameters: { MockUSDC: { From 3a143deef1997ce897a73b250fcd78a005d434d9 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 16:21:44 +0500 Subject: [PATCH 67/88] fix: increase publish cound --- .../interfaces/IEnclave.sol/IEnclave.json | 4 +- .../enclave-contracts/test/Enclave.spec.ts | 45 ++++++++++++------- .../CiphernodeRegistryOwnable.spec.ts | 8 ++-- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 4099b6b7fc..b7d1d76a68 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -977,5 +977,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-a2f64967aeae699bd499cc90bbcf76e2314a0651" -} + "buildInfoId": "solc-0_8_28-c5db5579375d04ced938f4f4a02b4414441687bc" +} \ No newline at end of file diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 6d7d43f3a6..35613cfee1 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -22,7 +22,6 @@ import MockInputValidatorModule from "../ignition/modules/mockInputValidator"; import MockStableTokenModule from "../ignition/modules/mockStableToken"; import SlashingManagerModule from "../ignition/modules/slashingManager"; import { - CiphernodeRegistryOwnable, CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, Enclave__factory as EnclaveFactory, MockUSDC__factory as MockUSDCFactory, @@ -63,7 +62,8 @@ describe("Enclave", function () { const hash = (a: bigint, b: bigint) => poseidon2([a, b]); const setupAndPublishCommittee = async ( - registry: CiphernodeRegistryOwnable, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registry: any, e3Id: number, nodes: string[], publicKey: string, @@ -1537,24 +1537,35 @@ describe("Enclave", function () { .withArgs(e3Id, inputData, expectedHash, 0); }); it("increases the input count", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const inputData = "0x12345678"; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); - await enclave.activate(0, ethers.ZeroHash); + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + await enclave.activate(0, data); await enclave.publishInput(0, inputData); expect(await enclave.getInputsLength(0)).to.equal(1n); diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 7203769320..c5f09606a7 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -15,10 +15,7 @@ import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken" import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import SlashingManagerModule from "../../ignition/modules/slashingManager"; -import { - CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, - CiphernodeRegistryOwnable, -} from "../../types"; +import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory } from "../../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; @@ -36,7 +33,8 @@ const hash = (a: bigint, b: bigint) => poseidon2([a, b]); describe("CiphernodeRegistryOwnable", function () { async function finalizeCommitteeAfterWindow( - registry: CiphernodeRegistryOwnable, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registry: any, e3Id: number, ): Promise { await networkHelpers.time.increase(SORTITION_SUBMISSION_WINDOW + 1); From 79efc7574e2aefa0d09f9522a9e381207be49c70 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 17:16:31 +0500 Subject: [PATCH 68/88] fix: increase E3 window time --- examples/CRISP/server/.env.example | 4 ++-- examples/CRISP/server/src/cli/commands.rs | 17 +++++++++++++++-- .../registry/CiphernodeRegistryOwnable.sol | 11 +++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 75e0932a93..7585abd5ec 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -19,10 +19,10 @@ E3_PROGRAM_ADDRESS="0xc5a5C42992dECbae36851359345FE25997F5C42d" # CRISPProgram C FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config -E3_WINDOW_SIZE=40 +E3_WINDOW_SIZE=60 E3_THRESHOLD_MIN=1 E3_THRESHOLD_MAX=2 -E3_DURATION=160 +E3_DURATION=180 # E3 Compute Provider Config E3_COMPUTE_PROVIDER_NAME="RISC0" diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index 5991ec1d77..fb24f59ce4 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use super::approve; use super::CLI_DB; use alloy::primitives::{Address, Bytes, U256}; +use alloy::providers::{Provider, ProviderBuilder}; use crisp::config::CONFIG; use e3_sdk::bfv_helpers::{build_bfv_params_arc, encode_bfv_params, params::SET_2048_1032193_1}; use e3_sdk::evm_helpers::contracts::{EnclaveContract, EnclaveRead, EnclaveWrite}; @@ -57,6 +58,17 @@ struct CTRequest { ct_bytes: Vec, } +pub async fn get_current_timestamp() -> Result> { + let provider = ProviderBuilder::new().connect(&CONFIG.http_rpc_url).await?; + let block = provider + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .ok_or_else(|| anyhow::anyhow!("Latest block not found"))?; + + Ok(block.header.timestamp) +} + pub async fn initialize_crisp_round( token_address: &str, balance_threshold: &str, @@ -103,9 +115,10 @@ pub async fn initialize_crisp_round( let custom_params_bytes = Bytes::from(serde_json::to_vec(&custom_params)?); let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; + let current_timestamp = get_current_timestamp().await?; let start_window: [U256; 2] = [ - U256::from(Utc::now().timestamp()), - U256::from(Utc::now().timestamp() + CONFIG.e3_window_size as i64), + U256::from(current_timestamp), + U256::from(current_timestamp + CONFIG.e3_window_size as u64), ]; let duration: U256 = U256::from(CONFIG.e3_duration); let e3_params = Bytes::from(encode_bfv_params(&generate_bfv_parameters())); diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index a366c1ad7f..aef653816e 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -343,6 +343,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { block.timestamp >= c.submissionDeadline, SubmissionWindowNotClosed() ); + // TODO: Handle what happens if the threshold is not met. require(c.topNodes.length >= c.threshold[0], ThresholdNotMet()); c.finalized = true; @@ -492,20 +493,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { Committee storage c = committees[e3Id]; - // Get ticket balance at the time E3 was requested (snapshot) uint256 ticketBalance = IBondingRegistry(bondingRegistry) .getTicketBalanceAtBlock(node, c.requestBlock); uint256 ticketPrice = IBondingRegistry(bondingRegistry).ticketPrice(); require(ticketPrice > 0, InvalidTicketNumber()); - - // Calculate available tickets at snapshot uint256 availableTickets = ticketBalance / ticketPrice; - // Check node is eligible (has tickets at snapshot) require(availableTickets > 0, NodeNotEligible()); - - // Check ticket number is valid require(ticketNumber <= availableTickets, InvalidTicketNumber()); } @@ -530,7 +525,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // If list is full, only add if score is better than worst uint256 worstScore = c.scoreOf[topNodes[topNodes.length - 1]]; if (score < worstScore) { - topNodes.pop(); // Remove worst + topNodes.pop(); _insertSorted(c, node, score); } } @@ -557,7 +552,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } // Insert at position - topNodes.push(address(0)); // Extend array + topNodes.push(address(0)); for (uint256 i = topNodes.length - 1; i > insertPos; i--) { topNodes[i] = topNodes[i - 1]; } From b8208b0eb67d4cdec08422d7164a70eb31a76e92 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 17:36:58 +0500 Subject: [PATCH 69/88] fix: aggreagtor wallet setup --- examples/CRISP/scripts/dev_cipher.sh | 2 +- examples/CRISP/server/.env.example | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/CRISP/scripts/dev_cipher.sh b/examples/CRISP/scripts/dev_cipher.sh index 0f950a514d..933a325e2c 100755 --- a/examples/CRISP/scripts/dev_cipher.sh +++ b/examples/CRISP/scripts/dev_cipher.sh @@ -11,7 +11,7 @@ PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786 PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" -enclave wallet set --name ag --private-key "$PRIVATE_KEY" +enclave wallet set --name ag --private-key "$PRIVATE_KEY_AG" enclave wallet set --name cn1 --private-key "$PRIVATE_KEY_CN1" enclave wallet set --name cn2 --private-key "$PRIVATE_KEY_CN2" enclave wallet set --name cn3 --private-key "$PRIVATE_KEY_CN3" diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 7585abd5ec..06638501ef 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -19,7 +19,7 @@ E3_PROGRAM_ADDRESS="0xc5a5C42992dECbae36851359345FE25997F5C42d" # CRISPProgram C FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config -E3_WINDOW_SIZE=60 +E3_WINDOW_SIZE=80 E3_THRESHOLD_MIN=1 E3_THRESHOLD_MAX=2 E3_DURATION=180 From 5ebdd1bd15d8b3681d6cebcd9ebf81cf33e06859 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 20:20:16 +0500 Subject: [PATCH 70/88] fix: legacy integration tests --- crates/tests/tests/integration_legacy.rs | 105 +++++++++++++++--- .../crisp-contracts/deployed_contracts.json | 2 +- examples/CRISP/playwright.config.ts | 2 +- 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/crates/tests/tests/integration_legacy.rs b/crates/tests/tests/integration_legacy.rs index 5c97a96456..79b522d9e0 100644 --- a/crates/tests/tests/integration_legacy.rs +++ b/crates/tests/tests/integration_legacy.rs @@ -5,6 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::prelude::*; +use actix::Actor; +use alloy::primitives::{FixedBytes, I256, U256}; use anyhow::*; use e3_ciphernode_builder::CiphernodeBuilder; use e3_ciphernode_builder::CiphernodeHandle; @@ -13,9 +15,10 @@ use e3_data::GetDump; use e3_data::InMemStore; use e3_events::GetEvents; use e3_events::{ - CiphernodeSelected, CiphertextOutputPublished, E3Requested, E3id, EnclaveEvent, EventBus, - EventBusConfig, HistoryCollector, OrderedSet, PlaintextAggregated, PublicKeyAggregated, Seed, - Shutdown, Subscribe, TakeEvents, + CiphernodeSelected, CiphertextOutputPublished, CommitteeFinalized, ConfigurationUpdated, + E3Requested, E3id, EnclaveEvent, EventBus, EventBusConfig, HistoryCollector, + OperatorActivationChanged, OrderedSet, PlaintextAggregated, PublicKeyAggregated, Seed, + Shutdown, Subscribe, TakeEvents, TicketBalanceUpdated, }; use e3_net::events::GossipData; use e3_net::{events::NetEvent, NetEventTranslator}; @@ -55,7 +58,7 @@ async fn setup_local_ciphernode( .testmode_with_errors() .with_pubkey_aggregation() .with_plaintext_aggregation() - .with_sortition_distance(); // Using deprecated distance sortition for legacy tests + .with_sortition_score(); if let Some(data) = data { builder = builder.with_datastore((&data).into()); @@ -126,6 +129,43 @@ async fn add_ciphernodes( Ok(evts) } +async fn setup_score_sortition_environment( + bus: &Addr>, + eth_addrs: &Vec, + chain_id: u64, +) -> Result<()> { + bus.send(EnclaveEvent::from(ConfigurationUpdated { + parameter: "ticketPrice".to_string(), + old_value: U256::ZERO, + new_value: U256::from(10_000_000u64), + chain_id, + })) + .await?; + + let mut adder = AddToCommittee::new(bus, chain_id); + for addr in eth_addrs { + adder.add(addr).await?; + + bus.send(EnclaveEvent::from(TicketBalanceUpdated { + operator: addr.clone(), + delta: I256::try_from(1_000_000_000u64).unwrap(), + new_balance: U256::from(1_000_000_000u64), + reason: FixedBytes::ZERO, + chain_id, + })) + .await?; + + bus.send(EnclaveEvent::from(OperatorActivationChanged { + operator: addr.clone(), + active: true, + chain_id, + })) + .await?; + } + + Ok(()) +} + // Type for our tests to test against type PkSkShareTuple = (PublicKeyShare, SecretKey, String); @@ -138,7 +178,6 @@ fn aggregate_public_key(shares: &Vec) -> Result { } #[actix::test] -#[ignore] async fn test_public_key_aggregation_and_decryption() -> Result<()> { // Setup let (bus, rng, seed, params, crpoly, _, _) = get_common_setup(None)?; @@ -154,7 +193,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { println!("Adding ciphernodes..."); - add_ciphernodes(&bus, ð_addrs, 1).await?; + setup_score_sortition_environment(&bus, ð_addrs, 1).await?; let e3_request_event = EnclaveEvent::from(E3Requested { e3_id: e3_id.clone(), @@ -172,6 +211,14 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { // Test that we cannot send the same event twice bus.send(e3_request_event.clone()).await?; + // Finalize committee with all available nodes + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: e3_id.clone(), + committee: eth_addrs.clone(), + chain_id: 1, + })) + .await?; + // Generate the test shares and pubkey let rng_test = create_shared_rng_from_u64(42); let test_shares = generate_pk_shares(¶ms, &crpoly, &rng_test, ð_addrs)?; @@ -185,7 +232,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { let history_collector = ciphernodes.get(2).unwrap().history().unwrap(); let history = history_collector - .send(TakeEvents::::new(15)) + .send(TakeEvents::::new(18)) .await?; let aggregated_event: Vec<_> = history @@ -196,7 +243,11 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { }) .collect(); - assert_eq!(aggregated_event, vec![expected_aggregated_event]); + assert!( + !aggregated_event.is_empty(), + "No PublicKeyAggregated event found" + ); + assert_eq!(aggregated_event.last().unwrap(), &expected_aggregated_event); println!("Aggregating decryption..."); // Aggregate decryption @@ -236,16 +287,15 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { } #[actix::test] -#[ignore] async fn test_stopped_keyshares_retain_state() -> Result<()> { let e3_id = E3id::new("1234", 1); let (rng, cn1_address, cn1_data, cn2_address, cn2_data, cipher, history, params, crpoly) = { - let (bus, rng, seed, params, crpoly, ..) = get_common_setup(None)?; + let (bus, rng, seed, params, crpoly, _, _) = get_common_setup(None)?; let cipher = Arc::new(Cipher::from_password("Don't tell anyone my secret").await?); let ciphernodes = create_local_ciphernodes(&bus, &rng, 2, &cipher).await?; let eth_addrs = ciphernodes.iter().map(|n| n.address()).collect::>(); - add_ciphernodes(&bus, ð_addrs, 1).await?; + setup_score_sortition_environment(&bus, ð_addrs, 1).await?; let [cn1, cn2] = &ciphernodes.as_slice() else { panic!("Not enough elements") @@ -265,10 +315,17 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { ) .await?; + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: e3_id.clone(), + committee: eth_addrs.clone(), + chain_id: 1, + })) + .await?; + let history_collector = cn1.history().unwrap(); let error_collector = cn1.errors().unwrap(); let history = history_collector - .send(TakeEvents::::new(10)) + .send(TakeEvents::::new(14)) .await?; let errors = error_collector.send(GetEvents::new()).await?; @@ -352,7 +409,7 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { .await?; let history = history_collector - .send(TakeEvents::::new(4)) + .send(TakeEvents::::new(5)) .await?; let actual = history.iter().find_map(|evt| match evt { @@ -447,7 +504,6 @@ async fn test_p2p_actor_forwards_events_to_network() -> Result<()> { } #[actix::test] -#[ignore] async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { // Setup let (bus, rng, seed, params, crpoly, _, _) = get_common_setup(None)?; @@ -457,8 +513,8 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { let ciphernodes = create_local_ciphernodes(&bus, &rng, 3, &cipher).await?; let eth_addrs = ciphernodes.iter().map(|tup| tup.address()).collect(); - add_ciphernodes(&bus, ð_addrs, 1).await?; - add_ciphernodes(&bus, ð_addrs, 2).await?; + setup_score_sortition_environment(&bus, ð_addrs, 1).await?; + setup_score_sortition_environment(&bus, ð_addrs, 2).await?; // Send the computation requested event bus.send(EnclaveEvent::from(E3Requested { @@ -471,6 +527,12 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { })) .await?; + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: E3id::new("1234", 1), + committee: eth_addrs.clone(), + chain_id: 1, + })) + .await?; // Generate the test shares and pubkey let rng_test = create_shared_rng_from_u64(42); let test_pubkey = aggregate_public_key(&generate_pk_shares( @@ -479,7 +541,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { let history_collector = ciphernodes.last().unwrap().history().unwrap(); let history = history_collector - .send(TakeEvents::::new(15)) + .send(TakeEvents::::new(28)) .await?; assert_eq!( @@ -502,12 +564,19 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { })) .await?; + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: E3id::new("1234", 2), + committee: eth_addrs.clone(), + chain_id: 2, + })) + .await?; + let test_pubkey = aggregate_public_key(&generate_pk_shares( ¶ms, &crpoly, &rng_test, ð_addrs, )?)?; let history = history_collector - .send(TakeEvents::::new(9)) + .send(TakeEvents::::new(8)) .await?; assert_eq!( diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 22c64fe348..dd1bfffc8c 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -203,7 +203,7 @@ "blockNumber": 23, "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, - "RiscZeroGroth16Verifier": { + "MockRISC0Verifier": { "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidator": { diff --git a/examples/CRISP/playwright.config.ts b/examples/CRISP/playwright.config.ts index 991541d20b..ae724033bc 100644 --- a/examples/CRISP/playwright.config.ts +++ b/examples/CRISP/playwright.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ timeout: 5 * 60 * 10000, use: { baseURL: "http://localhost:3000", - actionTimeout: 60 * 1000, + actionTimeout: 75 * 1000, }, retries: process.env.CI ? 2 : 0, fullyParallel: true, From 065ea2ba9996011e8ed076590b320bd6c05388f4 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 20:46:16 +0500 Subject: [PATCH 71/88] fix: debug the correct timestamp --- examples/CRISP/server/src/cli/commands.rs | 2 +- .../contracts/registry/CiphernodeRegistryOwnable.sol | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index fb24f59ce4..6ec274aea5 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -158,7 +158,7 @@ pub async fn initialize_crisp_round( info!("Debug - start_window: {:?}", start_window); info!("Debug - duration: {}", duration); info!("Debug - e3_program: {}", e3_program); - info!("Debug - current timestamp: {}", Utc::now().timestamp()); + info!("Debug - current timestamp: {:?}", current_timestamp); info!( "Debug - Checking ciphernode registry at: {}", diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index aef653816e..3102538bfe 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -310,6 +310,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { SubmissionDeadlineReached() ); require(!c.submitted[msg.sender], NodeAlreadySubmitted()); + require(isCiphernodeEligible(msg.sender), NodeNotEligible()); // Validate node eligibility and ticket number _validateNodeEligibility(msg.sender, ticketNumber, e3Id); @@ -408,7 +409,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @inheritdoc ICiphernodeRegistry - function isCiphernodeEligible(address node) external view returns (bool) { + function isCiphernodeEligible(address node) public view returns (bool) { if (!isEnabled(node)) return false; require(bondingRegistry != address(0), BondingRegistryNotSet()); From 088df6abae597846f10724b26fb7f6f94e398dc9 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 28 Oct 2025 21:04:26 +0500 Subject: [PATCH 72/88] chore: documentation for score sortition flow --- crates/sortition/Readme.md | 387 +++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 crates/sortition/Readme.md diff --git a/crates/sortition/Readme.md b/crates/sortition/Readme.md new file mode 100644 index 0000000000..edda930634 --- /dev/null +++ b/crates/sortition/Readme.md @@ -0,0 +1,387 @@ +# Sortition and E3 Complete Flow + +This document describes the complete flow of the Enclave system, from operator registration through E3 computation request, sortition, committee selection, keyshare generation, public key aggregation, encryption, and decryption. + +## Overview + +The Enclave system uses a score-based sortition mechanism to select a committee of ciphernodes to perform threshold homomorphic encryption operations. The flow involves: + +1. **Operator Setup** - Bonding license tokens and ticket balance +2. **Registration** - Registering as a ciphernode operator +3. **E3 Request** - A computation request triggers sortition +4. **Score Sortition** - Nodes are selected based on ticket balances +5. **Committee Finalization** - Selected nodes form a committee +6. **Keyshare Generation** - Committee nodes generate threshold keyshares +7. **Public Key Aggregation** - Keyshares are aggregated into a public key +8. **Encryption & Decryption** - Data is encrypted and threshold-decrypted + +## Complete System Flow + +```mermaid +sequenceDiagram + participant Operator + participant BondingRegistry + participant CiphernodeRegistry + participant EventBus + participant NodeStateManager + participant Sortition + participant CiphernodeSelector + participant Keyshare + participant PublicKeyAggregator + participant PlaintextAggregator + + Note over Operator,BondingRegistry: Phase 1: Operator Setup & Registration + + Operator->>BondingRegistry: bondLicense(amount) + BondingRegistry->>BondingRegistry: Transfer ENCL tokens + BondingRegistry->>EventBus: LicenseBondUpdated + + Operator->>BondingRegistry: registerOperator() + BondingRegistry->>BondingRegistry: Check isLicensed() + BondingRegistry->>CiphernodeRegistry: addCiphernode(operator) + CiphernodeRegistry->>EventBus: CiphernodeAdded(operator, index, numNodes, chainId) + EventBus->>NodeStateManager: CiphernodeAdded + NodeStateManager->>NodeStateManager: Register operator in nodes HashMap + + Operator->>BondingRegistry: addTicketBalance(amount) + BondingRegistry->>BondingRegistry: Mint ticket tokens + BondingRegistry->>EventBus: TicketBalanceUpdated(operator, delta, newBalance, chainId) + EventBus->>NodeStateManager: TicketBalanceUpdated + NodeStateManager->>NodeStateManager: Update operator ticket balance + + BondingRegistry->>BondingRegistry: Check if balance >= minTicketBalance + BondingRegistry->>EventBus: OperatorActivationChanged(operator, active=true, chainId) + EventBus->>NodeStateManager: OperatorActivationChanged + NodeStateManager->>NodeStateManager: Set operator active status + + Note over Operator,PlaintextAggregator: Phase 2: E3 Request & Sortition + + Operator->>EventBus: E3Requested(e3Id, thresholdM, thresholdN, seed, params, chainId) + EventBus->>Sortition: E3Requested + Sortition->>NodeStateManager: GetNodeState(chainId) + NodeStateManager-->>Sortition: NodeStateStore { nodes, ticketPrice } + Sortition->>Sortition: Build sortition list from active nodes + Sortition->>Sortition: Run score sortition algorithm + Sortition->>Sortition: Generate tickets for selected nodes + + loop For each selected node + Sortition->>EventBus: TicketGenerated(e3Id, node, ticketId, chainId) + end + + Note over CiphernodeRegistry,EventBus: Phase 3: On-Chain Ticket Submission + + EventBus->>CiphernodeRegistry: TicketGenerated (if ticketId != 0) + CiphernodeRegistry->>CiphernodeRegistry: Submit ticket to contract + CiphernodeRegistry->>CiphernodeRegistry: Wait for threshold tickets + CiphernodeRegistry->>CiphernodeRegistry: Call finalizeCommittee() + CiphernodeRegistry->>EventBus: CommitteeFinalized(e3Id, committee[], chainId) + + Note over EventBus,Sortition: Phase 4: Committee Storage + + EventBus->>Sortition: CommitteeFinalized + Sortition->>Sortition: Store committee in finalized_committees HashMap + Sortition->>Sortition: Persist to disk + + Note over EventBus,CiphernodeSelector: Phase 5: Node Selection + + EventBus->>CiphernodeSelector: CommitteeFinalized + CiphernodeSelector->>CiphernodeSelector: Check if node address in committee + alt Node is in committee + CiphernodeSelector->>EventBus: CiphernodeSelected(e3Id, node, chainId) + end + + Note over EventBus,Keyshare: Phase 6: Keyshare Generation + + EventBus->>Keyshare: CiphernodeSelected + Keyshare->>Keyshare: fhe.generate_keyshare() + Keyshare->>Keyshare: Generate secret key (random) + Keyshare->>Keyshare: Generate public key share (sk + CRP) + Keyshare->>Keyshare: Persist secret key + Keyshare->>EventBus: KeyshareCreated(e3Id, node, pubkey, chainId) + + Note over EventBus,PublicKeyAggregator: Phase 7: Public Key Aggregation + + EventBus->>PublicKeyAggregator: KeyshareCreated + PublicKeyAggregator->>Sortition: GetNodesForE3(e3Id, chainId) + Sortition-->>PublicKeyAggregator: committee[] + PublicKeyAggregator->>PublicKeyAggregator: Verify node in committee + PublicKeyAggregator->>PublicKeyAggregator: Add keyshare to OrderedSet + + alt Threshold reached (all keyshares collected) + PublicKeyAggregator->>PublicKeyAggregator: fhe.get_aggregate_public_key(keyshares) + PublicKeyAggregator->>PublicKeyAggregator: Aggregate public key shares + PublicKeyAggregator->>EventBus: PublicKeyAggregated(e3Id, pubkey, nodes, chainId) + PublicKeyAggregator->>CiphernodeRegistry: publishPublicKey(e3Id, pubkey, nodes) + end + + Note over Operator,PlaintextAggregator: Phase 8: Encryption & Computation + + Operator->>Operator: Encrypt input data with aggregated pubkey + Operator->>EventBus: CiphertextOutputPublished(e3Id, ciphertext, chainId) + + Note over EventBus,PlaintextAggregator: Phase 9: Threshold Decryption + + EventBus->>Keyshare: CiphertextOutputPublished + Keyshare->>Keyshare: Load secret key from storage + Keyshare->>Keyshare: fhe.decrypt_ciphertext(secret, ciphertext) + Keyshare->>Keyshare: Generate decryption share + Keyshare->>EventBus: DecryptionshareCreated(e3Id, node, decryptionShare, chainId) + + EventBus->>PlaintextAggregator: DecryptionshareCreated + PlaintextAggregator->>Sortition: GetNodesForE3(e3Id, chainId) + Sortition-->>PlaintextAggregator: committee[] + PlaintextAggregator->>PlaintextAggregator: Verify node in committee + PlaintextAggregator->>PlaintextAggregator: Add decryption share to OrderedSet + + alt Threshold reached (all shares collected) + PlaintextAggregator->>PlaintextAggregator: fhe.get_aggregate_plaintext(shares, ciphertext) + PlaintextAggregator->>PlaintextAggregator: Aggregate decryption shares + PlaintextAggregator->>PlaintextAggregator: Decode plaintext + PlaintextAggregator->>EventBus: PlaintextAggregated(e3Id, plaintext, nodes, chainId) + end +``` + +## State Diagram: Node Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Unbonded + Unbonded --> Licensed: bondLicense(amount >= requiredBond) + Licensed --> Registered: registerOperator() + Registered --> Active: addTicketBalance(balance >= minBalance) + Active --> Inactive: removeTicketBalance() OR unbondLicense() + Inactive --> Active: addTicketBalance() OR bondLicense() + Active --> ExitPending: deregisterOperator() + Inactive --> ExitPending: deregisterOperator() + Registered --> ExitPending: deregisterOperator() + ExitPending --> [*]: claimExits() after exitDelay + ExitPending --> Registered: registerOperator() (cancels exit) +``` + +## Sortition Data Flow + +```mermaid +flowchart TD + A[E3Requested Event] --> B{Chain ID Match?} + B -->|No| Z[Ignore] + B -->|Yes| C[NodeStateManager: Get Active Nodes] + C --> D[Filter: ticket_balance > 0 AND active=true] + D --> E[Score Sortition: Build Weighted List] + E --> F[Calculate Total Ticket Weight] + F --> G{threshold_n nodes available?} + G -->|No| H[Error: Insufficient Nodes] + G -->|Yes| I[Select Top N Nodes by Weight] + I --> J[Generate Ticket IDs] + J --> K[Emit TicketGenerated Events] + K --> L[EVM: Submit Tickets On-Chain] + L --> M{Threshold Tickets Submitted?} + M -->|No| L + M -->|Yes| N[Contract: finalizeCommittee] + N --> O[Emit CommitteeFinalized Event] + O --> P[Sortition: Store Committee] + P --> Q[CiphernodeSelector: Check Membership] + Q --> R[Emit CiphernodeSelected] + R --> S[Keyshare Generation Starts] +``` + +## Committee Finalization Flow + +```mermaid +flowchart LR + A[TicketGenerated] --> B{ticket_id == 0?} + B -->|Yes| C[Distance Sortition - Skip Contract] + B -->|No| D[Score Sortition - Submit to Contract] + D --> E[Contract: Collect Tickets] + E --> F{Threshold Met?} + F -->|No| E + F -->|Yes| G[Contract: finalizeCommittee] + G --> H[Freeze Committee List] + H --> I[Emit CommitteeFinalized Event] + C --> J[Manual CommitteeFinalized Emission] + I --> K[All Nodes: Store Committee] + J --> K + K --> L[CiphernodeSelector: Process] +``` + +## Key Concepts + +### 1. Score Sortition + +- **Purpose**: Select committee based on ticket balance (stake-weighted) +- **Algorithm**: + - Build list of eligible nodes (active + ticket_balance > 0) + - Calculate weight for each node based on ticket balance + - Select top `threshold_n` nodes by weight + - Generate unique ticket IDs for selected nodes +- **On-Chain Integration**: Tickets submitted to contract for verification +- **Committee Finalization**: Contract finalizes committee when threshold tickets received + +### 2. Distance Sortition (Deprecated) + +- **Purpose**: Select committee based on cryptographic distance +- **Status**: Deprecated - does not work with on-chain contracts +- **Indicator**: Uses `ticket_id = 0` to prevent contract submission + +### 3. NodeStateManager + +- **Purpose**: Track state of all registered ciphernodes +- **State Per Node**: + - `ticket_balance`: Current ticket balance + - `active`: Whether node is active (has min ticket balance) + - `num_jobs`: Number of active E3 jobs +- **Persistence**: State survives node restarts +- **Events**: + - `CiphernodeAdded` / `CiphernodeRemoved` + - `TicketBalanceUpdated` + - `OperatorActivationChanged` + - `ConfigurationUpdated` (for ticketPrice) + +### 4. Sortition Actor + +- **Purpose**: Manage sortition algorithm and committee state +- **Persistent State**: + - `list`: Current sortition list (backend-specific) + - `finalized_committees`: HashMap of E3id → committee members +- **Messages**: + - `GetNodesForE3`: Query committee members for an E3 + - `GetCommittee`: Query full sortition list + - `GetNodeState`: Get current node state +- **Event Handlers**: + - `E3Requested`: Trigger sortition + - `CommitteeFinalized`: Store committee + - `TicketBalanceUpdated`, `OperatorActivationChanged`, etc. + +### 5. Committee Query Pattern + +- **Old Approach**: Store committee in EVM contract, query from there +- **New Approach**: Query `Sortition` actor via `GetNodesForE3` + - Benefits: Single source of truth, no EVM storage cost + - Used by: `PublicKeyAggregator`, `PlaintextAggregator` + - Validation: Ensures nodes are in finalized committee + +### 6. Event Deduplication + +- **Purpose**: Prevent processing same event multiple times +- **Mechanism**: EventBus with deduplication enabled +- **Hash-based**: Events with same content have same EventId +- **Important**: Allows safe event replay on restart + +### 7. Historical Event Synchronization + +- **Purpose**: Nodes can restart and catch up +- **Mechanism**: Fetch historical events from contracts on startup +- **Events**: + - `CiphernodeAdded` / `CiphernodeRemoved` + - `TicketBalanceUpdated` + - `OperatorActivationChanged` + - `ConfigurationUpdated` + - `CommitteeFinalized` +- **Deduplication**: EventBus ignores already-seen events + +### 8. Threshold Cryptography + +- **Scheme**: BFV Threshold Homomorphic Encryption +- **Parameters**: + - `threshold_m`: Minimum shares needed for decryption + - `threshold_n`: Total committee size +- **Common Random Polynomial (CRP)**: Shared randomness from E3 seed +- **Keyshare Generation**: + - Secret key: Random polynomial + - Public key share: Secret key + CRP +- **Aggregation**: Combining shares into single public key / plaintext + +### 9. Party IDs + +- **Purpose**: Identify position in threshold scheme +- **Assignment**: Based on order in `CommitteeFinalized.committee` array +- **Range**: `0..threshold_n` +- **Critical**: Order must be consistent across all nodes +- **Used In**: Keyshare creation, decryption share verification + +### 10. OrderedSet + +- **Purpose**: Maintain insertion order for aggregation +- **Usage**: + - Keyshares collected by `PublicKeyAggregator` + - Decryption shares collected by `PlaintextAggregator` +- **Critical**: Order affects aggregation result +- **Implementation**: Preserves order in which shares arrive + +## Event Reference + +### Bonding Registry Events + +| Event | Parameters | Purpose | +| --------------------------- | -------------------------------------------- | ---------------------------- | +| `LicenseBondUpdated` | operator, delta, newBalance, reason | Track license token bonding | +| `TicketBalanceUpdated` | operator, delta, newBalance, reason, chainId | Track ticket balance changes | +| `OperatorActivationChanged` | operator, active, chainId | Node activation status | +| `ConfigurationUpdated` | parameter, oldValue, newValue | System parameter changes | + +### Ciphernode Registry Events + +| Event | Parameters | Purpose | +| ------------------- | --------------------------------- | ----------------- | +| `CiphernodeAdded` | address, index, numNodes, chainId | Node registration | +| `CiphernodeRemoved` | address, index, numNodes, chainId | Node removal | + +### Enclave Events + +| Event | Parameters | Purpose | +| --------------------------- | --------------------------------------------------- | --------------------- | +| `E3Requested` | e3Id, thresholdM, thresholdN, seed, params, chainId | Computation request | +| `TicketGenerated` | e3Id, node, ticketId, chainId | Sortition ticket | +| `CommitteeFinalized` | e3Id, committee[], chainId | Committee selected | +| `CiphernodeSelected` | e3Id, node, chainId | Node is in committee | +| `KeyshareCreated` | e3Id, node, pubkey, chainId | Keyshare generated | +| `PublicKeyAggregated` | e3Id, pubkey, nodes, chainId | Public key ready | +| `CiphertextOutputPublished` | e3Id, ciphertext, chainId | Encrypted computation | +| `DecryptionshareCreated` | e3Id, node, partyId, decryptionShare, chainId | Decryption share | +| `PlaintextAggregated` | e3Id, plaintext, nodes, chainId | Decryption complete | + +## Testing Flow + +The integration tests follow this pattern: + +1. **Setup**: Create ciphernodes with shared event bus +2. **Register**: Use `setup_score_sortition_environment` to: + - Set ticket price via `ConfigurationUpdated` + - Add nodes via `CiphernodeAdded` + - Give nodes tickets via `TicketBalanceUpdated` + - Activate nodes via `OperatorActivationChanged` +3. **Request**: Send `E3Requested` event +4. **Finalize**: Send `CommitteeFinalized` event (manual in tests) +5. **Aggregate**: Wait for `PublicKeyAggregated` event +6. **Verify**: Check aggregated pubkey matches expected value + +## Persistence + +### What Gets Persisted? + +- **NodeStateManager**: `nodes` HashMap (ticket balances, activation status) +- **Sortition**: `list` (backend-specific), `finalized_committees` HashMap +- **Keyshare**: Secret keys per E3 + +### Where? + +- Default: In-memory (for tests) +- Production: RocksDB or other repository implementation +- Path: Configured via `RepositoriesFactory` + +### Restart Behavior + +1. Actor starts +2. Loads persisted state from repository +3. Subscribes to events +4. Processes new events +5. Event deduplication prevents re-processing old events + +## Chain ID Handling + +- **Purpose**: Support multiple chains simultaneously +- **Isolation**: Each chain has independent: + - Node state + - Committees + - E3 processes +- **Validation**: All operations validate chain ID matches +- **Critical**: Prevents cross-chain confusion From 424d42dda7299dc68838a95f91c78bcf2aa96909 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 29 Oct 2025 17:24:09 +0500 Subject: [PATCH 73/88] feat: remove node state manager actor and create a single sortition actor --- Cargo.lock | 1 - crates/aggregator/Cargo.toml | 1 - crates/aggregator/src/committee_finalizer.rs | 22 +- .../src/ciphernode_builder.rs | 21 +- .../src/enclave_event/ticket_generated.rs | 1 - crates/evm/src/ciphernode_registry_sol.rs | 4 - crates/evm/src/helpers.rs | 14 +- crates/keyshare/src/threshold_keyshare.rs | 1 - crates/sortition/Readme.md | 12 +- crates/sortition/src/backends.rs | 297 +++++ crates/sortition/src/ciphernode_selector.rs | 39 +- crates/sortition/src/distance.rs | 45 - crates/sortition/src/lib.rs | 5 +- crates/sortition/src/node_state.rs | 383 ------- crates/sortition/src/repo.rs | 7 +- crates/sortition/src/sortition.rs | 1017 ++++++----------- .../crisp-contracts/deployed_contracts.json | 2 +- 17 files changed, 678 insertions(+), 1194 deletions(-) create mode 100644 crates/sortition/src/backends.rs delete mode 100644 crates/sortition/src/distance.rs delete mode 100644 crates/sortition/src/node_state.rs diff --git a/Cargo.lock b/Cargo.lock index 5bb6ba25cb..1a54cf76e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2646,7 +2646,6 @@ name = "e3-aggregator" version = "0.1.5" dependencies = [ "actix", - "alloy", "anyhow", "async-trait", "bincode", diff --git a/crates/aggregator/Cargo.toml b/crates/aggregator/Cargo.toml index 3412fd266d..f41f43110a 100644 --- a/crates/aggregator/Cargo.toml +++ b/crates/aggregator/Cargo.toml @@ -8,7 +8,6 @@ repository = "https://github.com/gnosisguild/enclave/crates/aggregator" [dependencies] actix = { workspace = true } -alloy = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } bincode = { workspace = true } diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 4a3522df1e..fb6c78c583 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::prelude::*; -use alloy::providers::Provider; use e3_events::{CommitteeRequested, EnclaveEvent, EventBus, Shutdown, Subscribe}; use e3_evm::FinalizeCommittee; use std::collections::HashMap; @@ -14,24 +13,21 @@ use tracing::{error, info}; /// CommitteeFinalizer is an actor that listens to CommitteeRequested events and calls /// finalizeCommittee on the registry after the submission deadline has passed. -pub struct CommitteeFinalizer { +pub struct CommitteeFinalizer { #[allow(dead_code)] bus: Addr>, registry_writer: Recipient, - provider: P, pending_committees: HashMap, } -impl CommitteeFinalizer

{ +impl CommitteeFinalizer { pub fn new( bus: &Addr>, registry_writer: Recipient, - provider: P, ) -> Self { Self { bus: bus.clone(), registry_writer, - provider, pending_committees: HashMap::new(), } } @@ -39,9 +35,8 @@ impl CommitteeFinalizer

{ pub fn attach( bus: &Addr>, registry_writer: Recipient, - provider: P, ) -> Addr { - let addr = CommitteeFinalizer::new(bus, registry_writer, provider).start(); + let addr = CommitteeFinalizer::new(bus, registry_writer).start(); bus.do_send(Subscribe::new( "CommitteeRequested", @@ -53,11 +48,11 @@ impl CommitteeFinalizer

{ } } -impl Actor for CommitteeFinalizer

{ +impl Actor for CommitteeFinalizer { type Context = Context; } -impl Handler for CommitteeFinalizer

{ +impl Handler for CommitteeFinalizer { type Result = (); fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { @@ -68,19 +63,18 @@ impl Handler for CommitteeF } } -impl Handler for CommitteeFinalizer

{ +impl Handler for CommitteeFinalizer { type Result = (); fn handle(&mut self, msg: CommitteeRequested, ctx: &mut Self::Context) -> Self::Result { let e3_id = msg.e3_id.clone(); let submission_deadline = msg.submission_deadline; - let provider = self.provider.clone(); const FINALIZATION_BUFFER_SECONDS: u64 = 1; let e3_id_for_log = e3_id.clone(); let fut = async move { - match e3_evm::helpers::get_current_timestamp(&provider).await { + match e3_evm::helpers::get_current_timestamp().await { Ok(timestamp) => Some(timestamp), Err(e) => { error!( @@ -149,7 +143,7 @@ impl Handler for Comm } } -impl Handler for CommitteeFinalizer

{ +impl Handler for CommitteeFinalizer { type Result = (); fn handle(&mut self, _msg: Shutdown, ctx: &mut Self::Context) -> Self::Result { info!("Killing CommitteeFinalizer"); diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index a46ce11d56..a475b24fd1 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -204,19 +204,6 @@ impl CiphernodeBuilder { self } - /// Use distance-based sortition (DEPRECATED) - /// - /// # Deprecation Notice - /// Distance sortition is deprecated and does not work with on-chain contracts. - /// Use `with_sortition_score()` instead. - #[deprecated( - note = "Distance sortition is deprecated and does not work with on-chain contracts. Use with_sortition_score() instead." - )] - pub fn with_sortition_distance(mut self) -> Self { - self.sortition_backend = SortitionBackend::distance(); - self - } - /// Use score-based sortition (recommended) pub fn with_sortition_score(mut self) -> Self { self.sortition_backend = SortitionBackend::score(); @@ -299,17 +286,14 @@ impl CiphernodeBuilder { let repositories = store.repositories(); - let node_state_manager = - e3_sortition::NodeStateManager::attach(&local_bus, &repositories.node_state()).await?; - // Use the configured backend directly let default_backend = self.sortition_backend.clone(); - let sortition = Sortition::attach_with_backend( + let sortition = Sortition::attach( &local_bus, repositories.sortition(), + repositories.node_state(), repositories.finalized_committees(), - node_state_manager, default_backend, ) .await?; @@ -400,7 +384,6 @@ impl CiphernodeBuilder { e3_aggregator::CommitteeFinalizer::attach( &local_bus, writer.recipient(), - read_provider.provider().clone(), ); } } diff --git a/crates/events/src/enclave_event/ticket_generated.rs b/crates/events/src/enclave_event/ticket_generated.rs index c71297a932..1530240705 100644 --- a/crates/events/src/enclave_event/ticket_generated.rs +++ b/crates/events/src/enclave_event/ticket_generated.rs @@ -11,7 +11,6 @@ use std::fmt::{self, Display}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TicketId { - Distance, Score(u64), } diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index 110e3640d4..b052fd6f64 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -327,10 +327,6 @@ impl Handler fn handle(&mut self, msg: TicketGenerated, _: &mut Self::Context) -> Self::Result { match msg.ticket_id { - TicketId::Distance => { - info!("Distance sortition ticket generated for E3 {:?}, no contract submission needed", msg.e3_id); - return Box::pin(async move {}); - } TicketId::Score(ticket_id) => { info!( "Score sortition ticket generated for E3 {:?}, submitting to contract", diff --git a/crates/evm/src/helpers.rs b/crates/evm/src/helpers.rs index 437a2493de..d4f63eee5d 100644 --- a/crates/evm/src/helpers.rs +++ b/crates/evm/src/helpers.rs @@ -197,8 +197,20 @@ pub async fn load_signer_from_repository( private_key.parse().map_err(Into::into) } -pub async fn get_current_timestamp(provider: &P) -> Result { +pub async fn get_current_timestamp() -> Result { + let config = e3_config::load_config("_default", None, None)?; + let chain = config + .chains() + .first() + .ok_or_else(|| anyhow::anyhow!("No chains configured"))?; + + let rpc_url = chain.rpc_url()?; + let provider = ProviderConfig::new(rpc_url, chain.rpc_auth.clone()) + .create_readonly_provider() + .await?; + let block = provider + .provider() .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) .await .context("Failed to get latest block")? diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 83be7a3404..cab82d9f48 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -775,7 +775,6 @@ impl Handler for ThresholdKeyshare { self.pending_selections .insert(msg.e3_id.clone(), msg.clone()); - // Start keygen immediately (for distance sortition or if no CommitteeFinalized comes) // If CommitteeFinalized arrives later, it will verify committee membership match self.handle_ciphernode_selected(msg, ctx.address()) { Err(e) => error!("{e}"), diff --git a/crates/sortition/Readme.md b/crates/sortition/Readme.md index edda930634..83c46f4b90 100644 --- a/crates/sortition/Readme.md +++ b/crates/sortition/Readme.md @@ -188,18 +188,14 @@ flowchart TD ```mermaid flowchart LR - A[TicketGenerated] --> B{ticket_id == 0?} - B -->|Yes| C[Distance Sortition - Skip Contract] - B -->|No| D[Score Sortition - Submit to Contract] + A[TicketGenerated] --> D[Score Sortition - Submit to Contract] D --> E[Contract: Collect Tickets] E --> F{Threshold Met?} F -->|No| E F -->|Yes| G[Contract: finalizeCommittee] G --> H[Freeze Committee List] H --> I[Emit CommitteeFinalized Event] - C --> J[Manual CommitteeFinalized Emission] I --> K[All Nodes: Store Committee] - J --> K K --> L[CiphernodeSelector: Process] ``` @@ -216,12 +212,6 @@ flowchart LR - **On-Chain Integration**: Tickets submitted to contract for verification - **Committee Finalization**: Contract finalizes committee when threshold tickets received -### 2. Distance Sortition (Deprecated) - -- **Purpose**: Select committee based on cryptographic distance -- **Status**: Deprecated - does not work with on-chain contracts -- **Indicator**: Uses `ticket_id = 0` to prevent contract submission - ### 3. NodeStateManager - **Purpose**: Track state of all registered ciphernodes diff --git a/crates/sortition/src/backends.rs b/crates/sortition/src/backends.rs new file mode 100644 index 0000000000..6a51e0fff2 --- /dev/null +++ b/crates/sortition/src/backends.rs @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::sortition::NodeStateStore; +use crate::ticket::{RegisteredNode, Ticket}; +use crate::ticket_sortition::ScoreSortition; +use alloy::primitives::Address; +use anyhow::Result; +use e3_events::Seed; +use serde::{Deserialize, Serialize}; +use tracing::info; + +/// Minimal interface that all sortition backends must implement. +/// +/// Backends can store their own shapes (e.g., a `HashSet` of addresses +/// for Score) +pub trait SortitionList { + /// Return `true` if `address` appears in the size-`size` committee under `seed`. + /// + /// Implementations should return `Ok(false)` if the backend has no nodes + /// or if `size == 0`. + fn contains( + &self, + seed: Seed, + size: usize, + address: T, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result; + + /// Return an index if `address` appears in the committee under `seed`. + /// + /// Implementations should return `Ok(None)` if the backend has no nodes + /// or if `size == 0`. + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> Result)>>; + + /// Add a node to the backend. Backends should be idempotent on duplicates. + fn add(&mut self, address: T); + + /// Remove a node from the backend. Removing a non-existent node is a no-op. + fn remove(&mut self, address: T); + + /// Return all registered node addresses as hex strings. + fn nodes(&self) -> Vec; +} + +/// Score-sortition backend. +/// +/// Stores richer `RegisteredNode` entries (address + per-node ticket set). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ScoreBackend { + /// Nodes with their ticket sets (used by score-based committee selection). + registered: Vec, +} + +impl Default for ScoreBackend { + fn default() -> Self { + Self { + registered: Vec::new(), + } + } +} + +impl ScoreBackend { + /// Build a vector of ephemeral nodes from the node state. + /// + /// The nodes are built from the node state and the registered nodes. + fn build_nodes_from_state( + &self, + chain_id: u64, + node_state: &NodeStateStore, + ) -> Vec { + info!( + chain_id = chain_id, + registered_count = self.registered.len(), + node_state_count = node_state.nodes.len(), + "Building nodes from state for score sortition" + ); + + self.registered + .iter() + .filter_map(|n| { + let addr_str = n.address.to_string(); + let Some(ns) = node_state.nodes.get(&addr_str) else { + info!( + address = %addr_str, + chain_id = chain_id, + "Node not found in NodeStateStore" + ); + return None; + }; + if !ns.active { + info!( + address = %addr_str, + "Node is not active" + ); + return None; + } + + let count = node_state.available_tickets(&addr_str) as u64; + let total_tickets = (ns.ticket_balance / node_state.ticket_price) + .try_into() + .unwrap_or(0u64); + + if count == 0 { + info!( + address = %addr_str, + ticket_balance = ?ns.ticket_balance, + ticket_price = ?node_state.ticket_price, + total_tickets = total_tickets, + active_jobs = ns.active_jobs, + "Node has no available tickets" + ); + return None; + } + + let tickets = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); + Some(RegisteredNode { + address: n.address, + tickets, + }) + }) + .collect() + } +} + +impl SortitionList for ScoreBackend { + /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. + /// + /// Returns `Ok(false)` if there are no nodes or `size == 0`. + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result { + if size == 0 { + return Ok(false); + } + + let nodes = self.build_nodes_from_state(chain_id, node_state); + if nodes.is_empty() { + return Ok(false); + } + + let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; + let want: Address = address.parse()?; + Ok(winners.iter().any(|w| w.address == want)) + } + + /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. + /// + /// Returns `Ok(false)` if there are no nodes or `size == 0`. + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result)>> { + if size == 0 { + return Ok(None); + } + + let nodes: Vec = self.build_nodes_from_state(chain_id, node_state); + + if nodes.is_empty() { + return Ok(None); + } + + let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; + let want: alloy::primitives::Address = address.parse()?; + + let maybe = winners + .iter() + .enumerate() + .find_map(|(i, w)| (w.address == want).then(|| (i as u64, Some(w.ticket_id)))); + Ok(maybe) + } + + /// Add a node, creating an empty ticket set when first seen. + fn add(&mut self, address: String) { + match address.parse::

() { + Ok(addr) => { + if !self.registered.iter().any(|n| n.address == addr) { + self.registered.push(RegisteredNode { + address: addr, + tickets: Vec::new(), + }); + } + } + Err(e) => { + tracing::warn!("Failed to parse address '{}': {}", address, e); + } + } + } + + /// Remove the node (if present). + /// + /// Note: `used_ticket_ids` is a legacy field and clearing it here has + /// no effect on current per-node ticket ID semantics. + fn remove(&mut self, address: String) { + if let Ok(addr) = address.parse::
() { + if let Some(i) = self.registered.iter().position(|n| n.address == addr) { + self.registered.swap_remove(i); + } + } + } + + /// Return all registered node addresses as hex strings. + fn nodes(&self) -> Vec { + self.registered + .iter() + .map(|n| n.address.to_string()) + .collect() + } +} + +/// Enum wrapper around the supported backends. +/// +/// New chains default to `Score` sortition. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum SortitionBackend { + /// Score-based selection (stores `RegisteredNode`s with tickets). + Score(ScoreBackend), +} + +impl Default for SortitionBackend { + fn default() -> Self { + SortitionBackend::Score(ScoreBackend::default()) + } +} + +impl SortitionBackend { + pub fn score() -> Self { + SortitionBackend::Score(ScoreBackend::default()) + } +} + +impl SortitionList for SortitionBackend { + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result { + match self { + SortitionBackend::Score(b) => b.contains(seed, size, address, chain_id, node_state), + } + } + + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result)>> { + match self { + SortitionBackend::Score(b) => b.get_index(seed, size, address, chain_id, node_state), + } + } + + fn add(&mut self, address: String) { + match self { + SortitionBackend::Score(backend) => backend.add(address), + } + } + + fn remove(&mut self, address: String) { + match self { + SortitionBackend::Score(backend) => backend.remove(address), + } + } + + fn nodes(&self) -> Vec { + match self { + SortitionBackend::Score(backend) => backend.nodes(), + } + } +} diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index 7bdaf376c5..f0986c510d 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -4,9 +4,9 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{GetCommittee, GetNodeIndex, Sortition}; +use crate::sortition::{GetNodeIndex, Sortition}; /// CiphernodeSelector is an actor that determines if a ciphernode is part of a committee and if so -/// forwards a CiphernodeSelected event (distance sortition) or TicketGenerated event (score sortition) to the event bus +/// emits a TicketGenerated event (score sortition) to the event bus use actix::prelude::*; use e3_data::{DataStore, RepositoriesFactory}; use e3_events::{ @@ -116,41 +116,6 @@ impl Handler for CiphernodeSelector { ticket_id: TicketId::Score(tid), node: address.clone(), })); - } else { - info!(node = address, "Ciphernode selected via distance sortition"); - // This is a quick workaround to make distance sortition work until we remove it? - bus.do_send(EnclaveEvent::from(TicketGenerated { - e3_id: data.e3_id.clone(), - ticket_id: TicketId::Distance, - node: address.clone(), - })); - - let committee_fut = sortition.send(GetCommittee { - seed, - size, - chain_id, - }); - - match committee_fut.await { - Ok(Ok(committee)) => { - info!( - node = address, - committee_size = committee.len(), - "Emitting CommitteeFinalized for distance sortition" - ); - bus.do_send(EnclaveEvent::from(CommitteeFinalized { - e3_id: data.e3_id.clone(), - committee, - chain_id: data.e3_id.chain_id(), - })); - } - Ok(Err(e)) => { - info!("Failed to get committee: {}", e); - } - Err(e) => { - info!("Failed to send GetCommittee: {}", e); - } - } } } else { info!("This node is not selected"); diff --git a/crates/sortition/src/distance.rs b/crates/sortition/src/distance.rs deleted file mode 100644 index 8c7250c67e..0000000000 --- a/crates/sortition/src/distance.rs +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use alloy::primitives::{keccak256, Address}; -use anyhow::Result; -use num::{BigInt, Num}; - -pub struct DistanceSortition { - pub random_seed: u64, - pub registered_nodes: Vec
, - pub size: usize, -} - -impl DistanceSortition { - pub fn new(random_seed: u64, registered_nodes: Vec
, size: usize) -> Self { - Self { - random_seed, - registered_nodes, - size, - } - } - - pub fn get_committee(&mut self) -> Result> { - let mut scores = self - .registered_nodes - .iter() - .map(|address| { - let concat = address.to_string() + &self.random_seed.to_string(); - let hash = keccak256(concat).to_string(); - let without_prefix = hash.trim_start_matches("0x"); - let z = BigInt::from_str_radix(without_prefix, 16)?; - let score = z - BigInt::from(self.random_seed); - Ok((score, *address)) - }) - .collect::>>()?; - - scores.sort_by(|a, b| a.0.cmp(&b.0)); - let size = std::cmp::min(self.size, scores.len()); - let result = scores[0..size].to_vec(); - Ok(result) - } -} diff --git a/crates/sortition/src/lib.rs b/crates/sortition/src/lib.rs index f1645dcfd3..9d172efff2 100644 --- a/crates/sortition/src/lib.rs +++ b/crates/sortition/src/lib.rs @@ -4,16 +4,15 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod backends; mod ciphernode_selector; -mod distance; -mod node_state; mod repo; mod sortition; mod ticket; mod ticket_sortition; +pub use backends::*; pub use ciphernode_selector::*; -pub use node_state::*; pub use repo::*; pub use sortition::*; pub use ticket_sortition::*; diff --git a/crates/sortition/src/node_state.rs b/crates/sortition/src/node_state.rs deleted file mode 100644 index da3501ec3f..0000000000 --- a/crates/sortition/src/node_state.rs +++ /dev/null @@ -1,383 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use actix::prelude::*; -use alloy::primitives::U256; -use anyhow::Result; -use e3_data::{AutoPersist, Persistable, Repository}; -use e3_events::{ - BusError, CiphernodeAdded, CiphernodeRemoved, CommitteePublished, ConfigurationUpdated, - EnclaveErrorType, EnclaveEvent, EventBus, OperatorActivationChanged, PlaintextOutputPublished, - Subscribe, TicketBalanceUpdated, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tracing::info; - -/// State for a single ciphernode -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NodeState { - /// Current ticket balance for this node - pub ticket_balance: U256, - /// Number of active E3 jobs this node is currently participating in - pub active_jobs: u64, - /// Whether this node is active (has met minimum requirements) - pub active: bool, -} - -impl Default for NodeState { - fn default() -> Self { - Self { - ticket_balance: U256::ZERO, - active_jobs: 0, - active: false, - } - } -} - -/// State for all nodes across all chains -#[derive(Clone, Debug, Serialize, Deserialize, Default)] -pub struct NodeStateStore { - /// Map of (chain_id, node_address) to node state - pub nodes: HashMap<(u64, String), NodeState>, - /// Current ticket price per chain - pub ticket_prices: HashMap, - /// Map of E3 ID to the committee nodes for that E3 - /// This is used to track which nodes are in which E3 jobs - pub e3_committees: HashMap>, -} - -impl NodeStateStore { - /// Get available tickets for a node, accounting for active jobs - pub fn available_tickets(&self, chain_id: u64, address: &str) -> u64 { - let ticket_price = self - .ticket_prices - .get(&chain_id) - .copied() - .unwrap_or(U256::from(1)); - if ticket_price.is_zero() { - return 0; - } - - let key = (chain_id, address.to_string()); - let node = self.nodes.get(&key); - - if let Some(node) = node { - let total_tickets = (node.ticket_balance / ticket_price) - .try_into() - .unwrap_or(0u64); - // Subtract active jobs from available tickets - total_tickets.saturating_sub(node.active_jobs) - } else { - 0 - } - } - - /// Get all nodes for a chain with their available tickets - /// Only includes active nodes - pub fn get_nodes_with_tickets(&self, chain_id: u64) -> Vec<(String, u64)> { - self.nodes - .iter() - .filter(|((cid, _), node_state)| *cid == chain_id && node_state.active) - .map(|((_, addr), _)| (addr.clone(), self.available_tickets(chain_id, addr))) - .filter(|(_, tickets)| *tickets > 0) - .collect() - } -} - -#[derive(Message, Clone, Debug)] -#[rtype(result = "Option")] -pub struct GetNodeState; - -pub struct NodeStateManager { - state: Persistable, - bus: Addr>, -} - -impl NodeStateManager { - pub fn new(state: Persistable, bus: Addr>) -> Self { - Self { state, bus } - } - - pub async fn attach( - bus: &Addr>, - repository: &Repository, - ) -> Result> { - let state = repository - .clone() - .load_or_default(NodeStateStore::default()) - .await?; - - let addr = NodeStateManager::new(state, bus.clone()).start(); - - bus.send(Subscribe::new("CiphernodeAdded", addr.clone().into())) - .await?; - bus.send(Subscribe::new("CiphernodeRemoved", addr.clone().into())) - .await?; - bus.send(Subscribe::new("TicketBalanceUpdated", addr.clone().into())) - .await?; - bus.send(Subscribe::new( - "OperatorActivationChanged", - addr.clone().into(), - )) - .await?; - bus.send(Subscribe::new("ConfigurationUpdated", addr.clone().into())) - .await?; - bus.send(Subscribe::new("CommitteePublished", addr.clone().into())) - .await?; - bus.send(Subscribe::new( - "PlaintextOutputPublished", - addr.clone().into(), - )) - .await?; - - info!("NodeStateManager actor started"); - Ok(addr) - } -} - -impl Actor for NodeStateManager { - type Context = Context; -} - -impl Handler for NodeStateManager { - type Result = Option; - - fn handle(&mut self, _msg: GetNodeState, _: &mut Self::Context) -> Self::Result { - self.state.get() - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { - match msg { - EnclaveEvent::CiphernodeAdded { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::CiphernodeRemoved { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::TicketBalanceUpdated { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::OperatorActivationChanged { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::ConfigurationUpdated { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::CommitteePublished { data, .. } => { - ctx.notify(data); - } - EnclaveEvent::PlaintextOutputPublished { data, .. } => { - ctx.notify(data); - } - _ => (), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: TicketBalanceUpdated, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - let key = (msg.chain_id, msg.operator.clone()); - let node = state.nodes.entry(key).or_insert_with(NodeState::default); - - // Update ticket balance - node.ticket_balance = msg.new_balance; - - info!( - operator = %msg.operator, - chain_id = msg.chain_id, - new_balance = ?msg.new_balance, - "Updated ticket balance" - ); - - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: OperatorActivationChanged, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - // We don't have chain_id in this event, so we need to update all entries for this operator - // In practice, an operator should only be registered on one chain, but we handle all just in case - for ((_, addr), node) in state.nodes.iter_mut() { - if addr == &msg.operator { - node.active = msg.active; - info!( - operator = %msg.operator, - active = msg.active, - "Updated operator active status" - ); - } - } - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: ConfigurationUpdated, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - if msg.parameter == "ticketPrice" { - state.ticket_prices.insert(msg.chain_id, msg.new_value); - info!( - chain_id = msg.chain_id, - old_ticket_price = ?msg.old_value, - new_ticket_price = ?msg.new_value, - "ConfigurationUpdated - ticket price updated" - ); - } - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: CommitteePublished, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - let chain_id = msg.e3_id.chain_id(); - let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); - - // Store the committee mapping for this E3 - state - .e3_committees - .insert(e3_id_str.clone(), msg.nodes.clone()); - - // Increment active jobs for each node in the committee - for node_addr in &msg.nodes { - let key = (chain_id, node_addr.clone()); - let node = state.nodes.entry(key).or_insert_with(NodeState::default); - node.active_jobs += 1; - - info!( - node = %node_addr, - chain_id = chain_id, - e3_id = ?msg.e3_id, - active_jobs = node.active_jobs, - "Incremented active jobs for node in committee" - ); - } - - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: PlaintextOutputPublished, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - let chain_id = msg.e3_id.chain_id(); - let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); - - // Get the committee nodes for this E3 - if let Some(committee_nodes) = state.e3_committees.remove(&e3_id_str) { - // Decrement active jobs for each node in the committee - for node_addr in &committee_nodes { - let key = (chain_id, node_addr.clone()); - if let Some(node) = state.nodes.get_mut(&key) { - node.active_jobs = node.active_jobs.saturating_sub(1); - - info!( - node = %node_addr, - chain_id = chain_id, - e3_id = ?msg.e3_id, - active_jobs = node.active_jobs, - "Decremented active jobs for node after E3 completion" - ); - } - } - - info!( - e3_id = ?msg.e3_id, - committee_size = committee_nodes.len(), - "PlaintextOutputPublished - job completed, decremented active jobs" - ); - } else { - info!( - e3_id = ?msg.e3_id, - "PlaintextOutputPublished - no committee found (might have been completed already)" - ); - } - - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: CiphernodeAdded, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - let key = (msg.chain_id, msg.address.clone()); - // Only create entry if it doesn't exist - preserve existing state - state.nodes.entry(key).or_insert_with(NodeState::default); - - info!( - operator = %msg.address, - chain_id = msg.chain_id, - "Node registered in NodeStateManager" - ); - - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} - -impl Handler for NodeStateManager { - type Result = (); - - fn handle(&mut self, msg: CiphernodeRemoved, _: &mut Self::Context) -> Self::Result { - match self.state.try_mutate(|mut state| { - let key = (msg.chain_id, msg.address.clone()); - state.nodes.remove(&key); - - info!( - operator = %msg.address, - chain_id = msg.chain_id, - "Node removed from NodeStateManager" - ); - - Ok(state) - }) { - Ok(_) => (), - Err(err) => self.bus.err(EnclaveErrorType::Sortition, err), - } - } -} diff --git a/crates/sortition/src/repo.rs b/crates/sortition/src/repo.rs index ab21448fc8..419c8db7d4 100644 --- a/crates/sortition/src/repo.rs +++ b/crates/sortition/src/repo.rs @@ -4,7 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{NodeStateStore, SortitionBackend}; +use crate::backends::SortitionBackend; +use crate::sortition::NodeStateStore; use e3_config::StoreKeys; use e3_data::{Repositories, Repository}; use e3_events::E3id; @@ -21,11 +22,11 @@ impl SortitionRepositoryFactory for Repositories { } pub trait NodeStateRepositoryFactory { - fn node_state(&self) -> Repository; + fn node_state(&self) -> Repository>; } impl NodeStateRepositoryFactory for Repositories { - fn node_state(&self) -> Repository { + fn node_state(&self) -> Repository> { Repository::new(self.store.scope(StoreKeys::node_state())) } } diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index 949da89346..df67d0650b 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -4,30 +4,95 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::distance::DistanceSortition; -use crate::node_state::{GetNodeState, NodeStateManager, NodeStateStore}; -use crate::ticket::{RegisteredNode, Ticket}; -use crate::ticket_sortition::ScoreSortition; +use crate::backends::{SortitionBackend, SortitionList}; use actix::prelude::*; -use alloy::primitives::Address; -use anyhow::{anyhow, Context, Result}; +use alloy::primitives::U256; +use anyhow::Result; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ - BusError, CiphernodeAdded, CiphernodeRemoved, CommitteeFinalized, EnclaveErrorType, - EnclaveEvent, EventBus, Seed, Subscribe, + BusError, CiphernodeAdded, CiphernodeRemoved, CommitteeFinalized, CommitteePublished, + ConfigurationUpdated, EnclaveErrorType, EnclaveEvent, EventBus, OperatorActivationChanged, + PlaintextOutputPublished, Seed, Subscribe, TicketBalanceUpdated, }; -use num::BigInt; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use tracing::{info, instrument, trace}; +use std::collections::HashMap; +use tracing::info; +use tracing::instrument; -/// Message: ask the `Sortition` actor whether `address` would be in the +/// State for a single ciphernode +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NodeState { + /// Current ticket balance for this node + pub ticket_balance: U256, + /// Number of active E3 jobs this node is currently participating in + pub active_jobs: u64, + /// Whether this node is active (has met minimum requirements) + pub active: bool, +} + +impl Default for NodeState { + fn default() -> Self { + Self { + ticket_balance: U256::ZERO, + active_jobs: 0, + active: false, + } + } +} + +/// Unified state for all nodes across all chains +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct NodeStateStore { + /// Map of node_address to node state + pub nodes: HashMap, + /// Current ticket price + pub ticket_price: U256, + /// Map of E3 ID to the committee nodes for that E3 + /// This is used to track which nodes are in which E3 jobs + pub e3_committees: HashMap>, +} + +impl NodeStateStore { + /// Get available tickets for a node, accounting for active jobs + /// The Process for calculating available tickets is: + /// 1. Get the node state for the node + /// 2. Check if the node is active + /// 3. Check if the node has a ticket price + /// 4. Check if the node has a ticket balance + /// 5. Calculate the available tickets + /// 6. Subtract the active jobs from the available tickets + /// 7. Return the available tickets + pub fn available_tickets(&self, address: &str) -> u64 { + if self.ticket_price.is_zero() { + return 0; + } + + let node = self.nodes.get(address); + + if let Some(node) = node { + let total_tickets = (node.ticket_balance / self.ticket_price) + .try_into() + .unwrap_or(0u64); + total_tickets.saturating_sub(node.active_jobs) + } else { + 0 + } + } + + /// Get all nodes with their available tickets + /// Only includes active nodes + pub fn get_nodes_with_tickets(&self) -> Vec<(String, u64)> { + self.nodes + .iter() + .filter(|(_, node_state)| node_state.active) + .map(|(addr, _)| (addr.clone(), self.available_tickets(addr))) + .filter(|(_, tickets)| *tickets > 0) + .collect() + } +} + +/// Message: ask the `Sortition` whether `address` would be in the /// committee of size `size` for randomness `seed` on `chain_id`. -/// -/// Membership semantics depend on the backend for that chain: -/// - **Distance backend**: computes a committee using address distance. -/// - **Score backend**: computes each node’s best ticket score and sorts globally. -/// Returns `true` if `address` appears in the resulting top-`size` selection. #[derive(Message, Clone, Debug, PartialEq, Eq)] #[rtype(result = "Option<(u64, Option)>")] pub struct GetNodeIndex { @@ -59,530 +124,19 @@ pub struct GetNodesForE3 { pub chain_id: u64, } -/// Message to get the full committee for a specific sortition. +/// Message to get the current node state. #[derive(Message, Clone, Debug)] -#[rtype(result = "anyhow::Result>")] -pub struct GetCommittee { - /// Round seed / randomness used by the sortition algorithm - pub seed: Seed, - /// Committee size (top-N) - pub size: usize, - /// Target chain - pub chain_id: u64, -} - -/// Minimal interface that all sortition backends must implement. -/// -/// Backends can store their own shapes (e.g., a `HashSet` of addresses -/// for Distance, or a `Vec` for Score), but they must be able to: -/// - Check committee membership (`contains`) -/// - Add and remove nodes -/// - List all registered node addresses -pub trait SortitionList { - /// Return `true` if `address` appears in the size-`size` committee under `seed`. - /// - /// Implementations should return `Ok(false)` if the backend has no nodes - /// or if `size == 0`. - fn contains( - &self, - seed: Seed, - size: usize, - address: T, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result; - - /// Return an index if `address` appears in the committee under `seed`. - /// - /// Implementations should return `Ok(None)` if the backend has no nodes - /// or if `size == 0`. - fn get_index( - &self, - seed: Seed, - size: usize, - address: String, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> Result)>>; - - /// Add a node to the backend. Backends should be idempotent on duplicates. - fn add(&mut self, address: T); - - /// Remove a node from the backend. Removing a non-existent node is a no-op. - fn remove(&mut self, address: T); - - /// Return all registered node addresses as hex strings. - fn nodes(&self) -> Vec; - - /// Return the full committee for a specific sortition. - /// - /// Implementations should return an error if the backend has no nodes - /// or if `size == 0`. For backends that don't support this operation, - /// they should return an appropriate error. - fn get_committee( - &self, - seed: Seed, - size: usize, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result>; -} - -/// Distance-sortition backend. -/// -/// Stores a set of hex-encoded addresses and delegates committee selection -/// to `DistanceSortition`. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DistanceBackend { - /// Registered node addresses (hex). - nodes: HashSet, -} - -impl Default for DistanceBackend { - fn default() -> Self { - Self { - nodes: HashSet::new(), - } - } -} - -impl SortitionList for DistanceBackend { - /// Build the address list, run `DistanceSortition(seed, nodes, size)`, - /// then check whether `address` is in the result. - /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn contains( - &self, - seed: Seed, - size: usize, - address: String, - _node_state: Option<&NodeStateStore>, - _chain_id: u64, - ) -> anyhow::Result { - if size == 0 { - return Err(anyhow!("Size cannot be 0")); - } - - if self.nodes.len() == 0 { - return Err(anyhow!("No nodes registered!")); - } - - let committee = get_committee(seed, size, self.nodes.clone())?; - - Ok(committee - .iter() - .any(|(_, addr)| addr.to_string() == address)) - } - - fn get_index( - &self, - seed: Seed, - size: usize, - address: String, - _node_state: Option<&NodeStateStore>, - _chain_id: u64, - ) -> Result)>> { - if size == 0 { - return Err(anyhow!("Size cannot be 0")); - } - - if self.nodes.len() == 0 { - return Err(anyhow!("No nodes registered!")); - } - - let committee = get_committee(seed, size, self.nodes.clone())?; - - let maybe = committee - .iter() - .enumerate() - .find_map(|(i, (_, addr))| (addr.to_string() == address).then(|| (i as u64, None))); - - Ok(maybe) - } - - /// Insert a node address (hex). Duplicate inserts are harmless. - fn add(&mut self, address: String) { - self.nodes.insert(address); - } - - /// Remove a node address (hex). Missing entries are ignored. - fn remove(&mut self, address: String) { - self.nodes.remove(&address); - } - - /// Return all node addresses as hex strings. - fn nodes(&self) -> Vec { - self.nodes.iter().cloned().collect() - } - - /// Return the full committee for distance sortition. - fn get_committee( - &self, - seed: Seed, - size: usize, - _node_state: Option<&NodeStateStore>, - _chain_id: u64, - ) -> anyhow::Result> { - if size == 0 { - return Err(anyhow!("Size cannot be 0")); - } - if self.nodes.len() == 0 { - return Err(anyhow!("No nodes registered!")); - } - - let committee = get_committee(seed, size, self.nodes.clone())?; - Ok(committee.iter().map(|(_, addr)| addr.to_string()).collect()) - } -} - -fn get_committee( - seed: Seed, - size: usize, - nodes: HashSet, -) -> Result> { - let registered_nodes: Vec
= nodes - .into_iter() - .map(|b| b.parse().context(format!("Error parsing address {}", b))) - .collect::>()?; - - DistanceSortition::new(seed.into(), registered_nodes, size) - .get_committee() - .context("Could not get committee!") -} - -/// Score-sortition backend. -/// -/// Stores richer `RegisteredNode` entries (address + per-node ticket set). -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ScoreBackend { - /// Nodes with their ticket sets (used by score-based committee selection). - registered: Vec, -} - -impl Default for ScoreBackend { - fn default() -> Self { - Self { - registered: Vec::new(), - } - } -} - -impl ScoreBackend { - /// Build a vector of ephemeral nodes from the node state. - /// - /// The nodes are built from the node state and the registered nodes. - fn build_nodes_from_state( - &self, - chain_id: u64, - node_state: &NodeStateStore, - ) -> Vec { - info!( - chain_id = chain_id, - registered_count = self.registered.len(), - node_state_count = node_state.nodes.len(), - "Building nodes from state for score sortition" - ); - - self.registered - .iter() - .filter_map(|n| { - let addr_str = n.address.to_string(); - let key = (chain_id, addr_str.clone()); - let Some(ns) = node_state.nodes.get(&key) else { - info!( - address = %addr_str, - chain_id = chain_id, - "Node not found in NodeStateStore" - ); - return None; - }; - if !ns.active { - info!( - address = %addr_str, - "Node is not active" - ); - return None; - } - - let count = node_state.available_tickets(chain_id, &addr_str) as u64; - let ticket_price = node_state - .ticket_prices - .get(&chain_id) - .copied() - .unwrap_or(alloy::primitives::U256::from(1)); - let total_tickets = (ns.ticket_balance / ticket_price) - .try_into() - .unwrap_or(0u64); - - if count == 0 { - info!( - address = %addr_str, - ticket_balance = ?ns.ticket_balance, - ticket_price = ?ticket_price, - total_tickets = total_tickets, - active_jobs = ns.active_jobs, - "Node has no available tickets" - ); - return None; - } - - let tickets = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); - Some(RegisteredNode { - address: n.address, - tickets, - }) - }) - .collect() - } -} - -impl SortitionList for ScoreBackend { - /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. - /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn contains( - &self, - seed: Seed, - size: usize, - address: String, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result { - if size == 0 { - return Ok(false); - } - let Some(state) = node_state else { - return Ok(false); - }; - - let nodes = self.build_nodes_from_state(chain_id, state); - if nodes.is_empty() { - return Ok(false); - } - - let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; - let want: Address = address.parse()?; - Ok(winners.iter().any(|w| w.address == want)) - } - - /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. - /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn get_index( - &self, - seed: Seed, - size: usize, - address: String, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result)>> { - if size == 0 { - return Ok(None); - } - - if node_state.is_none() { - return Ok(None); - } - - let nodes: Vec = self.build_nodes_from_state(chain_id, node_state.unwrap()); - - if nodes.is_empty() { - return Ok(None); - } - - let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; - let want: alloy::primitives::Address = address.parse()?; - - let maybe = winners - .iter() - .enumerate() - .find_map(|(i, w)| (w.address == want).then(|| (i as u64, Some(w.ticket_id)))); - Ok(maybe) - } - - /// Add a node, creating an empty ticket set when first seen. - fn add(&mut self, address: String) { - match address.parse::
() { - Ok(addr) => { - if !self.registered.iter().any(|n| n.address == addr) { - self.registered.push(RegisteredNode { - address: addr, - tickets: Vec::new(), - }); - } - } - Err(e) => { - tracing::warn!("Failed to parse address '{}': {}", address, e); - } - } - } - - /// Remove the node (if present). - /// - /// Note: `used_ticket_ids` is a legacy field and clearing it here has - /// no effect on current per-node ticket ID semantics. - fn remove(&mut self, address: String) { - if let Ok(addr) = address.parse::
() { - if let Some(i) = self.registered.iter().position(|n| n.address == addr) { - self.registered.swap_remove(i); - } - } - } - - /// Return all registered node addresses as hex strings. - fn nodes(&self) -> Vec { - self.registered - .iter() - .map(|n| n.address.to_string()) - .collect() - } - - /// Return the full committee for score sortition. - /// - /// Note: This is not supported for score sortition as the committee - /// is determined by the contract after ticket submission. - fn get_committee( - &self, - _seed: Seed, - _size: usize, - _node_state: Option<&NodeStateStore>, - _chain_id: u64, - ) -> anyhow::Result> { - Err(anyhow!( - "get_committee not supported for ScoreBackend - committee is determined by contract" - )) - } -} - -/// Enum wrapper around the supported backends. -/// -/// New chains default to `Score` sortition. If a chain is intended to -/// use distance selection, construct it as `SortitionBackend::Distance(DistanceBackend::default())` -/// explicitly. -/// -/// # Deprecation Notice -/// Distance sortition is deprecated and does not work with on-chain contracts. -/// Use Score sortition for all new implementations. -/// Distance sortition will be removed in a future release. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum SortitionBackend { - /// Distance-based selection (stores a simple set of addresses). - #[deprecated( - note = "Distance sortition is deprecated and does not work with on-chain contracts. Use Score sortition instead." - )] - Distance(DistanceBackend), - /// Score-based selection (stores `RegisteredNode`s with tickets). - Score(ScoreBackend), -} - -impl Default for SortitionBackend { - fn default() -> Self { - SortitionBackend::Distance(DistanceBackend::default()) - } -} - -impl SortitionBackend { - /// Use score-based sortition (recommended) - pub fn score() -> Self { - SortitionBackend::Score(ScoreBackend::default()) - } - - /// Use distance-based sortition (DEPRECATED) - /// - /// # Deprecation Notice - /// Distance sortition is deprecated and does not work with on-chain contracts. - /// Use `SortitionBackend::score()` instead. - #[deprecated( - note = "Distance sortition is deprecated and does not work with on-chain contracts. Use score() instead." - )] - pub fn distance() -> Self { - SortitionBackend::Distance(DistanceBackend::default()) - } -} - -impl SortitionList for SortitionBackend { - fn contains( - &self, - seed: Seed, - size: usize, - address: String, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result { - match self { - SortitionBackend::Distance(b) => b.contains(seed, size, address, None, chain_id), - SortitionBackend::Score(b) => b.contains(seed, size, address, node_state, chain_id), - } - } - - fn get_index( - &self, - seed: Seed, - size: usize, - address: String, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result)>> { - match self { - SortitionBackend::Distance(b) => b.get_index(seed, size, address, None, chain_id), - SortitionBackend::Score(b) => b.get_index(seed, size, address, node_state, chain_id), - } - } - - fn add(&mut self, address: String) { - match self { - SortitionBackend::Distance(backend) => backend.add(address), - SortitionBackend::Score(backend) => backend.add(address), - } - } - - fn remove(&mut self, address: String) { - match self { - SortitionBackend::Distance(backend) => backend.remove(address), - SortitionBackend::Score(backend) => backend.remove(address), - } - } +#[rtype(result = "Option>")] +pub struct GetNodeState; - fn nodes(&self) -> Vec { - match self { - SortitionBackend::Distance(backend) => backend.nodes(), - SortitionBackend::Score(backend) => backend.nodes(), - } - } - - fn get_committee( - &self, - seed: Seed, - size: usize, - node_state: Option<&NodeStateStore>, - chain_id: u64, - ) -> anyhow::Result> { - match self { - SortitionBackend::Distance(backend) => { - backend.get_committee(seed, size, node_state, chain_id) - } - SortitionBackend::Score(backend) => { - backend.get_committee(seed, size, node_state, chain_id) - } - } - } -} - -/// `Sortition` is an Actix actor that owns per-chain backends and exposes -/// message handlers to: -/// - add/remove nodes from a chain, -/// - list nodes for a chain, -/// - check committee membership for a chain. -/// -/// Backends are persisted using `Persistable>` -/// keyed by `chain_id`. +/// Sortition actor that manages the sortition algorithm and the node state. pub struct Sortition { /// Persistent map of `chain_id -> SortitionBackend`. - list: Persistable>, + backends: Persistable>, + /// Persistent map of `chain_id -> NodeStateStore`. + node_state: Persistable>, /// Event bus for error reporting and enclave event subscription. bus: Addr>, - /// Optional reference to NodeStateManager for score-based sortition - node_state_manager: Option>, /// Persistent map of finalized committees per E3 finalized_committees: Persistable>>, } @@ -593,94 +147,76 @@ pub struct SortitionParams { /// Event bus address. pub bus: Addr>, /// Persisted per-chain backend map. - pub list: Persistable>, - /// Optional NodeStateManager for score-based sortition - pub node_state_manager: Option>, + pub backends: Persistable>, + /// Node state store per chain + pub node_state: Persistable>, /// Persistent map of finalized committees per E3 pub finalized_committees: Persistable>>, } impl Sortition { - /// Construct a new `Sortition` actor with the given bus and repository. pub fn new(params: SortitionParams) -> Self { Self { - list: params.list, + backends: params.backends, + node_state: params.node_state, bus: params.bus, - node_state_manager: params.node_state_manager, finalized_committees: params.finalized_committees, } } - /// Load persisted state, start the actor, and subscribe to `CiphernodeAdded/Removed`. - /// - /// The store is initialized with an empty `HashMap` if nothing is present. #[instrument(name = "sortition_attach", skip_all)] pub async fn attach( bus: &Addr>, - store: Repository>, - committees_store: Repository>>, - ) -> Result> { - let list = store.load_or_default(HashMap::new()).await?; - let finalized_committees = committees_store.load_or_default(HashMap::new()).await?; - let addr = Sortition::new(SortitionParams { - bus: bus.clone(), - list, - node_state_manager: None, // Legacy attach without node state - finalized_committees, - }) - .start(); - bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); - bus.do_send(Subscribe::new("CiphernodeRemoved", addr.clone().into())); - bus.do_send(Subscribe::new("CommitteeFinalized", addr.clone().into())); - Ok(addr) - } - - /// Load persisted state with node state support and configurable default backend. - /// - /// This version allows score-based backends to query ticket availability and - /// configures the default backend type for new chains. - #[instrument(name = "sortition_attach_with_backend", skip_all)] - pub async fn attach_with_backend( - bus: &Addr>, - store: Repository>, + backends_store: Repository>, + node_state_store: Repository>, committees_store: Repository>>, - node_state_manager: Addr, default_backend: SortitionBackend, - ) -> Result> { - let mut list = store.load_or_default(HashMap::new()).await?; + ) -> Result> { + let mut backends = backends_store.load_or_default(HashMap::new()).await?; + let node_state = node_state_store.load_or_default(HashMap::new()).await?; let finalized_committees = committees_store.load_or_default(HashMap::new()).await?; - list.try_mutate(|mut list| { + backends.try_mutate(|mut list| { list.insert(u64::MAX, default_backend); Ok(list) })?; let addr = Sortition::new(SortitionParams { bus: bus.clone(), - list, - node_state_manager: Some(node_state_manager), + backends, + node_state, finalized_committees, }) .start(); + + // Subscribe to all relevant events bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); bus.do_send(Subscribe::new("CiphernodeRemoved", addr.clone().into())); + bus.do_send(Subscribe::new("TicketBalanceUpdated", addr.clone().into())); + bus.do_send(Subscribe::new( + "OperatorActivationChanged", + addr.clone().into(), + )); + bus.do_send(Subscribe::new("ConfigurationUpdated", addr.clone().into())); + bus.do_send(Subscribe::new("CommitteePublished", addr.clone().into())); + bus.do_send(Subscribe::new( + "PlaintextOutputPublished", + addr.clone().into(), + )); bus.do_send(Subscribe::new("CommitteeFinalized", addr.clone().into())); + + info!("Sortition actor started"); Ok(addr) } - /// Return the current node addresses (hex) for `chain_id`. - /// - /// # Errors - /// - Returns an error if the persisted map cannot be loaded from memory. - /// - Returns an error if the given `chain_id` has no backend entry. pub fn get_nodes(&self, chain_id: u64) -> Result> { let map = self - .list + .backends .get() - .ok_or_else(|| anyhow!("Could not get sortition's list cache"))?; + .ok_or_else(|| anyhow::anyhow!("Could not get backends cache"))?; let backend = map .get(&chain_id) - .ok_or_else(|| anyhow!("No list for chain_id {}", chain_id))?; + .ok_or_else(|| anyhow::anyhow!("No backend for chain_id {}", chain_id))?; Ok(backend.nodes()) } } @@ -691,11 +227,16 @@ impl Actor for Sortition { impl Handler for Sortition { type Result = (); - /// Fan-in enclave events to the corresponding typed handlers. + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { EnclaveEvent::CiphernodeAdded { data, .. } => ctx.notify(data.clone()), EnclaveEvent::CiphernodeRemoved { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::TicketBalanceUpdated { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::OperatorActivationChanged { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::ConfigurationUpdated { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::CommitteePublished { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::PlaintextOutputPublished { data, .. } => ctx.notify(data.clone()), EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data.clone()), _ => (), } @@ -705,23 +246,28 @@ impl Handler for Sortition { impl Handler for Sortition { type Result = (); - /// Add a node to the target chain. - /// - /// If the chain does not exist yet, its backend is initialized to `Score` (default). - /// For distance-based chains, initialize explicitly with `SortitionBackend::Distance` - /// before any nodes are added. - #[instrument(name = "sortition_add_node", skip_all)] fn handle(&mut self, msg: CiphernodeAdded, _ctx: &mut Self::Context) -> Self::Result { - trace!("Adding node: {}", msg.address); let chain_id = msg.chain_id; let addr = msg.address.clone(); - if let Err(err) = self.list.try_mutate(move |mut list_map| { - // Use the configured default backend if available, otherwise fall back to Distance + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_state = state_map + .entry(chain_id) + .or_insert_with(NodeStateStore::default); + chain_state + .nodes + .entry(addr.clone()) + .or_insert_with(NodeState::default); + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + + if let Err(err) = self.backends.try_mutate(move |mut list_map| { let default_backend = list_map .get(&u64::MAX) .cloned() - .unwrap_or_else(|| SortitionBackend::distance()); + .unwrap_or_else(|| SortitionBackend::score()); list_map .entry(chain_id) @@ -731,22 +277,28 @@ impl Handler for Sortition { }) { self.bus.err(EnclaveErrorType::Sortition, err); } + + info!(address = %msg.address, chain_id = chain_id, "Node added to sortition state"); } } impl Handler for Sortition { type Result = (); - /// Remove a node from the target chain. - /// - /// If the chain entry is missing, nothing is created or removed. - #[instrument(name = "sortition_remove_node", skip_all)] fn handle(&mut self, msg: CiphernodeRemoved, _ctx: &mut Self::Context) -> Self::Result { - info!("Removing node: {}", msg.address); let chain_id = msg.chain_id; let addr = msg.address.clone(); - if let Err(err) = self.list.try_mutate(move |mut list_map| { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + if let Some(chain_state) = state_map.get_mut(&chain_id) { + chain_state.nodes.remove(&addr); + } + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + + if let Err(err) = self.backends.try_mutate(move |mut list_map| { if let Some(backend) = list_map.get_mut(&chain_id) { backend.remove(addr); } @@ -754,6 +306,189 @@ impl Handler for Sortition { }) { self.bus.err(EnclaveErrorType::Sortition, err); } + + info!(address = %msg.address, chain_id = chain_id, "Node removed from sortition state"); + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: TicketBalanceUpdated, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_state = state_map + .entry(msg.chain_id) + .or_insert_with(NodeStateStore::default); + let node = chain_state + .nodes + .entry(msg.operator.clone()) + .or_insert_with(NodeState::default); + node.ticket_balance = msg.new_balance; + + info!( + operator = %msg.operator, + chain_id = msg.chain_id, + new_balance = ?msg.new_balance, + "Updated ticket balance" + ); + + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: OperatorActivationChanged, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + // Update all entries for this operator across all chains + for (_, chain_state) in state_map.iter_mut() { + if let Some(node) = chain_state.nodes.get_mut(&msg.operator) { + node.active = msg.active; + info!( + operator = %msg.operator, + active = msg.active, + "Updated operator active status" + ); + } + } + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: ConfigurationUpdated, _ctx: &mut Self::Context) -> Self::Result { + if msg.parameter == "ticketPrice" { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_state = state_map + .entry(msg.chain_id) + .or_insert_with(NodeStateStore::default); + chain_state.ticket_price = msg.new_value; + info!( + chain_id = msg.chain_id, + old_ticket_price = ?msg.old_value, + new_ticket_price = ?msg.new_value, + "ConfigurationUpdated - ticket price updated" + ); + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: CommitteePublished, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_id = msg.e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); + let chain_state = state_map + .entry(chain_id) + .or_insert_with(NodeStateStore::default); + + chain_state + .e3_committees + .insert(e3_id_str.clone(), msg.nodes.clone()); + + for node_addr in &msg.nodes { + let node = chain_state + .nodes + .entry(node_addr.clone()) + .or_insert_with(NodeState::default); + node.active_jobs += 1; + + info!( + node = %node_addr, + chain_id = chain_id, + e3_id = ?msg.e3_id, + active_jobs = node.active_jobs, + "Incremented active jobs for node in committee" + ); + } + + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} +/// PlaintextOutputPublished is currently used as a signal to decrement the active jobs for the nodes in the committee +/// But in reality, E3 Jobs might not emit that in case there are no votes or the job fails. +/// We need to find a better way to handle the end of an E3, Reduce the jobs in case of of an Error +/// so the tickets do not get locked up. +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: PlaintextOutputPublished, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_id = msg.e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); + + // Get the committee nodes for this E3 + if let Some(chain_state) = state_map.get_mut(&chain_id) { + if let Some(committee_nodes) = chain_state.e3_committees.remove(&e3_id_str) { + // Decrement active jobs for each node in the committee + for node_addr in &committee_nodes { + if let Some(node) = chain_state.nodes.get_mut(node_addr) { + node.active_jobs = node.active_jobs.saturating_sub(1); + + info!( + node = %node_addr, + chain_id = chain_id, + e3_id = ?msg.e3_id, + active_jobs = node.active_jobs, + "Decremented active jobs for node after E3 completion" + ); + } + } + + info!( + e3_id = ?msg.e3_id, + committee_size = committee_nodes.len(), + "PlaintextOutputPublished - job completed, decremented active jobs" + ); + } else { + info!( + e3_id = ?msg.e3_id, + "PlaintextOutputPublished - no committee found (might have been completed already)" + ); + } + } + + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { + info!( + e3_id = %msg.e3_id, + committee_size = msg.committee.len(), + "Storing finalized committee" + ); + + if let Err(err) = self.finalized_committees.try_mutate(|mut committees| { + committees.insert(msg.e3_id.clone(), msg.committee.clone()); + Ok(committees) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } } } @@ -761,32 +496,17 @@ impl Handler for Sortition { type Result = ResponseFuture)>>; fn handle(&mut self, msg: GetNodeIndex, _ctx: &mut Self::Context) -> Self::Result { - let node_state_manager = self.node_state_manager.clone(); + let backends_snapshot = self.backends.get(); + let node_state_snapshot = self.node_state.get(); let bus = self.bus.clone(); - // Get the sortition backends synchronously - let backends_snapshot = self.list.get(); - Box::pin(async move { - // Query NodeStateManager for fresh state - let node_state_snapshot = if let Some(manager) = node_state_manager { - manager.send(GetNodeState).await.ok().flatten() - } else { - None - }; - let node_state_ref = node_state_snapshot.as_ref(); - - // Use the backends snapshot - if let Some(map) = backends_snapshot { - if let Some(backend) = map.get(&msg.chain_id) { + if let (Some(map), Some(state_map)) = (backends_snapshot, node_state_snapshot) { + if let (Some(backend), Some(state)) = + (map.get(&msg.chain_id), state_map.get(&msg.chain_id)) + { backend - .get_index( - msg.seed, - msg.size, - msg.address.clone(), - node_state_ref, - msg.chain_id, - ) + .get_index(msg.seed, msg.size, msg.address.clone(), msg.chain_id, state) .unwrap_or_else(|err| { bus.err(EnclaveErrorType::Sortition, err); None @@ -800,10 +520,10 @@ impl Handler for Sortition { }) } } + impl Handler for Sortition { type Result = Vec; - /// Return all registered node addresses for a chain, or `[]` on error. fn handle(&mut self, msg: GetNodes, _ctx: &mut Self::Context) -> Self::Result { self.get_nodes(msg.chain_id).unwrap_or_else(|err| { tracing::warn!("Failed to get nodes for chain {}: {}", msg.chain_id, err); @@ -812,25 +532,6 @@ impl Handler for Sortition { } } -impl Handler for Sortition { - type Result = (); - - fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { - info!( - e3_id = %msg.e3_id, - committee_size = msg.committee.len(), - "Storing finalized committee" - ); - - if let Err(err) = self.finalized_committees.try_mutate(|mut committees| { - committees.insert(msg.e3_id.clone(), msg.committee.clone()); - Ok(committees) - }) { - self.bus.err(EnclaveErrorType::Sortition, err); - } - } -} - impl Handler for Sortition { type Result = Vec; @@ -854,32 +555,10 @@ impl Handler for Sortition { } } -impl Handler for Sortition { - type Result = ResponseFuture>>; - - fn handle(&mut self, msg: GetCommittee, _ctx: &mut Self::Context) -> Self::Result { - let backends_snapshot = self.list.get(); +impl Handler for Sortition { + type Result = Option>; - Box::pin(async move { - if let Some(map) = backends_snapshot { - if let Some(backend) = map.get(&msg.chain_id) { - // Get node state for score backend - let node_state = if matches!(backend, SortitionBackend::Score(_)) { - // For score backend, we need node state - // This is a limitation - we'd need to pass node_state_manager - // For now, we'll return an error for score backend - return Err(anyhow!("GetCommittee not supported for ScoreBackend - use GetNodesForE3 instead")); - } else { - None - }; - - backend.get_committee(msg.seed, msg.size, node_state, msg.chain_id) - } else { - Err(anyhow!("No backend found for chain_id {}", msg.chain_id)) - } - } else { - Err(anyhow!("Could not get sortition's list cache")) - } - }) + fn handle(&mut self, _msg: GetNodeState, _: &mut Self::Context) -> Self::Result { + self.node_state.get() } } diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index dd1bfffc8c..22c64fe348 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -203,7 +203,7 @@ "blockNumber": 23, "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" }, - "MockRISC0Verifier": { + "RiscZeroGroth16Verifier": { "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidator": { From 8ccc0b727f38a70998230ad9719d6017c843fe13 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 29 Oct 2025 17:55:13 +0500 Subject: [PATCH 74/88] fix: a few little fixes --- crates/aggregator/src/committee_finalizer.rs | 2 +- crates/ciphernode-builder/src/ciphernode_builder.rs | 2 +- crates/evm/src/helpers.rs | 6 +++--- crates/sortition/Readme.md | 2 +- crates/sortition/src/backends.rs | 12 ++++++++---- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index fb6c78c583..7ad0b5f247 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -74,7 +74,7 @@ impl Handler for CommitteeFinalizer { let e3_id_for_log = e3_id.clone(); let fut = async move { - match e3_evm::helpers::get_current_timestamp().await { + match e3_evm::helpers::get_current_timestamp(msg.chain_id).await { Ok(timestamp) => Some(timestamp), Err(e) => { error!( diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index a475b24fd1..7e1435997e 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -222,7 +222,7 @@ impl CiphernodeBuilder { self } - /// Setup a writable RegistryFilter for every evm chain provided + /// Setup a writable BondingRegistry for every evm chain provided pub fn with_contract_bonding_registry(mut self) -> Self { self.contract_components.bonding_registry = true; self diff --git a/crates/evm/src/helpers.rs b/crates/evm/src/helpers.rs index d4f63eee5d..4fa23dc1be 100644 --- a/crates/evm/src/helpers.rs +++ b/crates/evm/src/helpers.rs @@ -197,12 +197,12 @@ pub async fn load_signer_from_repository( private_key.parse().map_err(Into::into) } -pub async fn get_current_timestamp() -> Result { +pub async fn get_current_timestamp(chain_id: u64) -> Result { let config = e3_config::load_config("_default", None, None)?; let chain = config .chains() - .first() - .ok_or_else(|| anyhow::anyhow!("No chains configured"))?; + .get(chain_id as usize) + .ok_or_else(|| anyhow::anyhow!("Chain {} not found", chain_id))?; let rpc_url = chain.rpc_url()?; let provider = ProviderConfig::new(rpc_url, chain.rpc_auth.clone()) diff --git a/crates/sortition/Readme.md b/crates/sortition/Readme.md index 83c46f4b90..508bce70d3 100644 --- a/crates/sortition/Readme.md +++ b/crates/sortition/Readme.md @@ -218,7 +218,7 @@ flowchart LR - **State Per Node**: - `ticket_balance`: Current ticket balance - `active`: Whether node is active (has min ticket balance) - - `num_jobs`: Number of active E3 jobs + - `active_jobs`: Number of active E3 jobs - **Persistence**: State survives node restarts - **Events**: - `CiphernodeAdded` / `CiphernodeRemoved` diff --git a/crates/sortition/src/backends.rs b/crates/sortition/src/backends.rs index 6a51e0fff2..1586964432 100644 --- a/crates/sortition/src/backends.rs +++ b/crates/sortition/src/backends.rs @@ -108,9 +108,13 @@ impl ScoreBackend { } let count = node_state.available_tickets(&addr_str) as u64; - let total_tickets = (ns.ticket_balance / node_state.ticket_price) - .try_into() - .unwrap_or(0u64); + let total_tickets = if node_state.ticket_price.is_zero() { + 0u64 + } else { + (ns.ticket_balance / node_state.ticket_price) + .try_into() + .unwrap_or(0u64) + }; if count == 0 { info!( @@ -162,7 +166,7 @@ impl SortitionList for ScoreBackend { /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. + /// Returns `Ok(None)` if there are no nodes or `size == 0`. fn get_index( &self, seed: Seed, From cc97082e04792ba4ffa261c8d6d8d5d4f19c1415 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 29 Oct 2025 18:24:07 +0500 Subject: [PATCH 75/88] fix: chain setter error --- crates/aggregator/src/committee_finalizer.rs | 2 +- crates/evm/src/helpers.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index 7ad0b5f247..fb6c78c583 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -74,7 +74,7 @@ impl Handler for CommitteeFinalizer { let e3_id_for_log = e3_id.clone(); let fut = async move { - match e3_evm::helpers::get_current_timestamp(msg.chain_id).await { + match e3_evm::helpers::get_current_timestamp().await { Ok(timestamp) => Some(timestamp), Err(e) => { error!( diff --git a/crates/evm/src/helpers.rs b/crates/evm/src/helpers.rs index 4fa23dc1be..d4f63eee5d 100644 --- a/crates/evm/src/helpers.rs +++ b/crates/evm/src/helpers.rs @@ -197,12 +197,12 @@ pub async fn load_signer_from_repository( private_key.parse().map_err(Into::into) } -pub async fn get_current_timestamp(chain_id: u64) -> Result { +pub async fn get_current_timestamp() -> Result { let config = e3_config::load_config("_default", None, None)?; let chain = config .chains() - .get(chain_id as usize) - .ok_or_else(|| anyhow::anyhow!("Chain {} not found", chain_id))?; + .first() + .ok_or_else(|| anyhow::anyhow!("No chains configured"))?; let rpc_url = chain.rpc_url()?; let provider = ProviderConfig::new(rpc_url, chain.rpc_auth.clone()) From d1aaee3760e0fa4b89b02565206e831ff03be88a Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 29 Oct 2025 19:52:49 +0500 Subject: [PATCH 76/88] fix: testing with a sleep fix --- examples/CRISP/server/src/server/indexer.rs | 45 +++++++++++---------- examples/CRISP/test/crisp.spec.ts | 3 ++ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index 27755e8a1a..f8ed142db2 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -291,27 +291,30 @@ pub async fn register_committee_published( return Ok(()); } - // Convert milliseconds to seconds for comparison with block.timestamp - let start_time = UNIX_EPOCH + Duration::from_secs(e3.startWindow[0].to::()); - - // Get current time - let now = SystemTime::now(); - - // Calculate wait duration - let wait_duration = match start_time.duration_since(now) { - Ok(duration) => { - info!("Need to wait {:?} ({}s) until activation", duration, duration.as_secs()); - duration - } - Err(_) => { - info!("Activating E3"); - Duration::ZERO - } - }; - - // Sleep until start time - let start_instant = Instant::now() + wait_duration; - sleep_until(start_instant).await; + // // Convert milliseconds to seconds for comparison with block.timestamp + // let start_time = UNIX_EPOCH + Duration::from_secs(e3.startWindow[0].to::()); + + // // Get current time + // let now = SystemTime::now(); + + // // Calculate wait duration + // let wait_duration = match start_time.duration_since(now) { + // Ok(duration) => { + // info!("Need to wait {:?} ({}s) until activation", duration, duration.as_secs()); + // duration + // } + // Err(_) => { + // info!("Activating E3"); + // Duration::ZERO + // } + // }; + + // // Sleep until start time + // let start_instant = Instant::now() + wait_duration; + // sleep_until(start_instant).await; + + // Sleep for 1 second + sleep_until(Instant::now() + Duration::from_secs(1)).await; // If not activated activate let tx = contract.activate(event.e3Id, event.publicKey).await?; diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 95e554920b..5c08ee9c59 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -48,12 +48,15 @@ test("CRISP smoke test", async ({ ); await runCliInit(); + // Wait 4 seconds for Committee to be published + await page.waitForTimeout(4000); await page.goto("/"); await ensureHomePageLoaded(page); await page.locator('button:has-text("Connect Wallet")').click(); await page.locator('button:has-text("MetaMask")').click(); await metamask.connectToDapp(); await page.locator('button:has-text("Try Demo")').click(); + await page.reload(); await page .locator("[data-test-id='poll-button-0'] > [data-test-id='card']") .click(); From 9366fe6f44b355b7eb1bd377aa06aa1579275be8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 29 Oct 2025 20:08:43 +0500 Subject: [PATCH 77/88] fix: revert test --- examples/CRISP/server/src/server/indexer.rs | 45 ++++++++++----------- examples/CRISP/test/crisp.spec.ts | 3 -- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index f8ed142db2..27755e8a1a 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -291,30 +291,27 @@ pub async fn register_committee_published( return Ok(()); } - // // Convert milliseconds to seconds for comparison with block.timestamp - // let start_time = UNIX_EPOCH + Duration::from_secs(e3.startWindow[0].to::()); - - // // Get current time - // let now = SystemTime::now(); - - // // Calculate wait duration - // let wait_duration = match start_time.duration_since(now) { - // Ok(duration) => { - // info!("Need to wait {:?} ({}s) until activation", duration, duration.as_secs()); - // duration - // } - // Err(_) => { - // info!("Activating E3"); - // Duration::ZERO - // } - // }; - - // // Sleep until start time - // let start_instant = Instant::now() + wait_duration; - // sleep_until(start_instant).await; - - // Sleep for 1 second - sleep_until(Instant::now() + Duration::from_secs(1)).await; + // Convert milliseconds to seconds for comparison with block.timestamp + let start_time = UNIX_EPOCH + Duration::from_secs(e3.startWindow[0].to::()); + + // Get current time + let now = SystemTime::now(); + + // Calculate wait duration + let wait_duration = match start_time.duration_since(now) { + Ok(duration) => { + info!("Need to wait {:?} ({}s) until activation", duration, duration.as_secs()); + duration + } + Err(_) => { + info!("Activating E3"); + Duration::ZERO + } + }; + + // Sleep until start time + let start_instant = Instant::now() + wait_duration; + sleep_until(start_instant).await; // If not activated activate let tx = contract.activate(event.e3Id, event.publicKey).await?; diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 5c08ee9c59..95e554920b 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -48,15 +48,12 @@ test("CRISP smoke test", async ({ ); await runCliInit(); - // Wait 4 seconds for Committee to be published - await page.waitForTimeout(4000); await page.goto("/"); await ensureHomePageLoaded(page); await page.locator('button:has-text("Connect Wallet")').click(); await page.locator('button:has-text("MetaMask")').click(); await metamask.connectToDapp(); await page.locator('button:has-text("Try Demo")').click(); - await page.reload(); await page .locator("[data-test-id='poll-button-0'] > [data-test-id='card']") .click(); From 54e295b100b7b653995c76fc6439feb41b7ea510 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 30 Oct 2025 17:20:43 +0500 Subject: [PATCH 78/88] fix: crisp version mismatch [skip ci] --- examples/CRISP/client/.env.example | 2 +- .../client/libs/wasm/pkg/crisp_worker.js | 84 +++++++++---------- examples/CRISP/server/.env.example | 4 +- examples/CRISP/server/src/server/indexer.rs | 67 +++++++++------ 4 files changed, 84 insertions(+), 73 deletions(-) diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 9f37fa99dd..233b53f9d7 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,4 +1,4 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0x09635F643e140090A9A8Dcd712eD6285858ceBef # Default E3 program address from hardhat +VITE_E3_PROGRAM_ADDRESS=0xc5a5C42992dECbae36851359345FE25997F5C42d # Default E3 program address from hardhat diff --git a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js index 96c965c001..ca7e32587b 100755 --- a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js +++ b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js @@ -4,51 +4,47 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { EnclaveSDK, FheProtocol } from '@enclave-e3/sdk'; -import circuit from "../../noir/crisp_circuit.json"; +import { EnclaveSDK, FheProtocol } from '@enclave-e3/sdk' +import circuit from '../../noir/crisp_circuit.json' self.onmessage = async function (event) { - const { type, data } = event.data; - switch (type) { - case 'encrypt_vote': - try { - const { voteId, publicKey } = data; - // use default params for now as they do not matter for what we are doing here, - // which is just encrypting the vote and generating a proof - const sdk = EnclaveSDK.create({ - chainId: 31337, - contracts: { - enclave: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - ciphernodeRegistry: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - }, - // local node - rpcUrl: "http://localhost:8545", - // default Anvil private key - privateKey: - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", - protocol: FheProtocol.BFV, - }); + const { type, data } = event.data + switch (type) { + case 'encrypt_vote': + try { + const { voteId, publicKey } = data + // use default params for now as they do not matter for what we are doing here, + // which is just encrypting the vote and generating a proof + const sdk = EnclaveSDK.create({ + chainId: 31337, + contracts: { + enclave: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + ciphernodeRegistry: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + feeToken: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + }, + // local node + rpcUrl: 'http://localhost:8545', + // default Anvil private key + privateKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + protocol: FheProtocol.BFV, + }) - const result = await sdk.encryptNumberAndGenProof( - voteId, - publicKey, - circuit - ); - - self.postMessage({ - type: 'encrypt_vote', - success: true, - encryptedVote: { - vote: result.encryptedVote, - proofData: result.proof, - }, - }); - } catch (error) { - self.postMessage({ type: 'encrypt_vote', success: false, error: error.message }); - } - break; + const result = await sdk.encryptNumberAndGenProof(voteId, publicKey, circuit) - default: - console.error(`Unknown message type: ${type}`); - } -}; + self.postMessage({ + type: 'encrypt_vote', + success: true, + encryptedVote: { + vote: result.encryptedData, + proofData: result.proof, + }, + }) + } catch (error) { + self.postMessage({ type: 'encrypt_vote', success: false, error: error.message }) + } + break + + default: + console.error(`Unknown message type: ${type}`) + } +} diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 06638501ef..75e0932a93 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -19,10 +19,10 @@ E3_PROGRAM_ADDRESS="0xc5a5C42992dECbae36851359345FE25997F5C42d" # CRISPProgram C FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config -E3_WINDOW_SIZE=80 +E3_WINDOW_SIZE=40 E3_THRESHOLD_MIN=1 E3_THRESHOLD_MAX=2 -E3_DURATION=180 +E3_DURATION=160 # E3 Compute Provider Config E3_COMPUTE_PROVIDER_NAME="RISC0" diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index 27755e8a1a..b33a7a7072 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -12,8 +12,8 @@ use crate::server::{ token_holders::{build_tree, compute_token_holder_hashes}, CONFIG, }; +use alloy::providers::{Provider, ProviderBuilder}; use alloy::sol_types::{sol_data, SolType}; - use alloy_primitives::Address; use e3_sdk::{ evm_helpers::{ @@ -32,8 +32,8 @@ use eyre::Context; use log::info; use num_bigint::BigUint; use std::error::Error; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::time::{sleep_until, Instant}; +use std::time::Duration; +use tokio::time::sleep; type Result = std::result::Result>; @@ -154,13 +154,18 @@ pub async fn register_e3_activated( .await?; // Calculate expiration time to sleep until - let expiration = Instant::now() - + (UNIX_EPOCH + Duration::from_secs(expiration)) - .duration_since(SystemTime::now()) - .unwrap_or_else(|_| Duration::ZERO); - - sleep_until(expiration).await; - + let now = get_current_timestamp_rpc().await?; + let wait_duration = if expiration > now { + let secs = expiration - now; + info!("Need to wait {} seconds until expiration", secs); + Duration::from_secs(secs) + } else { + info!("Expired E3"); + Duration::ZERO + }; + if !wait_duration.is_zero() { + sleep(wait_duration).await; + } let e3: e3_sdk::indexer::models::E3 = repo.get_e3().await?; repo.update_status("Expired").await?; @@ -291,27 +296,27 @@ pub async fn register_committee_published( return Ok(()); } - // Convert milliseconds to seconds for comparison with block.timestamp - let start_time = UNIX_EPOCH + Duration::from_secs(e3.startWindow[0].to::()); + // Read Start time in Seconds + let start_time = e3.startWindow[0].to::(); + info!("Start time: {}", start_time); // Get current time - let now = SystemTime::now(); - + let now = get_current_timestamp_rpc().await?; + info!("Current time: {}", now); // Calculate wait duration - let wait_duration = match start_time.duration_since(now) { - Ok(duration) => { - info!("Need to wait {:?} ({}s) until activation", duration, duration.as_secs()); - duration - } - Err(_) => { - info!("Activating E3"); - Duration::ZERO - } + let wait_duration = if start_time > now { + let secs = start_time - now; + info!("Need to wait {} seconds until activation", secs); + Duration::from_secs(secs) + } else { + info!("Activating E3"); + Duration::ZERO }; - + info!("Wait duration: {:?}", wait_duration); // Sleep until start time - let start_instant = Instant::now() + wait_duration; - sleep_until(start_instant).await; + if !wait_duration.is_zero() { + sleep(wait_duration).await; + } // If not activated activate let tx = contract.activate(event.e3Id, event.publicKey).await?; @@ -323,6 +328,16 @@ pub async fn register_committee_published( Ok(listener) } +pub async fn get_current_timestamp_rpc() -> eyre::Result { + let provider = ProviderBuilder::new().connect(&CONFIG.http_rpc_url).await?; + let block = provider + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await? + .ok_or_else(|| eyre::eyre!("Latest block not found"))?; + + Ok(block.header.timestamp) +} + pub async fn start_indexer( ws_url: &str, contract_address: &str, From 4fce86233a100afabdb5170b45dc745bf790f31b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 30 Oct 2025 17:39:17 +0500 Subject: [PATCH 79/88] fix: remove fee token from sdk --- examples/CRISP/client/libs/wasm/pkg/crisp_worker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js index ca7e32587b..41f72219f0 100755 --- a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js +++ b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js @@ -20,7 +20,6 @@ self.onmessage = async function (event) { contracts: { enclave: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', ciphernodeRegistry: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', - feeToken: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', }, // local node rpcUrl: 'http://localhost:8545', From ffdcf80150dc67368e592616ac53602469efb436 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 30 Oct 2025 17:45:14 +0500 Subject: [PATCH 80/88] fix: trying out with workspace * --- examples/CRISP/client/package.json | 2 +- pnpm-lock.yaml | 59 +----------------------------- 2 files changed, 3 insertions(+), 58 deletions(-) diff --git a/examples/CRISP/client/package.json b/examples/CRISP/client/package.json index 3106ce5666..f8b325dcae 100644 --- a/examples/CRISP/client/package.json +++ b/examples/CRISP/client/package.json @@ -19,7 +19,7 @@ "deploy": "gh-pages -d dist" }, "dependencies": { - "@enclave-e3/sdk": "^0.1.5", + "@enclave-e3/sdk": "workspace:*", "@aztec/bb.js": "^0.82.2", "@emotion/babel-plugin": "^11.11.0", "@emotion/react": "^11.11.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c648c9af45..7c5eac29b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: ^11.11.4 version: 11.14.0(@types/react@18.3.24)(react@18.3.1) '@enclave-e3/sdk': - specifier: ^0.1.5 - version: 0.1.5(@openzeppelin/contracts@5.3.0)(@types/node@22.7.5)(bufferutil@4.0.9)(rollup@4.49.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(vite@5.4.19(@types/node@22.7.5))(zod@3.25.76) + specifier: workspace:* + version: link:../../../packages/enclave-sdk '@noir-lang/acvm_js': specifier: 1.0.0-beta.3 version: 1.0.0-beta.3 @@ -1551,15 +1551,6 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@enclave-e3/contracts@0.1.5': - resolution: {integrity: sha512-C1Mo9z2JG6netDhpOsk0kZV8DBjVd4ftE5oqhpXDDbK+HHD4XHozjKMqmlUYTooSzzT3INGYNS/IEDUHxYOqTA==} - - '@enclave-e3/sdk@0.1.5': - resolution: {integrity: sha512-AZGIYrlJZ6FeK6DIQWqNKZDwvDpw+l1zCd7hc97giFX2LrNbSAzf/UJmNewAXFfaGtlnOePSVsuixk/ZQd1TVw==} - - '@enclave-e3/wasm@0.1.5': - resolution: {integrity: sha512-PZ/TpABK/PAAzO0d2+q17i3plzCs/E3chihc4yOi9DqCxdGFhm76hTueY/lDXusDnM+LUrEuaD11YR/2kBRvTg==} - '@esbuild/aix-ppc64@0.20.0': resolution: {integrity: sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==} engines: {node: '>=12'} @@ -10540,52 +10531,6 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@enclave-e3/contracts@0.1.5(@openzeppelin/contracts@5.3.0)': - dependencies: - '@openzeppelin/contracts-upgradeable': 5.4.0(@openzeppelin/contracts@5.3.0) - '@zk-kit/lean-imt.sol': 2.0.1 - poseidon-solidity: 0.0.5 - transitivePeerDependencies: - - '@openzeppelin/contracts' - - '@enclave-e3/sdk@0.1.5(@openzeppelin/contracts@5.3.0)(@types/node@22.7.5)(bufferutil@4.0.9)(rollup@4.49.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(vite@5.4.19(@types/node@22.7.5))(zod@3.25.76)': - dependencies: - '@aztec/bb.js': 0.82.3 - '@enclave-e3/contracts': 0.1.5(@openzeppelin/contracts@5.3.0) - '@enclave-e3/wasm': 0.1.5 - '@noir-lang/noir_js': 1.0.0-beta.3 - comlink: 4.4.2 - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) - vite-plugin-top-level-await: 1.6.0(rollup@4.49.0)(vite@5.4.19(@types/node@22.7.5)) - vite-plugin-wasm: 3.5.0(vite@5.4.19(@types/node@22.7.5)) - vitest: 1.6.1(@types/node@22.7.5) - web-worker: 1.5.0 - transitivePeerDependencies: - - '@edge-runtime/vm' - - '@openzeppelin/contracts' - - '@swc/helpers' - - '@types/node' - - '@vitest/browser' - - '@vitest/ui' - - bufferutil - - happy-dom - - jsdom - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - typescript - - utf-8-validate - - vite - - zod - - '@enclave-e3/wasm@0.1.5': {} - '@esbuild/aix-ppc64@0.20.0': optional: true From b269b1468df9e23d3174ad0452d883e46f2f5320 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 30 Oct 2025 18:09:47 +0500 Subject: [PATCH 81/88] fix: fetch timestamp after feeQuote --- examples/CRISP/server/src/cli/commands.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index 6ec274aea5..2ad1116e34 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -115,8 +115,8 @@ pub async fn initialize_crisp_round( let custom_params_bytes = Bytes::from(serde_json::to_vec(&custom_params)?); let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; - let current_timestamp = get_current_timestamp().await?; - let start_window: [U256; 2] = [ + let mut current_timestamp = get_current_timestamp().await?; + let mut start_window: [U256; 2] = [ U256::from(current_timestamp), U256::from(current_timestamp + CONFIG.e3_window_size as u64), ]; @@ -129,6 +129,11 @@ pub async fn initialize_crisp_round( }; let compute_provider_params_bytes = Bytes::from(serde_json::to_vec(&compute_provider_params)?); + info!("Debug Before Fee Quote - start_window: {:?}", start_window); + info!( + "Debug Before Fee Quote - current timestamp: {:?}", + current_timestamp + ); info!("Getting fee quote..."); let fee_amount = contract .get_e3_quote( @@ -152,13 +157,19 @@ pub async fn initialize_crisp_round( ) .await?; + current_timestamp = get_current_timestamp().await?; + start_window = [ + U256::from(current_timestamp), + U256::from(current_timestamp + CONFIG.e3_window_size as u64), + ]; + info!("Requesting E3 on contract: {}", CONFIG.enclave_address); info!("Debug - threshold: {:?}", threshold); info!("Debug - start_window: {:?}", start_window); + info!("Debug - current timestamp: {:?}", current_timestamp); info!("Debug - duration: {}", duration); info!("Debug - e3_program: {}", e3_program); - info!("Debug - current timestamp: {:?}", current_timestamp); info!( "Debug - Checking ciphernode registry at: {}", From a11a8b38d274b0335879eb8cc956032f2092f2bd Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 30 Oct 2025 18:57:17 +0500 Subject: [PATCH 82/88] fix: add feetoken to sdk and committee publish timeout to e2e test --- examples/CRISP/client/libs/wasm/pkg/crisp_worker.js | 1 + examples/CRISP/server/src/server/indexer.rs | 2 ++ examples/CRISP/test/crisp.spec.ts | 12 +++++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js index 41f72219f0..ca7e32587b 100755 --- a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js +++ b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js @@ -20,6 +20,7 @@ self.onmessage = async function (event) { contracts: { enclave: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', ciphernodeRegistry: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + feeToken: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', }, // local node rpcUrl: 'http://localhost:8545', diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index b33a7a7072..558d9c61a9 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -303,6 +303,7 @@ pub async fn register_committee_published( // Get current time let now = get_current_timestamp_rpc().await?; info!("Current time: {}", now); + // Calculate wait duration let wait_duration = if start_time > now { let secs = start_time - now; @@ -313,6 +314,7 @@ pub async fn register_committee_published( Duration::ZERO }; info!("Wait duration: {:?}", wait_duration); + // Sleep until start time if !wait_duration.is_zero() { sleep(wait_duration).await; diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 69f4e7bda5..e12bccd1bc 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -15,7 +15,7 @@ async function runCliInit() { // Execute the command and wait for it to complete const output = execSync( "pnpm cli init --token-address 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 --balance-threshold 1000", - { encoding: "utf-8" }, + { encoding: "utf-8" } ); console.log("Command output:", output); return output; @@ -30,7 +30,7 @@ const { expect } = test; async function ensureHomePageLoaded(page: Page) { return await expect(page.locator("h4")).toHaveText( - "Coercion-Resistant Impartial Selection Protocol", + "Coercion-Resistant Impartial Selection Protocol" ); } @@ -44,10 +44,12 @@ test("CRISP smoke test", async ({ context, metamaskPage, basicSetup.walletPassword, - extensionId, + extensionId ); await runCliInit(); + // Wait 4 seconds for committee to be published + await page.waitForTimeout(4_000); await page.goto("/"); await ensureHomePageLoaded(page); await page.locator('button:has-text("Connect Wallet")').click(); @@ -62,9 +64,9 @@ test("CRISP smoke test", async ({ await page.locator('a:has-text("Historic polls")').click(); await expect(page.locator("h1")).toHaveText("Historic polls"); await expect( - page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-0'] h3"), + page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-0'] h3") ).toHaveText("100%"); await expect( - page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-1'] h3"), + page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-1'] h3") ).toHaveText("0%"); }); From da1badc91035980d2070c897a144e30f96db190b Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 31 Oct 2025 04:28:56 +0500 Subject: [PATCH 83/88] fix: router scoping issue --- crates/keyshare/src/threshold_keyshare.rs | 60 ++------------------- crates/request/src/context.rs | 10 +--- crates/request/src/router.rs | 22 ++------ crates/sortition/src/ciphernode_selector.rs | 7 ++- crates/tests/tests/integration.rs | 4 -- deploy/local/contracts.sh | 2 +- 6 files changed, 17 insertions(+), 88 deletions(-) diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index cab82d9f48..401363b278 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -9,9 +9,9 @@ use anyhow::{anyhow, bail, Result}; use e3_crypto::{Cipher, SensitiveBytes}; use e3_data::Persistable; use e3_events::{ - CiphernodeSelected, CiphertextOutputPublished, CommitteeFinalized, ComputeRequest, - ComputeResponse, DecryptionshareCreated, E3id, EnclaveEvent, EventBus, KeyshareCreated, - PartyId, ThresholdShare, ThresholdShareCreated, + CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, ComputeResponse, + DecryptionshareCreated, E3id, EnclaveEvent, EventBus, KeyshareCreated, PartyId, ThresholdShare, + ThresholdShareCreated, }; use e3_fhe::create_crp; use e3_multithread::Multithread; @@ -281,8 +281,6 @@ pub struct ThresholdKeyshare { decryption_key_collector: Option>, multithread: Addr, state: Persistable, - /// Store pending E3 selections waiting for committee finalization (for score sortition) - pending_selections: HashMap, } impl ThresholdKeyshare { @@ -293,7 +291,6 @@ impl ThresholdKeyshare { decryption_key_collector: None, multithread: params.multithread, state: params.state, - pending_selections: HashMap::new(), } } } @@ -758,7 +755,6 @@ impl Handler for ThresholdKeyshare { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { EnclaveEvent::CiphernodeSelected { data, .. } => ctx.notify(data), - EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data), EnclaveEvent::CiphertextOutputPublished { data, .. } => ctx.notify(data), EnclaveEvent::ThresholdShareCreated { data, .. } => { let _ = self.handle_threshold_share_created(data, ctx.address()); @@ -771,11 +767,6 @@ impl Handler for ThresholdKeyshare { impl Handler for ThresholdKeyshare { type Result = (); fn handle(&mut self, msg: CiphernodeSelected, ctx: &mut Self::Context) -> Self::Result { - // Store selection for potential committee finalization - self.pending_selections - .insert(msg.e3_id.clone(), msg.clone()); - - // If CommitteeFinalized arrives later, it will verify committee membership match self.handle_ciphernode_selected(msg, ctx.address()) { Err(e) => error!("{e}"), Ok(_) => (), @@ -783,51 +774,6 @@ impl Handler for ThresholdKeyshare { } } -impl Handler for ThresholdKeyshare { - type Result = (); - fn handle(&mut self, msg: CommitteeFinalized, _: &mut Self::Context) -> Self::Result { - // Check if we have a pending selection for this E3 - let Some(_selection) = self.pending_selections.get(&msg.e3_id) else { - info!( - "CommitteeFinalized for E3 {:?} but no pending selection found", - msg.e3_id - ); - return; - }; - - // Get our node address from state - let Some(state) = self.state.get() else { - error!("State not found when handling CommitteeFinalized"); - return; - }; - - // Check if we're in the finalized committee - let our_address = state.address.to_lowercase(); - let in_committee = msg - .committee - .iter() - .any(|addr| addr.to_lowercase() == our_address); - - if in_committee { - info!( - "Node {} is in finalized committee for E3 {:?}, keygen already started", - our_address, msg.e3_id - ); - // Keygen already started in CiphernodeSelected handler - // Clean up pending selection - self.pending_selections.remove(&msg.e3_id); - } else { - info!( - "Node {} was selected but NOT in finalized committee for E3 {:?}", - our_address, msg.e3_id - ); - // TODO: Should we stop/cancel the keygen that was started? - // For now, just remove from pending - self.pending_selections.remove(&msg.e3_id); - } - } -} - impl Handler for ThresholdKeyshare { type Result = ResponseActFuture; fn handle(&mut self, msg: GenEsiSss, _: &mut Self::Context) -> Self::Result { diff --git a/crates/request/src/context.rs b/crates/request/src/context.rs index 6a024e4b81..d45d3b44a0 100644 --- a/crates/request/src/context.rs +++ b/crates/request/src/context.rs @@ -9,8 +9,7 @@ use actix::Recipient; use anyhow::Result; use async_trait::async_trait; use e3_data::{ - Checkpoint, DataStore, FromSnapshotWithParams, Repositories, RepositoriesFactory, Repository, - Snapshot, + Checkpoint, FromSnapshotWithParams, Repositories, RepositoriesFactory, Repository, Snapshot, }; use e3_events::{E3id, EnclaveEvent}; use serde::{Deserialize, Serialize}; @@ -39,8 +38,6 @@ pub struct E3Context { pub dependencies: HetrogenousMap, /// A Repository for storing this context's data snapshot pub repository: Repository, - /// Root data store for accessing global repositories - root_store: DataStore, } #[derive(Serialize, Deserialize)] @@ -60,7 +57,6 @@ pub struct E3ContextParams { pub repository: Repository, pub e3_id: E3id, pub extensions: Arc>>, - pub root_store: DataStore, } impl E3Context { @@ -70,7 +66,6 @@ impl E3Context { repository: params.repository, recipients: init_recipients(), dependencies: HetrogenousMap::new(), - root_store: params.root_store, } } @@ -137,7 +132,7 @@ impl E3Context { impl RepositoriesFactory for E3Context { fn repositories(&self) -> Repositories { - self.root_store.repositories() + self.repository().clone().into() } } @@ -163,7 +158,6 @@ impl FromSnapshotWithParams for E3Context { repository: params.repository, recipients: init_recipients(), dependencies: HetrogenousMap::new(), - root_store: params.root_store, }; for extension in params.extensions.iter() { diff --git a/crates/request/src/router.rs b/crates/request/src/router.rs index 3af74a99a6..33246db5fe 100644 --- a/crates/request/src/router.rs +++ b/crates/request/src/router.rs @@ -105,15 +105,12 @@ pub struct E3Router { bus: Addr>, /// A repository for storing snapshots store: Repository, - /// Root data store for creating E3 contexts - root_store: DataStore, } pub struct E3RouterParams { extensions: Arc>>, bus: Addr>, store: Repository, - root_store: DataStore, } impl E3Router { @@ -123,7 +120,6 @@ impl E3Router { bus: bus.clone(), extensions: vec![], store: repositories.router(), - root_store: store, }; // Everything needs the committe meta factory so adding it here by default @@ -135,7 +131,6 @@ impl E3Router { extensions: params.extensions, bus: params.bus.clone(), store: params.store.clone(), - root_store: params.root_store.clone(), completed: HashSet::new(), contexts: HashMap::new(), buffer: EventBuffer { @@ -169,13 +164,12 @@ impl Handler for E3Router { return; } - let repositories = self.root_store.repositories(); + let repositories = self.repository().repositories(); let context = self.contexts.entry(e3_id.clone()).or_insert_with(|| { E3Context::from_params(E3ContextParams { e3_id: e3_id.clone(), repository: repositories.context(&e3_id), extensions: self.extensions.clone(), - root_store: self.root_store.clone(), }) }); @@ -252,7 +246,7 @@ impl FromSnapshotWithParams for E3Router { async fn from_snapshot(params: Self::Params, snapshot: Self::Snapshot) -> Result { let mut contexts = HashMap::new(); - let repositories = params.root_store.repositories(); + let repositories = params.store.repositories(); for e3_id in snapshot.contexts { let Some(ctx_snapshot) = repositories.context(&e3_id).read().await? else { continue; @@ -265,7 +259,6 @@ impl FromSnapshotWithParams for E3Router { repository: repositories.context(&e3_id), e3_id: e3_id.clone(), extensions: params.extensions.clone(), - root_store: params.root_store.clone(), }, ctx_snapshot, ) @@ -279,8 +272,7 @@ impl FromSnapshotWithParams for E3Router { extensions: params.extensions.into(), buffer: EventBuffer::default(), bus: params.bus, - store: repositories.router(), - root_store: params.root_store, + store: params.store, }) } } @@ -290,7 +282,6 @@ pub struct E3RouterBuilder { pub bus: Addr>, pub extensions: Vec>, pub store: Repository, - pub root_store: DataStore, } impl E3RouterBuilder { @@ -300,14 +291,11 @@ impl E3RouterBuilder { } pub async fn build(self) -> Result> { - let repositories = self.store.repositories(); - let router_repo = repositories.router(); - let snapshot: Option = router_repo.read().await?; + let snapshot: Option = self.store.read().await?; let params = E3RouterParams { extensions: self.extensions.into(), bus: self.bus.clone(), - store: router_repo, - root_store: self.root_store, + store: self.store.clone(), }; let e3r = match snapshot { diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index f0986c510d..d89b09cba2 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -8,6 +8,7 @@ use crate::sortition::{GetNodeIndex, Sortition}; /// CiphernodeSelector is an actor that determines if a ciphernode is part of a committee and if so /// emits a TicketGenerated event (score sortition) to the event bus use actix::prelude::*; +use e3_config::StoreKeys; use e3_data::{DataStore, RepositoriesFactory}; use e3_events::{ CiphernodeSelected, CommitteeFinalized, E3Requested, EnclaveEvent, EventBus, Shutdown, @@ -130,8 +131,12 @@ impl Handler for CiphernodeSelector { fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { let address = self.address.clone(); let bus = self.bus.clone(); - let repositories = self.data_store.repositories(); let e3_id = msg.e3_id.clone(); + let repositories = self + .data_store + .scope(StoreKeys::router()) + .scope(StoreKeys::context(&e3_id)) + .repositories(); // Check if this node is in the finalized committee if !msg.committee.contains(&address) { diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 629b78e53a..f3ff418bc2 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -207,8 +207,6 @@ async fn test_trbfv_actor() -> Result<()> { // For score sortition, we need to wait for nodes to process E3Requested and run sortition // Since TicketGenerated is a local-only event (not shared across network), we can't collect it // we need to manually construct the committee that sortition would select - println!("Waiting for nodes to process E3Requested..."); - tokio::time::sleep(Duration::from_millis(200)).await; // For seed=123, these 5 nodes get selected by sortition: // 0x8f32E487328F04927f20c4B14399e4F3123763df (ticket 6) @@ -233,8 +231,6 @@ async fn test_trbfv_actor() -> Result<()> { })) .await?; - tokio::time::sleep(Duration::from_millis(200)).await; - let expected = vec![ "E3Requested", "CommitteeFinalized", diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index 356e1f08bf..8df4747723 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -4,7 +4,7 @@ # cargo install --locked --path ./crates/cli --bin enclave -f # Deploy CRISP Contracts -(cd examples/CRISP/packages/crisp-contracts && pnpm deploy:contracts:full --network localhost) +(cd examples/CRISP/packages/crisp-contracts && USE_MOCK_VERIFIER=true pnpm deploy:contracts:full --network localhost) # Add Ciphernodes to Enclave sleep 2 # wait for enclave to start From d3672d9963f82c22446d8a4a7c7ac3964d49bcd0 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 31 Oct 2025 15:47:28 +0500 Subject: [PATCH 84/88] fix: use setup in integration test & remove recipient from committee finalizer --- crates/aggregator/src/committee_finalizer.rs | 32 ++++----- .../src/ciphernode_builder.rs | 5 +- .../committee_finalize_requested.rs | 22 ++++++ crates/events/src/enclave_event/mod.rs | 10 +++ crates/evm/src/ciphernode_registry_sol.rs | 27 +++++--- crates/evm/src/lib.rs | 1 - crates/tests/tests/integration.rs | 69 +++++++++++-------- crates/tests/tests/integration_legacy.rs | 14 ---- 8 files changed, 100 insertions(+), 80 deletions(-) create mode 100644 crates/events/src/enclave_event/committee_finalize_requested.rs diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index fb6c78c583..587c8311ea 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -5,38 +5,30 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::prelude::*; -use e3_events::{CommitteeRequested, EnclaveEvent, EventBus, Shutdown, Subscribe}; -use e3_evm::FinalizeCommittee; +use e3_events::{ + CommitteeFinalizeRequested, CommitteeRequested, EnclaveEvent, EventBus, Shutdown, Subscribe, +}; use std::collections::HashMap; use std::time::Duration; use tracing::{error, info}; -/// CommitteeFinalizer is an actor that listens to CommitteeRequested events and calls -/// finalizeCommittee on the registry after the submission deadline has passed. +/// CommitteeFinalizer is an actor that listens to CommitteeRequested events and dispatches +/// CommitteeFinalizeRequested events after the submission deadline has passed. pub struct CommitteeFinalizer { - #[allow(dead_code)] bus: Addr>, - registry_writer: Recipient, pending_committees: HashMap, } impl CommitteeFinalizer { - pub fn new( - bus: &Addr>, - registry_writer: Recipient, - ) -> Self { + pub fn new(bus: &Addr>) -> Self { Self { bus: bus.clone(), - registry_writer, pending_committees: HashMap::new(), } } - pub fn attach( - bus: &Addr>, - registry_writer: Recipient, - ) -> Addr { - let addr = CommitteeFinalizer::new(bus, registry_writer).start(); + pub fn attach(bus: &Addr>) -> Addr { + let addr = CommitteeFinalizer::new(bus).start(); bus.do_send(Subscribe::new( "CommitteeRequested", @@ -112,17 +104,17 @@ impl Handler for CommitteeFinalizer { "Scheduling committee finalization" ); - let registry_writer = act.registry_writer.clone(); + let bus = act.bus.clone(); let e3_id_clone = e3_id_for_async.clone(); let handle = ctx.run_later( Duration::from_secs(seconds_until_deadline), move |act, _ctx| { - info!(e3_id = %e3_id_clone, "Calling finalizeCommittee"); + info!(e3_id = %e3_id_clone, "Dispatching CommitteeFinalizeRequested event"); - registry_writer.do_send(FinalizeCommittee { + bus.do_send(EnclaveEvent::from(CommitteeFinalizeRequested { e3_id: e3_id_clone.clone(), - }); + })); act.pending_committees.remove(&e3_id_clone.to_string()); }, diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 7e1435997e..fdff7df363 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -381,10 +381,7 @@ impl CiphernodeBuilder { if self.pubkey_agg && matches!(self.sortition_backend, SortitionBackend::Score(_)) { info!("Attaching CommitteeFinalizer for score sortition"); - e3_aggregator::CommitteeFinalizer::attach( - &local_bus, - writer.recipient(), - ); + e3_aggregator::CommitteeFinalizer::attach(&local_bus); } } Err(e) => error!( diff --git a/crates/events/src/enclave_event/committee_finalize_requested.rs b/crates/events/src/enclave_event/committee_finalize_requested.rs new file mode 100644 index 0000000000..b91780f62c --- /dev/null +++ b/crates/events/src/enclave_event/committee_finalize_requested.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeFinalizeRequested { + pub e3_id: E3id, +} + +impl Display for CommitteeFinalizeRequested { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 8f799588dd..4c6cb2cf39 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -8,6 +8,7 @@ mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; mod ciphertext_output_published; +mod committee_finalize_requested; mod committee_finalized; mod committee_published; mod committee_requested; @@ -35,6 +36,7 @@ pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; pub use ciphertext_output_published::*; +pub use committee_finalize_requested::*; pub use committee_finalized::*; pub use committee_published::*; pub use committee_requested::*; @@ -145,6 +147,10 @@ pub enum EnclaveEvent { id: EventId, data: CommitteeRequested, }, + CommitteeFinalizeRequested { + id: EventId, + data: CommitteeFinalizeRequested, + }, CommitteeFinalized { id: EventId, data: CommitteeFinalized, @@ -248,6 +254,7 @@ impl From for EventId { EnclaveEvent::OperatorActivationChanged { id, .. } => id, EnclaveEvent::CommitteePublished { id, .. } => id, EnclaveEvent::CommitteeRequested { id, .. } => id, + EnclaveEvent::CommitteeFinalizeRequested { id, .. } => id, EnclaveEvent::PlaintextOutputPublished { id, .. } => id, EnclaveEvent::EnclaveError { id, .. } => id, EnclaveEvent::E3RequestComplete { id, .. } => id, @@ -275,6 +282,7 @@ impl EnclaveEvent { EnclaveEvent::ThresholdShareCreated { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteePublished { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteeRequested { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteeFinalizeRequested { data, .. } => Some(data.e3_id), EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), EnclaveEvent::CommitteeFinalized { data, .. } => Some(data.e3_id), EnclaveEvent::TicketGenerated { data, .. } => Some(data.e3_id), @@ -299,6 +307,7 @@ impl EnclaveEvent { EnclaveEvent::OperatorActivationChanged { data, .. } => format!("{:?}", data), EnclaveEvent::CommitteePublished { data, .. } => format!("{:?}", data), EnclaveEvent::CommitteeRequested { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteeFinalizeRequested { data, .. } => format!("{:?}", data), EnclaveEvent::PlaintextOutputPublished { data, .. } => format!("{:?}", data), EnclaveEvent::E3RequestComplete { data, .. } => format!("{}", data), EnclaveEvent::EnclaveError { data, .. } => format!("{:?}", data), @@ -331,6 +340,7 @@ impl_from_event!( OperatorActivationChanged, CommitteePublished, CommitteeRequested, + CommitteeFinalizeRequested, CommitteeFinalized, TicketGenerated, TicketSubmitted, diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index b052fd6f64..9ba569b813 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -16,8 +16,9 @@ use alloy::{ use anyhow::Result; use e3_data::Repository; use e3_events::{ - BusError, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, - PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, TicketId, + BusError, CommitteeFinalizeRequested, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, + EventBus, OrderedSet, PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, + TicketId, }; use tracing::{error, info, trace}; @@ -275,6 +276,12 @@ impl CiphernodeRegistrySolWriter let _ = bus .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) .await; + let _ = bus + .send(Subscribe::new( + "CommitteeFinalizeRequested", + addr.clone().into(), + )) + .await; } // Subscribe to TicketGenerated for ticket submission @@ -308,6 +315,11 @@ impl Handler ctx.notify(data); } } + EnclaveEvent::CommitteeFinalizeRequested { data, .. } => { + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } EnclaveEvent::TicketGenerated { data, .. } => { // Submit ticket if chain matches if self.provider.chain_id() == data.e3_id.chain_id() { @@ -359,19 +371,12 @@ impl Handler } } -/// Message to trigger committee finalization (called by aggregator) -#[derive(Message, Clone, Debug)] -#[rtype(result = "()")] -pub struct FinalizeCommittee { - pub e3_id: E3id, -} - -impl Handler +impl Handler for CiphernodeRegistrySolWriter

{ type Result = ResponseFuture<()>; - fn handle(&mut self, msg: FinalizeCommittee, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: CommitteeFinalizeRequested, _: &mut Self::Context) -> Self::Result { let e3_id = msg.e3_id.clone(); let contract_address = self.contract_address; let provider = self.provider.clone(); diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 60b462d8d3..477e20798c 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -16,7 +16,6 @@ mod repo; pub use bonding_registry_sol::{BondingRegistrySol, BondingRegistrySolReader}; pub use ciphernode_registry_sol::{ CiphernodeRegistrySol, CiphernodeRegistrySolReader, CiphernodeRegistrySolWriter, - FinalizeCommittee, }; pub use enclave_sol::EnclaveSol; pub use enclave_sol_reader::EnclaveSolReader; diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index f3ff418bc2..a5e0571781 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -32,6 +32,43 @@ pub fn save_snapshot(file_name: &str, bytes: &[u8]) { fs::write(format!("tests/{file_name}"), bytes).unwrap(); } +async fn setup_score_sortition_environment( + bus: &actix::Addr>, + eth_addrs: &Vec, + chain_id: u64, +) -> Result<()> { + bus.send(EnclaveEvent::from(ConfigurationUpdated { + parameter: "ticketPrice".to_string(), + old_value: U256::ZERO, + new_value: U256::from(10_000_000u64), + chain_id, + })) + .await?; + + let mut adder = AddToCommittee::new(bus, chain_id); + for addr in eth_addrs { + adder.add(addr).await?; + + bus.send(EnclaveEvent::from(TicketBalanceUpdated { + operator: addr.clone(), + delta: I256::try_from(1_000_000_000u64).unwrap(), + new_balance: U256::from(1_000_000_000u64), + reason: FixedBytes::ZERO, + chain_id, + })) + .await?; + + bus.send(EnclaveEvent::from(OperatorActivationChanged { + operator: addr.clone(), + active: true, + chain_id, + })) + .await?; + } + + Ok(()) +} + /// Test trbfv #[actix::test] #[serial_test::serial] @@ -96,7 +133,6 @@ async fn test_trbfv_actor() -> Result<()> { // Cipher let cipher = Arc::new(Cipher::from_password("I am the music man.").await?); - let mut adder = AddToCommittee::new(&bus, 1); // Actor system setup let multithread = Multithread::attach( @@ -143,35 +179,8 @@ async fn test_trbfv_actor() -> Result<()> { .await?; let chain_id = 1u64; - bus.send(EnclaveEvent::from(ConfigurationUpdated { - parameter: "ticketPrice".to_string(), - old_value: U256::ZERO, - new_value: U256::from(10_000_000u64), - chain_id, - })) - .await?; - - for node in nodes.iter() { - let addr = node.address(); - adder.add(&addr).await?; - - // TODO: is a 100 tickets worth of tokens enough? - bus.send(EnclaveEvent::from(TicketBalanceUpdated { - operator: addr.clone(), - delta: I256::try_from(1_000_000_000u64).unwrap(), - new_balance: U256::from(1_000_000_000u64), - reason: FixedBytes::ZERO, - chain_id, - })) - .await?; - - bus.send(EnclaveEvent::from(OperatorActivationChanged { - operator: addr.clone(), - active: true, - chain_id, - })) - .await?; - } + let eth_addrs: Vec = nodes.iter().map(|n| n.address()).collect(); + setup_score_sortition_environment(&bus, ð_addrs, chain_id).await?; // Flush all events nodes.flush_all_history(100).await?; diff --git a/crates/tests/tests/integration_legacy.rs b/crates/tests/tests/integration_legacy.rs index 79b522d9e0..b961682435 100644 --- a/crates/tests/tests/integration_legacy.rs +++ b/crates/tests/tests/integration_legacy.rs @@ -115,20 +115,6 @@ async fn create_local_ciphernodes( Ok(result) } -async fn add_ciphernodes( - bus: &Addr>, - addrs: &Vec, - chain_id: u64, -) -> Result> { - let mut committee = AddToCommittee::new(&bus, chain_id); - let mut evts: Vec = vec![]; - - for addr in addrs { - evts.push(committee.add(addr).await?); - } - Ok(evts) -} - async fn setup_score_sortition_environment( bus: &Addr>, eth_addrs: &Vec, From ea97768fb81a5e07e96e4e1b21e81446c0b25359 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 31 Oct 2025 17:49:47 +0500 Subject: [PATCH 85/88] fix: update package.json to include browser conditions --- examples/CRISP/client/vite.config.ts | 1 + packages/enclave-sdk/package.json | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/examples/CRISP/client/vite.config.ts b/examples/CRISP/client/vite.config.ts index b50269281c..4865893556 100644 --- a/examples/CRISP/client/vite.config.ts +++ b/examples/CRISP/client/vite.config.ts @@ -30,6 +30,7 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), libs: path.resolve(__dirname, './libs'), }, + conditions: ['import', 'module', 'browser', 'default'], }, worker: { format: 'es', diff --git a/packages/enclave-sdk/package.json b/packages/enclave-sdk/package.json index 76ab98ab33..95e0bbdcea 100644 --- a/packages/enclave-sdk/package.json +++ b/packages/enclave-sdk/package.json @@ -7,6 +7,10 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs", + "browser": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, "default": "./dist/index.js" } }, From de8ab47d7e3acb77be075c719e5e7baee66471b6 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 31 Oct 2025 18:02:17 +0500 Subject: [PATCH 86/88] fix: update package.json to include browser conditions --- packages/enclave-sdk/package.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/enclave-sdk/package.json b/packages/enclave-sdk/package.json index 95e0bbdcea..a254184301 100644 --- a/packages/enclave-sdk/package.json +++ b/packages/enclave-sdk/package.json @@ -5,12 +5,9 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js", "require": "./dist/index.cjs", - "browser": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, + "import": "./dist/index.js", + "browser": "./dist/index.js", "default": "./dist/index.js" } }, From 0ace70b2a04c20535f85fb80ad1e7272204868fe Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 31 Oct 2025 19:18:42 +0500 Subject: [PATCH 87/88] fix: test out sdk version in crisp --- examples/CRISP/client/libs/wasm/pkg/crisp_worker.js | 3 +-- examples/CRISP/client/package.json | 2 +- examples/CRISP/client/vite.config.ts | 1 - packages/enclave-sdk/package.json | 3 +-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js index ca7e32587b..35a7aa5656 100755 --- a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js +++ b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js @@ -20,7 +20,6 @@ self.onmessage = async function (event) { contracts: { enclave: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', ciphernodeRegistry: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', - feeToken: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', }, // local node rpcUrl: 'http://localhost:8545', @@ -35,7 +34,7 @@ self.onmessage = async function (event) { type: 'encrypt_vote', success: true, encryptedVote: { - vote: result.encryptedData, + vote: result.encryptedVote, proofData: result.proof, }, }) diff --git a/examples/CRISP/client/package.json b/examples/CRISP/client/package.json index f8b325dcae..3106ce5666 100644 --- a/examples/CRISP/client/package.json +++ b/examples/CRISP/client/package.json @@ -19,7 +19,7 @@ "deploy": "gh-pages -d dist" }, "dependencies": { - "@enclave-e3/sdk": "workspace:*", + "@enclave-e3/sdk": "^0.1.5", "@aztec/bb.js": "^0.82.2", "@emotion/babel-plugin": "^11.11.0", "@emotion/react": "^11.11.4", diff --git a/examples/CRISP/client/vite.config.ts b/examples/CRISP/client/vite.config.ts index 4865893556..b50269281c 100644 --- a/examples/CRISP/client/vite.config.ts +++ b/examples/CRISP/client/vite.config.ts @@ -30,7 +30,6 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), libs: path.resolve(__dirname, './libs'), }, - conditions: ['import', 'module', 'browser', 'default'], }, worker: { format: 'es', diff --git a/packages/enclave-sdk/package.json b/packages/enclave-sdk/package.json index a254184301..76ab98ab33 100644 --- a/packages/enclave-sdk/package.json +++ b/packages/enclave-sdk/package.json @@ -5,9 +5,8 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "require": "./dist/index.cjs", "import": "./dist/index.js", - "browser": "./dist/index.js", + "require": "./dist/index.cjs", "default": "./dist/index.js" } }, From adfa7a91ef779e0524dba9ee7424bf7b4549f804 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 31 Oct 2025 20:34:43 +0500 Subject: [PATCH 88/88] fix: update pnpm lock --- pnpm-lock.yaml | 59 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c5eac29b6..c648c9af45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: ^11.11.4 version: 11.14.0(@types/react@18.3.24)(react@18.3.1) '@enclave-e3/sdk': - specifier: workspace:* - version: link:../../../packages/enclave-sdk + specifier: ^0.1.5 + version: 0.1.5(@openzeppelin/contracts@5.3.0)(@types/node@22.7.5)(bufferutil@4.0.9)(rollup@4.49.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(vite@5.4.19(@types/node@22.7.5))(zod@3.25.76) '@noir-lang/acvm_js': specifier: 1.0.0-beta.3 version: 1.0.0-beta.3 @@ -1551,6 +1551,15 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@enclave-e3/contracts@0.1.5': + resolution: {integrity: sha512-C1Mo9z2JG6netDhpOsk0kZV8DBjVd4ftE5oqhpXDDbK+HHD4XHozjKMqmlUYTooSzzT3INGYNS/IEDUHxYOqTA==} + + '@enclave-e3/sdk@0.1.5': + resolution: {integrity: sha512-AZGIYrlJZ6FeK6DIQWqNKZDwvDpw+l1zCd7hc97giFX2LrNbSAzf/UJmNewAXFfaGtlnOePSVsuixk/ZQd1TVw==} + + '@enclave-e3/wasm@0.1.5': + resolution: {integrity: sha512-PZ/TpABK/PAAzO0d2+q17i3plzCs/E3chihc4yOi9DqCxdGFhm76hTueY/lDXusDnM+LUrEuaD11YR/2kBRvTg==} + '@esbuild/aix-ppc64@0.20.0': resolution: {integrity: sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==} engines: {node: '>=12'} @@ -10531,6 +10540,52 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} + '@enclave-e3/contracts@0.1.5(@openzeppelin/contracts@5.3.0)': + dependencies: + '@openzeppelin/contracts-upgradeable': 5.4.0(@openzeppelin/contracts@5.3.0) + '@zk-kit/lean-imt.sol': 2.0.1 + poseidon-solidity: 0.0.5 + transitivePeerDependencies: + - '@openzeppelin/contracts' + + '@enclave-e3/sdk@0.1.5(@openzeppelin/contracts@5.3.0)(@types/node@22.7.5)(bufferutil@4.0.9)(rollup@4.49.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(vite@5.4.19(@types/node@22.7.5))(zod@3.25.76)': + dependencies: + '@aztec/bb.js': 0.82.3 + '@enclave-e3/contracts': 0.1.5(@openzeppelin/contracts@5.3.0) + '@enclave-e3/wasm': 0.1.5 + '@noir-lang/noir_js': 1.0.0-beta.3 + comlink: 4.4.2 + viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + vite-plugin-top-level-await: 1.6.0(rollup@4.49.0)(vite@5.4.19(@types/node@22.7.5)) + vite-plugin-wasm: 3.5.0(vite@5.4.19(@types/node@22.7.5)) + vitest: 1.6.1(@types/node@22.7.5) + web-worker: 1.5.0 + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@openzeppelin/contracts' + - '@swc/helpers' + - '@types/node' + - '@vitest/browser' + - '@vitest/ui' + - bufferutil + - happy-dom + - jsdom + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + - utf-8-validate + - vite + - zod + + '@enclave-e3/wasm@0.1.5': {} + '@esbuild/aix-ppc64@0.20.0': optional: true