@@ -8,7 +8,6 @@ import {ISmartnodesCore} from "./interfaces/ISmartnodesCore.sol";
88 * @title SmartnodesCoordinator
99 * @notice Manages job and user participation updates to SmartnodesCore contract. Updates are
1010 * @notice controlled by a rotating set of validators that vote on these state updates periodically.
11- * @dev Optimized multi-signature coordinator for managing SmartnodesCore contract
1211 */
1312contract SmartnodesCoordinator is ReentrancyGuard {
1413 // ============= Errors ==============
@@ -27,6 +26,7 @@ contract SmartnodesCoordinator is ReentrancyGuard {
2726 error Coordinator__NotEnoughActiveValidators ();
2827 error Coordinator__InvalidApprovalPercentage ();
2928 error Coordinator__InvalidAddress ();
29+ error Coordinator__ProposalExpired ();
3030
3131 // ============= Structs ==============
3232 struct Proposal {
@@ -57,6 +57,11 @@ contract SmartnodesCoordinator is ReentrancyGuard {
5757
5858 uint256 public requiredValidators;
5959
60+ // Proposal cleanup configuration
61+ uint256 public constant MAX_PROPOSAL_AGE = 7 days ; // Proposals older than 7 days can be cleaned up
62+ uint256 public constant CLEANUP_BATCH_SIZE = 50 ; // Max proposals to clean in one call
63+ uint256 public oldestProposalId = 1 ; // Track oldest proposal for efficient cleanup
64+
6065 // Validator management
6166 address [] public validators;
6267 address [] public currentRoundValidators;
@@ -78,6 +83,7 @@ contract SmartnodesCoordinator is ReentrancyGuard {
7883 bytes32 indexed proposalHash
7984 );
8085 event ProposalExpired (uint256 indexed proposalId );
86+ event ProposalsCleanedUp (uint256 fromId , uint256 toId , uint256 count );
8187 event ValidatorAdded (address indexed validator );
8288 event ValidatorRemoved (address indexed validator );
8389 event RoundStarted (uint256 indexed roundId , address [] selectedValidators );
@@ -113,6 +119,16 @@ contract SmartnodesCoordinator is ReentrancyGuard {
113119 _;
114120 }
115121
122+ modifier validProposal (uint256 proposalId ) {
123+ Proposal storage proposal = proposals[proposalId];
124+ if (proposal.creator == address (0 ))
125+ revert Coordinator__InvalidProposalNumber ();
126+ if (proposal.executed) revert Coordinator__InvalidProposalNumber ();
127+ if (_isProposalExpired (proposalId))
128+ revert Coordinator__ProposalExpired ();
129+ _;
130+ }
131+
116132 constructor (
117133 uint128 _updateTime ,
118134 uint8 _requiredApprovalsPercentage ,
@@ -177,13 +193,7 @@ contract SmartnodesCoordinator is ReentrancyGuard {
177193 */
178194 function voteForProposal (
179195 uint256 proposalId
180- ) external onlyValidator nonReentrant {
181- Proposal storage proposal = proposals[proposalId];
182-
183- if (proposal.creator == address (0 ) || proposal.executed) {
184- revert Coordinator__InvalidProposalNumber ();
185- }
186-
196+ ) external onlyValidator validProposal (proposalId) nonReentrant {
187197 if (! _isCurrentRoundExpired () && validatorVote[msg .sender ] != 0 ) {
188198 revert Coordinator__AlreadyVoted ();
189199 }
@@ -196,6 +206,7 @@ contract SmartnodesCoordinator is ReentrancyGuard {
196206 validatorVote[msg .sender ] = proposalId;
197207
198208 // Increment votes and check threshold in one go
209+ Proposal storage proposal = proposals[proposalId];
199210 uint16 newVotes = ++ proposal.votes;
200211 uint256 requiredVotes = _calculateRequiredVotes ();
201212
@@ -218,13 +229,12 @@ contract SmartnodesCoordinator is ReentrancyGuard {
218229 bytes32 [] calldata jobHashes ,
219230 address [] calldata jobWorkers ,
220231 uint256 [] calldata jobCapacities
221- ) external onlyValidator nonReentrant {
232+ ) external onlyValidator validProposal (proposalId) nonReentrant {
222233 Proposal storage proposal = proposals[proposalId];
223234
224235 // Batch validation
225236 if (proposal.creator != msg .sender )
226237 revert Coordinator__MustBeProposalCreator ();
227- if (proposal.executed) revert Coordinator__InvalidProposalNumber ();
228238 if (proposal.votes < _calculateRequiredVotes ())
229239 revert Coordinator__NotEnoughVotes ();
230240
@@ -264,6 +274,33 @@ contract SmartnodesCoordinator is ReentrancyGuard {
264274 _updateRound ();
265275 }
266276
277+ // ============= Cleanup Functions =============
278+ /**
279+ * @notice Clean up old proposals to save storage costs
280+ * @dev Can be called by anyone to incentivize cleanup
281+ * @return cleanedCount Number of proposals cleaned up
282+ */
283+ function cleanupOldProposals () external returns (uint256 cleanedCount ) {
284+ return _cleanupProposals (CLEANUP_BATCH_SIZE);
285+ }
286+
287+ /**
288+ * @notice Force expire a specific proposal that's older than MAX_PROPOSAL_AGE
289+ * @param proposalId The proposal to expire
290+ */
291+ function expireProposal (uint256 proposalId ) external {
292+ if (! _isProposalExpired (proposalId))
293+ revert Coordinator__ProposalTooEarly ();
294+
295+ Proposal storage proposal = proposals[proposalId];
296+ if (proposal.creator == address (0 ))
297+ revert Coordinator__InvalidProposalNumber ();
298+ if (proposal.executed) return ; // Already handled
299+
300+ delete proposals[proposalId];
301+ emit ProposalExpired (proposalId);
302+ }
303+
267304 // ============= Validator Management =============
268305 /**
269306 * @notice Add validator with stake verification
@@ -292,6 +329,59 @@ contract SmartnodesCoordinator is ReentrancyGuard {
292329 }
293330
294331 // ============= INTERNAL FUNCTIONS =============
332+ function _cleanupProposals (
333+ uint256 batchSize
334+ ) internal returns (uint256 cleanedCount ) {
335+ uint256 currentProposalId = roundData.nextProposalId;
336+ uint256 startId = oldestProposalId;
337+ uint256 endId = startId + batchSize;
338+
339+ if (endId > currentProposalId) {
340+ endId = currentProposalId;
341+ }
342+
343+ if (startId >= endId) return 0 ;
344+
345+ uint256 cutoffTime = block .timestamp - MAX_PROPOSAL_AGE;
346+
347+ unchecked {
348+ for (uint256 i = startId; i < endId; ++ i) {
349+ Proposal storage proposal = proposals[i];
350+
351+ // Skip if proposal doesn't exist
352+ if (proposal.creator == address (0 )) {
353+ continue ;
354+ } else if (proposal.createdAt > cutoffTime) {
355+ break ;
356+ }
357+
358+ // Clean up executed or expired proposals
359+ if (proposal.executed || _isProposalExpired (i)) {
360+ delete proposals[i];
361+ ++ cleanedCount;
362+ }
363+ }
364+ }
365+
366+ // Update oldest proposal pointer
367+ oldestProposalId = endId;
368+
369+ if (cleanedCount > 0 ) {
370+ emit ProposalsCleanedUp (startId, endId - 1 , cleanedCount);
371+ }
372+
373+ return cleanedCount;
374+ }
375+
376+ function _isProposalExpired (
377+ uint256 proposalId
378+ ) internal view returns (bool ) {
379+ Proposal storage proposal = proposals[proposalId];
380+ if (proposal.creator == address (0 )) return false ;
381+
382+ return block .timestamp > proposal.createdAt + MAX_PROPOSAL_AGE;
383+ }
384+
295385 function _addValidator (address validator ) internal {
296386 if (! i_smartnodesCore.isLockedValidator (validator)) {
297387 revert Coordinator__NotValidator ();
@@ -355,18 +445,11 @@ contract SmartnodesCoordinator is ReentrancyGuard {
355445 }
356446
357447 function _cleanupExpiredRound () internal {
358- address [] memory vals = validators;
359- uint256 validatorCount = vals.length ;
448+ _resetValidatorStates ();
360449
361- unchecked {
362- for (uint256 i = 0 ; i < validatorCount; ++ i) {
363- address validator = vals[i];
364- validatorVote[validator] = 0 ;
365- validatorToProposal[validator] = 0 ;
366- }
367- }
450+ // Enhanced cleanup: also clean up old proposals during round expiry
451+ _cleanupProposals (CLEANUP_BATCH_SIZE / 2 ); // Use smaller batch during round transitions
368452
369- delete currentRoundValidators;
370453 emit ProposalExpired (roundData.currentRoundId);
371454 }
372455
@@ -389,10 +472,10 @@ contract SmartnodesCoordinator is ReentrancyGuard {
389472
390473 unchecked {
391474 for (uint256 i = 0 ; i < validatorCount; ++ i) {
392- validatorVote[vals[i]] = 0 ;
475+ delete validatorVote[vals[i]];
393476 }
394477 for (uint256 i = 0 ; i < selectedCount; ++ i) {
395- validatorToProposal[currentVals[i]] = 0 ;
478+ delete validatorToProposal[currentVals[i]];
396479 }
397480 }
398481 }
@@ -515,6 +598,12 @@ contract SmartnodesCoordinator is ReentrancyGuard {
515598 return _isCurrentRoundExpired ();
516599 }
517600
601+ function isProposalExpired (
602+ uint256 proposalId
603+ ) external view returns (bool ) {
604+ return _isProposalExpired (proposalId);
605+ }
606+
518607 function getRequiredApprovals () external view returns (uint256 ) {
519608 return _calculateRequiredVotes ();
520609 }
@@ -547,6 +636,14 @@ contract SmartnodesCoordinator is ReentrancyGuard {
547636 return proposals[proposalId];
548637 }
549638
639+ function getCleanupInfo ()
640+ external
641+ view
642+ returns (uint256 oldestId , uint256 nextId , uint256 maxAge )
643+ {
644+ return (oldestProposalId, roundData.nextProposalId, MAX_PROPOSAL_AGE);
645+ }
646+
550647 function getState ()
551648 external
552649 view
0 commit comments