From 02bb6e95c9b322d087bb0bf9e15c449a18b9ec4e Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 25 Nov 2024 10:21:03 +0800 Subject: [PATCH] feat: consensus activate/deactivate --- src/core/UTXOGateway.sol | 120 ++++++++++---- src/libraries/Errors.sol | 47 +++--- src/storage/UTXOGatewayStorage.sol | 36 ++++- test/foundry/unit/UTXOGateway.t.sol | 238 ++++++++++++++++++++++++---- 4 files changed, 357 insertions(+), 84 deletions(-) diff --git a/src/core/UTXOGateway.sol b/src/core/UTXOGateway.sol index 4433a3a2..ff07b495 100644 --- a/src/core/UTXOGateway.sol +++ b/src/core/UTXOGateway.sol @@ -30,32 +30,6 @@ contract UTXOGateway is using ExocoreBytes for address; using SignatureVerifier for bytes32; - /** - * @dev Modifier to restrict access to authorized witnesses only. - */ - modifier onlyAuthorizedWitness() { - if (!_isAuthorizedWitness(msg.sender)) { - revert Errors.UnauthorizedWitness(); - } - _; - } - - /** - * @notice Pauses the contract. - * @dev Can only be called by the contract owner. - */ - function pause() external onlyOwner { - _pause(); - } - - /** - * @notice Unpauses the contract. - * @dev Can only be called by the contract owner. - */ - function unpause() external onlyOwner { - _unpause(); - } - /** * @notice Constructor to initialize the contract with the client chain ID. * @dev Sets up initial configuration for testing purposes. @@ -65,14 +39,23 @@ contract UTXOGateway is } /** - * @notice Initializes the contract with the Exocore witness address and owner address. + * @notice Initializes the contract with the Exocore witness address, owner address and required proofs. + * @dev If the witnesses length is greater or equal to the required proofs, the consensus requirement for stake + * message + * would be activated. * @param owner_ The address of the owner. * @param witnesses The addresses of the witnesses. + * @param requiredProofs_ The number of required proofs. */ - function initialize(address owner_, address[] calldata witnesses) external initializer { + function initialize(address owner_, address[] calldata witnesses, uint256 requiredProofs_) external initializer { if (owner_ == address(0) || witnesses.length == 0) { revert Errors.ZeroAddress(); } + if (requiredProofs_ < MIN_REQUIRED_PROOFS || requiredProofs_ > MAX_REQUIRED_PROOFS) { + revert Errors.InvalidRequiredProofs(); + } + + requiredProofs = requiredProofs_; for (uint256 i = 0; i < witnesses.length; i++) { _addWitness(witnesses[i]); } @@ -81,6 +64,22 @@ contract UTXOGateway is _transferOwnership(owner_); } + /** + * @notice Pauses the contract. + * @dev Can only be called by the contract owner. + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpauses the contract. + * @dev Can only be called by the contract owner. + */ + function unpause() external onlyOwner { + _unpause(); + } + /** * @notice Activates token staking by registering or updating the chain and token with the Exocore system. */ @@ -95,6 +94,33 @@ contract UTXOGateway is } } + /** + * @notice Updates the required proofs for consensus. + * @notice The consensus requirement for stake message would be activated if the current authorized witness count is + * greater than or equal to the new required proofs. + * @dev Can only be called by the contract owner. + * @param newRequiredProofs The new required proofs. + */ + function updateRequiredProofs(uint256 newRequiredProofs) external onlyOwner whenNotPaused { + if (newRequiredProofs < MIN_REQUIRED_PROOFS || newRequiredProofs > MAX_REQUIRED_PROOFS) { + revert Errors.InvalidRequiredProofs(); + } + + bool wasConsensusRequired = _isConsensusRequired(); + uint256 oldRequiredProofs = requiredProofs; + requiredProofs = newRequiredProofs; + + emit RequiredProofsUpdated(oldRequiredProofs, newRequiredProofs); + + // Check if consensus state changed due to new requirement + bool isConsensusRequired_ = _isConsensusRequired(); + if (!wasConsensusRequired && isConsensusRequired_) { + emit ConsensusActivated(requiredProofs, authorizedWitnessCount); + } else if (wasConsensusRequired && !isConsensusRequired_) { + emit ConsensusDeactivated(requiredProofs, authorizedWitnessCount); + } + } + /** * @notice Adds a new authorized witness. * @param _witness The address of the witness to be added. @@ -117,9 +143,17 @@ contract UTXOGateway is if (!authorizedWitnesses[_witness]) { revert Errors.WitnessNotAuthorized(_witness); } + + bool wasConsensusRequired = _isConsensusRequired(); + authorizedWitnesses[_witness] = false; authorizedWitnessCount--; emit WitnessRemoved(_witness); + + // Emit only when crossing the threshold from true to false + if (wasConsensusRequired && !_isConsensusRequired()) { + emit ConsensusDeactivated(requiredProofs, authorizedWitnessCount); + } } /** @@ -148,6 +182,10 @@ contract UTXOGateway is nonReentrant whenNotPaused { + if (!_isConsensusRequired()) { + revert Errors.ConsensusNotRequired(); + } + if (!_isAuthorizedWitness(witness)) { revert Errors.WitnessNotAuthorized(witness); } @@ -178,7 +216,7 @@ contract UTXOGateway is emit ProofSubmitted(messageHash, witness); // Check for consensus - if (txn.proofCount >= REQUIRED_PROOFS) { + if (txn.proofCount >= requiredProofs) { processedTransactions[messageHash] = true; _processStakeMsg(txn.stakeMsg); // we delete the transaction after it has been processed to refund some gas, so no need to worry about @@ -200,6 +238,10 @@ contract UTXOGateway is nonReentrant whenNotPaused { + if (_isConsensusRequired()) { + revert Errors.ConsensusRequired(); + } + if (!_isAuthorizedWitness(witness)) { revert Errors.WitnessNotAuthorized(witness); } @@ -432,6 +474,18 @@ contract UTXOGateway is return transactions[messageHash].witnessTime[witness]; } + function isConsensusRequired() external view returns (bool) { + return _isConsensusRequired(); + } + + /** + * @notice Checks if consensus is required for a stake message. + * @return True if count of authorized witnesses is greater than or equal to REQUIRED_PROOFS, false otherwise. + */ + function _isConsensusRequired() internal view returns (bool) { + return authorizedWitnessCount >= requiredProofs; + } + /** * @notice Checks if a witness is authorized. * @param witness The witness address. @@ -448,9 +502,17 @@ contract UTXOGateway is if (_isAuthorizedWitness(_witness)) { revert Errors.WitnessAlreadyAuthorized(_witness); } + + bool wasConsensusRequired = _isConsensusRequired(); + authorizedWitnesses[_witness] = true; authorizedWitnessCount++; emit WitnessAdded(_witness); + + // Emit only when crossing the threshold from false to true + if (!wasConsensusRequired && _isConsensusRequired()) { + emit ConsensusActivated(requiredProofs, authorizedWitnessCount); + } } /** diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 3645b329..f79cdd84 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -312,61 +312,70 @@ library Errors { error InsufficientBalance(); /* -------------------------------------------------------------------------- */ - /* ExocoreBtcGateway Errors */ + /* UTXOGateway Errors */ /* -------------------------------------------------------------------------- */ - /// @dev ExocoreBtcGateway: witness has already submitted proof + /// @dev UTXOGateway: witness has already submitted proof error WitnessAlreadySubmittedProof(); - /// @dev ExocoreBtcGateway: invalid stake message + /// @dev UTXOGateway: invalid stake message error InvalidStakeMessage(); - /// @dev ExocoreBtcGateway: transaction tag has already been processed + /// @dev UTXOGateway: transaction tag has already been processed error TxTagAlreadyProcessed(); - /// @dev ExocoreBtcGateway: invalid operator address + /// @dev UTXOGateway: invalid operator address error InvalidOperator(); - /// @dev ExocoreBtcGateway: invalid token + /// @dev UTXOGateway: invalid token error InvalidToken(); - /// @dev ExocoreBtcGateway: witness has already been authorized + /// @dev UTXOGateway: witness has already been authorized error WitnessAlreadyAuthorized(address witness); - /// @dev ExocoreBtcGateway: witness has not been authorized + /// @dev UTXOGateway: witness has not been authorized error WitnessNotAuthorized(address witness); - /// @dev ExocoreBtcGateway: cannot remove the last witness + /// @dev UTXOGateway: cannot remove the last witness error CannotRemoveLastWitness(); - /// @dev ExocoreBtcGateway: invalid client chain + /// @dev UTXOGateway: invalid client chain error InvalidClientChain(); - /// @dev ExocoreBtcGateway: deposit failed + /// @dev UTXOGateway: deposit failed error DepositFailed(bytes txTag); - /// @dev ExocoreBtcGateway: address not registered + /// @dev UTXOGateway: address not registered error AddressNotRegistered(); - /// @dev ExocoreBtcGateway: delegation failed + /// @dev UTXOGateway: delegation failed error DelegationFailed(); - /// @dev ExocoreBtcGateway: withdraw principal failed + /// @dev UTXOGateway: withdraw principal failed error WithdrawPrincipalFailed(); - /// @dev ExocoreBtcGateway: undelegation failed + /// @dev UTXOGateway: undelegation failed error UndelegationFailed(); - /// @dev ExocoreBtcGateway: withdraw reward failed + /// @dev UTXOGateway: withdraw reward failed error WithdrawRewardFailed(); - /// @dev ExocoreBtcGateway: request not found + /// @dev UTXOGateway: request not found error RequestNotFound(uint64 requestId); - /// @dev ExocoreBtcGateway: request already exists + /// @dev UTXOGateway: request already exists error RequestAlreadyExists(uint32 clientChain, uint64 requestId); - /// @dev ExocoreBtcGateway: witness not authorized + /// @dev UTXOGateway: witness not authorized error UnauthorizedWitness(); + /// @dev UTXOGateway: consensus is not activated + error ConsensusNotRequired(); + + /// @dev UTXOGateway: consensus is required + error ConsensusRequired(); + + /// @dev UTXOGateway: invalid required proofs + error InvalidRequiredProofs(); + } diff --git a/src/storage/UTXOGatewayStorage.sol b/src/storage/UTXOGatewayStorage.sol index 01c60b82..1e5b2457 100644 --- a/src/storage/UTXOGatewayStorage.sol +++ b/src/storage/UTXOGatewayStorage.sol @@ -115,13 +115,18 @@ contract UTXOGatewayStorage { string public constant BTC_METADATA = "BTC"; string public constant BTC_ORACLE_INFO = "BTC,BITCOIN,8"; - address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - uint256 public constant REQUIRED_PROOFS = 2; uint256 public constant PROOF_TIMEOUT = 1 days; uint256 public bridgeFeeRate; // e.g., 100 (basis points) means 1% uint256 public constant BASIS_POINTS = 10_000; // 100% = 10000 basis points uint256 public constant MAX_BRIDGE_FEE_RATE = 1000; // 10% + // Add min/max bounds for safety + uint256 public constant MIN_REQUIRED_PROOFS = 1; + uint256 public constant MAX_REQUIRED_PROOFS = 10; + + /// @notice The number of proofs required for consensus + uint256 public requiredProofs; + /// @notice The count of authorized witnesses uint256 public authorizedWitnessCount; @@ -195,6 +200,13 @@ contract UTXOGatewayStorage { // Events + /** + * @dev Emitted when the required proofs is updated + * @param oldRequired The old required proofs + * @param newRequired The new required proofs + */ + event RequiredProofsUpdated(uint256 oldRequired, uint256 newRequired); + /** * @dev Emitted when a stake message is executed * @param chainId The chain ID of the client chain, should not violate the layerzero chain id @@ -435,6 +447,16 @@ contract UTXOGatewayStorage { /// @param token The address of the token. event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); + /// @notice Emitted when consensus is activated + /// @param requiredWitnessesCount The number of required witnesses + /// @param authorizedWitnessesCount The number of authorized witnesses + event ConsensusActivated(uint256 requiredWitnessesCount, uint256 authorizedWitnessesCount); + + /// @notice Emitted when consensus is deactivated + /// @param requiredWitnessesCount The number of required witnesses + /// @param authorizedWitnessesCount The number of authorized witnesses + event ConsensusDeactivated(uint256 requiredWitnessesCount, uint256 authorizedWitnessesCount); + /** * @dev Modifier to check if an amount is valid * @param amount The amount to check @@ -453,6 +475,16 @@ contract UTXOGatewayStorage { _; } + /** + * @dev Modifier to restrict access to authorized witnesses only. + */ + modifier onlyAuthorizedWitness() { + if (!authorizedWitnesses[msg.sender]) { + revert Errors.UnauthorizedWitness(); + } + _; + } + /// @notice Checks if the provided string is a valid Exocore address. /// @param addressToValidate The string to check. /// @return True if the string is valid, false otherwise. diff --git a/test/foundry/unit/UTXOGateway.t.sol b/test/foundry/unit/UTXOGateway.t.sol index ec96d5ea..914ec92a 100644 --- a/test/foundry/unit/UTXOGateway.t.sol +++ b/test/foundry/unit/UTXOGateway.t.sol @@ -55,7 +55,7 @@ contract UTXOGatewayTest is Test { string public constant BTC_METADATA = "BTC"; string public constant BTC_ORACLE_INFO = "BTC,BITCOIN,8"; - uint256 public constant REQUIRED_PROOFS = 2; + uint256 public initialRequiredProofs = 3; uint256 public constant PROOF_TIMEOUT = 1 days; event WitnessAdded(address indexed witness); @@ -124,6 +124,10 @@ contract UTXOGatewayTest is Test { uint256 amount ); + event ConsensusActivated(uint256 requiredProofs, uint256 authorizedWitnessCount); + event ConsensusDeactivated(uint256 requiredProofs, uint256 authorizedWitnessCount); + event RequiredProofsUpdated(uint256 oldRequired, uint256 newRequired); + function setUp() public { owner = address(1); user = address(2); @@ -140,13 +144,68 @@ contract UTXOGatewayTest is Test { gateway = UTXOGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0xab), ""))); address[] memory initialWitnesses = new address[](1); initialWitnesses[0] = witnesses[0].addr; - gateway.initialize(owner, initialWitnesses); + gateway.initialize(owner, initialWitnesses, initialRequiredProofs); } function test_initialize() public { assertEq(gateway.owner(), owner); assertTrue(gateway.authorizedWitnesses(witnesses[0].addr)); assertEq(gateway.authorizedWitnessCount(), 1); + assertEq(gateway.requiredProofs(), initialRequiredProofs); + assertFalse(gateway.isConsensusRequired()); + } + + function test_UpdateRequiredProofs_Success() public { + uint256 oldRequiredProofs = gateway.requiredProofs(); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit RequiredProofsUpdated(oldRequiredProofs, 2); + gateway.updateRequiredProofs(2); + + assertEq(gateway.requiredProofs(), 2); + } + + function test_UpdateRequiredProofs_ConsensusStateChange() public { + // Initially consensus should be inactive (1 witnesses < 3 required) + assertFalse(gateway.isConsensusRequired()); + uint256 oldRequiredProofs = gateway.requiredProofs(); + uint256 witnessCount = gateway.authorizedWitnessCount(); + + // Lower required proofs to 1, should activate consensus + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit RequiredProofsUpdated(oldRequiredProofs, witnessCount); + vm.expectEmit(true, true, true, true); + emit ConsensusActivated(witnessCount, witnessCount); + gateway.updateRequiredProofs(witnessCount); + + assertTrue(gateway.isConsensusRequired()); + } + + function test_UpdateRequiredProofs_RevertInvalidValue() public { + vm.startPrank(owner); + vm.expectRevert(Errors.InvalidRequiredProofs.selector); + gateway.updateRequiredProofs(0); // Below minimum + + vm.expectRevert(Errors.InvalidRequiredProofs.selector); + gateway.updateRequiredProofs(11); // Above maximum + vm.stopPrank(); + } + + function test_UpdateRequiredProofs_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.updateRequiredProofs(2); + } + + function test_UpdateRequiredProofs_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.updateRequiredProofs(2); + vm.stopPrank(); } function test_AddWitness_Success() public { @@ -192,6 +251,27 @@ contract UTXOGatewayTest is Test { vm.stopPrank(); } + function test_AddWitness_ConsensusActivation() public { + // initially we have 1 witness, and required proofs is 3 + assertEq(gateway.authorizedWitnessCount(), 1); + assertEq(gateway.requiredProofs(), 3); + assertFalse(gateway.isConsensusRequired()); + + vm.startPrank(owner); + // Add second witness - no consensus event + gateway.addWitness(witnesses[1].addr); + + // Add third witness - should emit ConsensusActivated + vm.expectEmit(true, true, true, true); + emit ConsensusActivated(gateway.requiredProofs(), gateway.authorizedWitnessCount() + 1); + gateway.addWitness(witnesses[2].addr); + + // Add fourth witness - no consensus event + gateway.addWitness(address(0xaa)); + + vm.stopPrank(); + } + function test_RemoveWitness() public { vm.startPrank(owner); @@ -272,6 +352,24 @@ contract UTXOGatewayTest is Test { vm.stopPrank(); } + function test_RemoveWitness_ConsensusDeactivation() public { + // add total 3 witnesses + _addAllWitnesses(); + + // set + vm.startPrank(owner); + gateway.updateRequiredProofs(2); + + // Remove one witness - no consensus event + gateway.removeWitness(witnesses[2].addr); + + // Remove another witness - should emit ConsensusDeactivated + vm.expectEmit(true, true, true, true); + emit ConsensusDeactivated(gateway.requiredProofs(), gateway.authorizedWitnessCount() - 1); + gateway.removeWitness(witnesses[1].addr); + vm.stopPrank(); + } + function test_UpdateBridgeFee() public { uint256 newFee = 500; // 5% @@ -540,6 +638,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_Success() public { _addAllWitnesses(); + _activateConsensus(); // Create stake message UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ @@ -561,6 +660,14 @@ contract UTXOGatewayTest is Test { emit ProofSubmitted(txId, witnesses[0].addr); gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature); + // Submit proof from second witness + signature = _generateSignature(stakeMsg, witnesses[1].privateKey); + vm.prank(relayer); + vm.expectEmit(true, true, false, true); + emit ProofSubmitted(txId, witnesses[1].addr); + gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature); + + // Submit proof from thrid witness and trigger message execution as we have enough proofs // mock Assets precompile deposit success and Delegation precompile delegate success vm.mockCall( ASSETS_PRECOMPILE_ADDRESS, @@ -571,26 +678,46 @@ contract UTXOGatewayTest is Test { DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) ); - // Submit proof from second witness - signature = _generateSignature(stakeMsg, witnesses[1].privateKey); + signature = _generateSignature(stakeMsg, witnesses[2].privateKey); vm.prank(relayer); - vm.expectEmit(true, true, false, true); - emit ProofSubmitted(txId, witnesses[1].addr); - - // This should trigger message execution as we have enough proofs vm.expectEmit(true, false, false, false); emit StakeMsgExecuted(stakeMsg.chainId, stakeMsg.nonce, stakeMsg.exocoreAddress, stakeMsg.amount); vm.expectEmit(true, false, false, false); emit TransactionProcessed(txId); - gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature); + gateway.submitProofForStakeMsg(witnesses[2].addr, stakeMsg, signature); // Verify message was processed assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); assertTrue(gateway.processedTransactions(txId)); } + function test_SubmitProofForStakeMsg_RevertConsensusDeactivated() public { + _addAllWitnesses(); + + // deactivate consensus for stake message by updating the value of requiredProofs + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // First witness submits proof + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(Errors.ConsensusNotRequired.selector)); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature); + } + function test_SubmitProofForStakeMsg_RevertInvalidSignature() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -611,6 +738,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_RevertUnauthorizedWitness() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -632,6 +760,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_ExpiredBeforeConsensus() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -643,8 +772,8 @@ contract UTXOGatewayTest is Test { txTag: bytes("tx1") }); - // Submit proofs from REQUIRED_PROOFS - 1 witnesses - for (uint256 i = 0; i < REQUIRED_PROOFS - 1; i++) { + // Submit proofs from requiredProofs - 1 witnesses + for (uint256 i = 0; i < gateway.requiredProofs() - 1; i++) { bytes memory signature = _generateSignature(stakeMsg, witnesses[i].privateKey); vm.prank(relayer); gateway.submitProofForStakeMsg(witnesses[i].addr, stakeMsg, signature); @@ -654,9 +783,9 @@ contract UTXOGatewayTest is Test { vm.warp(block.timestamp + PROOF_TIMEOUT + 1); // Submit the last proof - bytes memory lastSignature = _generateSignature(stakeMsg, witnesses[REQUIRED_PROOFS - 1].privateKey); + bytes memory lastSignature = _generateSignature(stakeMsg, witnesses[gateway.requiredProofs() - 1].privateKey); vm.prank(relayer); - gateway.submitProofForStakeMsg(witnesses[REQUIRED_PROOFS - 1].addr, stakeMsg, lastSignature); + gateway.submitProofForStakeMsg(witnesses[gateway.requiredProofs() - 1].addr, stakeMsg, lastSignature); // Verify transaction is restarted owing to expired and not processed bytes32 messageHash = _getMessageHash(stakeMsg); @@ -667,6 +796,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_RestartExpiredTransaction() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -702,6 +832,9 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_JoinRestartedTransaction() public { _addAllWitnesses(); + _activateConsensus(); + // afater activating consensus, required proofs should be set as 3 + assertEq(gateway.requiredProofs(), 3); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -726,19 +859,8 @@ contract UTXOGatewayTest is Test { vm.prank(relayer); gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature1); - // as PROOFS_REQUIRED is 2, the transaction should be processed after another witness submits proof - - // mock Assets precompile deposit success and Delegation precompile delegate success - vm.mockCall( - ASSETS_PRECOMPILE_ADDRESS, - abi.encodeWithSelector(IAssets.depositLST.selector), - abi.encode(true, stakeMsg.amount) - ); - vm.mockCall( - DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) - ); - // First witness can submit proof again in new round + // as requiredProofs is 3, the transaction should not be processed even if the first witness submits proof bytes memory signature0New = _generateSignature(stakeMsg, witnesses[0].privateKey); vm.prank(relayer); gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature0New); @@ -746,19 +868,17 @@ contract UTXOGatewayTest is Test { bytes32 messageHash = _getMessageHash(stakeMsg); // Verify both witnesses' proofs are counted - assertEq( - uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.NotStartedOrProcessed) - ); - assertTrue(gateway.processedTransactions(messageHash)); - assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); - assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); // mapping can not be deleted - // even if we delete txn after processing - assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[1].addr) > 0); // mapping can not be deleted - // even if we delete txn after processing + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 2); + assertFalse(gateway.processedTransactions(messageHash)); + assertFalse(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[1].addr) > 0); } function test_SubmitProofForStakeMsg_RevertDuplicateProofInSameRound() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -782,7 +902,29 @@ contract UTXOGatewayTest is Test { gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signatureSecond); } + function test_ProcessStakeMessage_RevertConsensusActivated() public { + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(Errors.ConsensusRequired.selector); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + function test_ProcessStakeMessage_RegisterNewAddress() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -818,6 +960,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_WithBridgeFee() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -864,6 +1008,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_WithDelegation() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -935,6 +1081,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertOnDepositFailure() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -958,6 +1106,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertWhenPaused() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -979,6 +1129,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertUnauthorizedWitness() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -998,6 +1150,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertInvalidStakeMessage() public { + _deactivateConsensus(); + // Create invalid message with all zero values UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.None, @@ -1017,6 +1171,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertZeroExocoreAddressBeforeRegistration() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -1035,6 +1191,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertInvalidNonce() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -1603,4 +1761,16 @@ contract UTXOGatewayTest is Test { return abi.encodePacked(r, s, v); } + function _activateConsensus() internal { + vm.startPrank(owner); + gateway.updateRequiredProofs(gateway.authorizedWitnessCount()); + vm.stopPrank(); + } + + function _deactivateConsensus() internal { + vm.startPrank(owner); + gateway.updateRequiredProofs(gateway.authorizedWitnessCount() + 1); + vm.stopPrank(); + } + }