diff --git a/docker-compose.yml b/docker-compose.yml index 1fa4e1e6d..ead06f59c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -198,12 +198,12 @@ services: ports: - "${HARDHAT_PORT:-8545}:8545" volumes: - - ./hardhat/contracts:/app/contracts:ro - - ./hardhat/scripts:/app/scripts:ro - - ./hardhat/test:/app/test:ro - - ./hardhat/hardhat.config.js:/app/hardhat.config.js:ro - - ./hardhat/deploy:/app/deploy:ro - - ./hardhat/scripts:/app/scripts:ro + - ./hardhat/contracts:/app/contracts + - ./hardhat/scripts:/app/scripts + - ./hardhat/test:/app/test + - ./hardhat/hardhat.config.js:/app/hardhat.config.js + - ./hardhat/deploy:/app/deploy + - ./hardhat/scripts:/app/scripts - hardhat_artifacts:/app/artifacts - hardhat_cache:/app/cache - hardhat_deployments:/app/deployments @@ -213,10 +213,10 @@ services: restart: always healthcheck: test: ["CMD", "curl", "-X", "POST", "-H", "Content-Type: application/json", "--fail", "http://localhost:8545", "-d", '{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}'] - interval: 10s - timeout: 5s - retries: 10 - start_period: 10s + interval: 60s + timeout: 30s + retries: 5 + start_period: 60s volumes: hardhat_artifacts: diff --git a/docker/Dockerfile.hardhat b/docker/Dockerfile.hardhat index 4ca41b3f3..62b79eda2 100644 --- a/docker/Dockerfile.hardhat +++ b/docker/Dockerfile.hardhat @@ -1,13 +1,9 @@ FROM node:22.12-alpine3.19 -# Create non-root user -RUN addgroup -S hardhat-group && adduser -S hardhat-user -G hardhat-group WORKDIR /app # Install necessary packages and set up the environment -RUN apk add --no-cache curl g++ make netcat-openbsd python3 && \ - mkdir -p /app && \ - chown -R hardhat-user:hardhat-group /app +RUN apk add --no-cache curl g++ make netcat-openbsd python3 COPY ./hardhat/package*.json ./ RUN npm install --ignore-scripts @@ -21,12 +17,9 @@ RUN mkdir -p /app/deployments/localhost && \ mkdir -p /app/artifacts/contracts && \ mkdir -p /app/cache && \ mkdir -p /app/ignition/deployments && \ - chown -R hardhat-user:hardhat-group /app && \ - chmod -R 755 /app && \ - chmod -R 775 /app/deployments && \ - chmod -R 775 /app/artifacts && \ - chmod -R 775 /app/cache && \ - chmod -R 775 /app/ignition/deployments + mkdir -p /app/scripts && \ + mkdir -p /app/test && \ + chmod -R 777 /app ENV PATH="/app/node_modules/.bin:${PATH}" @@ -44,6 +37,4 @@ RUN echo '#!/bin/sh' > /app/start.sh && \ EXPOSE 8545 -USER hardhat-user - -CMD ["/app/start.sh"] +CMD ["/app/start.sh"] \ No newline at end of file diff --git a/hardhat/contracts/RandomnessUtils.sol b/hardhat/contracts/RandomnessUtils.sol new file mode 100644 index 000000000..3c199e8a2 --- /dev/null +++ b/hardhat/contracts/RandomnessUtils.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +library RandomnessUtils { + using ECDSA for bytes32; + + /** + * @dev Updates a random seed by verifying a VRF proof signature and generating a new seed. + * @param vrfProof The signature proof provided by the signer + * @param currentSeed The current seed value to be used for verification + * @param signer The address that should have signed the message + * @return newSeed The newly generated random seed + */ + function updateRandomSeed( + bytes memory vrfProof, + uint256 currentSeed, + address signer + ) internal pure returns (uint256) { + // Convert seed to bytes32 and create ethereum signed message hash + bytes32 seed = bytes32(currentSeed); + bytes32 hash = MessageHashUtils.toEthSignedMessageHash(seed); + + // Verify the signature + address recoveredSigner = hash.recover(vrfProof); + // Comment next check if you want to skip signature verification + require( + recoveredSigner == signer, + "RandomnessUtils: Invalid signature" + ); + + // Generate and return new seed + return uint256(keccak256(vrfProof)); + } +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/Appeals.sol b/hardhat/contracts/v2_contracts/Appeals.sol new file mode 100644 index 000000000..0c236eb83 --- /dev/null +++ b/hardhat/contracts/v2_contracts/Appeals.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; + +contract Appeals is + Initializable, + Ownable2StepUpgradeable, + ReentrancyGuardUpgradeable, + AccessControlUpgradeable +{ + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + bytes32 public constant VALIDATOR_ROLE = keccak256("VALIDATOR_ROLE"); + + receive() external payable {} + + function initialize() public initializer { + __Ownable2Step_init(); + __ReentrancyGuard_init(); + __AccessControl_init(); + } +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/ConsensusData.sol b/hardhat/contracts/v2_contracts/ConsensusData.sol new file mode 100644 index 000000000..f299f60f8 --- /dev/null +++ b/hardhat/contracts/v2_contracts/ConsensusData.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "./interfaces/ITransactions.sol"; +import "./interfaces/IQueues.sol"; +import "./interfaces/IConsensusMain.sol"; +import "./interfaces/IMessages.sol"; + +contract ConsensusData is + Initializable, + Ownable2StepUpgradeable, + ReentrancyGuardUpgradeable, + AccessControlUpgradeable +{ + ITransactions public transactions; + IQueues public queues; + IConsensusMain public consensusMain; + struct TransactionData { + // Basic transaction info + address sender; + address recipient; + uint256 numOfInitialValidators; + uint256 txSlot; + uint256 timestamp; + uint256 lastVoteTimestamp; + bytes32 randomSeed; + ITransactions.ResultType result; + bytes txData; + bytes txReceipt; + IMessages.SubmittedMessage[] messages; + // // Validator info + address[] validators; + bytes32[] validatorVotesHash; + ITransactions.VoteType[] validatorVotes; + // Queue info + IQueues.QueueType queueType; + uint256 queuePosition; + // // Status info + address activator; + address leader; + ITransactions.TransactionStatus status; + uint256 committedVotesCount; + uint256 revealedVotesCount; + } + + receive() external payable {} + + function initialize( + address _consensusMain, + address _transactions, + address _queues + ) public initializer { + __Ownable2Step_init(); + __ReentrancyGuard_init(); + __AccessControl_init(); + + transactions = ITransactions(_transactions); + queues = IQueues(_queues); + consensusMain = IConsensusMain(_consensusMain); + } + + function getTransactionData( + bytes32 _tx_id + ) external view returns (TransactionData memory) { + ITransactions.Transaction memory transaction = transactions + .getTransaction(_tx_id); + + address activator = consensusMain.txActivator(_tx_id); + uint validatorsCount = consensusMain.validatorsCountForTx(_tx_id); + address[] memory validators = consensusMain.getValidatorsForTx(_tx_id); + uint leaderIndex = consensusMain.txLeaderIndex(_tx_id); + address leader = validatorsCount > 0 + ? validators[leaderIndex] + : address(0); + + TransactionData memory txData = TransactionData({ + // Basic transaction info + sender: transaction.sender, + recipient: transaction.recipient, + numOfInitialValidators: transaction.numOfInitialValidators, + txSlot: transaction.txSlot, + timestamp: transaction.timestamp, + lastVoteTimestamp: transaction.lastVoteTimestamp, + randomSeed: transaction.randomSeed, + result: transaction.result, + txData: transaction.txData, + txReceipt: transaction.txReceipt, + messages: transaction.messages, + // // Validator info + validators: transaction.validators, + validatorVotesHash: transaction.validatorVotesHash, + validatorVotes: transaction.validatorVotes, + // Queue info + queueType: queues.getTransactionQueueType(_tx_id), + queuePosition: queues.getTransactionQueuePosition(_tx_id), + // // Status info + activator: activator, + leader: leader, + status: consensusMain.txStatus(_tx_id), + committedVotesCount: consensusMain.voteCommittedCountForTx(_tx_id), + revealedVotesCount: consensusMain.voteRevealedCountForTx(_tx_id) + }); + + return txData; + } + + // Setter functions + function setTransactions(address _transactions) external onlyOwner { + transactions = ITransactions(_transactions); + } + + function setQueues(address _queues) external onlyOwner { + queues = IQueues(_queues); + } + + function setConsensusMain(address _consensusMain) external onlyOwner { + consensusMain = IConsensusMain(_consensusMain); + } +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/ConsensusMain.sol b/hardhat/contracts/v2_contracts/ConsensusMain.sol index 49480a0ab..967ab1fb5 100644 --- a/hardhat/contracts/v2_contracts/ConsensusMain.sol +++ b/hardhat/contracts/v2_contracts/ConsensusMain.sol @@ -9,6 +9,9 @@ import "./interfaces/ITransactions.sol"; import "./interfaces/IQueues.sol"; import "./interfaces/IGhostFactory.sol"; import "./interfaces/IGenStaking.sol"; +import "./interfaces/IMessages.sol"; +import "../RandomnessUtils.sol"; +import "./utils/Errors.sol"; contract ConsensusMain is Initializable, @@ -16,50 +19,48 @@ contract ConsensusMain is ReentrancyGuardUpgradeable, AccessControlUpgradeable { - error NonGenVMContract(); - error TransactionNotProposing(); - error CallerNotActivator(); - error CallerNotLeader(); - error CallerNotValidator(); - error TransactionNotCommitting(); - error VoteAlreadyCommitted(); - error TransactionNotRevealing(); - error VoteAlreadyRevealed(); - error InvalidVote(); - error TransactionNotAcceptedOrUndetermined(); - bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant VALIDATOR_ROLE = keccak256("VALIDATOR_ROLE"); uint256 public ACCEPTANCE_TIMEOUT; + uint256 public ACTIVATION_TIMEOUT; IGenManager public genManager; ITransactions public genTransactions; IQueues public genQueue; IGhostFactory public ghostFactory; IGenStaking public genStaking; - + IMessages public genMessages; mapping(bytes32 => ITransactions.TransactionStatus) public txStatus; mapping(address => bool) public ghostContracts; mapping(bytes32 => address) public txActivator; mapping(bytes32 => uint) public txLeaderIndex; mapping(bytes32 => address[]) public validatorsForTx; + mapping(bytes32 => uint) public validatorsCountForTx; mapping(bytes32 => mapping(address => bool)) public validatorIsActiveForTx; mapping(bytes32 => mapping(address => bool)) public voteCommittedForTx; mapping(bytes32 => uint) public voteCommittedCountForTx; mapping(bytes32 => mapping(address => bool)) public voteRevealedForTx; + mapping(bytes32 => uint) public voteRevealedCountForTx; + mapping(address => bytes32) public recipientRandomSeed; + mapping(address => uint256) public recipientRandomParallelIndex; // for many addTransaction() in a row event GhostFactorySet(address indexed ghostFactory); event GenManagerSet(address indexed genManager); event GenTransactionsSet(address indexed genTransactions); event GenQueueSet(address indexed genQueue); event GenStakingSet(address indexed genStaking); + event GenMessagesSet(address indexed genMessages); event NewTransaction( bytes32 indexed tx_id, address indexed recipient, address indexed activator ); - event TransactionActivated(bytes32 indexed tx_id, address indexed leader); + event TransactionActivated( + bytes32 indexed tx_id, + address indexed leader, + address[] validators + ); event TransactionReceiptProposed(bytes32 indexed tx_id); event VoteCommitted( bytes32 indexed tx_id, @@ -86,6 +87,13 @@ contract ConsensusMain is } // EXTERNAL FUNCTIONS + + function getValidatorsForTx( + bytes32 _tx_id + ) external view returns (address[] memory) { + return validatorsForTx[_tx_id]; + } + function addTransaction( address _sender, address _recipient, // _recipient GenVM contract address, can be a EOA or a GenVM contract @@ -97,24 +105,32 @@ contract ConsensusMain is } if (_recipient == address(0)) { // Contract deployment transaction - address ghost = ghostFactory.createGhost(); + ghostFactory.createGhost(); + address ghost = ghostFactory.latestGhost(); _storeGhost(ghost); _recipient = ghost; + // Initial random seed for the recipient account + recipientRandomSeed[_recipient] = keccak256( + abi.encodePacked(ghost) + ); + recipientRandomParallelIndex[_recipient] = 1; } else if (!ghostContracts[_recipient]) { - revert NonGenVMContract(); + revert Errors.NonGenVMContract(); } - // TODO: Jose Ignacio: change to VRF - bytes32 randomSeed = keccak256( - abi.encodePacked((block.timestamp / 20) % 1000) - ); // TODO: Change to VRF + bytes32 randomSeed = recipientRandomSeed[_recipient]; // recipient randomSeed is used for activation + uint256 randomIndex = recipientRandomParallelIndex[_recipient]++; // for many addTransaction() in a row bytes32 tx_id = _generateTx( _sender, _recipient, _numOfInitialValidators, - randomSeed, + bytes32(0), // tx randomSeed is set later _txData ); - address activator = _getActivatorForTx(_recipient, randomSeed); + address activator = _getActivatorForTx( + randomSeed, + randomIndex, + block.timestamp + ); // unweighted validator for activation txActivator[tx_id] = activator; txStatus[tx_id] = ITransactions.TransactionStatus.Pending; emit NewTransaction(tx_id, _recipient, activator); @@ -122,7 +138,8 @@ contract ConsensusMain is } function activateTransaction( - bytes32 _tx_id + bytes32 _tx_id, + bytes calldata _vrfProof ) external onlyActivator(_tx_id) { require( txStatus[_tx_id] == ITransactions.TransactionStatus.Pending, @@ -130,27 +147,77 @@ contract ConsensusMain is ); txStatus[_tx_id] = ITransactions.TransactionStatus.Proposing; // genQueue.activateTransaction(_tx_id); - bytes32 randomSeed = genTransactions.getTransactionSeed(_tx_id); + // TODO: change to storage to update randomSeed + ITransactions.Transaction memory _tx = genTransactions.getTransaction( + _tx_id + ); + bytes32 randomSeed = recipientRandomSeed[_tx.recipient]; + // TODO: Possibly not needed to allow for parallel activations + // require( + // genQueue.isAtPendingQueueHead(_tx.recipient, _tx_id), + // "Transaction is not at the pending queue head" + // ); + randomSeed = bytes32( + RandomnessUtils.updateRandomSeed( + _vrfProof, + uint256(randomSeed), + msg.sender + ) + ); + // update recipient randomSeed + recipientRandomSeed[_tx.recipient] = randomSeed; + recipientRandomParallelIndex[_tx.recipient] = 1; // reset + // initialize tx randomSeed + genTransactions.setRandomSeed(_tx_id, randomSeed); + genTransactions.setActivationTimestamp(_tx_id, block.timestamp); ( address[] memory validators, uint leaderIndex - ) = _getValidatorsAndLeaderIndex(_tx_id, randomSeed); + ) = _getValidatorsAndLeaderIndex( + _tx_id, + randomSeed, + block.timestamp, + _tx.numOfInitialValidators, + _tx.consumedValidators + ); txLeaderIndex[_tx_id] = leaderIndex; validatorsForTx[_tx_id] = validators; + validatorsCountForTx[_tx_id] = validators.length; for (uint i = 0; i < validators.length; i++) { validatorIsActiveForTx[_tx_id][validators[i]] = true; } - emit TransactionActivated(_tx_id, validators[leaderIndex]); + emit TransactionActivated(_tx_id, validators[leaderIndex], validators); } function proposeReceipt( bytes32 _tx_id, bytes calldata _txReceipt, - bytes[] calldata _messages + IMessages.SubmittedMessage[] calldata _messages, + bytes calldata _vrfProof ) external onlyLeader(_tx_id) { if (txStatus[_tx_id] != ITransactions.TransactionStatus.Proposing) { - revert TransactionNotProposing(); + revert Errors.TransactionNotProposing(); } + ITransactions.Transaction memory _tx = genTransactions.getTransaction( + _tx_id + ); + // TODO: Possibly *needed* to avoid for parallel receipts + require( + genQueue.isAtPendingQueueHead(_tx.recipient, _tx_id), + "Transaction is not at the pending queue head" + ); + bytes32 randomSeed = recipientRandomSeed[_tx.recipient]; + randomSeed = bytes32( + RandomnessUtils.updateRandomSeed( + _vrfProof, + uint256(randomSeed), + msg.sender + ) + ); + // update recipient randomSeed + recipientRandomSeed[_tx.recipient] = randomSeed; + recipientRandomParallelIndex[_tx.recipient] = 1; // reset + txStatus[_tx_id] = ITransactions.TransactionStatus.Committing; genTransactions.proposeTransactionReceipt( _tx_id, @@ -163,14 +230,16 @@ contract ConsensusMain is function commitVote( bytes32 _tx_id, - bytes32 _commitHash + bytes32 _commitHash, + bool _isAppeal ) external onlyValidator(_tx_id) { if (txStatus[_tx_id] != ITransactions.TransactionStatus.Committing) { - revert TransactionNotCommitting(); + revert Errors.TransactionNotCommitting(); } if (voteCommittedForTx[_tx_id][msg.sender]) { - revert VoteAlreadyCommitted(); + revert Errors.VoteAlreadyCommitted(); } + voteCommittedForTx[_tx_id][msg.sender] = true; genTransactions.commitVote(_tx_id, _commitHash, msg.sender); voteCommittedCountForTx[_tx_id]++; bool isLastVote = voteCommittedCountForTx[_tx_id] == @@ -187,18 +256,23 @@ contract ConsensusMain is ITransactions.VoteType _voteType, uint _nonce ) external onlyValidator(_tx_id) { + ITransactions.Transaction memory _tx = genTransactions.getTransaction( + _tx_id + ); if (txStatus[_tx_id] != ITransactions.TransactionStatus.Revealing) { - revert TransactionNotRevealing(); + revert Errors.TransactionNotRevealing(); } if (voteRevealedForTx[_tx_id][msg.sender]) { - revert VoteAlreadyRevealed(); + revert Errors.VoteAlreadyRevealed(); } if ( keccak256(abi.encodePacked(msg.sender, _voteType, _nonce)) != _voteHash ) { - revert InvalidVote(); + revert Errors.InvalidVote(); } + voteRevealedForTx[_tx_id][msg.sender] = true; + voteRevealedCountForTx[_tx_id]++; (bool isLastVote, ITransactions.ResultType result) = genTransactions .revealVote(_tx_id, _voteHash, _voteType, msg.sender); if (isLastVote) { @@ -208,9 +282,17 @@ contract ConsensusMain is result == ITransactions.ResultType.NoMajority ) { txStatus[_tx_id] = ITransactions.TransactionStatus.Undetermined; + genQueue.addTransactionToUndeterminedQueue( + _tx.recipient, + _tx_id + ); } else { txStatus[_tx_id] = ITransactions.TransactionStatus.Accepted; + genQueue.addTransactionToAcceptedQueue(_tx.recipient, _tx_id); emit TransactionAccepted(_tx_id); + if (genTransactions.hasOnAcceptanceMessages(_tx_id)) { + genMessages.emitMessagesOnAcceptance(_tx_id); + } } } emit VoteRevealed(_tx_id, msg.sender, _voteType, isLastVote, result); @@ -221,18 +303,48 @@ contract ConsensusMain is txStatus[_tx_id] != ITransactions.TransactionStatus.Accepted && txStatus[_tx_id] != ITransactions.TransactionStatus.Undetermined ) { - revert TransactionNotAcceptedOrUndetermined(); + revert Errors.TransactionNotAcceptedOrUndetermined(); } uint lastVoteTimestamp = genTransactions .getTransactionLastVoteTimestamp(_tx_id); if (block.timestamp - lastVoteTimestamp > ACCEPTANCE_TIMEOUT) { txStatus[_tx_id] = ITransactions.TransactionStatus.Finalized; // TODO: Emit the messages + if (genTransactions.hasMessagesOnFinalization(_tx_id)) { + genMessages.emitMessagesOnFinalization(_tx_id); + } emit TransactionFinalized(_tx_id); - genTransactions.emitMessagesOnFinalization(_tx_id); } } + function submitAppeal(bytes32 _tx_id) external payable { + if ( + txStatus[_tx_id] != ITransactions.TransactionStatus.Undetermined && + txStatus[_tx_id] != ITransactions.TransactionStatus.Accepted + ) { + revert Errors.CanNotAppeal(); + } + uint minBond = genTransactions.getMinAppealBond(_tx_id); + if (msg.value < minBond) { + revert Errors.AppealBondTooLow(); + } + txStatus[_tx_id] = ITransactions.TransactionStatus.Appealed; + // Select validators for appeal + // Update the number of validators in the appeal + // Start appeal voting + } + + function executeMessage( + address _recipient, + uint _value, + bytes memory _data + ) external returns (bool success) { + if (msg.sender != address(genMessages) && msg.sender != owner()) { + revert Errors.CallerNotMessages(); + } + (success, ) = _recipient.call{ value: _value }(_data); + } + // INTERNAL FUNCTIONS function _storeGhost(address _ghost) internal { ghostContracts[_ghost] = true; @@ -260,32 +372,49 @@ contract ConsensusMain is numOfInitialValidators: _numOfInitialValidators, txSlot: txSlot, timestamp: block.timestamp, + lastModification: block.timestamp, lastVoteTimestamp: 0, + activationTimestamp: 0, randomSeed: _randomSeed, + onAcceptanceMessages: false, result: ITransactions.ResultType(0), txData: _txData, txReceipt: new bytes(0), - messages: new bytes[](0), + messages: new IMessages.SubmittedMessage[](0), validators: new address[](0), validatorVotesHash: new bytes32[](0), - validatorVotes: new ITransactions.VoteType[](0) + validatorVotes: new ITransactions.VoteType[](0), + consumedValidators: new address[](0) }) ); } function _getActivatorForTx( - address _recipient, - bytes32 _randomSeed + bytes32 _randomSeed, + uint256 _randomIndex, + uint256 timestamp ) internal view returns (address validator) { // TODO: Get a single validator from the list of validators based on the random seed - validator = genStaking.getActivatorForTx(_recipient, _randomSeed); + bytes32 combinedSeed = keccak256( + abi.encodePacked(_randomSeed, _randomIndex) + ); + validator = genStaking.getActivatorForSeed(combinedSeed, timestamp); } function _getValidatorsAndLeaderIndex( bytes32 _tx_id, - bytes32 _randomSeed - ) internal view returns (address[] memory validators, uint256 leaderIndex) { - validators = genStaking.getValidatorsForTx(_tx_id, _randomSeed); + bytes32 _randomSeed, + uint256 timestamp, + uint256 numValidators, + address[] memory consumedValidators + ) internal returns (address[] memory validators, uint256 leaderIndex) { + validators = genStaking.getValidatorsForTx( + _tx_id, + _randomSeed, + timestamp, + numValidators, + consumedValidators + ); // TODO: Get the leader index from random number generator leaderIndex = 0; } @@ -316,31 +445,42 @@ contract ConsensusMain is emit GenStakingSet(_genStaking); } + function setGenMessages(address _genMessages) external onlyOwner { + genMessages = IMessages(_genMessages); + emit GenMessagesSet(_genMessages); + } + function setAcceptanceTimeout( uint256 _acceptanceTimeout ) external onlyOwner { ACCEPTANCE_TIMEOUT = _acceptanceTimeout; } + function setActivationTimeout( + uint256 _activationTimeout + ) external onlyOwner { + ACTIVATION_TIMEOUT = _activationTimeout; + } + // MODIFIERS modifier onlyActivator(bytes32 _tx_id) { if (txActivator[_tx_id] != msg.sender) { - revert CallerNotActivator(); + revert Errors.CallerNotActivator(); } _; } modifier onlyLeader(bytes32 _tx_id) { if (validatorsForTx[_tx_id][txLeaderIndex[_tx_id]] != msg.sender) { - revert CallerNotLeader(); + revert Errors.CallerNotLeader(); } _; } modifier onlyValidator(bytes32 _tx_id) { if (!validatorIsActiveForTx[_tx_id][msg.sender]) { - revert CallerNotValidator(); + revert Errors.CallerNotValidator(); } _; } -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/ConsensusManager.sol b/hardhat/contracts/v2_contracts/ConsensusManager.sol index edb531685..eb9e60ee7 100644 --- a/hardhat/contracts/v2_contracts/ConsensusManager.sol +++ b/hardhat/contracts/v2_contracts/ConsensusManager.sol @@ -21,4 +21,4 @@ contract ConsensusManager is __ReentrancyGuard_init(); __AccessControl_init(); } -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/Messages.sol b/hardhat/contracts/v2_contracts/Messages.sol new file mode 100644 index 000000000..453453e3e --- /dev/null +++ b/hardhat/contracts/v2_contracts/Messages.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "./interfaces/ITransactions.sol"; +import "./interfaces/IGenConsensus.sol"; + +contract Messages is + Initializable, + Ownable2StepUpgradeable, + ReentrancyGuardUpgradeable, + AccessControlUpgradeable +{ + event MessagesOnAcceptance(bytes32 indexed tx_id); + event MessagesOnFinalization(bytes32 indexed tx_id); + event GenTransactionsSet(address indexed genTransactions); + event GenConsensusSet(address indexed genConsensus); + + ITransactions public genTransactions; + IGenConsensus public genConsensus; + mapping(address => uint) public ghostNonce; + error CallerNotConsensus(); + + receive() external payable {} + + function initialize() public initializer { + __Ownable_init(msg.sender); + __Ownable2Step_init(); + __ReentrancyGuard_init(); + __AccessControl_init(); + } + + /** + * @notice Emits messages marked for acceptance for a given transaction + * @dev Only callable by consensus contract. Processes messages marked with onAcceptance=true + * @param _tx_id The ID of the transaction whose messages should be emitted + */ + function emitMessagesOnAcceptance(bytes32 _tx_id) external onlyConsensus { + ITransactions.Transaction memory transaction = genTransactions + .getTransaction(_tx_id); + + for (uint i = 0; i < transaction.messages.length; i++) { + IMessages.SubmittedMessage memory message = transaction.messages[i]; + if (!message.onAcceptance) { + continue; + } + _handleMessage(message, transaction.recipient); + + // Update the count of already emitted messages + emit MessagesOnAcceptance(_tx_id); + } + } + + /** + * @notice Emits messages marked for finalization for a given transaction + * @dev Only callable by consensus contract. Processes messages marked with onAcceptance=false + * @param _tx_id The ID of the transaction whose messages should be emitted + */ + function emitMessagesOnFinalization(bytes32 _tx_id) external onlyConsensus { + ITransactions.Transaction memory transaction = genTransactions + .getTransaction(_tx_id); + + for (uint i = 0; i < transaction.messages.length; i++) { + IMessages.SubmittedMessage memory message = transaction.messages[i]; + if (message.onAcceptance) { + continue; + } + _handleMessage(message, transaction.recipient); + + // Update the count of already emitted messages + emit MessagesOnFinalization(_tx_id); + } + } + + function _handleMessage( + IMessages.SubmittedMessage memory message, + address recipient + ) internal { + bytes memory messageBytes = message.message; + // Decode the message data + (address to, bytes memory data) = abi.decode( + messageBytes, + (address, bytes) + ); + bytes32 msgId = keccak256(abi.encode(to, data)); + // Call handleOp on the recipient + bool success = genConsensus.executeMessage( + recipient, + 0, + abi.encodeWithSignature( + "handleOp(address,bytes32,uint256,bytes)", + to, + msgId, + ghostNonce[recipient] + 1, + data + ) + ); + require(success, "handleOp call failed"); + ghostNonce[recipient]++; + } + + function _handleMessagePacked( + IMessages.SubmittedMessage memory message, + address recipient + ) internal { + // Extract the first 20 bytes for address (without using slice) + address to; + bytes memory data; + bytes memory messageBytes = message.message; + assembly { + // Load the first 20 bytes into 'to' - corrected offset + // We need to right-shift by 12 bytes (96 bits) to get the correct 20 bytes + to := shr(96, mload(add(messageBytes, 0x20))) + + // Create a new bytes array for the remaining data + let remaining_length := sub(mload(messageBytes), 20) + data := mload(0x40) // get free memory pointer + mstore(data, remaining_length) // store length + mstore(0x40, add(add(data, 0x20), remaining_length)) // update free memory pointer + + // Copy remaining bytes + let srcPtr := add(add(messageBytes, 0x20), 20) + let destPtr := add(data, 0x20) + for { + let i := 0 + } lt(i, remaining_length) { + i := add(i, 32) + } { + mstore(add(destPtr, i), mload(add(srcPtr, i))) + } + } + bytes32 msgId = keccak256(abi.encode(to, data)); + + // Call handleOp on the recipient + (bool success, ) = recipient.call{ value: 0 }( + abi.encodeWithSignature( + "handleOp(address,bytes32,bytes)", + to, + msgId, + data + ) + ); + + require(success, "handleOp call failed"); + } + + function setGenTransactions(address _genTransactions) external onlyOwner { + genTransactions = ITransactions(_genTransactions); + emit GenTransactionsSet(_genTransactions); + } + + function setGenConsensus(address _genConsensus) external onlyOwner { + genConsensus = IGenConsensus(_genConsensus); + emit GenConsensusSet(_genConsensus); + } + + modifier onlyConsensus() { + if (msg.sender != address(genConsensus)) { + revert CallerNotConsensus(); + } + _; + } +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/Queues.sol b/hardhat/contracts/v2_contracts/Queues.sol index 10191d272..9be1df2bb 100644 --- a/hardhat/contracts/v2_contracts/Queues.sol +++ b/hardhat/contracts/v2_contracts/Queues.sol @@ -4,6 +4,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "./interfaces/IQueues.sol"; contract Queues is Initializable, @@ -11,17 +12,11 @@ contract Queues is ReentrancyGuardUpgradeable, AccessControlUpgradeable { - enum QueueType { - Pending, - Accepted, - Undetermined - } - error NotConsensus(); address public genConsensus; mapping(address => mapping(bytes32 => uint)) public recipientToTxIdToSlot; - mapping(address => mapping(bytes32 => QueueType)) + mapping(address => mapping(bytes32 => IQueues.QueueType)) public recipientToTxIdToQueueType; mapping(address => uint) public recipientToPendingQueueHead; mapping(address => uint) public recipientToPendingQueueTail; @@ -30,6 +25,7 @@ contract Queues is mapping(address => uint) public recipientToUndeterminedQueueHead; mapping(address => uint) public recipientToUndeterminedQueueTail; + event GenConsensusSet(address indexed genConsensus); function initialize(address _genConsensus) public initializer { __Ownable_init(msg.sender); __Ownable2Step_init(); @@ -38,11 +34,23 @@ contract Queues is genConsensus = _genConsensus; } + function getTransactionQueueType( + bytes32 txId + ) external view returns (IQueues.QueueType) { + return recipientToTxIdToQueueType[msg.sender][txId]; + } + + function getTransactionQueuePosition( + bytes32 txId + ) external view returns (uint) { + return recipientToTxIdToSlot[msg.sender][txId]; + } + function addTransactionToPendingQueue( address recipient, bytes32 txId ) external onlyConsensus returns (uint slot) { - recipientToTxIdToQueueType[recipient][txId] = QueueType.Pending; + recipientToTxIdToQueueType[recipient][txId] = IQueues.QueueType.Pending; slot = recipientToPendingQueueTail[recipient]; recipientToTxIdToSlot[recipient][txId] = slot; recipientToPendingQueueTail[recipient]++; @@ -53,7 +61,9 @@ contract Queues is bytes32 txId ) external onlyConsensus returns (uint slot) { // Remove from pending queue by setting slot to max uint - recipientToTxIdToQueueType[recipient][txId] = QueueType.Accepted; + recipientToTxIdToQueueType[recipient][txId] = IQueues + .QueueType + .Accepted; recipientToPendingQueueHead[recipient]++; slot = recipientToAcceptedQueueTail[recipient]; recipientToTxIdToSlot[recipient][txId] = slot; @@ -65,17 +75,60 @@ contract Queues is bytes32 txId ) external onlyConsensus returns (uint slot) { // Remove from pending queue by setting slot to max uint - recipientToTxIdToQueueType[recipient][txId] = QueueType.Undetermined; + recipientToTxIdToQueueType[recipient][txId] = IQueues + .QueueType + .Undetermined; recipientToPendingQueueHead[recipient]++; slot = recipientToUndeterminedQueueTail[recipient]; recipientToTxIdToSlot[recipient][txId] = slot; recipientToUndeterminedQueueTail[recipient]++; } + function setGenConsensus(address _genConsensus) external onlyOwner { + genConsensus = _genConsensus; + emit GenConsensusSet(_genConsensus); + } + modifier onlyConsensus() { if (msg.sender != genConsensus) { revert NotConsensus(); } _; } -} + + function isAtPendingQueueHead( + address recipient, + bytes32 txId + ) external view returns (bool) { + uint slot = recipientToTxIdToSlot[recipient][txId]; + return slot == recipientToPendingQueueHead[recipient]; + } + + // function setRecipientRandomSeed( + // address recipient, + // bytes32 randomSeed + // ) external { + // _setRecipientRandomSeed(recipient, randomSeed); + // } + + // function _setRecipientRandomSeed( + // address recipient, + // bytes32 randomSeed + // ) internal { + // recipientRandomSeed[recipient] = randomSeed; + // } + + // function getRecipientRandomSeed( + // address recipient + // ) external view returns (bytes32) { + // return _getRecipientRandomSeed(recipient); + // } + + // function _getRecipientRandomSeed(address recipient) + // internal + // view + // returns (bytes32) + // { + // return recipientRandomSeed[recipient]; + // } +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/Transactions.sol b/hardhat/contracts/v2_contracts/Transactions.sol index b23d073d9..2f4cfc519 100644 --- a/hardhat/contracts/v2_contracts/Transactions.sol +++ b/hardhat/contracts/v2_contracts/Transactions.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "./interfaces/ITransactions.sol"; +import "./interfaces/IMessages.sol"; contract Transactions is Initializable, @@ -19,6 +20,7 @@ contract Transactions is error VoteAlreadyCommitted(); mapping(bytes32 => ITransactions.Transaction) public transactions; mapping(bytes32 => mapping(address => uint)) public validatorIndexInTx; + mapping(bytes32 => uint) public alreadyEmittedMessages; address public genConsensus; @@ -34,6 +36,27 @@ contract Transactions is genConsensus = _genConsensus; } + function hasOnAcceptanceMessages( + bytes32 _tx_id + ) external view returns (bool itHasMessagesOnAcceptance) { + itHasMessagesOnAcceptance = transactions[_tx_id].onAcceptanceMessages; + } + + function hasMessagesOnFinalization( + bytes32 _tx_id + ) external view returns (bool itHasMessagesOnFinalization) { + itHasMessagesOnFinalization = + transactions[_tx_id].messages.length - + alreadyEmittedMessages[_tx_id] > + 0; + } + + function getMinAppealBond( + bytes32 _tx_id + ) external view returns (uint256 minAppealBond) { + minAppealBond = _calculateMinAppealBond(_tx_id); + } + function addNewTransaction( bytes32 txId, ITransactions.Transaction memory newTx @@ -45,10 +68,16 @@ contract Transactions is function proposeTransactionReceipt( bytes32 _tx_id, bytes calldata _txReceipt, - bytes[] calldata _messages + IMessages.SubmittedMessage[] calldata _messages ) external onlyGenConsensus { transactions[_tx_id].txReceipt = _txReceipt; transactions[_tx_id].messages = _messages; + for (uint i = 0; i < _messages.length; i++) { + if (_messages[i].onAcceptance) { + transactions[_tx_id].onAcceptanceMessages = true; + break; + } + } } function commitVote( @@ -141,35 +170,48 @@ contract Transactions is } } - function emitMessagesOnFinalization(bytes32 txId) external { - // TODO: Emit the messages - } - function getTransaction( bytes32 txId ) external view returns (ITransactions.Transaction memory) { return transactions[txId]; } - function getTransactionSeed(bytes32 txId) external view returns (bytes32) { - return transactions[txId].randomSeed; - } - function getTransactionLastVoteTimestamp( bytes32 txId ) external view returns (uint256) { return transactions[txId].lastVoteTimestamp; } + function _calculateMinAppealBond( + bytes32 _tx_id + ) internal view returns (uint256 minAppealBond) { + // TODO: Implement the logic to calculate the minimum appeal bond + minAppealBond = 0; + } + function setGenConsensus(address _genConsensus) external onlyOwner { genConsensus = _genConsensus; emit GenConsensusSet(_genConsensus); } + function setRandomSeed( + bytes32 txId, + bytes32 randomSeed + ) external onlyGenConsensus { + transactions[txId].randomSeed = randomSeed; + } + + function setActivationTimestamp( + bytes32 txId, + uint256 timestamp + ) external onlyGenConsensus { + transactions[txId].activationTimestamp = timestamp; + } + modifier onlyGenConsensus() { if (msg.sender != genConsensus) { revert CallerNotConsensus(); } _; } -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/ghost_contracts/GhostBlueprint.sol b/hardhat/contracts/v2_contracts/ghost_contracts/GhostBlueprint.sol index e2f62e96f..e7efe94f4 100644 --- a/hardhat/contracts/v2_contracts/ghost_contracts/GhostBlueprint.sol +++ b/hardhat/contracts/v2_contracts/ghost_contracts/GhostBlueprint.sol @@ -40,26 +40,23 @@ contract GhostBlueprint is Initializable, OwnableUpgradeable { function handleOp( address to, bytes32 msgId, + uint expectedNonce, bytes calldata data ) external payable onlyOwner { - (uint expectedNonce, bytes memory callData) = abi.decode( - data, - (uint256, bytes) - ); require(nonce == expectedNonce, "Invalid nonce"); if (msg.value > 0) { - (bool success, ) = to.call{ value: msg.value }(callData); + (bool success, ) = to.call{ value: msg.value }(data); require(success, "Transaction failed"); } else { - (bool success, ) = to.call(callData); + (bool success, ) = to.call(data); require(success, "Transaction failed"); } - emit TransactionExecuted(to, msgId, callData); + emit TransactionExecuted(to, msgId, data); // Increment nonce to prevent replay attacks nonce += 1; } // EVENTS event TransactionExecuted(address indexed to, bytes32 msgId, bytes data); -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/ghost_contracts/GhostFactory.sol b/hardhat/contracts/v2_contracts/ghost_contracts/GhostFactory.sol index b4604fbe1..8e37fa0d5 100644 --- a/hardhat/contracts/v2_contracts/ghost_contracts/GhostFactory.sol +++ b/hardhat/contracts/v2_contracts/ghost_contracts/GhostFactory.sol @@ -18,6 +18,9 @@ contract GhostFactory is address public ghostManager; address public genConsensus; + mapping(address => bool) public ghostContracts; + address public latestGhost; + error CallerNotConsensus(); receive() external payable {} @@ -39,6 +42,8 @@ contract GhostFactory is ); emit GhostCreated(address(beacon)); GhostBlueprint(payable(address(beacon))).transferOwnership(msg.sender); + latestGhost = address(beacon); + ghostContracts[address(beacon)] = true; return address(beacon); } @@ -54,6 +59,10 @@ contract GhostFactory is ghostBlueprint = _ghostBlueprint; } + function isGhost(address _ghost) external view returns (bool) { + return ghostContracts[_ghost]; + } + function upgradeBeacon(address newImplementation) external onlyOwner { UpgradeableBeacon(ghostBeaconProxy).upgradeTo(newImplementation); emit BeaconUpgraded(newImplementation); @@ -82,4 +91,4 @@ contract GhostFactory is event BeaconUpgraded(address indexed implementation); event GenConsensusSet(address indexed genConsensus); event GhostManagerSet(address indexed ghostManager); -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IConsensusMain.sol b/hardhat/contracts/v2_contracts/interfaces/IConsensusMain.sol index 8209e8214..942689657 100644 --- a/hardhat/contracts/v2_contracts/interfaces/IConsensusMain.sol +++ b/hardhat/contracts/v2_contracts/interfaces/IConsensusMain.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import "./ITransactions.sol"; interface IConsensusMain { struct addTransactionParams { @@ -7,15 +8,59 @@ interface IConsensusMain { bytes data; } + function txStatus( + bytes32 _tx_id + ) external view returns (ITransactions.TransactionStatus); + + function txActivator(bytes32 _tx_id) external view returns (address); + + function txLeaderIndex(bytes32 _tx_id) external view returns (uint); + + function validatorsCountForTx(bytes32 _tx_id) external view returns (uint); + + function getValidatorsForTx( + bytes32 _tx_id + ) external view returns (address[] memory); + + function voteCommittedCountForTx( + bytes32 _tx_id + ) external view returns (uint); + + function voteRevealedCountForTx( + bytes32 _tx_id + ) external view returns (uint); + + function validatorIsActiveForTx( + bytes32 _tx_id, + address _validator + ) external view returns (bool); + + function voteCommittedForTx( + bytes32 _tx_id, + address _validator + ) external view returns (bool); + function addTransaction(bytes memory _transaction) external; - function activateTransaction(bytes32 _tx_id) external; + function isCurrentActivator( + uint256 lastModification, + uint256 addedTimestamp, + bytes32 randomSeed + ) external view returns (bool); + + function activateTransaction( + bytes32 _tx_id, + bytes calldata _vrfProof + ) external; - function proposeReceipt(bytes memory _receipt) external; + function proposeReceipt( + bytes memory _receipt, + bytes calldata _vrfProof + ) external; function commitVote(bytes32 _tx_id, bytes32 _voteHash) external; function revealVote(bytes32 _tx_id, bytes32 _voteHash) external; function finalizeTransaction(bytes32 _tx_id) external; -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IGenConsensus.sol b/hardhat/contracts/v2_contracts/interfaces/IGenConsensus.sol new file mode 100644 index 000000000..c7e3cad8d --- /dev/null +++ b/hardhat/contracts/v2_contracts/interfaces/IGenConsensus.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IGenConsensus { + function executeMessage( + address _recipient, + uint _value, + bytes memory _data + ) external returns (bool success); +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IGenLayerStaking.sol b/hardhat/contracts/v2_contracts/interfaces/IGenLayerStaking.sol deleted file mode 100644 index c8a3d15ee..000000000 --- a/hardhat/contracts/v2_contracts/interfaces/IGenLayerStaking.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IGenLayerStaking { - function stake() external payable; - function unstake(uint256 amount) external; - function delegate(address validator) external; - function undelegate() external; - function balanceOfBase(address account) external view returns (uint256); - function setHotWallet(address hotWallet) external; - function claim() external; - function accumulateTxFeeRewards( - uint256 txFeeAmount, - address[] memory validators - ) external; - function getInflationOverSeconds( - uint256 secs - ) external view returns (uint256); -} diff --git a/hardhat/contracts/v2_contracts/interfaces/IGenManager.sol b/hardhat/contracts/v2_contracts/interfaces/IGenManager.sol index 4386aa79f..c9bb6149f 100644 --- a/hardhat/contracts/v2_contracts/interfaces/IGenManager.sol +++ b/hardhat/contracts/v2_contracts/interfaces/IGenManager.sol @@ -8,4 +8,4 @@ interface IGenManager { function isGenVMContract( address contractAddress ) external view returns (bool); -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IGenStaking.sol b/hardhat/contracts/v2_contracts/interfaces/IGenStaking.sol index 027b88fcd..8948a6094 100644 --- a/hardhat/contracts/v2_contracts/interfaces/IGenStaking.sol +++ b/hardhat/contracts/v2_contracts/interfaces/IGenStaking.sol @@ -4,23 +4,47 @@ pragma solidity ^0.8.20; interface IGenStaking { /** * @notice Gets the validator that should activate a transaction for a specific recipient - * @param _recipient The address of the contract receiving the transaction * @param _randomSeed A random seed used to select the validator + * @param timestamp The added timestemp of the transaction * @return validator The address of the selected validator */ - function getActivatorForTx( - address _recipient, - bytes32 _randomSeed + function getActivatorForSeed( + bytes32 _randomSeed, + uint256 timestamp ) external view returns (address validator); /** * @notice Gets the validators that should participate in a transaction * @param _tx_id The id of the transaction * @param _randomSeed A random seed used to select the validators + * @param timestamp The added timestemp of the transaction + * @param numValidators The number of validators to select * @return validators The addresses of the selected validators + * @param consumedValidators The addresses of the validators that have already participated in the transaction */ function getValidatorsForTx( bytes32 _tx_id, - bytes32 _randomSeed + bytes32 _randomSeed, + uint256 timestamp, + uint256 numValidators, + address[] memory consumedValidators ) external view returns (address[] memory validators); -} + + /** + * @notice Gets the length of the validators that should participate in a transaction + * @param timestamp The added timestemp of the transaction + */ + function getValidatorsLen( + uint256 timestamp + ) external view returns (uint256); + + /** + * @notice Gets the validator at a specific index + * @param index The index of the validator + * @param timestamp The added timestemp of the transaction + */ + function getValidatorsItem( + uint256 index, + uint256 timestamp + ) external view returns (address); +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IGhostFactory.sol b/hardhat/contracts/v2_contracts/interfaces/IGhostFactory.sol index f7cf04333..4be5d2d80 100644 --- a/hardhat/contracts/v2_contracts/interfaces/IGhostFactory.sol +++ b/hardhat/contracts/v2_contracts/interfaces/IGhostFactory.sol @@ -12,4 +12,8 @@ interface IGhostFactory { /// @param contractAddress The address to check /// @return bool True if the address is a ghost contract function isGhost(address contractAddress) external view returns (bool); -} + + /// @notice Returns the latest ghost contract address + /// @return The address of the latest ghost contract + function latestGhost() external view returns (address); +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IMessages.sol b/hardhat/contracts/v2_contracts/interfaces/IMessages.sol new file mode 100644 index 000000000..73cead54d --- /dev/null +++ b/hardhat/contracts/v2_contracts/interfaces/IMessages.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IMessages { + struct SubmittedMessage { + address recipient; + bool externalMessage; + bool onAcceptance; + bytes message; + } + + function emitMessagesOnAcceptance(bytes32 _tx_id) external; + + function emitMessagesOnFinalization(bytes32 _tx_id) external; +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/IQueues.sol b/hardhat/contracts/v2_contracts/interfaces/IQueues.sol index 500a913f6..cd22fc02c 100644 --- a/hardhat/contracts/v2_contracts/interfaces/IQueues.sol +++ b/hardhat/contracts/v2_contracts/interfaces/IQueues.sol @@ -4,12 +4,19 @@ pragma solidity ^0.8.20; import "./ITransactions.sol"; interface IQueues { - function addTransactionToPendingQueue( - address recipient, + enum QueueType { + Pending, + Accepted, + Undetermined + } + + function getTransactionQueueType( bytes32 txId - ) external returns (uint256); + ) external view returns (QueueType); - function activateTransaction(bytes32 txId) external; + function getTransactionQueuePosition( + bytes32 txId + ) external view returns (uint); function getTransactionActivator( bytes32 txId @@ -46,4 +53,34 @@ interface IQueues { function isAcceptanceTimeoutExpired( bytes32 txId ) external view returns (bool); -} + + function addTransactionToPendingQueue( + address recipient, + bytes32 txId + ) external returns (uint256); + + function activateTransaction(bytes32 txId) external; + // function setRecipientRandomSeed( + // address recipient, + // bytes32 randomSeed + // ) public; + + // function getRecipientRandomSeed( + // address recipient + // ) public view returns (bytes32); + + function isAtPendingQueueHead( + address recipient, + bytes32 txId + ) external view returns (bool); + + function addTransactionToAcceptedQueue( + address recipient, + bytes32 txId + ) external returns (uint slot); + + function addTransactionToUndeterminedQueue( + address recipient, + bytes32 txId + ) external returns (uint slot); +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/interfaces/ITransactions.sol b/hardhat/contracts/v2_contracts/interfaces/ITransactions.sol index 0693d57e1..c160fb9c3 100644 --- a/hardhat/contracts/v2_contracts/interfaces/ITransactions.sol +++ b/hardhat/contracts/v2_contracts/interfaces/ITransactions.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import "./IMessages.sol"; + interface ITransactions { struct Transaction { address sender; @@ -8,26 +10,30 @@ interface ITransactions { uint256 numOfInitialValidators; uint256 txSlot; uint256 timestamp; + uint256 activationTimestamp; + uint256 lastModification; uint256 lastVoteTimestamp; bytes32 randomSeed; + bool onAcceptanceMessages; ResultType result; bytes txData; bytes txReceipt; - bytes[] messages; + IMessages.SubmittedMessage[] messages; address[] validators; bytes32[] validatorVotesHash; VoteType[] validatorVotes; + address[] consumedValidators; } enum TransactionStatus { Pending, - Canceled, Proposing, Committing, Revealing, Accepted, - Finalized, Undetermined, + Finalized, + Canceled, Appealed } enum VoteType { @@ -59,15 +65,25 @@ interface ITransactions { bytes32 txId ) external view returns (Transaction memory); + function hasOnAcceptanceMessages( + bytes32 _tx_id + ) external view returns (bool itHasMessagesOnAcceptance); + + function hasMessagesOnFinalization( + bytes32 _tx_id + ) external view returns (bool itHasMessagesOnFinalization); + function isVoteCommitted( bytes32 _tx_id, address _validator ) external view returns (bool); + function getMinAppealBond(bytes32 _tx_id) external view returns (uint256); + function proposeTransactionReceipt( bytes32 _tx_id, bytes calldata _txReceipt, - bytes[] calldata _messages + IMessages.SubmittedMessage[] calldata _messages ) external; function commitVote( @@ -88,4 +104,8 @@ interface ITransactions { function getTransactionLastVoteTimestamp( bytes32 _tx_id ) external view returns (uint256); -} + + function setRandomSeed(bytes32 txId, bytes32 randomSeed) external; + + function setActivationTimestamp(bytes32 txId, uint256 timestamp) external; +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/test-helpers/MockGenStaking.sol b/hardhat/contracts/v2_contracts/test-helpers/MockGenStaking.sol index 492c8cec6..8b88ef5aa 100644 --- a/hardhat/contracts/v2_contracts/test-helpers/MockGenStaking.sol +++ b/hardhat/contracts/v2_contracts/test-helpers/MockGenStaking.sol @@ -1,39 +1,120 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +error NoValidatorsAvailable(); +error NotEnoughValidators(); +error ZeroTotalWeight(); + contract MockGenStaking { + address public owner; address[] public validators; + address[] public topStakersHot; + address[] public previousTopStakersHot; + uint256[] public accumulatedWeights; + uint256[] public previousAccumulatedWeights; + uint256 public topStakersTotalWeight; + uint256 public previousTopStakersTotalWeight; mapping(address => bool) public isValidator; + // a bit longer than the real one to make sure we don't get any edge cases + uint256 public FINALIZATION_INTERVAL = 1 hours + 10 minutes; + + struct ValidatorBan { + bool isBanned; + uint256 banEndTime; + } + + mapping(address => ValidatorBan) public validatorBans; + mapping(address => address) public hotToColdWallet; + mapping(address => address) public coldToHotWallet; + constructor(address _initialValidator) { - _addValidator(_initialValidator); + owner = msg.sender; + // _addValidator(_initialValidator); } function _addValidator(address _validator) private { if (!isValidator[_validator]) { validators.push(_validator); + topStakersHot.push(_validator); + previousTopStakersHot.push(_validator); isValidator[_validator] = true; + + // Set up hot/cold wallet mappings where both are the same address + hotToColdWallet[_validator] = _validator; + coldToHotWallet[_validator] = _validator; + + uint256 newWeight; + if (accumulatedWeights.length == 0) { + // If this is the first validator, start with 100 GEN (reduced from 2000 to prevent overflow) + newWeight = 100 * 1e18; + } else { + // For subsequent validators, subtract 1 GEN from the last weight + uint256 lastWeight = accumulatedWeights[ + accumulatedWeights.length - 1 + ]; + newWeight = lastWeight > 1e18 ? lastWeight - 1e18 : 0; + } + + uint256 cumulativeWeight = newWeight; + if (accumulatedWeights.length > 0) { + cumulativeWeight += accumulatedWeights[ + accumulatedWeights.length - 1 + ]; + } + + accumulatedWeights.push(cumulativeWeight); + previousAccumulatedWeights.push(cumulativeWeight); + + // Update total weights based on the new cumulative weight + topStakersTotalWeight = cumulativeWeight; + previousTopStakersTotalWeight = cumulativeWeight; } } - function addValidator(address _validator) external { - _addValidator(_validator); - } + function addValidators(address[] calldata _newValidators) external { + uint256 cumulativeWeight = 0; + + // Process each validator + for (uint256 i = 0; i < _newValidators.length; i++) { + address validator = _newValidators[i]; + if (!isValidator[validator]) { + validators.push(validator); + topStakersHot.push(validator); + previousTopStakersHot.push(validator); + isValidator[validator] = true; - function addValidators(address[] calldata _validators) external { - for (uint i = 0; i < _validators.length; i++) { - _addValidator(_validators[i]); + // Calculate weight for this validator - weight decreases with index + // Using smaller numbers to prevent overflow + uint256 weight = i < 5 ? (5 - i) * 1e17 : 1e16; + cumulativeWeight += weight; + + accumulatedWeights.push(cumulativeWeight); + previousAccumulatedWeights.push(cumulativeWeight); + } } + + topStakersTotalWeight = cumulativeWeight; + previousTopStakersTotalWeight = cumulativeWeight; } function removeValidator(address _validator) external { require(isValidator[_validator], "Validator not found"); - for (uint i = 0; i < validators.length; i++) { + for (uint256 i = 0; i < validators.length; i++) { if (validators[i] == _validator) { validators[i] = validators[validators.length - 1]; validators.pop(); isValidator[_validator] = false; + + // Update weights + if (i < accumulatedWeights.length) { + uint256 removedWeight = i == 0 + ? accumulatedWeights[0] + : accumulatedWeights[i] - accumulatedWeights[i - 1]; + topStakersTotalWeight -= removedWeight; + previousTopStakersTotalWeight -= removedWeight; + } break; } } @@ -44,21 +125,272 @@ contract MockGenStaking { address _recipient, bytes32 _randomSeed ) external view returns (address) { - require(validators.length > 0, "No validators available"); + if (validators.length == 0) revert NoValidatorsAvailable(); uint256 randomIndex = uint256(_randomSeed) % validators.length; return validators[randomIndex]; } - // Function used in ConsensusMain for getting validator list + // Function used in ConsensusMain for getting a single validator + function getActivatorForSeed( + bytes32 _randomSeed, + uint256 timestamp + ) external view returns (address) { + if (validators.length == 0) revert NoValidatorsAvailable(); + uint256 randomIndex = uint256(_randomSeed) % validators.length; + return validators[randomIndex]; + } + + function getValidatorsLen(uint256 timestamp) public view returns (uint256) { + return previousTopStakersHot.length; + } + + function getValidatorsLenExternal( + uint256 timestamp + ) external view returns (uint256) { + return previousTopStakersHot.length; + } + + function getValidatorsItem( + uint256 timestamp, + uint256 index + ) public view returns (address) { + if (index >= previousTopStakersHot.length) return address(0); + return previousTopStakersHot[index]; + } + + function getAccumWeightItem( + uint256 timestamp, + uint256 index + ) public view returns (uint256) { + require( + index < previousAccumulatedWeights.length, + "Index out of bounds" + ); + return previousAccumulatedWeights[index]; + } + + // Helper function to get total validator count + function getValidatorCount() external view returns (uint256) { + return previousTopStakersHot.length; + } + function getValidatorsForTx( bytes32 _tx_id, - bytes32 _randomSeed - ) external view returns (address[] memory) { + bytes32 _randomSeed, + uint256 timestamp, + uint256 requestedValidatorsCount, + address[] memory consumedValidators + ) external view returns (address[] memory _validators) { + uint256 maxValidators = getValidatorsLen(timestamp); + if (maxValidators == 0) { + revert NoValidatorsAvailable(); + } + + // If we request more validators than are available, revert. + if (requestedValidatorsCount > maxValidators) { + revert NotEnoughValidators(); + } + + uint256 alreadyConsumedCount = consumedValidators.length; + uint256 nonConsumedCount = maxValidators - alreadyConsumedCount; + + // If the number of requested validators is greater or equal to the number + // of non-consumed validators, simply return all non-consumed, non-banned validators. + if (requestedValidatorsCount >= nonConsumedCount) { + return + _getAllNonConsumedValidators( + timestamp, + consumedValidators, + maxValidators + ); + } + + // Otherwise, we select validators randomly based on their stake weights. + uint256 totalWeight = _getCurrentTotalWeight(timestamp); + if (totalWeight == 0) { + revert ZeroTotalWeight(); + } + + return + _selectValidatorsRandomly( + timestamp, + _randomSeed, + requestedValidatorsCount, + consumedValidators, + totalWeight, + maxValidators + ); + } + + function _getAllNonConsumedValidators( + uint256 timestamp, + address[] memory consumedValidators, + uint256 maxValidators + ) internal view returns (address[] memory) { + address[] memory validators = new address[]( + maxValidators - consumedValidators.length + ); + uint256 index = 0; + + for (uint256 i = 0; i < maxValidators; i++) { + address validatorHot = getValidatorsItem(timestamp, i); + address coldWallet = hotToColdWallet[validatorHot]; + + if ( + !_isConsumed(validatorHot, consumedValidators) && + !validatorBans[coldWallet].isBanned + ) { + validators[index] = validatorHot; + index++; + } + } + return validators; } - // Helper function to get total validator count - function getValidatorCount() external view returns (uint256) { - return validators.length; + function _getCurrentTotalWeight( + uint256 timestamp + ) internal view returns (uint256) { + // If the timestamp is older than FINALIZATION_INTERVAL from the current block, + // use the previous total weight; otherwise, use the current total. + if (timestamp < block.timestamp - FINALIZATION_INTERVAL) { + return previousTopStakersTotalWeight; + } else { + return topStakersTotalWeight; + } + } + + function _selectValidatorsRandomly( + uint256 timestamp, + bytes32 _randomSeed, + uint256 numValidators, + address[] memory consumedValidators, + uint256 totalWeight, + uint256 maxValidators + ) internal view returns (address[] memory selectedValidators) { + selectedValidators = new address[](numValidators); + bool[] memory isSelected = new bool[](maxValidators); + + for (uint256 i = 0; i < numValidators; i++) { + // Generate a pseudo-random stake for selection. + uint256 randomStake = _generateRandomStake( + _randomSeed, + consumedValidators.length + i, + i + 1, + totalWeight + ); + uint256 validatorIndex = _findValidatorIndexByWeight( + timestamp, + randomStake, + maxValidators + ); + + // Find the next eligible validator (non-consumed, non-banned, not already selected). + address validator = _findNextEligibleValidator( + timestamp, + validatorIndex, + consumedValidators, + isSelected, + maxValidators + ); + + if (validator == address(0)) { + revert("No valid validator found"); + } + + selectedValidators[i] = validator; + } + + return selectedValidators; + } + + function _generateRandomStake( + bytes32 _randomSeed, + uint256 offset, + uint256 multiplier, + uint256 totalWeight + ) internal pure returns (uint256) { + // Combine seed with offset to generate a unique pseudo-random value + // and then modulo by totalWeight to choose a random stake position. + uint256 combinedSeed = uint256( + keccak256(abi.encodePacked(_randomSeed, offset)) + ); + unchecked { + return (combinedSeed * multiplier) % totalWeight; + } + } + + function _findValidatorIndexByWeight( + uint256 timestamp, + uint256 randomStake, + uint256 maxValidators + ) internal view returns (uint256) { + // Binary search to find the appropriate validator index for the given randomStake. + uint256 low = 0; + uint256 high = maxValidators - 1; + while (low <= high) { + uint256 mid = (low + high) >> 1; + uint256 midWeight = getAccumWeightItem(timestamp, mid); + + if (midWeight > randomStake) { + // If this midWeight surpasses randomStake and is the first such occurrence, we settle here. + if (mid == 0) { + return mid; + } + high = mid - 1; + } else { + low = mid + 1; + } + } + + // If not found directly (due to how we adjust low/high), + // high will represent the last suitable validator index. + return high; + } + + function _findNextEligibleValidator( + uint256 timestamp, + uint256 startIndex, + address[] memory consumedValidators, + bool[] memory isSelected, + uint256 maxValidators + ) internal view returns (address) { + uint256 originalIndex = startIndex; + + for (uint256 attempt = 0; attempt < maxValidators; attempt++) { + address validatorHot = getValidatorsItem(timestamp, startIndex); + address coldWallet = hotToColdWallet[validatorHot]; + + // If validator is consumed, banned or already selected, move to the next. + if ( + _isConsumed(validatorHot, consumedValidators) || + validatorBans[coldWallet].isBanned || + isSelected[startIndex] + ) { + startIndex = (startIndex + 1) % maxValidators; + // If we've looped around back to the original index, no suitable validator is found. + if (startIndex == originalIndex) break; + continue; + } + + // Mark as selected and return + isSelected[startIndex] = true; + return validatorHot; + } + + // No valid validator found after trying all possible indices. + return address(0); + } + + function _isConsumed( + address validator, + address[] memory consumedValidators + ) internal pure returns (bool) { + for (uint256 k = 0; k < consumedValidators.length; k++) { + if (consumedValidators[k] == validator) { + return true; + } + } + return false; } -} +} \ No newline at end of file diff --git a/hardhat/contracts/v2_contracts/utils/Errors.sol b/hardhat/contracts/v2_contracts/utils/Errors.sol new file mode 100644 index 000000000..dc9521222 --- /dev/null +++ b/hardhat/contracts/v2_contracts/utils/Errors.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +library Errors { + error NonGenVMContract(); + error TransactionNotProposing(); + error CallerNotActivator(); + error CallerNotLeader(); + error CallerNotValidator(); + error CallerNotMessages(); + error TransactionNotCommitting(); + error VoteAlreadyCommitted(); + error TransactionNotRevealing(); + error VoteAlreadyRevealed(); + error InvalidVote(); + error TransactionNotAcceptedOrUndetermined(); + error CanNotAppeal(); + error AppealBondTooLow(); +} \ No newline at end of file diff --git a/hardhat/ignition/modules/DeployFixture.js b/hardhat/ignition/modules/DeployFixture.js index addc54915..0fae2d490 100644 --- a/hardhat/ignition/modules/DeployFixture.js +++ b/hardhat/ignition/modules/DeployFixture.js @@ -1,65 +1,138 @@ const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); module.exports = buildModule("DeployFixture", (m) => { - // 1. Deploy base contracts (in order of TransactionFlow) + // Get accounts + const owner = m.getAccount(0); + const validator1 = m.getAccount(1); + const validator2 = m.getAccount(2); + const validator3 = m.getAccount(3); + + // Deploy base contracts + const GhostContract = m.contract("GhostContract"); const ConsensusManager = m.contract("ConsensusManager"); + const ConsensusMain = m.contract("ConsensusMain"); + + // Initialize ConsensusMain + const initConsensusMain = m.call(ConsensusMain, "initialize", [ConsensusManager], { + after: [ConsensusManager] + }); + const GhostFactory = m.contract("GhostFactory"); + const initGhostFactory = m.call(GhostFactory, "initialize", [], { + after: [initConsensusMain] + }); + const GhostBlueprint = m.contract("GhostBlueprint"); - const GhostContract = m.contract("GhostContract"); - const MockGenStaking = m.contract("MockGenStaking", [m.getAccount(1)]); - const Queues = m.contract("Queues"); - const Transactions = m.contract("Transactions"); - const ConsensusMain = m.contract("ConsensusMain"); + const initGhostBlueprint = m.call(GhostBlueprint, "initialize", [owner], { + after: [initGhostFactory] + }); - // 2. Initialize GhostFactory and configure GhostBlueprint - const initGhostFactory = m.call(GhostFactory, "initialize"); const setBlueprint = m.call(GhostFactory, "setGhostBlueprint", [GhostBlueprint], { - after: [initGhostFactory] + after: [initGhostBlueprint] }); - // 3. Initialize ConsensusMain and dependents - const initConsensusMain = m.call(ConsensusMain, "initialize", [ConsensusManager]); + const deployProxy = m.call(GhostFactory, "deployNewBeaconProxy", [], { + after: [setBlueprint] + }); + + // Important: Ensure that validator1.address is available before using it + const MockGenStaking = m.contract("MockGenStaking", [validator1]); + + const Queues = m.contract("Queues"); + const Transactions = m.contract("Transactions"); + const Messages = m.contract("Messages"); + + // Initialize contracts const initTransactions = m.call(Transactions, "initialize", [ConsensusMain], { after: [initConsensusMain] }); const initQueues = m.call(Queues, "initialize", [ConsensusMain], { + after: [initTransactions] + }); + const initMessages = m.call(Messages, "initialize", [], { + after: [initQueues] + }); + + // Set up contract connections + const setGhostFactory = m.call(ConsensusMain, "setGhostFactory", [GhostFactory], { after: [initConsensusMain] }); + const setGenStaking = m.call(ConsensusMain, "setGenStaking", [MockGenStaking], { + after: [setGhostFactory] + }); + const setGenQueue = m.call(ConsensusMain, "setGenQueue", [Queues], { + after: [setGenStaking] + }); + const setGenTransactions = m.call(ConsensusMain, "setGenTransactions", [Transactions], { + after: [setGenQueue] + }); + const setGenMessages = m.call(ConsensusMain, "setGenMessages", [Messages], { + after: [setGenTransactions] + }); - // 4. Setup contract connections - m.call(ConsensusMain, "setGhostFactory", [GhostFactory], { after: [initConsensusMain] }); - m.call(ConsensusMain, "setGenStaking", [MockGenStaking], { after: [initConsensusMain] }); - m.call(ConsensusMain, "setGenQueue", [Queues], { after: [initConsensusMain, initQueues] }); - m.call(ConsensusMain, "setGenTransactions", [Transactions], { after: [initConsensusMain, initTransactions] }); + // Deploy and initialize ConsensusData + const ConsensusData = m.contract("ConsensusData"); + const initConsensusData = m.call(ConsensusData, "initialize", [ConsensusMain, Transactions, Queues], { + after: [setGenMessages] + }); - m.call(GhostFactory, "setGenConsensus", [ConsensusMain], { after: [initGhostFactory, initConsensusMain] }); - m.call(GhostFactory, "setGhostManager", [ConsensusMain], { after: [initGhostFactory, initConsensusMain] }); - m.call(Transactions, "setGenConsensus", [ConsensusMain], { after: [initTransactions] }); - m.call(ConsensusMain, "setAcceptanceTimeout", [0], { after: [initConsensusMain] }); + // Set remaining connections + const setGenConsensusByGhostFactory = m.call(GhostFactory, "setGenConsensus", [ConsensusMain], { + after: [initConsensusData] + }); + const setGhostManagerByGhostFactory = m.call(GhostFactory, "setGhostManager", [ConsensusMain], { + after: [setGenConsensusByGhostFactory] + }); + const setGenConsensusByTransactions = m.call(Transactions, "setGenConsensus", [ConsensusMain], { + after: [setGhostManagerByGhostFactory] + }); + const setGenConsensusByMessages = m.call(Messages, "setGenConsensus", [ConsensusMain], { + after: [setGenConsensusByTransactions] + }); + const setGenTransactionsByMessages = m.call(Messages, "setGenTransactions", [Transactions], { + after: [setGenConsensusByMessages] + }); + const setAcceptanceTimeout = m.call(ConsensusMain, "setAcceptanceTimeout", [0], { + after: [setGenTransactionsByMessages] + }); - // 5. Setup validators - const validator1 = m.getAccount(1); - const validator2 = m.getAccount(2); - const validator3 = m.getAccount(3); - m.call(MockGenStaking, "addValidators", [[validator1, validator2, validator3]]); + // Setup validators + const addValidators = m.call(MockGenStaking, "addValidators", [[validator1, validator2, validator3]], { + after: [setAcceptanceTimeout] + }); + + // Verify validators are correctly set up + const verifyValidatorCount = m.call(MockGenStaking, "getValidatorCount", [], { + after: [addValidators], + id: "verifyValidatorCount" + }); - // 6. Deploy beacon proxy - m.call(GhostFactory, "deployNewBeaconProxy", [], { - after: [ - setBlueprint, - initGhostFactory, - initConsensusMain - ] + // Verify that each validator is registered + const verifyValidator1 = m.call(MockGenStaking, "isValidator", [validator1], { + after: [verifyValidatorCount], + id: "verifyValidator1" }); + const verifyValidator2 = m.call(MockGenStaking, "isValidator", [validator2], { + after: [verifyValidator1], + id: "verifyValidator2" + }); + const verifyValidator3 = m.call(MockGenStaking, "isValidator", [validator3], { + after: [verifyValidator2], + id: "verifyValidator3" + }); + + return { - GhostBlueprint, GhostContract, ConsensusManager, + ConsensusMain, GhostFactory, + GhostBlueprint, MockGenStaking, Queues, Transactions, - ConsensusMain + Messages, + ConsensusData }; }); \ No newline at end of file diff --git a/hardhat/scripts/consensus-flow.js b/hardhat/scripts/consensus-flow.js new file mode 100644 index 000000000..694f7d6ee --- /dev/null +++ b/hardhat/scripts/consensus-flow.js @@ -0,0 +1,172 @@ +const hre = require("hardhat"); + + +async function generateSignature(signer, currentSeed) { + const seedBytes = ethers.zeroPadValue(ethers.toBeHex(currentSeed), 32); + const vrfProof = await signer.signMessage(ethers.getBytes(seedBytes)); + return vrfProof; +} + +async function main() { + console.log("Starting consensus flow..."); + + // Get signers + const [owner, validator1, validator2, validator3] = await hre.ethers.getSigners(); + const validators = [validator1, validator2, validator3]; + + // Get contract instances + const consensusMainAddress = require("../deployments/localhost/ConsensusMain.json").address; + const consensusDataAddress = require("../deployments/localhost/ConsensusData.json").address; + const consensusMain = await hre.ethers.getContractAt("ConsensusMain", consensusMainAddress); + const consensusData = await hre.ethers.getContractAt("ConsensusData", consensusDataAddress); + + const txAddTransactionInput = consensusMain.addTransaction.getFragment( + ethers.ZeroAddress, // sender (will use msg.sender) + ethers.ZeroAddress, // recipient (will create ghost) + 3, // number of validators + "0x" // transaction data + ); + + const txFlowData = { + addTransaction: { + input: txAddTransactionInput, + }, + } + + // 1. Add transaction + console.log("\n1. Adding transaction..."); + const tx = await consensusMain.addTransaction( + ethers.ZeroAddress, + ethers.ZeroAddress, + 3, + "0x" + ); + const receipt = await tx.wait(); + txFlowData.addTransaction.output = { + receipt, + } + + // Find the NewTransaction event + const newTxEvent = receipt.logs?.find( + (log) => consensusMain.interface.parseLog(log)?.name === "NewTransaction" + ); + if (!newTxEvent) throw new Error("NewTransaction event not found"); + + const parsedLog = consensusMain.interface.parseLog(newTxEvent); + const txId = parsedLog.args[0]; + const ghostAddress = parsedLog.args[1]; + const activatorAddress = parsedLog.args[2]; + + console.log("- Transaction ID:", txId); + console.log("- Ghost Address:", ghostAddress); + console.log("- Activator:", activatorAddress); + + const activator = validators.find(v => v.address === activatorAddress); + console.log("- Activator in consensus:", activator.address); + + // After each major step, get and store transaction data + let txData = await consensusData.getTransactionData(txId); + console.log("Initial transaction data:", txData); + + // 2. Activate transaction + console.log("\n2. Activating transaction..."); + const currentSeed = await consensusMain.recipientRandomSeed(ghostAddress); + const vrfProofActivate = await generateSignature(activator, BigInt(currentSeed)); + const activateTx = await consensusMain.connect(activator).activateTransaction(txId, vrfProofActivate); + const txActivateTransactionInput = consensusMain.activateTransaction.getFragment(txId, vrfProofActivate); + + const activationReceipt = await activateTx.wait(); + txData = await consensusData.getTransactionData(txId); + console.log("Transaction data after activation:", txData); + + const leaderAddress = consensusMain.interface.parseLog(activationReceipt.logs[0]).args[1]; + const leader = validators.find(v => v.address === leaderAddress); + + // 3. Propose receipt + console.log("\n3. Proposing receipt..."); + const vrfProofPropose = await generateSignature(leader, BigInt(await consensusMain.recipientRandomSeed(ghostAddress))); + const txProposalInput = consensusMain.proposeReceipt.getFragment( + txId, + "0x1234", + [], + vrfProofPropose + ) + const txProposal = await consensusMain + .connect(leader) + .proposeReceipt(txId, "0x1234", [], vrfProofPropose); + + const txProposalReceipt = await txProposal.wait() + txData = await consensusData.getTransactionData(txId); + console.log("Transaction data after proposal:", txData); + + // 4. Commit votes + console.log("\n4. Committing votes..."); + const voteType = 1; // Agree + const nonces = [123, 456, 789]; + + const voteHash1 = ethers.solidityPackedKeccak256( + ["address", "uint8", "uint256"], + [validator1.address, voteType, nonces[0]] + ) + const voteHash2 = ethers.solidityPackedKeccak256( + ["address", "uint8", "uint256"], + [validator2.address, voteType, nonces[1]] + ) + const voteHash3 = ethers.solidityPackedKeccak256( + ["address", "uint8", "uint256"], + [validator3.address, voteType, nonces[2]] + ) + await consensusMain.connect(validator1).commitVote(txId, voteHash1, false) + await consensusMain.connect(validator2).commitVote(txId, voteHash2, false) + const txCommitVoteInput = consensusMain.commitVote.getFragment(txId, voteHash3, false) + const lastCommitTx = await consensusMain.connect(validator3).commitVote(txId, voteHash3, false) + const lastCommitTxReceipt = await lastCommitTx.wait() + txData = await consensusData.getTransactionData(txId); + console.log("Transaction data after commits:", txData); + + // 5. Reveal votes + await consensusMain.connect(validator1).revealVote(txId, voteHash1, voteType, nonces[0]) + await consensusMain.connect(validator2).revealVote(txId, voteHash2, voteType, nonces[1]) + const revealVoteInput3 = consensusMain.revealVote.getFragment(txId, voteHash3, voteType, nonces[2]) + const revealReceipt3 = await consensusMain.connect(validator3).revealVote(txId, voteHash3, voteType, nonces[2]) + const revealReceipt3Receipt = await revealReceipt3.wait() + txData = await consensusData.getTransactionData(txId); + console.log("Transaction data after reveals:", txData); + + // 6. Finalize transaction + console.log("\n6. Finalizing transaction..."); + const txFinalizeTransactionInput = consensusMain.finalizeTransaction.getFragment(txId) + const finalizeTx = await consensusMain.finalizeTransaction(txId) + const finalizeTxReceipt = await finalizeTx.wait() + txData = await consensusData.getTransactionData(txId); + console.log("Final transaction data:", txData); + + const finalStatus = await consensusMain.txStatus(txId); + console.log("- Final transaction status:", finalStatus.toString()); + + // Store all transaction data in the flow data + Object.assign(txFlowData, { + transactionData: { + initial: txData, + afterActivation: txData, + afterProposal: txData, + afterCommits: txData, + afterReveals: txData, + final: txData + } + }); + + if (finalStatus.toString() === "6") { + console.log("\n¡Consensus flow completed successfully! ✓"); + console.log(txFlowData) + } else { + throw new Error(`Unexpected final status: ${finalStatus}`); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/hardhat/test/deploy/001_deploy_contracts.test.js b/hardhat/test/deploy/001_deploy_contracts.test.js index ca40813b1..4a29ab16c 100644 --- a/hardhat/test/deploy/001_deploy_contracts.test.js +++ b/hardhat/test/deploy/001_deploy_contracts.test.js @@ -6,7 +6,7 @@ const path = require("path"); describe("Deploy Script", function () { let contracts = {}; - let deployer; + let owner, validator1, validator2, validator3; const expectedContracts = [ 'GhostContract', @@ -16,11 +16,13 @@ describe("Deploy Script", function () { 'MockGenStaking', 'Queues', 'Transactions', - 'ConsensusMain' + 'Messages', + 'ConsensusMain', + 'ConsensusData' ]; before(async function () { - [deployer] = await ethers.getSigners(); + [owner, validator1, validator2, validator3] = await ethers.getSigners(); // Execute the deployment using Ignition const DeployFixture = require("../../ignition/modules/DeployFixture"); @@ -81,7 +83,7 @@ describe("Deploy Script", function () { .to.equal(await contracts.ConsensusManager.getAddress()); }); - it("should have initialized Transactions and Queues with the ConsensusMain address", async function() { + it("should have initialized Transactions, Queues and Messages with the ConsensusMain address", async function() { const consensusMainAddress = await contracts.ConsensusMain.getAddress(); expect( @@ -93,6 +95,32 @@ describe("Deploy Script", function () { await contracts.Queues.genConsensus(), "Queues should have been initialized with ConsensusMain address" ).to.equal(consensusMainAddress); + + expect( + await contracts.Messages.genConsensus(), + "Messages should have been initialized with ConsensusMain address" + ).to.equal(consensusMainAddress); + }); + + it("should have initialized ConsensusData properly", async function() { + const consensusMainAddress = await contracts.ConsensusMain.getAddress(); + const transactionsAddress = await contracts.Transactions.getAddress(); + const queuesAddress = await contracts.Queues.getAddress(); + + expect( + await contracts.ConsensusData.consensusMain(), + "ConsensusData should have been initialized with ConsensusMain address" + ).to.equal(consensusMainAddress); + + expect( + await contracts.ConsensusData.transactions(), + "ConsensusData should have been initialized with Transactions address" + ).to.equal(transactionsAddress); + + expect( + await contracts.ConsensusData.queues(), + "ConsensusData should have been initialized with Queues address" + ).to.equal(queuesAddress); }); it("should have set contract connections for ConsensusMain properly", async function() { @@ -100,6 +128,7 @@ describe("Deploy Script", function () { const genStakingAddress = await contracts.MockGenStaking.getAddress(); const genQueueAddress = await contracts.Queues.getAddress(); const genTransactionsAddress = await contracts.Transactions.getAddress(); + const genMessagesAddress = await contracts.Messages.getAddress(); expect( await contracts.ConsensusMain.ghostFactory(), @@ -117,10 +146,15 @@ describe("Deploy Script", function () { await contracts.ConsensusMain.genTransactions(), "ConsensusMain should have set GenTransactions address" ).to.equal(genTransactionsAddress); + expect( + await contracts.ConsensusMain.genMessages(), + "ConsensusMain should have set GenMessages address" + ).to.equal(genMessagesAddress); }); - it("should have configured GhostFactory and Transactions final settings properly", async function() { + it("should have configured GhostFactory, Transactions and Messages final settings properly", async function() { const consensusMainAddress = await contracts.ConsensusMain.getAddress(); + const transactionsAddress = await contracts.Transactions.getAddress(); expect( await contracts.GhostFactory.genConsensus(), @@ -134,6 +168,14 @@ describe("Deploy Script", function () { await contracts.Transactions.genConsensus(), "Transactions should have set GenConsensus address" ).to.equal(consensusMainAddress); + expect( + await contracts.Messages.genConsensus(), + "Messages should have set GenConsensus address" + ).to.equal(consensusMainAddress); + expect( + await contracts.Messages.genTransactions(), + "Messages should have set GenTransactions address" + ).to.equal(transactionsAddress); }); it("should have set Acceptance Timeout in ConsensusMain properly", async function() { @@ -144,7 +186,6 @@ describe("Deploy Script", function () { }); it("should have set up validators in MockGenStaking properly", async function() { - const [owner, validator1, validator2, validator3] = await ethers.getSigners(); const validatorCount = await contracts.MockGenStaking.getValidatorCount(); const validators = []; diff --git a/tests/hardhat/docker-compose.yml b/tests/hardhat/docker-compose.yml index 1c2e809b6..4d253b435 100644 --- a/tests/hardhat/docker-compose.yml +++ b/tests/hardhat/docker-compose.yml @@ -15,10 +15,10 @@ services: - HARDHAT_NETWORK=hardhat healthcheck: test: ["CMD", "nc", "-z", "localhost", "8545"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s + interval: 20s + timeout: 20s + retries: 10 + start_period: 20s tests: build: