diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..96aae60 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +### Replace placeholder values before running scripts + +# Private key used by forge script (hex without quotes) +PRIVATE_KEY=0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd + +# External RPC endpoint to broadcast transactions +RPC_URL=https://rpc.testnet3.goat.network + +# Previously deployed Bitcoin SPV verifier contract +BITCOINSPV_ADDR=0x0000000000000000000000000000000000000001 + +# Committee member addresses (add or remove sequentially numbered keys) +COMMITTEE_0=0x0000000000000000000000000000000000000001 +COMMITTEE_1=0x0000000000000000000000000000000000000002 + +# Watchtower identifiers (32-byte hex strings). Add WATCHTOWER_2, etc. as needed. +WATCHTOWER_0=0x0000000000000000000000000000000000000000000000000000000000000001 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..176bb8f --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +deployMain: + forge script script/DeployGateway.sol:DeployGateway --rpc-url goatMainnet --broadcast -vvvv --verify --verifier blockscout --verifier-url https://explorer.goat.network/api/ + +deployTest: + forge script script/DeployGateway.sol:DeployGateway --rpc-url goatTestnet --broadcast -vvvv --verify --verifier blockscout --verifier-url https://explorer.testnet3.goat.network/api/ + diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..7646659 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/openzeppelin-contracts-upgradeable": { + "rev": "3d5fa5c24c411112bab47bec25cfa9ad0af0e6e8" + }, + "lib/forge-std": { + "rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981" + } +} \ No newline at end of file diff --git a/script/CommitteeRegisterPeerId.sol b/script/CommitteeRegisterPeerId.sol new file mode 100644 index 0000000..0d75b05 --- /dev/null +++ b/script/CommitteeRegisterPeerId.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {CommitteeManagement} from "../src/CommitteeManagement.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; + +/* + Script: Register a peer ID for the committee. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (committee member) + - GATEWAY_ADDR: address of deployed Gateway (proxy) + - PEER_ID: bytes peer ID to register +*/ +contract RegisterPeerId is Script { + function run() public { + uint256 committeePrivateKey = vm.envUint("PRIVATE_KEY"); + address committee = vm.addr(committeePrivateKey); + + address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + address committeeManagementAddr = address(GatewayUpgradeable(payable(gatewayAddr)).committeeManagement()); + bytes memory peerId = vm.envBytes("PEER_ID"); + + console.log("Committee:", committee); + console.log("Gateway:", gatewayAddr); + console.log("Committee Management:", committeeManagementAddr); + console.log("Peer ID:", vm.toString(peerId)); + + vm.startBroadcast(committeePrivateKey); + + CommitteeManagement(committeeManagementAddr).registerPeerId(peerId); + + console.log("Peer ID registered successfully"); + + vm.stopBroadcast(); + } +} diff --git a/script/DebugCancelWithdraw.sol b/script/DebugCancelWithdraw.sol deleted file mode 100644 index 95bbf5c..0000000 --- a/script/DebugCancelWithdraw.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {Script, console} from "forge-std/Script.sol"; - -import {GatewayDebug} from "../src/GatewayDebug.sol"; - -/* - Script: call GatewayDebug.debugCancelWithdraw(graphId) as an operator. - Required env vars: - - PRIVATE_KEY: uint256 private key of the operator who initialized the withdraw - - GATEWAY_ADDR: address of the Gateway (proxy) deployed on the network - - GRAPH_ID: bytes16 hex -*/ -contract DebugCancelWithdraw is Script { - function run() external { - uint256 pk = vm.envUint("PRIVATE_KEY"); - address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); - bytes memory graphRaw = vm.envBytes("GRAPH_ID"); - require(graphRaw.length == 16, "GRAPH_ID must be 16 bytes hex"); - bytes16 graphId; - assembly { - graphId := mload(add(graphRaw, 0x20)) - } - address operator = vm.addr(pk); - console.log("Gateway:", gatewayAddr); - console.log("Operator:", operator); - console.log("GraphId:", vm.toString(graphId)); - - vm.startBroadcast(pk); - GatewayDebug(gatewayAddr).debugCancelWithdraw(graphId); - vm.stopBroadcast(); - - console.log("debugCancelWithdraw sent"); - } -} \ No newline at end of file diff --git a/script/DebugMintPegBTC.sol b/script/DebugMintPegBTC.sol deleted file mode 100644 index ed49129..0000000 --- a/script/DebugMintPegBTC.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Script, console} from "forge-std/Script.sol"; -import {GatewayDebug} from "../src/GatewayDebug.sol"; - -/* - Required env vars: - - PRIVATE_KEY: uint256 private key of the operator who initialized the withdraw - - GATEWAY_ADDR: address of the Gateway (proxy) deployed on the network - - TO: recipient_address - - AMOUNT: amount_to_mint -*/ -contract DebugMintPegBTC is Script { - address public sender; - address payable public gateway; - - function setUp() public virtual { - gateway = payable(vm.envAddress("GATEWAY_ADDR")); - } - - function run() public { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - sender = vm.createWallet(senderPrivateKey).addr; - console.log("sender address: ", sender); - - vm.startBroadcast(senderPrivateKey); - _mintPegBTC(); - vm.stopBroadcast(); - } - - function _mintPegBTC() public { - address to = vm.envAddress("TO"); - uint256 amount = vm.envUint("AMOUNT"); - require(to != address(0), "TO env required"); - require(amount > 0, "AMOUNT env required"); - - GatewayDebug(gateway).debugMintPegBTC(to, amount); - console.log("debugMintPegBTC called with to:", to, "amount:", amount); - } -} \ No newline at end of file diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index f4a0a59..0107e55 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -6,12 +6,20 @@ import {IPegBTC} from "../src/interfaces/IPegBTC.sol"; import {IBitcoinSPV} from "../src/interfaces/IBitcoinSPV.sol"; import {CommitteeManagement} from "../src/CommitteeManagement.sol"; import {StakeManagement} from "../src/StakeManagement.sol"; +import {ICommitteeManagement} from "../src/interfaces/ICommitteeManagement.sol"; +import {IStakeManagement} from "../src/interfaces/IStakeManagement.sol"; import {GatewayDebug, CommitteeManagementDebug} from "../src/GatewayDebug.sol"; import {PegBTC} from "../src/PegBTC.sol"; import {UpgradeableProxy} from "../src/UpgradeableProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/* + Script: Deploy the GatewayDebug contract and related contracts. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (deployer) + - BITCOINSPV_ADDR: address of the BitcoinSPV contract +*/ contract DeployGateway is Script { address public deployer; address public bitcoinSPV; @@ -30,47 +38,97 @@ contract DeployGateway is Script { } function deploy() public { - // deploy contracts GatewayDebug gatewayImpl = new GatewayDebug(); - console.log("Gateway implementation contract address: ", address(gatewayImpl)); + console.log( + "Gateway implementation contract address: ", + address(gatewayImpl) + ); - UpgradeableProxy proxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); - console.log("Gateway proxy contract contract address: ", address(proxy)); - GatewayDebug gateway = GatewayDebug(payable(proxy)); + UpgradeableProxy gatewayProxy = new UpgradeableProxy( + address(gatewayImpl), + deployer, + "" + ); + console.log("Gateway proxy contract address: ", address(gatewayProxy)); + GatewayDebug gateway = GatewayDebug(payable(gatewayProxy)); PegBTC pegBTC = new PegBTC(address(gateway)); console.log("PegBTC contract address: ", address(pegBTC)); - // Read committee config from env - // - COMMITTEE_0, COMMITTEE_1, ... (addresses) - // - WATCHTOWER_0, WATCHTOWER_1, ... (bytes32) address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; - bytes32[] memory initialWatchtowers = _readSequentialBytes32("WATCHTOWER"); + bytes32[] memory initialWatchtowers = _readSequentialBytes32( + "WATCHTOWER" + ); + require(initialMembers.length > 0, "COMMITTEE list empty"); + require(initialWatchtowers.length > 0, "WATCHTOWER list empty"); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); - CommitteeManagementDebug committeeManagement = - new CommitteeManagementDebug(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); - console.log("CommitteeManagement contract address: ", address(committeeManagement)); - StakeManagement stakeManagement = new StakeManagement(IERC20(address(pegBTC)), address(gateway)); - console.log("StakeManagement contract address: ", address(stakeManagement)); + CommitteeManagementDebug committeeImpl = new CommitteeManagementDebug(); + console.log( + "CommitteeManagement implementation contract address: ", + address(committeeImpl) + ); + bytes memory committeeInitData = abi.encodeWithSelector( + CommitteeManagement.initialize.selector, + initialMembers, + initialRequired, + initialAuthorizedCallers, + initialWatchtowers + ); + UpgradeableProxy committeeProxy = new UpgradeableProxy( + address(committeeImpl), + deployer, + committeeInitData + ); + console.log( + "CommitteeManagement proxy contract address: ", + address(committeeProxy) + ); + ICommitteeManagement committeeManagement = ICommitteeManagement( + address(committeeProxy) + ); + + StakeManagement stakeImpl = new StakeManagement(); + console.log( + "StakeManagement implementation contract address: ", + address(stakeImpl) + ); + bytes memory stakeInitData = abi.encodeWithSelector( + StakeManagement.initialize.selector, + IERC20(address(pegBTC)), + address(gateway) + ); + UpgradeableProxy stakeProxy = new UpgradeableProxy( + address(stakeImpl), + deployer, + stakeInitData + ); + console.log( + "StakeManagement proxy contract address: ", + address(stakeProxy) + ); + IStakeManagement stakeManagement = IStakeManagement( + address(stakeProxy) + ); gateway.initialize( IPegBTC(address(pegBTC)), IBitcoinSPV(bitcoinSPV), - CommitteeManagement(address(committeeManagement)), - StakeManagement(address(stakeManagement)) + committeeManagement, + stakeManagement ); } - // Helpers: read env arrays as sequential variables with numeric suffixes - // Example: BASE=PREFIX, reads PREFIX_0, PREFIX_1, ... until a default is hit. - function _readSequentialAddresses(string memory baseKey) internal view returns (address[] memory out) { - // first pass: count + function _readSequentialAddresses( + string memory baseKey + ) internal view returns (address[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); address val = vm.envOr(key, address(0)); if (val == address(0)) break; unchecked { @@ -79,16 +137,21 @@ contract DeployGateway is Script { } out = new address[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envAddress(key); } } - function _readSequentialBytes32(string memory baseKey) internal view returns (bytes32[] memory out) { - // first pass: count + function _readSequentialBytes32( + string memory baseKey + ) internal view returns (bytes32[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); bytes32 val = vm.envOr(key, bytes32(0)); if (val == bytes32(0)) break; unchecked { @@ -97,7 +160,9 @@ contract DeployGateway is Script { } out = new bytes32[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envBytes32(key); } } diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol index aa7c001..325e842 100644 --- a/script/DeployGateway.sol +++ b/script/DeployGateway.sol @@ -6,14 +6,20 @@ import {IPegBTC} from "../src/interfaces/IPegBTC.sol"; import {IBitcoinSPV} from "../src/interfaces/IBitcoinSPV.sol"; import {CommitteeManagement} from "../src/CommitteeManagement.sol"; import {StakeManagement} from "../src/StakeManagement.sol"; +import {ICommitteeManagement} from "../src/interfaces/ICommitteeManagement.sol"; +import {IStakeManagement} from "../src/interfaces/IStakeManagement.sol"; import {GatewayUpgradeable} from "../src/Gateway.sol"; import {PegBTC} from "../src/PegBTC.sol"; import {UpgradeableProxy} from "../src/UpgradeableProxy.sol"; -import {CommitteeManagement} from "../src/CommitteeManagement.sol"; -import {StakeManagement} from "../src/StakeManagement.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/* + Script: Deploy the Gateway contract and related contracts. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (deployer) + - BITCOINSPV_ADDR: address of the BitcoinSPV contract +*/ contract DeployGateway is Script { address public deployer; address public bitcoinSPV; @@ -32,47 +38,101 @@ contract DeployGateway is Script { } function deploy() public { - // deploy contracts + // deploy gateway implementation + proxy GatewayUpgradeable gatewayImpl = new GatewayUpgradeable(); - console.log("Gateway implementation contract address: ", address(gatewayImpl)); + console.log( + "Gateway implementation contract address: ", + address(gatewayImpl) + ); - UpgradeableProxy proxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); - console.log("Gateway proxy contract contract address: ", address(proxy)); - GatewayUpgradeable gateway = GatewayUpgradeable(payable(proxy)); + UpgradeableProxy gatewayProxy = new UpgradeableProxy( + address(gatewayImpl), + deployer, + "" + ); + console.log("Gateway proxy contract address: ", address(gatewayProxy)); + GatewayUpgradeable gateway = GatewayUpgradeable(payable(gatewayProxy)); PegBTC pegBTC = new PegBTC(address(gateway)); console.log("PegBTC contract address: ", address(pegBTC)); // Read committee config from env - // - COMMITTEE_0, COMMITTEE_1, ... (addresses) - // - WATCHTOWER_0, WATCHTOWER_1, ... (bytes32) address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; - bytes32[] memory initialWatchtowers = _readSequentialBytes32("WATCHTOWER"); + bytes32[] memory initialWatchtowers = _readSequentialBytes32( + "WATCHTOWER" + ); + require(initialMembers.length > 0, "COMMITTEE list empty"); + require(initialWatchtowers.length > 0, "WATCHTOWER list empty"); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); - CommitteeManagement committeeManagement = - new CommitteeManagement(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); - console.log("CommitteeManagement contract address: ", address(committeeManagement)); - StakeManagement stakeManagement = new StakeManagement(IERC20(address(pegBTC)), address(gateway)); - console.log("StakeManagement contract address: ", address(stakeManagement)); + // Deploy CommitteeManagement implementation + proxy + CommitteeManagement committeeImpl = new CommitteeManagement(); + console.log( + "CommitteeManagement implementation contract address: ", + address(committeeImpl) + ); + bytes memory committeeInitData = abi.encodeWithSelector( + CommitteeManagement.initialize.selector, + initialMembers, + initialRequired, + initialAuthorizedCallers, + initialWatchtowers + ); + UpgradeableProxy committeeProxy = new UpgradeableProxy( + address(committeeImpl), + deployer, + committeeInitData + ); + console.log( + "CommitteeManagement proxy contract address: ", + address(committeeProxy) + ); + ICommitteeManagement committeeManagement = ICommitteeManagement( + address(committeeProxy) + ); + + // Deploy StakeManagement implementation + proxy + StakeManagement stakeImpl = new StakeManagement(); + console.log( + "StakeManagement implementation contract address: ", + address(stakeImpl) + ); + bytes memory stakeInitData = abi.encodeWithSelector( + StakeManagement.initialize.selector, + IERC20(address(pegBTC)), + address(gateway) + ); + UpgradeableProxy stakeProxy = new UpgradeableProxy( + address(stakeImpl), + deployer, + stakeInitData + ); + console.log( + "StakeManagement proxy contract address: ", + address(stakeProxy) + ); + IStakeManagement stakeManagement = IStakeManagement( + address(stakeProxy) + ); gateway.initialize( IPegBTC(address(pegBTC)), IBitcoinSPV(bitcoinSPV), - CommitteeManagement(address(committeeManagement)), - StakeManagement(address(stakeManagement)) + committeeManagement, + stakeManagement ); } - // Helpers: read env arrays as sequential variables with numeric suffixes - // Example: BASE=PREFIX, reads PREFIX_0, PREFIX_1, ... until a default is hit. - function _readSequentialAddresses(string memory baseKey) internal view returns (address[] memory out) { - // first pass: count + function _readSequentialAddresses( + string memory baseKey + ) internal view returns (address[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); address val = vm.envOr(key, address(0)); if (val == address(0)) break; unchecked { @@ -81,16 +141,21 @@ contract DeployGateway is Script { } out = new address[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envAddress(key); } } - function _readSequentialBytes32(string memory baseKey) internal view returns (bytes32[] memory out) { - // first pass: count + function _readSequentialBytes32( + string memory baseKey + ) internal view returns (bytes32[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); bytes32 val = vm.envOr(key, bytes32(0)); if (val == bytes32(0)) break; unchecked { @@ -99,7 +164,9 @@ contract DeployGateway is Script { } out = new bytes32[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envBytes32(key); } } diff --git a/script/InitWithdraw.sol b/script/InitWithdraw.sol index 67fc510..a0837fc 100644 --- a/script/InitWithdraw.sol +++ b/script/InitWithdraw.sol @@ -42,7 +42,7 @@ contract InitWithdraw is Script { // Read pegin data to compute the exact lock amount in PegBTC GatewayUpgradeable.PeginData memory pegin = gateway.getPeginData(instanceId); require(pegin.peginAmountSats > 0, "pegin not found or amount=0"); - uint256 lockAmount = Converter.amountFromSats(pegin.peginAmountSats); + uint256 lockAmount = Converter._amountFromSats(pegin.peginAmountSats); // derive sender address from private key in a view-only way address operator = vm.addr(pk); diff --git a/script/ListInstance.sol b/script/ListInstance.sol new file mode 100644 index 0000000..a1af434 --- /dev/null +++ b/script/ListInstance.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; + +/* + Script: List instances from the Gateway. + Required env vars: + - GATEWAY_ADDR: address of deployed Gateway (proxy) + - START_INDEX: (optional) uint256 start index for listing (default: 0) + - END_INDEX: (optional) uint256 end index for listing (default: max) + - FILTER_PEGIN_STATUS: (optional) int256 filter by pegin status (default: -1 for all) + - FILTER_WITHDRAW_STATUS: (optional) int256 filter by withdraw status (default: -1 for all) +*/ +contract DebugListInstance is Script { + GatewayUpgradeable gateway; + int256 filterPeginStatus; + int256 filterWithdrawStatus; + + function run() public { + address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + gateway = GatewayUpgradeable(gatewayAddr); + + console.log("Listing instances for Gateway at:", gatewayAddr); + + uint256 startIndex = vm.envOr("START_INDEX", uint256(0)); + uint256 endIndex = vm.envOr("END_INDEX", type(uint256).max); + + filterPeginStatus = vm.envOr("FILTER_PEGIN_STATUS", int256(-1)); + filterWithdrawStatus = vm.envOr("FILTER_WITHDRAW_STATUS", int256(-1)); + + uint256 i = startIndex; + while (i <= endIndex) { + try gateway.instanceIds(i) returns (bytes16 instanceId) { + listInstance(i, instanceId); + i++; + } catch { + break; + } + } + } + + function getPeginStatusString(uint256 status) internal pure returns (string memory) { + if (status == uint256(IGateway.PeginStatus.None)) return "None"; + if (status == uint256(IGateway.PeginStatus.Pending)) return "Pending"; + if (status == uint256(IGateway.PeginStatus.Withdrawbale)) return "Withdrawbale"; + if (status == uint256(IGateway.PeginStatus.Processing)) return "Processing"; + if (status == uint256(IGateway.PeginStatus.Locked)) return "Locked"; + if (status == uint256(IGateway.PeginStatus.Claimed)) return "Claimed"; + if (status == uint256(IGateway.PeginStatus.Discarded)) return "Discarded"; + return "Unknown"; + } + + function getWithdrawStatusString(uint256 status) internal pure returns (string memory) { + if (status == uint256(IGateway.WithdrawStatus.None)) return "None"; + if (status == uint256(IGateway.WithdrawStatus.Processing)) return "Processing"; + if (status == uint256(IGateway.WithdrawStatus.Initialized)) return "Initialized"; + if (status == uint256(IGateway.WithdrawStatus.Canceled)) return "Canceled"; + if (status == uint256(IGateway.WithdrawStatus.Complete)) return "Complete"; + if (status == uint256(IGateway.WithdrawStatus.Disproved)) return "Disproved"; + return "Unknown"; + } + + function listInstance(uint256 index, bytes16 instanceId) internal view { + // Get Pegin Status + (bool success, bytes memory data) = address(gateway).staticcall( + abi.encodeWithSelector(gateway.peginDataMap.selector, instanceId) + ); + + uint256 statusVal; + bool statusFetched = false; + if (success && data.length >= 32) { + statusVal = abi.decode(data, (uint256)); + statusFetched = true; + } + + if (filterPeginStatus != -1) { + if (!statusFetched || int256(statusVal) != filterPeginStatus) { + return; + } + } + + console.log("--------------------------------------------------"); + console.log("Index:", index); + console.log("Instance ID:"); + console.logBytes16(instanceId); + + if (statusFetched) { + console.log("Pegin Status:", getPeginStatusString(statusVal)); + } else { + console.log("Failed to fetch Pegin Status"); + } + + // List graphs + uint256 j = 0; + while (true) { + try gateway.instanceIdToGraphIds(instanceId, j) returns (bytes16 graphId) { + listGraph(graphId); + j++; + } catch { + break; + } + } + } + + function listGraph(bytes16 graphId) internal view { + // Get Withdraw Status + (bool success, bytes memory data) = address(gateway).staticcall( + abi.encodeWithSelector(gateway.withdrawDataMap.selector, graphId) + ); + + uint256 statusVal; + bool statusFetched = false; + if (success && data.length >= 32) { + statusVal = abi.decode(data, (uint256)); + statusFetched = true; + } + + if (filterWithdrawStatus != -1) { + if (!statusFetched || int256(statusVal) != filterWithdrawStatus) { + return; + } + } + + console.log(" Graph ID:"); + console.logBytes16(graphId); + + if (statusFetched) { + console.log(" Withdraw Status:", getWithdrawStatusString(statusVal)); + } else { + console.log(" Failed to fetch Withdraw Status"); + } + } +} \ No newline at end of file diff --git a/script/MockProceedWithdraw.sol b/script/MockProceedWithdraw.sol deleted file mode 100644 index 47bfcaf..0000000 --- a/script/MockProceedWithdraw.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Script, console} from "forge-std/Script.sol"; -import {GatewayDebug} from "../src/GatewayDebug.sol"; - -/* - Required env vars: - - PRIVATE_KEY: uint256 private key to broadcast from (operator) - - GATEWAY_ADDR: address of deployed Gateway (proxy) - - GRAPH_ID: bytes16 hex -*/ -contract MockProceedWithdraw is Script { - address public sender; - address payable public gateway; - - function setUp() public virtual { - gateway = payable(vm.envAddress("GATEWAY_ADDR")); - } - - function run() public { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - sender = vm.createWallet(senderPrivateKey).addr; - console.log("sender address:", sender); - - vm.startBroadcast(senderPrivateKey); - _mockProceedWithdraw(); - vm.stopBroadcast(); - } - - function _mockProceedWithdraw() public { - bytes memory graphRaw = vm.envBytes("GRAPH_ID"); - require(graphRaw.length == 16, "GRAPH_ID must be 16 bytes hex"); - bytes16 graphId; - assembly { - graphId := mload(add(graphRaw, 0x20)) - } - console.log("\ngraphId:", vm.toString(graphId)); - - GatewayDebug(gateway).mockProceedWithdraw(graphId); - console.log("mockProceedWithdraw sent for graphId:", vm.toString(uint256(uint128(bytes16(graphId))))); - } -} \ No newline at end of file diff --git a/script/OperatorRegisterPubkey.sol b/script/OperatorRegisterPubkey.sol new file mode 100644 index 0000000..348a612 --- /dev/null +++ b/script/OperatorRegisterPubkey.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; +import {StakeManagement} from "../src/StakeManagement.sol"; + +/* + Script: Register a public key for the operator. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (operator) + - GATEWAY_ADDR: address of deployed Gateway (proxy) + - PUBKEY: bytes32 public key to register +*/ +contract OperatorRegisterPubkey is Script { + function run() public { + uint256 operatorPrivateKey = vm.envUint("PRIVATE_KEY"); + address operator = vm.addr(operatorPrivateKey); + + address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + bytes32 pubkey = vm.envBytes32("PUBKEY"); + + console.log("Operator:", operator); + console.log("Gateway:", gatewayAddr); + console.log("Pubkey:", vm.toString(pubkey)); + + vm.startBroadcast(operatorPrivateKey); + address stakeManagementAddr = address(GatewayUpgradeable(payable(gatewayAddr)).stakeManagement()); + console.log("StakeManagement:", stakeManagementAddr); + + StakeManagement(stakeManagementAddr).registerPubkey(pubkey); + + console.log("Pubkey registered successfully"); + + vm.stopBroadcast(); + } +} diff --git a/script/OperatorStake.sol b/script/OperatorStake.sol new file mode 100644 index 0000000..6f0013e --- /dev/null +++ b/script/OperatorStake.sol @@ -0,0 +1,96 @@ +pragma solidity ^0.8.0; + +import {Script, console} from "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {StakeManagement} from "../src/StakeManagement.sol"; +import {IStakeManagement} from "../src/interfaces/IStakeManagement.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; + +/* +# Operator stake and lock + +Stake PBTC as an operator and lock it via StakeManagement fetched from the Gateway. + +Env vars: +- GATEWAY_ADDR: address of the Gateway proxy +- PRIVATE_KEY: operator's private key +- STAKE_AMOUNT: amount of PBTC to stake (wei) +- LOCK_AMOUNT: amount to lock (wei), optional; defaults to STAKE_AMOUNT when omitted or 0 + +Example: + +```sh +export GATEWAY_ADDR=0x... +export PRIVATE_KEY=... +export STAKE_AMOUNT=60000000000000000 +export LOCK_AMOUNT=60000000000000000 # optional +``` +*/ +contract OperatorStake is Script { + address public operator; + address public stakeManagementAddr; + address public gatewayAddr; + + uint256 public stakeAmount; + uint256 public lockAmount; + + function setUp() public virtual { + // Read Gateway address from env, then fetch StakeManagement from it + gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + IStakeManagement sm = GatewayUpgradeable(payable(gatewayAddr)).stakeManagement(); + stakeManagementAddr = address(sm); + + stakeAmount = vm.envUint("STAKE_AMOUNT"); + uint256 lockAmt = vm.envOr("LOCK_AMOUNT", uint256(0)); + lockAmount = lockAmt == 0 ? stakeAmount : lockAmt; + } + + function run() public { + uint256 operatorPk = vm.envUint("PRIVATE_KEY"); + operator = vm.createWallet(operatorPk).addr; + console.log("operator:", operator); + console.log("Gateway:", gatewayAddr); + console.log("StakeManagement:", stakeManagementAddr); + + vm.startBroadcast(operatorPk); + _stakeAndLock(); + vm.stopBroadcast(); + } + + function _stakeAndLock() internal { + // Resolve token from StakeManagement + address tokenAddr = IStakeManagement(stakeManagementAddr).stakeTokenAddress(); + IERC20 token = IERC20(tokenAddr); + + console.log("stake token:", tokenAddr); + console.log("stake amount:", stakeAmount); + console.log("lock amount:", lockAmount); + + // Check balance for a friendly message (not a hard requirement here) + uint256 bal = token.balanceOf(operator); + console.log("operator balance:", bal); + + // Approve if needed + uint256 allowance = token.allowance(operator, stakeManagementAddr); + if (allowance < stakeAmount) { + // Approve exactly the shortfall to minimize allowance; simple path: set to stakeAmount + // If an allowance already exists, some ERC20s require resetting to 0 first; PegBTC is standard OpenZeppelin ERC20, so direct set is fine. + bool ok = token.approve(stakeManagementAddr, stakeAmount); + require(ok, "approve failed"); + console.log("approved:", stakeAmount); + } else { + console.log("sufficient allowance, skipping approve"); + } + + // Stake + StakeManagement(stakeManagementAddr).stake(stakeAmount); + console.log("staked"); + + // Lock if requested (> 0) + if (lockAmount > 0) { + StakeManagement(stakeManagementAddr).lockStake(operator, lockAmount); + console.log("locked"); + } + } +} diff --git a/script/README.md b/script/README.md index c876aac..2535d9b 100644 --- a/script/README.md +++ b/script/README.md @@ -1,13 +1,30 @@ -# Deploy +# Deployment Scripts +## Environment setup +1. Copy `.env.example` to `.env` and replace the placeholder values. +2. Add as many `COMMITTEE_` and `WATCHTOWER_` entries as needed. Indexes must be sequential (e.g., `_0`, `_1`, `_2`). +3. Export the variables into your shell (`set -a && source .env && set +a`) or let Foundry load them via `--env .env`. + +## Deploying the Gateway stack + +The `DeployGateway` script provisions the PegBTC token, Gateway (implementation + proxy), CommitteeManagement proxy, and StakeManagement proxy. Each proxy is initialized via constructor-calldata so there is no uninitialized window. + +```bash +make deployTest +``` + +or for mainnet + +```bash +make deployMain ``` -export prv=... -export OWNER=0x8943545177806ED17B9F23F0a21ee5948eCaa776 -forge script script/SSPDeploy.s.sol:Deploy \ - --rpc-url https://rpc.testnet3.goat.network --private-key=$prv --broadcast --legacy +Under the hood these targets expand to `forge script script/DeployGateway.sol:DeployGateway` with the Goat RPC aliases defined in `foundry.toml`, `--broadcast -vvvv`, and Blockscout verification flags (`--verifier blockscout --verifier-url ...`). Provide `PRIVATE_KEY`, `BITCOINSPV_ADDR`, committee, and watchtower env vars before invoking the Makefile. -forge verify-contract --compiler-version 0.8.28 0x3901C4670aA92a626636f7Ea1e3F029A0ECd6b68 SequencerSetPublisher --verifier blockscout --verifier-url 'https://explorer.testnet3.goat.network/api/' +Key environment variables consumed by the script: -``` \ No newline at end of file +- `PRIVATE_KEY`: broadcaster key (hex string, no quotes). +- `BITCOINSPV_ADDR`: already deployed Bitcoin SPV contract on the target chain. +- `COMMITTEE_`: sequential list of committee member addresses (at least one required). +- `WATCHTOWER_`: sequential list of 32-byte watchtower identifiers (at least one required). diff --git a/script/SSPDeploy.s.sol b/script/SSPDeploy.s.sol index 2001fb7..524ff84 100644 --- a/script/SSPDeploy.s.sol +++ b/script/SSPDeploy.s.sol @@ -1,9 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "forge-std/Script.sol"; -import "../src/SequencerSetPublisher.sol"; - +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {SequencerSetPublisher} from "../src/SequencerSetPublisher.sol"; +import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; + +/* + Script: Deploy SequencerSetPublisher and MultiSigVerifier. + Required env vars: + - OWNER: address of the initial owner +*/ contract Deploy is Script { function run() external { // Load from environment variables or replace inline @@ -17,17 +24,33 @@ contract Deploy is Script { initPublishers[4] = 0xa0F88c27B535615A8D8808c6023986a540161021; bytes[] memory initPublisherBTCPubkeys = new bytes[](5); - initPublisherBTCPubkeys[0] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; - initPublisherBTCPubkeys[1] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; - initPublisherBTCPubkeys[2] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; - initPublisherBTCPubkeys[3] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; - initPublisherBTCPubkeys[4] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; + initPublisherBTCPubkeys[ + 0 + ] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; + initPublisherBTCPubkeys[ + 1 + ] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; + initPublisherBTCPubkeys[ + 2 + ] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; + initPublisherBTCPubkeys[ + 3 + ] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; + initPublisherBTCPubkeys[ + 4 + ] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; vm.startBroadcast(); SequencerSetPublisher publisher = new SequencerSetPublisher(); - - publisher.initialize(initialOwner, initPublishers, initPublisherBTCPubkeys); + MultiSigVerifier multiSigVerifier = new MultiSigVerifier(); + + publisher.initialize( + initialOwner, + address(multiSigVerifier), + initPublishers, + initPublisherBTCPubkeys + ); vm.stopBroadcast(); diff --git a/script/UpgradeGateway.sol b/script/UpgradeGateway.sol index c768c9c..c7c0c37 100644 --- a/script/UpgradeGateway.sol +++ b/script/UpgradeGateway.sol @@ -5,6 +5,13 @@ import {GatewayUpgradeable} from "../src/Gateway.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +/* + Script: Upgrade the Gateway contract. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (sender) + - PROXYADMIN_ADDR: address of the ProxyAdmin contract + - GATEWAY_ADDR: address of the Gateway proxy +*/ contract UpgradeGateway is Script { address public sender; address public proxyAdmin; diff --git a/script/UpgradeGatewayDebug.sol b/script/UpgradeGatewayDebug.sol index aae4127..d45526f 100644 --- a/script/UpgradeGatewayDebug.sol +++ b/script/UpgradeGatewayDebug.sol @@ -5,6 +5,13 @@ import {GatewayDebug} from "../src/GatewayDebug.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +/* + Script: Upgrade the Gateway contract to GatewayDebug. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (sender) + - PROXYADMIN_ADDR: address of the ProxyAdmin contract + - GATEWAY_ADDR: address of the Gateway proxy +*/ contract UpgradeGateway is Script { address public sender; address public proxyAdmin; diff --git a/src/CommitteeManagement.sol b/src/CommitteeManagement.sol index 489e316..7ac4383 100644 --- a/src/CommitteeManagement.sol +++ b/src/CommitteeManagement.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.28; import {MultiSigVerifier} from "./MultiSigVerifier.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { + EnumerableSet +} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; /// @title CommitteeManagement /// @notice Manages committee membership verification, authorized message execution with anti-replay, @@ -28,17 +30,23 @@ contract CommitteeManagement is MultiSigVerifier { /// @notice Tracks whether a nonced message hash has been consumed on-chain to prevent replay. mapping(bytes32 => bool) public executed; - // ========== Constructor ========== + // ========== Initialization ========== + constructor() { + _disableInitializers(); + } + + /// @notice Initializes committee membership, watchtowers, and authorized callers. /// @param initialMembers Initial committee owner addresses /// @param requiredSignatures Threshold for a message to be considered authorized /// @param initialAuthorizedCallers Initial authorized caller addresses (Gateway, etc.) /// @param initialWatchtowers Initial watchtower addresses - constructor( + function initialize( address[] memory initialMembers, uint256 requiredSignatures, address[] memory initialAuthorizedCallers, bytes32[] memory initialWatchtowers - ) MultiSigVerifier(initialMembers, requiredSignatures) { + ) public initializer { + __MultiSigVerifier_init(initialMembers, requiredSignatures); for (uint256 i = 0; i < initialAuthorizedCallers.length; i++) { authorizedCallers.add(initialAuthorizedCallers[i]); } @@ -66,7 +74,10 @@ contract CommitteeManagement is MultiSigVerifier { /// @notice Helper to verify signatures using the inherited MultiSigVerifier /// @param msgHash The message hash to verify (already domain- and nonce-bound if applicable) /// @param signatures Committee signatures - function verifySignatures(bytes32 msgHash, bytes[] memory signatures) external view returns (bool) { + function verifySignatures( + bytes32 msgHash, + bytes[] memory signatures + ) external view returns (bool) { return verify(msgHash, signatures); } @@ -90,7 +101,10 @@ contract CommitteeManagement is MultiSigVerifier { // Enforce uniqueness of peerId across members (by hash) bytes32 h = keccak256(peerId); address current = peerIdOwnerByHash[h]; - require(current == address(0) || current == msg.sender, "peerId already registered by another member"); + require( + current == address(0) || current == msg.sender, + "peerId already registered by another member" + ); committeePeerId[msg.sender] = peerId; peerIdOwnerByHash[h] = msg.sender; @@ -99,7 +113,9 @@ contract CommitteeManagement is MultiSigVerifier { } /// @notice Get the stored PeerId of a committee member - function getCommitteePeerId(address member) external view returns (bytes memory) { + function getCommitteePeerId( + address member + ) external view returns (bytes memory) { require(isOwner[member], "Not a committee member"); bytes memory id = committeePeerId[member]; require(id.length != 0, "Member has no registered PeerId"); @@ -121,7 +137,10 @@ contract CommitteeManagement is MultiSigVerifier { // ========== Modifiers ========== /// @dev Restricts external execution of nonced signatures to whitelisted callers. modifier onlyAuthorizedCaller() { - require(authorizedCallers.contains(msg.sender), "caller not authorized"); + require( + authorizedCallers.contains(msg.sender), + "caller not authorized" + ); _; } @@ -130,10 +149,17 @@ contract CommitteeManagement is MultiSigVerifier { /// @param msgHash Preimage message hash (without nonce). Must be domain-bound by this contract in its encoder /// @param nonce Per-usage nonce agreed off-chain and included in signatures /// @param signatures Committee signatures authorizing the action - function _executeNoncedSignatures(bytes32 msgHash, uint256 nonce, bytes[] memory signatures) internal { + function _executeNoncedSignatures( + bytes32 msgHash, + uint256 nonce, + bytes[] memory signatures + ) internal { bytes32 noncedHash = getNoncedDigest(msgHash, nonce); require(!executed[noncedHash], "Already executed"); - require(verify(noncedHash, signatures), "Not enough valid committee signatures"); + require( + verify(noncedHash, signatures), + "Not enough valid committee signatures" + ); executed[noncedHash] = true; } @@ -142,57 +168,81 @@ contract CommitteeManagement is MultiSigVerifier { /// @param msgHash Preimage message hash (without nonce). Must be domain-bound by this contract in its encoder /// @param nonce Per-usage nonce agreed off-chain and included in signatures /// @param signatures Committee signatures authorizing the action - function executeNoncedSignatures(bytes32 msgHash, uint256 nonce, bytes[] memory signatures) - external - onlyAuthorizedCaller - { + function executeNoncedSignatures( + bytes32 msgHash, + uint256 nonce, + bytes[] memory signatures + ) external onlyAuthorizedCaller { _executeNoncedSignatures(msgHash, nonce, signatures); } // ========== Watchtower Management ========== /// @notice Add a watchtower address via committee authorization - function addWatchtower(bytes32 watchtower, uint256 nonce, bytes[] memory authSignatures) external { - bytes32 msgHash = getAddWatchtowerDigest(watchtower); + function addWatchtower( + bytes32 watchtower, + uint256 nonce, + bytes[] memory authSignatures + ) external { + bytes32 msgHash = _getAddWatchtowerDigest(watchtower); _executeNoncedSignatures(msgHash, nonce, authSignatures); watchtowerList.add(watchtower); } /// @notice Remove a watchtower address via committee authorization - function removeWatchtower(bytes32 watchtower, uint256 nonce, bytes[] memory authSignatures) external { - bytes32 msgHash = getRemoveWatchtowerDigest(watchtower); + function removeWatchtower( + bytes32 watchtower, + uint256 nonce, + bytes[] memory authSignatures + ) external { + bytes32 msgHash = _getRemoveWatchtowerDigest(watchtower); _executeNoncedSignatures(msgHash, nonce, authSignatures); watchtowerList.remove(watchtower); } // ========== Digest Helpers (Watchtower) ========== /// @dev Returns the domain-bound message hash for adding a watchtower (without nonce) - function getAddWatchtowerDigest(bytes32 watchtower) internal view returns (bytes32) { + function _getAddWatchtowerDigest( + bytes32 watchtower + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("ADD_WATCHTOWER(bytes32 watchtower)"); return keccak256(abi.encode(typeHash, address(this), watchtower)); } /// @notice Returns the fully nonced digest for adding a watchtower - function getAddWatchtowerDigestNonced(bytes32 watchtower, uint256 nonce) public view returns (bytes32) { - bytes32 msgHash = getAddWatchtowerDigest(watchtower); + function getAddWatchtowerDigestNonced( + bytes32 watchtower, + uint256 nonce + ) public view returns (bytes32) { + bytes32 msgHash = _getAddWatchtowerDigest(watchtower); return getNoncedDigest(msgHash, nonce); } /// @dev Returns the domain-bound message hash for removing a watchtower (without nonce) - function getRemoveWatchtowerDigest(bytes32 watchtower) internal view returns (bytes32) { + function _getRemoveWatchtowerDigest( + bytes32 watchtower + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("REMOVE_WATCHTOWER(bytes32 watchtower)"); return keccak256(abi.encode(typeHash, address(this), watchtower)); } /// @notice Returns the fully nonced digest for removing a watchtower - function getRemoveWatchtowerDigestNonced(bytes32 watchtower, uint256 nonce) public view returns (bytes32) { - bytes32 msgHash = getRemoveWatchtowerDigest(watchtower); + function getRemoveWatchtowerDigestNonced( + bytes32 watchtower, + uint256 nonce + ) public view returns (bytes32) { + bytes32 msgHash = _getRemoveWatchtowerDigest(watchtower); return getNoncedDigest(msgHash, nonce); } /// @notice Returns the fully nonced digest for an action-specific preimage hash /// @dev Domain-bound by this contract address and includes the provided nonce. - function getNoncedDigest(bytes32 msgHash, uint256 nonce) public view returns (bytes32) { - bytes32 typeHash = keccak256("NONCED_MESSAGE(bytes32 msgHash,uint256 nonce)"); + function getNoncedDigest( + bytes32 msgHash, + uint256 nonce + ) public view returns (bytes32) { + bytes32 typeHash = keccak256( + "NONCED_MESSAGE(bytes32 msgHash,uint256 nonce)" + ); return keccak256(abi.encode(typeHash, address(this), msgHash, nonce)); } @@ -209,16 +259,24 @@ contract CommitteeManagement is MultiSigVerifier { } /// @notice Add an authorized external caller via committee authorization - function addAuthorizedCaller(address caller, uint256 nonce, bytes[] memory authSignatures) external { - bytes32 msgHash = getAddAuthorizedCallerDigest(caller); + function addAuthorizedCaller( + address caller, + uint256 nonce, + bytes[] memory authSignatures + ) external { + bytes32 msgHash = _getAddAuthorizedCallerDigest(caller); _executeNoncedSignatures(msgHash, nonce, authSignatures); authorizedCallers.add(caller); emit AuthorizedCallerAdded(caller); } /// @notice Remove an authorized external caller via committee authorization - function removeAuthorizedCaller(address caller, uint256 nonce, bytes[] memory authSignatures) external { - bytes32 msgHash = getRemoveAuthorizedCallerDigest(caller); + function removeAuthorizedCaller( + address caller, + uint256 nonce, + bytes[] memory authSignatures + ) external { + bytes32 msgHash = _getRemoveAuthorizedCallerDigest(caller); _executeNoncedSignatures(msgHash, nonce, authSignatures); authorizedCallers.remove(caller); emit AuthorizedCallerRemoved(caller); @@ -226,26 +284,38 @@ contract CommitteeManagement is MultiSigVerifier { // ========== Digest Helpers (Authorized Callers) ========== /// @dev Returns the domain-bound message hash for adding an authorized external caller (without nonce) - function getAddAuthorizedCallerDigest(address caller) internal view returns (bytes32) { + function _getAddAuthorizedCallerDigest( + address caller + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("ADD_AUTH_CALLER(address caller)"); return keccak256(abi.encode(typeHash, address(this), caller)); } /// @notice Returns the fully nonced digest for adding an authorized external caller - function getAddAuthorizedCallerDigestNonced(address caller, uint256 nonce) public view returns (bytes32) { - bytes32 msgHash = getAddAuthorizedCallerDigest(caller); + function getAddAuthorizedCallerDigestNonced( + address caller, + uint256 nonce + ) public view returns (bytes32) { + bytes32 msgHash = _getAddAuthorizedCallerDigest(caller); return getNoncedDigest(msgHash, nonce); } /// @dev Returns the domain-bound message hash for removing an authorized external caller (without nonce) - function getRemoveAuthorizedCallerDigest(address caller) internal view returns (bytes32) { + function _getRemoveAuthorizedCallerDigest( + address caller + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("REMOVE_AUTH_CALLER(address caller)"); return keccak256(abi.encode(typeHash, address(this), caller)); } /// @notice Returns the fully nonced digest for removing an authorized external caller - function getRemoveAuthorizedCallerDigestNonced(address caller, uint256 nonce) public view returns (bytes32) { - bytes32 msgHash = getRemoveAuthorizedCallerDigest(caller); + function getRemoveAuthorizedCallerDigestNonced( + address caller, + uint256 nonce + ) public view returns (bytes32) { + bytes32 msgHash = _getRemoveAuthorizedCallerDigest(caller); return getNoncedDigest(msgHash, nonce); } + + uint256[50] private __gap; } diff --git a/src/Gateway.sol b/src/Gateway.sol index 7951868..ab43521 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -1,48 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {IBitcoinSPV} from "./interfaces/IBitcoinSPV.sol"; import {IPegBTC} from "./interfaces/IPegBTC.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {CommitteeManagement} from "./CommitteeManagement.sol"; -import {StakeManagement} from "./StakeManagement.sol"; +import {ICommitteeManagement} from "./interfaces/ICommitteeManagement.sol"; +import {IStakeManagement} from "./interfaces/IStakeManagement.sol"; +import {IGateway} from "./interfaces/IGateway.sol"; import {Converter} from "./libraries/Converter.sol"; import {BitvmTxParser} from "./libraries/BitvmTxParser.sol"; import {MerkleProof} from "./libraries/MerkleProof.sol"; -// Custom errors to reduce bytecode size (replace long revert strings) -error NotCommittee(); -error NotOperator(); -error InstanceUsed(); -error NotPending(); -error WindowExpired(); -error WindowNotExpired(); -error NotEnoughCommittee(); -error InvalidPubkeyLen(); -error InvalidPubkeyParity(); -error InstanceMismatch(); -error PeginAmountMismatch(); -error InvalidHeader(); -error MerkleVerifyFail(); -error InvalidSignatures(); -error FeeTooHigh(); -error OperatorNotRegistered(); -error StakeInsufficient(); -error GraphAlreadyPosted(); -error GraphPeginTxidMismatch(); -error WithdrawStatusInvalid(); -error NotWithdrawable(); -error TimelockNotExpired(); -error KickoffHeightLow(); -error TxidMismatch(); -error AlreadyDisproved(); -error IndexOutOfRange(); -error UnknownDisproveType(); -error DisproveInvalidHeader(); - contract BitvmPolicy { uint64 constant rateMultiplier = 10000; @@ -52,185 +25,59 @@ contract BitvmPolicy { uint64 public minOperatorRewardSats; uint64 public operatorRewardRate; - uint64 public minStakeAmount; // TODO: uint256 - uint64 public minChallengerReward; // TODO: uint256 - uint64 public minDisproverReward; // TODO: uint256 - uint64 public minSlashAmount; // TODO: uint256 + uint256 public minStakeAmount; + uint256 public minChallengerReward; + uint256 public minDisproverReward; + uint256 public minSlashAmount; // TODO Initializer & setters } -contract GatewayUpgradeable is BitvmPolicy, Initializable { +contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { using ECDSA for bytes32; // EIP-712-like typehash constants to avoid recomputing literals bytes32 private constant POST_PEGIN_TYPEHASH = - keccak256("POST_PEGIN_DATA(address contract,bytes16 instanceId,bytes32 peginTxid)"); + keccak256( + "POST_PEGIN_DATA(address contract,bytes16 instanceId,bytes32 peginTxid)" + ); bytes32 private constant POST_GRAPH_TYPEHASH = - keccak256("POST_GRAPH_DATA(address contract,bytes16 instanceId,bytes16 graphId,bytes32 graphDataHash)"); - bytes32 private constant CANCEL_WITHDRAW_TYPEHASH = keccak256("CANCEL_WITHDRAW(address contract,bytes16 graphId)"); + keccak256( + "POST_GRAPH_DATA(address contract,bytes16 instanceId,bytes16 graphId,bytes32 graphDataHash)" + ); + bytes32 private constant CANCEL_WITHDRAW_TYPEHASH = + keccak256("CANCEL_WITHDRAW(address contract,bytes16 graphId)"); bytes32 private constant UNLOCK_STAKE_TYPEHASH = - keccak256("UNLOCK_OPERATOR_STAKE(address contract,address operator,uint256 amount)"); - - event BridgeInRequest( - bytes16 indexed instanceId, - address indexed depositorAddress, - uint64 peginAmountSats, - uint64[3] txnFees, - Utxo[] userInputs, - bytes32 userXonlyPubkey, - string userChangeAddress, - string userRefundAddress - ); - event CommitteeResponse(bytes16 indexed instanceId, address indexed committeeAddress, bytes committeePubkey); - event BridgeIn( - address indexed depositorAddress, - bytes16 indexed instanceId, - uint64 indexed peginAmountSats, - uint64 feeAmountSats - ); - event PostGraphData(bytes16 indexed instanceId, bytes16 indexed graphId); - event InitWithdraw( - bytes16 indexed instanceId, bytes16 indexed graphId, address indexed operatorAddress, uint64 withdrawAmountSats - ); - event CancelWithdraw(bytes16 indexed instanceId, bytes16 indexed graphId, address indexed triggerAddress); - event ProceedWithdraw(bytes16 indexed instanceId, bytes16 indexed graphId, bytes32 kickoffTxid); - event WithdrawHappyPath( - bytes16 indexed instanceId, - bytes16 indexed graphId, - bytes32 take1Txid, - address indexed operatorAddress, - uint64 rewardAmountSats - ); - event WithdrawUnhappyPath( - bytes16 indexed instanceId, - bytes16 indexed graphId, - bytes32 take2Txid, - address indexed operatorAddress, - uint64 rewardAmountSats - ); - event WithdrawDisproved( - bytes16 indexed instanceId, - bytes16 indexed graphId, - DisproveTxType disproveTxType, - uint256 txnIndex, - bytes32 challengeStartTxid, - bytes32 challengeFinishTxid, - address challengerAddress, - address disproverAddress, - uint64 challengerRewardAmount, - uint64 disproverRewardAmount - ); - - enum DisproveTxType { - AssertTimeout, - OperatorCommitTimeout, - OperatorNack, - Disprove, - QuickChallenge, - ChallengeIncompeleteKickoff - } - enum PeginStatus { - None, - Pending, - Withdrawbale, - Processing, - Locked, - Claimed, - Discarded - } - enum WithdrawStatus { - None, - Processing, - Initialized, - Canceled, - Complete, - Disproved - } - - struct Utxo { - bytes32 txid; - uint32 vout; - uint64 amountSats; - } - - struct PeginDataInner { - PeginStatus status; - bytes16 instanceId; - address depositorAddress; - uint64 peginAmountSats; - uint64[3] txnFees; - Utxo[] userInputs; - bytes32 userXonlyPubkey; - string userChangeAddress; - string userRefundAddress; - bytes32 peginTxid; - uint256 createdAt; - // EnumerableMap - address[] committeeAddresses; - mapping(address value => uint256) committeeAddressPositions; - mapping(address => bytes1) committeePubkeyParitys; // even (0x02), odd (0x03) - mapping(address => bytes32) committeeXonlyPubkeys; - } - - struct PeginData { - PeginStatus status; - bytes16 instanceId; - address depositorAddress; - uint64 peginAmountSats; - uint64[3] txnFees; // [ peginPrepare , peginConfirm peginCancel ] - Utxo[] userInputs; - bytes32 userXonlyPubkey; - string userChangeAddress; - string userRefundAddress; - bytes32 peginTxid; - uint256 createdAt; - address[] committeeAddresses; - bytes[] committeePubkeys; - } - - struct WithdrawData { - WithdrawStatus status; - bytes32 peginTxid; - address operatorAddress; - bytes16 instanceId; - uint256 lockAmount; - uint256 btcBlockHeightAtWithdraw; - } - - struct GraphData { - bytes1 operatorPubkeyPrefix; - bytes32 operatorPubkey; - bytes32 peginTxid; - bytes32 kickoffTxid; - bytes32 take1Txid; - bytes32 take2Txid; - bytes32 commitTimoutTxid; - bytes32[] assertTimoutTxids; - bytes32[] NackTxids; - } + keccak256( + "UNLOCK_OPERATOR_STAKE(address contract,address operator,uint256 amount)" + ); IPegBTC public pegBTC; IBitcoinSPV public bitcoinSPV; - CommitteeManagement public committeeManagement; - StakeManagement public stakeManagement; + ICommitteeManagement public committeeManagement; + IStakeManagement public stakeManagement; uint256 public responseWindowBlocks = 40; // 40 goat blocks ~ 2 minutes uint256 public cancelWithdrawTimelock = 144; // 144 btc blocks ~ 24 hours bytes16[] public instanceIds; - mapping(bytes16 instanceId => bytes16[] graphIds) public instanceIdToGraphIds; + mapping(bytes16 instanceId => bytes16[] graphIds) + public instanceIdToGraphIds; mapping(bytes16 instanceId => PeginDataInner) public peginDataMap; mapping(bytes16 graphId => GraphData) public graphDataMap; mapping(bytes16 graphId => WithdrawData) public withdrawDataMap; + constructor() { + _disableInitializers(); + } + // initializer function initialize( IPegBTC _pegBTC, IBitcoinSPV _bitcoinSPV, - CommitteeManagement _committeeManagement, - StakeManagement _stakeManagement + ICommitteeManagement _committeeManagement, + IStakeManagement _stakeManagement ) external initializer { // set initial parameters minChallengeAmountSats = 1000000; // 0.01 BTC @@ -254,39 +101,46 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { } // getters - function getGraphIdsByInstanceId(bytes16 instanceId) external view returns (bytes16[] memory) { + function getGraphIdsByInstanceId( + bytes16 instanceId + ) external view returns (bytes16[] memory) { return instanceIdToGraphIds[instanceId]; } - function getPeginData(bytes16 instanceId) external view returns (PeginData memory) { + function getPeginData( + bytes16 instanceId + ) external view returns (PeginData memory) { PeginDataInner storage data = peginDataMap[instanceId]; - return PeginData({ - status: data.status, - instanceId: data.instanceId, - depositorAddress: data.depositorAddress, - peginAmountSats: data.peginAmountSats, - txnFees: data.txnFees, - userInputs: data.userInputs, - userXonlyPubkey: data.userXonlyPubkey, - userChangeAddress: data.userChangeAddress, - userRefundAddress: data.userRefundAddress, - peginTxid: data.peginTxid, - createdAt: data.createdAt, - committeeAddresses: data.committeeAddresses, - committeePubkeys: getCommitteePubkeysUnsafe(instanceId) - }); - } - - function getGraphData(bytes16 graphId) external view returns (GraphData memory) { + return + PeginData({ + status: data.status, + instanceId: data.instanceId, + depositorAddress: data.depositorAddress, + peginAmountSats: data.peginAmountSats, + txnFees: data.txnFees, + userInputs: data.userInputs, + userXonlyPubkey: data.userXonlyPubkey, + userChangeAddress: data.userChangeAddress, + userRefundAddress: data.userRefundAddress, + peginTxid: data.peginTxid, + createdAt: data.createdAt, + committeeAddresses: data.committeeAddresses, + committeePubkeys: getCommitteePubkeysUnsafe(instanceId) + }); + } + + function getGraphData( + bytes16 graphId + ) external view returns (GraphData memory) { return graphDataMap[graphId]; } // helpers - function verifyCommitteeSignatures(bytes32 msgHash, bytes[] memory signatures, address[] memory members) - public - pure - returns (bool) - { + function verifyCommitteeSignatures( + bytes32 msgHash, + bytes[] memory signatures, + address[] memory members + ) public pure returns (bool) { address[] memory signers = new address[](signatures.length); for (uint256 i = 0; i < signatures.length; i++) { address signer = msgHash.recover(signatures[i]); @@ -308,48 +162,166 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { return true; } - function getPostPeginDigest(bytes16 instanceId, bytes32 peginTxid) public view returns (bytes32) { - return keccak256(abi.encode(POST_PEGIN_TYPEHASH, address(this), instanceId, peginTxid)); + function getPostPeginDigest( + bytes16 instanceId, + bytes32 peginTxid + ) public view returns (bytes32) { + return + keccak256( + abi.encode( + POST_PEGIN_TYPEHASH, + address(this), + instanceId, + peginTxid + ) + ); } - function getPostGraphDigest(bytes16 instanceId, bytes16 graphId, GraphData calldata graphData) - public - view - returns (bytes32) - { + function getPostGraphDigest( + bytes16 instanceId, + bytes16 graphId, + GraphData calldata graphData + ) public view returns (bytes32) { bytes32 graphDataHash = keccak256(abi.encode(graphData)); - return keccak256(abi.encode(POST_GRAPH_TYPEHASH, address(this), instanceId, graphId, graphDataHash)); + return + keccak256( + abi.encode( + POST_GRAPH_TYPEHASH, + address(this), + instanceId, + graphId, + graphDataHash + ) + ); } - function getCancelWithdrawDigest(bytes16 graphId) internal view returns (bytes32) { - return keccak256(abi.encode(CANCEL_WITHDRAW_TYPEHASH, address(this), graphId)); + function _getCancelWithdrawDigest( + bytes16 graphId + ) internal view returns (bytes32) { + return + keccak256( + abi.encode(CANCEL_WITHDRAW_TYPEHASH, address(this), graphId) + ); } - function getCancelWithdrawDigestNonced(bytes16 graphId, uint256 nonce) public view returns (bytes32) { - bytes32 msgHash = getCancelWithdrawDigest(graphId); + function getCancelWithdrawDigestNonced( + bytes16 graphId, + uint256 nonce + ) public view returns (bytes32) { + bytes32 msgHash = _getCancelWithdrawDigest(graphId); return committeeManagement.getNoncedDigest(msgHash, nonce); } - function getUnlockStakeDigest(address operator, uint256 amount) internal view returns (bytes32) { - return keccak256(abi.encode(UNLOCK_STAKE_TYPEHASH, address(this), operator, amount)); + function _getUnlockStakeDigest( + address operator, + uint256 amount + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + UNLOCK_STAKE_TYPEHASH, + address(this), + operator, + amount + ) + ); } - function getUnlockStakeDigestNonced(address operator, uint256 amount, uint256 nonce) - public - view - returns (bytes32) - { - bytes32 msgHash = getUnlockStakeDigest(operator, amount); + function getUnlockStakeDigestNonced( + address operator, + uint256 amount, + uint256 nonce + ) public view returns (bytes32) { + bytes32 msgHash = _getUnlockStakeDigest(operator, amount); return committeeManagement.getNoncedDigest(msgHash, nonce); } + function _operatorReward( + uint64 peginAmountSats + ) internal view returns (uint64) { + return + minOperatorRewardSats + + (peginAmountSats * operatorRewardRate) / + rateMultiplier; + } + + function _verifyMerkleInclusion( + MerkleProof.BitcoinTxProof calldata proof, + bytes32 txid, + bool disproveContext + ) internal view { + (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof + .parseBtcBlockHeader(proof.rawHeader); + if (bitcoinSPV.blockHash(proof.height) != blockHash) { + if (disproveContext) revert DisproveInvalidHeader(); + revert InvalidHeader(); + } + if ( + !MerkleProof.verifyMerkleProof( + merkleRoot, + proof.proof, + txid, + proof.index + ) + ) { + revert MerkleVerifyFail(); + } + } + + function _finalizeWithdraw( + bytes16 graphId, + BitvmTxParser.BitcoinTx calldata rawTakeTx, + MerkleProof.BitcoinTxProof calldata takeProof, + bytes32 expectedTxid, + bool happyPath + ) internal { + WithdrawData storage withdrawData = withdrawDataMap[graphId]; + bytes16 instanceId = withdrawData.instanceId; + PeginDataInner storage peginData = peginDataMap[instanceId]; + if (withdrawData.status != WithdrawStatus.Processing) + revert WithdrawStatusInvalid(); + + bytes32 takeTxid = BitvmTxParser._computeTxid(rawTakeTx); + if (takeTxid != expectedTxid) revert TxidMismatch(); + _verifyMerkleInclusion(takeProof, takeTxid, false); + + peginData.status = PeginStatus.Claimed; + withdrawData.status = WithdrawStatus.Complete; + + uint64 rewardAmountSats = _operatorReward(peginData.peginAmountSats); + pegBTC.transfer( + withdrawData.operatorAddress, + Converter._amountFromSats(rewardAmountSats) + ); + + if (happyPath) { + emit WithdrawHappyPath( + instanceId, + graphId, + takeTxid, + withdrawData.operatorAddress, + rewardAmountSats + ); + } else { + emit WithdrawUnhappyPath( + instanceId, + graphId, + takeTxid, + withdrawData.operatorAddress, + rewardAmountSats + ); + } + } + modifier onlyCommittee() { - if (!committeeManagement.isCommitteeMember(msg.sender)) revert NotCommittee(); + if (!committeeManagement.isCommitteeMember(msg.sender)) + revert NotCommittee(); _; } modifier onlyOperator(bytes16 graphId) { - if (withdrawDataMap[graphId].operatorAddress != msg.sender) revert NotOperator(); + if (withdrawDataMap[graphId].operatorAddress != msg.sender) + revert NotOperator(); _; } @@ -392,13 +364,18 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ); } - function answerPeginRequest(bytes16 instanceId, bytes memory committeePubkey) external onlyCommittee { + function answerPeginRequest( + bytes16 instanceId, + bytes memory committeePubkey + ) external onlyCommittee { PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.Pending) revert NotPending(); - if (peginData.createdAt + responseWindowBlocks < block.number) revert WindowExpired(); + if (peginData.createdAt + responseWindowBlocks < block.number) + revert WindowExpired(); if (committeePubkey.length != 33) revert InvalidPubkeyLen(); bytes1 committeePubkeyParity = committeePubkey[0]; - if (!(committeePubkeyParity == 0x02 || committeePubkeyParity == 0x03)) revert InvalidPubkeyParity(); + if (!(committeePubkeyParity == 0x02 || committeePubkeyParity == 0x03)) + revert InvalidPubkeyParity(); bytes32 committeeXonlyPubkey; assembly { committeeXonlyPubkey := mload(add(committeePubkey, 0x21)) @@ -408,33 +385,55 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { if (peginData.committeeAddressPositions[committeeAddress] == 0) { peginData.committeeAddresses.push(committeeAddress); // The value is stored at length-1, but we add 1 to all indexes and use 0 as a sentinel value - peginData.committeeAddressPositions[committeeAddress] = peginData.committeeAddresses.length; + peginData.committeeAddressPositions[committeeAddress] = peginData + .committeeAddresses + .length; } - peginData.committeePubkeyParitys[committeeAddress] = committeePubkeyParity; - peginData.committeeXonlyPubkeys[committeeAddress] = committeeXonlyPubkey; + peginData.committeePubkeyParitys[ + committeeAddress + ] = committeePubkeyParity; + peginData.committeeXonlyPubkeys[ + committeeAddress + ] = committeeXonlyPubkey; emit CommitteeResponse(instanceId, committeeAddress, committeePubkey); } - function getCommitteePubkeys(bytes16 instanceId) public view returns (bytes[] memory committeePubkeys) { - if (peginDataMap[instanceId].createdAt + responseWindowBlocks >= block.number) revert WindowNotExpired(); + function getCommitteePubkeys( + bytes16 instanceId + ) public view returns (bytes[] memory committeePubkeys) { + if ( + peginDataMap[instanceId].createdAt + responseWindowBlocks >= + block.number + ) revert WindowNotExpired(); committeePubkeys = getCommitteePubkeysUnsafe(instanceId); - if (committeePubkeys.length < committeeManagement.quorumSize()) revert NotEnoughCommittee(); + if (committeePubkeys.length < committeeManagement.quorumSize()) + revert NotEnoughCommittee(); } - function getCommitteeAddresses(bytes16 instanceId) public view returns (address[] memory committeeAddresses) { - if (peginDataMap[instanceId].createdAt + responseWindowBlocks >= block.number) revert WindowNotExpired(); + function getCommitteeAddresses( + bytes16 instanceId + ) public view returns (address[] memory committeeAddresses) { + if ( + peginDataMap[instanceId].createdAt + responseWindowBlocks >= + block.number + ) revert WindowNotExpired(); committeeAddresses = peginDataMap[instanceId].committeeAddresses; - if (committeeAddresses.length < committeeManagement.quorumSize()) revert NotEnoughCommittee(); + if (committeeAddresses.length < committeeManagement.quorumSize()) + revert NotEnoughCommittee(); } - function getCommitteePubkeysUnsafe(bytes16 instanceId) public view returns (bytes[] memory committeePubkeys) { + function getCommitteePubkeysUnsafe( + bytes16 instanceId + ) public view returns (bytes[] memory committeePubkeys) { PeginDataInner storage peginData = peginDataMap[instanceId]; committeePubkeys = new bytes[](peginData.committeeAddresses.length); for (uint256 i = 0; i < peginData.committeeAddresses.length; ++i) { address committeeAddress = peginData.committeeAddresses[i]; bytes1 parity = peginData.committeePubkeyParitys[committeeAddress]; - bytes32 XonlyPubkeys = peginData.committeeXonlyPubkeys[committeeAddress]; + bytes32 XonlyPubkeys = peginData.committeeXonlyPubkeys[ + committeeAddress + ]; committeePubkeys[i] = abi.encodePacked(parity, XonlyPubkeys); } } @@ -449,21 +448,28 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ) external onlyCommittee { PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.Pending) revert NotPending(); - (bytes32 peginTxid, uint64 peginAmountSats, address depositorAddress, bytes16 parsedInstanceId) = - BitvmTxParser.parsePegin(rawPeginTx); + ( + bytes32 peginTxid, + uint64 peginAmountSats, + address depositorAddress, + bytes16 parsedInstanceId + ) = BitvmTxParser._parsePegin(rawPeginTx); if (parsedInstanceId != instanceId) revert InstanceMismatch(); - if (peginAmountSats != peginData.peginAmountSats) revert PeginAmountMismatch(); + if (peginAmountSats != peginData.peginAmountSats) + revert PeginAmountMismatch(); // validate pegin tx - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(peginProof.rawHeader); - if (bitcoinSPV.blockHash(peginProof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, peginProof.proof, peginTxid, peginProof.index)) { - revert MerkleVerifyFail(); - } + _verifyMerkleInclusion(peginProof, peginTxid, false); // validate committeeSigs bytes32 pegin_digest = getPostPeginDigest(instanceId, peginTxid); - if (!verifyCommitteeSignatures(pegin_digest, committeeSigs, getCommitteeAddresses(instanceId))) { + if ( + !verifyCommitteeSignatures( + pegin_digest, + committeeSigs, + getCommitteeAddresses(instanceId) + ) + ) { revert InvalidSignatures(); } @@ -473,12 +479,22 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { // mint pegBTC to user // deduct a fee from the User to cover the Operator's peg-out reward - uint64 feeAmountSats = minPeginFeeSats + peginAmountSats * peginFeeRate / rateMultiplier; + uint64 feeAmountSats = minPeginFeeSats + + (peginAmountSats * peginFeeRate) / + rateMultiplier; if (feeAmountSats >= peginAmountSats) revert FeeTooHigh(); - pegBTC.mint(depositorAddress, Converter.amountFromSats(peginAmountSats - feeAmountSats)); - pegBTC.mint(address(this), Converter.amountFromSats(feeAmountSats)); + pegBTC.mint( + depositorAddress, + Converter._amountFromSats(peginAmountSats - feeAmountSats) + ); + pegBTC.mint(address(this), Converter._amountFromSats(feeAmountSats)); - emit BridgeIn(depositorAddress, instanceId, peginAmountSats, feeAmountSats); + emit BridgeIn( + depositorAddress, + instanceId, + peginAmountSats, + feeAmountSats + ); } function postGraphData( @@ -489,20 +505,35 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ) public onlyCommittee { // check operator stake // Note:committee should check operator's locked stake before pre-signed any graph txns - address operatorStakeAddress = stakeManagement.pubkeyToAddress(graphData.operatorPubkey); + address operatorStakeAddress = stakeManagement.pubkeyToAddress( + graphData.operatorPubkey + ); if (operatorStakeAddress == address(0)) revert OperatorNotRegistered(); - if (stakeManagement.lockedStakeOf(operatorStakeAddress) < minStakeAmount) revert StakeInsufficient(); + if ( + stakeManagement.lockedStakeOf(operatorStakeAddress) < minStakeAmount + ) revert StakeInsufficient(); // check committeeSigs - bytes32 graph_digest = getPostGraphDigest(instanceId, graphId, graphData); - if (!verifyCommitteeSignatures(graph_digest, committeeSigs, getCommitteeAddresses(instanceId))) { + bytes32 graph_digest = getPostGraphDigest( + instanceId, + graphId, + graphData + ); + if ( + !verifyCommitteeSignatures( + graph_digest, + committeeSigs, + getCommitteeAddresses(instanceId) + ) + ) { revert InvalidSignatures(); } // check graph data if (graphDataMap[graphId].peginTxid != 0) revert GraphAlreadyPosted(); PeginDataInner storage peginData = peginDataMap[instanceId]; - if (graphData.peginTxid != peginData.peginTxid) revert GraphPeginTxidMismatch(); + if (graphData.peginTxid != peginData.peginTxid) + revert GraphPeginTxidMismatch(); // store graph data graphDataMap[graphId] = graphData; @@ -513,17 +544,23 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { function initWithdraw(bytes16 instanceId, bytes16 graphId) external { WithdrawData storage withdrawData = withdrawDataMap[graphId]; - if (!(withdrawData.status == WithdrawStatus.None || withdrawData.status == WithdrawStatus.Canceled)) { + if ( + !(withdrawData.status == WithdrawStatus.None || + withdrawData.status == WithdrawStatus.Canceled) + ) { revert WithdrawStatusInvalid(); } PeginDataInner storage peginData = peginDataMap[instanceId]; - if (peginData.status != PeginStatus.Withdrawbale) revert NotWithdrawable(); + if (peginData.status != PeginStatus.Withdrawbale) + revert NotWithdrawable(); // lock the pegin utxo so others can not withdraw it peginData.status = PeginStatus.Locked; // lock operator's pegBTC - uint256 lockAmount = Converter.amountFromSats(peginData.peginAmountSats); + uint256 lockAmount = Converter._amountFromSats( + peginData.peginAmountSats + ); pegBTC.transferFrom(msg.sender, address(this), lockAmount); withdrawData.peginTxid = peginData.peginTxid; @@ -533,35 +570,57 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { withdrawData.lockAmount = lockAmount; withdrawData.btcBlockHeightAtWithdraw = bitcoinSPV.latestHeight(); - emit InitWithdraw(instanceId, graphId, withdrawData.operatorAddress, peginData.peginAmountSats); + emit InitWithdraw( + instanceId, + graphId, + withdrawData.operatorAddress, + peginData.peginAmountSats + ); } function cancelWithdraw(bytes16 graphId) external onlyOperator(graphId) { WithdrawData storage withdrawData = withdrawDataMap[graphId]; - PeginDataInner storage peginData = peginDataMap[withdrawData.instanceId]; - if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); - if (withdrawData.btcBlockHeightAtWithdraw + cancelWithdrawTimelock >= bitcoinSPV.latestHeight()) { + PeginDataInner storage peginData = peginDataMap[ + withdrawData.instanceId + ]; + if (withdrawData.status != WithdrawStatus.Initialized) + revert WithdrawStatusInvalid(); + if ( + withdrawData.btcBlockHeightAtWithdraw + cancelWithdrawTimelock >= + bitcoinSPV.latestHeight() + ) { revert TimelockNotExpired(); } withdrawData.status = WithdrawStatus.Canceled; - pegBTC.transfer(msg.sender, withdrawData.lockAmount); + pegBTC.transfer(withdrawData.operatorAddress, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; - emit CancelWithdraw(withdrawData.instanceId, graphId, msg.sender); + emit CancelWithdraw(withdrawData.instanceId, graphId, withdrawData.operatorAddress); } - function committeeCancelWithdraw(bytes16 graphId, uint256 nonce, bytes[] calldata committeeSigs) external { + function committeeCancelWithdraw( + bytes16 graphId, + uint256 nonce, + bytes[] calldata committeeSigs + ) external { // validate committeeSigs WithdrawData storage withdrawData = withdrawDataMap[graphId]; - bytes32 cancel_digest = getCancelWithdrawDigest(graphId); - committeeManagement.executeNoncedSignatures(cancel_digest, nonce, committeeSigs); + bytes32 cancel_digest = _getCancelWithdrawDigest(graphId); + committeeManagement.executeNoncedSignatures( + cancel_digest, + nonce, + committeeSigs + ); // update storage - PeginDataInner storage peginData = peginDataMap[withdrawData.instanceId]; - if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); + PeginDataInner storage peginData = peginDataMap[ + withdrawData.instanceId + ]; + if (withdrawData.status != WithdrawStatus.Initialized) + revert WithdrawStatusInvalid(); withdrawData.status = WithdrawStatus.Canceled; - pegBTC.transfer(msg.sender, withdrawData.lockAmount); + pegBTC.transfer(withdrawData.operatorAddress, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; - emit CancelWithdraw(withdrawData.instanceId, graphId, msg.sender); + emit CancelWithdraw(withdrawData.instanceId, graphId, withdrawData.operatorAddress); } // post kickoff tx @@ -572,17 +631,15 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ) external onlyCommittee { WithdrawData storage withdrawData = withdrawDataMap[graphId]; bytes16 instanceId = withdrawData.instanceId; - if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); - if (withdrawData.btcBlockHeightAtWithdraw >= kickoffProof.height) revert KickoffHeightLow(); + if (withdrawData.status != WithdrawStatus.Initialized) + revert WithdrawStatusInvalid(); + if (withdrawData.btcBlockHeightAtWithdraw >= kickoffProof.height) + revert KickoffHeightLow(); GraphData storage graphData = graphDataMap[graphId]; - bytes32 kickoffTxid = BitvmTxParser.computeTxid(rawKickoffTx); + bytes32 kickoffTxid = BitvmTxParser._computeTxid(rawKickoffTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(kickoffProof.rawHeader); - if (bitcoinSPV.blockHash(kickoffProof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, kickoffProof.proof, kickoffTxid, kickoffProof.index)) { - revert MerkleVerifyFail(); - } + _verifyMerkleInclusion(kickoffProof, kickoffTxid, false); // once kickoff is braodcasted , operator will not be able to cancel withdrawal withdrawData.status = WithdrawStatus.Processing; @@ -598,29 +655,14 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { BitvmTxParser.BitcoinTx calldata rawTake1Tx, MerkleProof.BitcoinTxProof calldata take1Proof ) external onlyCommittee { - WithdrawData storage withdrawData = withdrawDataMap[graphId]; - bytes16 instanceId = withdrawData.instanceId; - PeginDataInner storage peginData = peginDataMap[instanceId]; - if (withdrawData.status != WithdrawStatus.Processing) revert WithdrawStatusInvalid(); - GraphData storage graphData = graphDataMap[graphId]; - bytes32 take1Txid = BitvmTxParser.computeTxid(rawTake1Tx); - if (take1Txid != graphData.take1Txid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(take1Proof.rawHeader); - if (bitcoinSPV.blockHash(take1Proof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, take1Proof.proof, take1Txid, take1Proof.index)) { - revert MerkleVerifyFail(); - } - - peginData.status = PeginStatus.Claimed; - withdrawData.status = WithdrawStatus.Complete; - - // incentive mechanism for honest Operators - uint64 rewardAmountSats = - minOperatorRewardSats + peginData.peginAmountSats * operatorRewardRate / rateMultiplier; - pegBTC.transfer(withdrawData.operatorAddress, Converter.amountFromSats(rewardAmountSats)); - - emit WithdrawHappyPath(instanceId, graphId, take1Txid, withdrawData.operatorAddress, rewardAmountSats); + _finalizeWithdraw( + graphId, + rawTake1Tx, + take1Proof, + graphData.take1Txid, + true + ); } function finishWithdrawUnhappyPath( @@ -628,29 +670,14 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { BitvmTxParser.BitcoinTx calldata rawTake2Tx, MerkleProof.BitcoinTxProof calldata take2Proof ) external onlyCommittee { - WithdrawData storage withdrawData = withdrawDataMap[graphId]; - bytes16 instanceId = withdrawData.instanceId; - PeginDataInner storage peginData = peginDataMap[instanceId]; - if (withdrawData.status != WithdrawStatus.Processing) revert WithdrawStatusInvalid(); - GraphData storage graphData = graphDataMap[graphId]; - bytes32 take2Txid = BitvmTxParser.computeTxid(rawTake2Tx); - if (take2Txid != graphData.take2Txid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(take2Proof.rawHeader); - if (bitcoinSPV.blockHash(take2Proof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, take2Proof.proof, take2Txid, take2Proof.index)) { - revert MerkleVerifyFail(); - } - - peginData.status = PeginStatus.Claimed; - withdrawData.status = WithdrawStatus.Complete; - - // incentive mechanism for honest Operators - uint64 rewardAmountSats = - minOperatorRewardSats + peginData.peginAmountSats * operatorRewardRate / rateMultiplier; - pegBTC.transfer(withdrawData.operatorAddress, Converter.amountFromSats(rewardAmountSats)); - - emit WithdrawUnhappyPath(instanceId, graphId, take2Txid, withdrawData.operatorAddress, rewardAmountSats); + _finalizeWithdraw( + graphId, + rawTake2Tx, + take2Proof, + graphData.take2Txid, + false + ); } // if no challengeStartTx happens (for QuickChallenge & ChallengeIncompeleteKickoff), set rawChallengeStartTx.inputVector to empty @@ -667,87 +694,118 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { GraphData storage graphData = graphDataMap[graphId]; bytes16 instanceId = withdrawData.instanceId; // Malicious operator may skip initWithdraw & procceedWithdraw - if (withdrawData.status == WithdrawStatus.Disproved) revert AlreadyDisproved(); + if (withdrawData.status == WithdrawStatus.Disproved) + revert AlreadyDisproved(); // verify ChallengeStart tx bytes32 challengeStartTxid; address challengerAddress; bytes32 kickoffTxid; uint32 kickoffVout; - bytes32 blockHash; - bytes32 merkleRoot; if ( - ( - disproveTxType == DisproveTxType.QuickChallenge - || disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff - ) && (rawChallengeStartTx.inputVector.length == 0) + (disproveTxType == DisproveTxType.QuickChallenge || + disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff) && + (rawChallengeStartTx.inputVector.length == 0) ) { // no challenge start tx } else { - (challengeStartTxid, kickoffTxid, kickoffVout, challengerAddress) = - BitvmTxParser.parseChallengeTx(rawChallengeStartTx); + ( + challengeStartTxid, + kickoffTxid, + kickoffVout, + challengerAddress + ) = BitvmTxParser._parseChallengeTx(rawChallengeStartTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.CHALLENGE_CONNECTOR_VOUT) revert TxidMismatch(); - (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader(challengeStartTxProof.rawHeader); - if (bitcoinSPV.blockHash(challengeStartTxProof.height) != blockHash) revert DisproveInvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, challengeStartTxProof.proof, challengeStartTxid, challengeStartTxProof.index - ) - ) revert MerkleVerifyFail(); + if (kickoffVout != BitvmTxParser.CHALLENGE_CONNECTOR_VOUT) + revert TxidMismatch(); + _verifyMerkleInclusion( + challengeStartTxProof, + challengeStartTxid, + true + ); } // verify ChallengeFinish tx bytes32 challengeFinishTxid; address disproverAddress; if (disproveTxType == DisproveTxType.AssertTimeout) { - (challengeFinishTxid) = BitvmTxParser.computeTxid(rawChallengeFinishTx); - if (graphData.assertTimoutTxids.length <= txnIndex) revert IndexOutOfRange(); - if (challengeFinishTxid != graphData.assertTimoutTxids[txnIndex]) revert TxidMismatch(); + (challengeFinishTxid) = BitvmTxParser._computeTxid( + rawChallengeFinishTx + ); + if (graphData.assertTimoutTxids.length <= txnIndex) + revert IndexOutOfRange(); + if (challengeFinishTxid != graphData.assertTimoutTxids[txnIndex]) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.OperatorCommitTimeout) { - (challengeFinishTxid) = BitvmTxParser.computeTxid(rawChallengeFinishTx); - if (challengeFinishTxid != graphData.commitTimoutTxid) revert TxidMismatch(); + (challengeFinishTxid) = BitvmTxParser._computeTxid( + rawChallengeFinishTx + ); + if (challengeFinishTxid != graphData.commitTimoutTxid) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.OperatorNack) { - (challengeFinishTxid) = BitvmTxParser.computeTxid(rawChallengeFinishTx); - if (graphData.NackTxids.length <= txnIndex) revert IndexOutOfRange(); - if (challengeFinishTxid != graphData.NackTxids[txnIndex]) revert TxidMismatch(); + (challengeFinishTxid) = BitvmTxParser._computeTxid( + rawChallengeFinishTx + ); + if (graphData.NackTxids.length <= txnIndex) + revert IndexOutOfRange(); + if (challengeFinishTxid != graphData.NackTxids[txnIndex]) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.Disprove) { - (challengeFinishTxid, kickoffTxid, kickoffVout, disproverAddress) = - BitvmTxParser.parseDisproveTx(rawChallengeFinishTx); + ( + challengeFinishTxid, + kickoffTxid, + kickoffVout, + disproverAddress + ) = BitvmTxParser._parseDisproveTx(rawChallengeFinishTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.DISPROVE_CONNECTOR_VOUT) revert TxidMismatch(); + if (kickoffVout != BitvmTxParser.DISPROVE_CONNECTOR_VOUT) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.QuickChallenge) { - (challengeFinishTxid, kickoffTxid, kickoffVout, disproverAddress) = - BitvmTxParser.parseQuickChallengeTx(rawChallengeFinishTx); + ( + challengeFinishTxid, + kickoffTxid, + kickoffVout, + disproverAddress + ) = BitvmTxParser._parseQuickChallengeTx(rawChallengeFinishTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) revert TxidMismatch(); - } else if (disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff) { - (challengeFinishTxid, kickoffTxid, kickoffVout, disproverAddress) = - BitvmTxParser.parseChallengeIncompleteKickoffTx(rawChallengeFinishTx); + if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) + revert TxidMismatch(); + } else if ( + disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff + ) { + ( + challengeFinishTxid, + kickoffTxid, + kickoffVout, + disproverAddress + ) = BitvmTxParser._parseChallengeIncompleteKickoffTx( + rawChallengeFinishTx + ); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) revert TxidMismatch(); + if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) + revert TxidMismatch(); } else { revert UnknownDisproveType(); } - (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader(challengeFinishTxProof.rawHeader); - if (bitcoinSPV.blockHash(challengeFinishTxProof.height) != blockHash) revert DisproveInvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, challengeFinishTxProof.proof, challengeFinishTxid, challengeFinishTxProof.index - ) - ) revert MerkleVerifyFail(); + _verifyMerkleInclusion( + challengeFinishTxProof, + challengeFinishTxid, + true + ); withdrawData.status = WithdrawStatus.Disproved; // slash Operator & reward Challenger and Disprover IERC20 stakeToken = IERC20(stakeManagement.stakeTokenAddress()); - address operatorStakeAddress = stakeManagement.pubkeyToAddress(graphData.operatorPubkey); + address operatorStakeAddress = stakeManagement.pubkeyToAddress( + graphData.operatorPubkey + ); uint256 slashAmount = minSlashAmount; uint256 operatorStake = stakeManagement.stakeOf(operatorStakeAddress); if (operatorStake < slashAmount) slashAmount = operatorStake; stakeManagement.slashStake(operatorStakeAddress, slashAmount); - uint64 challengerRewardAmount = minChallengerReward; - uint64 disproverRewardAmount = minDisproverReward; + uint256 challengerRewardAmount = minChallengerReward; + uint256 disproverRewardAmount = minDisproverReward; if (challengerAddress != address(0)) { stakeToken.transfer(challengerAddress, challengerRewardAmount); } @@ -775,11 +833,18 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { prekickoff-connector through another path). Once the committee members have verified this, they provide their signatures. */ - function unlockOperatorStake(address operator, uint256 amount, uint256 nonce, bytes[] calldata committeeSigs) - external - { - bytes32 msgHash = getUnlockStakeDigest(operator, amount); - committeeManagement.executeNoncedSignatures(msgHash, nonce, committeeSigs); + function unlockOperatorStake( + address operator, + uint256 amount, + uint256 nonce, + bytes[] calldata committeeSigs + ) external { + bytes32 msgHash = _getUnlockStakeDigest(operator, amount); + committeeManagement.executeNoncedSignatures( + msgHash, + nonce, + committeeSigs + ); stakeManagement.unlockStake(operator, amount); } } diff --git a/src/GatewayDebug.sol b/src/GatewayDebug.sol index 3543439..5d9f09c 100644 --- a/src/GatewayDebug.sol +++ b/src/GatewayDebug.sol @@ -84,12 +84,6 @@ contract CommitteeManagementDebug is CommitteeManagement { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; - constructor( - address[] memory initialMembers, - uint256 initialRequired, - address[] memory initialAuthorizedCallers, - bytes32[] memory initialWatchtowers - ) CommitteeManagement(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers) {} modifier onlyCommittee() { require(isOwner[msg.sender], "only committee member can call"); diff --git a/src/MultiSigVerifier.sol b/src/MultiSigVerifier.sol index 9e055c2..0111cd5 100644 --- a/src/MultiSigVerifier.sol +++ b/src/MultiSigVerifier.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; /// @title Simple Multi-Signature Verifier with Owner Rotation and Anti-Replay -contract MultiSigVerifier { +contract MultiSigVerifier is Initializable { using ECDSA for bytes32; using MessageHashUtils for bytes32; @@ -16,21 +21,48 @@ contract MultiSigVerifier { uint256 public requiredSignatures; uint256 public nonce; - event OwnersUpdated(address[] newOwners, uint256 newRequired, uint256 newNonce); + event OwnersUpdated( + address[] newOwners, + uint256 newRequired, + uint256 newNonce + ); + + function initialize( + address[] calldata owners, + uint256 _requiredSignatures + ) external initializer { + __MultiSigVerifier_init(owners, _requiredSignatures); + } + + function __MultiSigVerifier_init( + address[] memory owners, + uint256 _requiredSignatures + ) internal onlyInitializing { + __MultiSigVerifier_init_unchained(owners, _requiredSignatures); + } - constructor(address[] memory owners, uint256 _requiredSignatures) { + function __MultiSigVerifier_init_unchained( + address[] memory owners, + uint256 _requiredSignatures + ) internal onlyInitializing { _setOwners(owners, _requiredSignatures); nonce = 0; } /// @notice Verify if a message has enough valid signatures from the current owner set. - function verify(bytes32 messageHash, bytes[] memory signatures) public view returns (bool) { + function verify( + bytes32 messageHash, + bytes[] memory signatures + ) public view returns (bool) { uint256 validSignatures = 0; address[] memory seen = new address[](signatures.length); for (uint256 i = 0; i < signatures.length; i++) { address signer = messageHash.recover(signatures[i]); - if (isOwner[signer] && !_alreadySigned(seen, signer, validSignatures)) { + if ( + isOwner[signer] && + !_alreadySigned(seen, signer, validSignatures) + ) { seen[validSignatures] = signer; validSignatures++; } @@ -39,11 +71,20 @@ contract MultiSigVerifier { } /// @notice Update the owner set and threshold, authorized by the CURRENT owners. - function updateOwners(address[] calldata newOwners, uint256 newRequired, bytes[] calldata signatures) external { + function updateOwners( + address[] calldata newOwners, + uint256 newRequired, + bytes[] calldata signatures + ) external { require(newOwners.length > 0, "Owners required"); - require(newRequired > 0 && newRequired <= newOwners.length, "Invalid threshold"); + require( + newRequired > 0 && newRequired <= newOwners.length, + "Invalid threshold" + ); - bytes32 digest = keccak256(abi.encodePacked(nonce, newOwners, newRequired)); + bytes32 digest = keccak256( + abi.encodePacked(nonce, newOwners, newRequired) + ); require(verify(digest, signatures), "No enough valid owner sigs"); @@ -56,9 +97,15 @@ contract MultiSigVerifier { /// ---------------------------------------------------------------- /// internal helpers /// ---------------------------------------------------------------- - function _setOwners(address[] memory owners, uint256 _requiredSignatures) internal { + function _setOwners( + address[] memory owners, + uint256 _requiredSignatures + ) internal { require(owners.length > 0, "Owners required"); - require(_requiredSignatures > 0 && _requiredSignatures <= owners.length, "Invalid threshold"); + require( + _requiredSignatures > 0 && _requiredSignatures <= owners.length, + "Invalid threshold" + ); for (uint256 i = 0; i < owners.length; i++) { address o = owners[i]; @@ -71,7 +118,10 @@ contract MultiSigVerifier { requiredSignatures = _requiredSignatures; } - function _applyOwners(address[] calldata newOwners, uint256 _requiredSignatures) internal { + function _applyOwners( + address[] calldata newOwners, + uint256 _requiredSignatures + ) internal { // clear old owners for (uint256 i = 0; i < ownerList.length; i++) { isOwner[ownerList[i]] = false; @@ -82,9 +132,7 @@ contract MultiSigVerifier { for (uint256 i = 0; i < newOwners.length; i++) { address o = newOwners[i]; require(o != address(0), "Zero address"); - for (uint256 j = 0; j < i; j++) { - require(newOwners[j] != o, "Duplicate owner"); - } + require(!isOwner[o], "Duplicate owner"); isOwner[o] = true; ownerList.push(o); } @@ -93,7 +141,11 @@ contract MultiSigVerifier { requiredSignatures = _requiredSignatures; } - function _alreadySigned(address[] memory signers, address signer, uint256 count) internal pure returns (bool) { + function _alreadySigned( + address[] memory signers, + address signer, + uint256 count + ) internal pure returns (bool) { for (uint256 i = 0; i < count; i++) { if (signers[i] == signer) return true; } @@ -103,4 +155,6 @@ contract MultiSigVerifier { function getOwners() external view returns (address[] memory) { return ownerList; } + + uint256[50] private __gap; } diff --git a/src/PegBTC.sol b/src/PegBTC.sol index eee1dca..c53de1e 100644 --- a/src/PegBTC.sol +++ b/src/PegBTC.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract PegBTC is ERC20, Ownable { constructor(address owner) Ownable(owner) ERC20("PegBTC", "PBTC") {} diff --git a/src/SequencerSetPublisher.sol b/src/SequencerSetPublisher.sol index 13017d8..37979f6 100644 --- a/src/SequencerSetPublisher.sol +++ b/src/SequencerSetPublisher.sol @@ -1,21 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import "./MultiSigVerifier.sol"; -import "./interfaces/ISequencerSetPublisher.sol"; -import "./Constants.sol"; +import {MultiSigVerifier} from "./MultiSigVerifier.sol"; +import {ISequencerSetPublisher} from "./interfaces/ISequencerSetPublisher.sol"; +import {Constants} from "./Constants.sol"; // Sequencer Set Publisher -contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerSetPublisher { +contract SequencerSetPublisher is + Initializable, + OwnableUpgradeable, + ISequencerSetPublisher +{ using ECDSA for bytes32; using MessageHashUtils for bytes32; - mapping(uint256 height => mapping(address publisher => bytes32 cmt)) public heightSequencerCmt; + mapping(uint256 height => mapping(address publisher => bytes32 cmt)) + public heightSequencerCmt; mapping(bytes32 cmt => uint256 cnt) public sequencerCmtCnt; mapping(uint256 height => address[]) public heightPublishers; @@ -27,11 +38,25 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS function initialize( address initialOwner, + address multiSigVerifierAddress, address[] calldata initPublishers, bytes[] calldata initPublisherBTCPubkeys ) public initializer { - require(initPublisherBTCPubkeys.length == initPublishers.length, "Invalid Publishers"); + require( + multiSigVerifierAddress != address(0), + "Invalid multisig address" + ); + require( + initPublisherBTCPubkeys.length == initPublishers.length, + "Invalid Publishers" + ); __Ownable_init(initialOwner); + multiSigVerifier = MultiSigVerifier(multiSigVerifierAddress); + require( + multiSigVerifier.ownerCount() == 0, + "Verifier already initialized" + ); + // TODO: deploy a dedicated MultiSigVerifier proxy here once we stop injecting the address externally. // ensure valid sigs >= 2/3 uint256 quorum = (initPublishers.length * 2 + 2) / 3; latestConfirmedHeight = 0; @@ -39,20 +64,34 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS assert(initPublisherBTCPubkeys[i].length == 33); publisherBTCPubkeys[initPublishers[i]] = initPublisherBTCPubkeys[i]; } - multiSigVerifier = new MultiSigVerifier(initPublishers, quorum); + multiSigVerifier.initialize(initPublishers, quorum); heightPublishers[0] = initPublishers; } /// @notice Publish a new sequencer set, which should be signed by the older publishers. /// @param ss The Sequencer Set /// @param signature The P2WSH signature - function updateSequencerSet(SequencerSet calldata ss, bytes calldata signature) external override { - require(ss.goatBlockNumber >= latestConfirmedHeight, InvalidGOATHeight()); + function updateSequencerSet( + SequencerSet calldata ss, + bytes calldata signature + ) external override { + require( + ss.goatBlockNumber >= latestConfirmedHeight, + InvalidGOATHeight() + ); require(multiSigVerifier.isOwner(msg.sender), InvalidPublisherSet()); - require(msg.sender == ss.p2wshSigHash.recover(signature), P2WSHSignatureMismatch()); + require( + msg.sender == ss.p2wshSigHash.recover(signature), + P2WSHSignatureMismatch() + ); // Ensure the publisher set is not changed. - bytes32 expectedPublishersHash = keccak256(abi.encodePacked(multiSigVerifier.getOwners())); - require(ss.publishersHash == expectedPublishersHash, MismatchPublisher()); + bytes32 expectedPublishersHash = keccak256( + abi.encodePacked(multiSigVerifier.getOwners()) + ); + require( + ss.publishersHash == expectedPublishersHash, + MismatchPublisher() + ); bytes32 cmt = keccak256( abi.encodePacked( @@ -64,7 +103,10 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS ) ); // Avoid double commit - require(heightSequencerCmt[ss.goatBlockNumber][msg.sender] == bytes32(0), DoubleCommit()); + require( + heightSequencerCmt[ss.goatBlockNumber][msg.sender] == bytes32(0), + DoubleCommit() + ); heightSequencerCmt[ss.goatBlockNumber][msg.sender] = cmt; sequencerCmtCnt[cmt] += 1; @@ -88,19 +130,33 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS require(height == ss.goatBlockNumber, InvalidGOATHeight()); address[] memory publishers = multiSigVerifier.getOwners(); - bytes32 expectedPublishersHash = keccak256(abi.encodePacked(publishers)); - require(ss.publishersHash == expectedPublishersHash, MismatchPublisher()); + bytes32 expectedPublishersHash = keccak256( + abi.encodePacked(publishers) + ); + require( + ss.publishersHash == expectedPublishersHash, + MismatchPublisher() + ); if (latestConfirmedHeight > 0) { // check the continuality of the update chain - bytes32 prevCmt = calcMajoritySequencerSetCmtAtHeightOrLatest(latestConfirmedHeight); + bytes32 prevCmt = calcMajoritySequencerSetCmtAtHeightOrLatest( + latestConfirmedHeight + ); SequencerSet storage prevSs = cmtSequencerSet[prevCmt]; - require(prevSs.nextPublishersHash == ss.publishersHash, InvalidPublisherSet()); + require( + prevSs.nextPublishersHash == ss.publishersHash, + InvalidPublisherSet() + ); } // ensure valid sigs >= 2/3 uint256 quorum = (newPublishers.length * 2 + 2) / 3; - multiSigVerifier.updateOwners(newPublishers, quorum, changePublisherSigs); + multiSigVerifier.updateOwners( + newPublishers, + quorum, + changePublisherSigs + ); for (uint256 i = 0; i < newPublisherBTCPubkeys.length; i++) { assert(newPublisherBTCPubkeys[i].length == 33); @@ -110,7 +166,9 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS } /// @notice Check if we have an aggrement on the cmt of the latest height. - function calcMajoritySequencerSetCmtAtHeightOrLatest(uint256 height) public view returns (bytes32) { + function calcMajoritySequencerSetCmtAtHeightOrLatest( + uint256 height + ) public view returns (bytes32) { require(height > 0, InvalidGOATHeight()); address[] memory publishers = heightPublishers[height]; @@ -125,7 +183,10 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS } } - require(quorum * 3 >= 2 * publishers.length, InvalidQuorumSequencerSet()); + require( + quorum * 3 >= 2 * publishers.length, + InvalidQuorumSequencerSet() + ); return agreement; } } diff --git a/src/StakeManagement.sol b/src/StakeManagement.sol index a9c0242..489e7b7 100644 --- a/src/StakeManagement.sol +++ b/src/StakeManagement.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "./interfaces/IStakeManagement.sol"; - +import {IStakeManagement} from "./interfaces/IStakeManagement.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract StakeManagement is IStakeManagement { - IERC20 public immutable stakeToken; - address public immutable gatewayAddress; +contract StakeManagement is IStakeManagement, Initializable { + IERC20 public stakeToken; + address public gatewayAddress; mapping(address => uint256) private stakes; mapping(address => uint256) private lockedStakes; @@ -15,7 +17,16 @@ contract StakeManagement is IStakeManagement { mapping(address => bytes32) public addressToPubkey; // XOnlyPubkey mapping(bytes32 => address) public pubkeyToAddress; - constructor(IERC20 _stakeToken, address _gatewayAddress) { + constructor() { + _disableInitializers(); + } + + function initialize( + IERC20 _stakeToken, + address _gatewayAddress + ) public initializer { + require(address(_stakeToken) != address(0), "stake token zero"); + require(_gatewayAddress != address(0), "gateway zero"); stakeToken = _stakeToken; gatewayAddress = _gatewayAddress; } @@ -24,15 +35,20 @@ contract StakeManagement is IStakeManagement { return address(stakeToken); } - function stakeOf(address operator) external view override returns (uint256) { + function stakeOf( + address operator + ) external view override returns (uint256) { return stakes[operator]; } - function lockedStakeOf(address operator) external view override returns (uint256) { + function lockedStakeOf( + address operator + ) external view override returns (uint256) { return lockedStakes[operator]; } function slashStake(address operator, uint256 amount) external override { + require(msg.sender == gatewayAddress, "only gateway can slash stake"); require(stakes[operator] >= amount, "insufficient stake to slash"); if (lockedStakes[operator] > amount) { lockedStakes[operator] -= amount; @@ -40,19 +56,27 @@ contract StakeManagement is IStakeManagement { lockedStakes[operator] = 0; } stakes[operator] -= amount; - // Transfer the slashed tokens to the gateway contract, which will handle distribution - require(stakeToken.transfer(gatewayAddress, amount), "stake transfer failed"); + // Transfer the slashed tokens to the gateway which redistributes rewards + require( + stakeToken.transfer(gatewayAddress, amount), + "stake transfer failed" + ); } function lockStake(address operator, uint256 amount) external override { - require(msg.sender == operator || msg.sender == gatewayAddress, "only operator or gateway can lock stake"); - require(stakes[operator] - lockedStakes[operator] >= amount, "insufficient available stake to lock"); + require( + msg.sender == operator || msg.sender == gatewayAddress, + "only operator or gateway can lock stake" + ); + require( + stakes[operator] - lockedStakes[operator] >= amount, + "insufficient available stake to lock" + ); lockedStakes[operator] += amount; } function unlockStake(address operator, uint256 amount) external override { require(msg.sender == gatewayAddress, "only gateway can unlock stake"); - // unlock up to the amount, if less locked, unlock all if (lockedStakes[operator] >= amount) { lockedStakes[operator] -= amount; } else { @@ -61,21 +85,37 @@ contract StakeManagement is IStakeManagement { } function stake(uint256 amount) external { - require(stakeToken.transferFrom(msg.sender, address(this), amount), "stake transfer failed"); + require( + stakeToken.transferFrom(msg.sender, address(this), amount), + "stake transfer failed" + ); stakes[msg.sender] += amount; } function unstake(uint256 amount) external { - require(stakes[msg.sender] - lockedStakes[msg.sender] >= amount, "insufficient available stake to unstake"); + require( + stakes[msg.sender] - lockedStakes[msg.sender] >= amount, + "insufficient available stake to unstake" + ); stakes[msg.sender] -= amount; - require(stakeToken.transfer(msg.sender, amount), "stake transfer failed"); + require( + stakeToken.transfer(msg.sender, amount), + "stake transfer failed" + ); } - // can only register pubkey once, and cannot update function registerPubkey(bytes32 pubkey) external { - require(addressToPubkey[msg.sender] == bytes32(0), "already registered a pubkey"); - require(pubkeyToAddress[pubkey] == address(0), "pubkey already registered by another address"); + require( + addressToPubkey[msg.sender] == bytes32(0), + "already registered a pubkey" + ); + require( + pubkeyToAddress[pubkey] == address(0), + "pubkey already registered by another address" + ); addressToPubkey[msg.sender] = pubkey; pubkeyToAddress[pubkey] = msg.sender; } + + uint256[50] private __gap; } diff --git a/src/interfaces/ICommitteeManagement.sol b/src/interfaces/ICommitteeManagement.sol index 2e5325b..a04b8b1 100644 --- a/src/interfaces/ICommitteeManagement.sol +++ b/src/interfaces/ICommitteeManagement.sol @@ -5,5 +5,17 @@ interface ICommitteeManagement { function isCommitteeMember(address member) external view returns (bool); function committeeSize() external view returns (uint256); function quorumSize() external view returns (uint256); - function verifySignatures(bytes32 msgHash, bytes[] memory signatures) external view returns (bool); + function verifySignatures( + bytes32 msgHash, + bytes[] memory signatures + ) external view returns (bool); + function getNoncedDigest( + bytes32 msgHash, + uint256 nonce + ) external view returns (bytes32); + function executeNoncedSignatures( + bytes32 msgHash, + uint256 nonce, + bytes[] memory signatures + ) external; } diff --git a/src/interfaces/IGateway.sol b/src/interfaces/IGateway.sol new file mode 100644 index 0000000..0da8365 --- /dev/null +++ b/src/interfaces/IGateway.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +/// @notice Interface housing shared Gateway declarations. Keeps the main contract lean by +/// centralizing events, errors, enums, and structs that integrators need to reference. +interface IGateway { + // ===== Errors ===== + error NotCommittee(); + error NotOperator(); + error InstanceUsed(); + error NotPending(); + error WindowExpired(); + error WindowNotExpired(); + error NotEnoughCommittee(); + error InvalidPubkeyLen(); + error InvalidPubkeyParity(); + error InstanceMismatch(); + error PeginAmountMismatch(); + error InvalidHeader(); + error MerkleVerifyFail(); + error InvalidSignatures(); + error FeeTooHigh(); + error OperatorNotRegistered(); + error StakeInsufficient(); + error GraphAlreadyPosted(); + error GraphPeginTxidMismatch(); + error WithdrawStatusInvalid(); + error NotWithdrawable(); + error TimelockNotExpired(); + error KickoffHeightLow(); + error TxidMismatch(); + error AlreadyDisproved(); + error IndexOutOfRange(); + error UnknownDisproveType(); + error DisproveInvalidHeader(); + + // ===== Enums ===== + enum DisproveTxType { + AssertTimeout, + OperatorCommitTimeout, + OperatorNack, + Disprove, + QuickChallenge, + ChallengeIncompeleteKickoff + } + + enum PeginStatus { + None, + Pending, + Withdrawbale, + Processing, + Locked, + Claimed, + Discarded + } + + enum WithdrawStatus { + None, + Processing, + Initialized, + Canceled, + Complete, + Disproved + } + + // ===== Structs ===== + struct Utxo { + bytes32 txid; + uint32 vout; + uint64 amountSats; + } + + struct PeginDataInner { + PeginStatus status; + bytes16 instanceId; + address depositorAddress; + uint64 peginAmountSats; + uint64[3] txnFees; + Utxo[] userInputs; + bytes32 userXonlyPubkey; + string userChangeAddress; + string userRefundAddress; + bytes32 peginTxid; + uint256 createdAt; + address[] committeeAddresses; + mapping(address value => uint256) committeeAddressPositions; + mapping(address => bytes1) committeePubkeyParitys; + mapping(address => bytes32) committeeXonlyPubkeys; + } + + struct PeginData { + PeginStatus status; + bytes16 instanceId; + address depositorAddress; + uint64 peginAmountSats; + uint64[3] txnFees; + Utxo[] userInputs; + bytes32 userXonlyPubkey; + string userChangeAddress; + string userRefundAddress; + bytes32 peginTxid; + uint256 createdAt; + address[] committeeAddresses; + bytes[] committeePubkeys; + } + + struct WithdrawData { + WithdrawStatus status; + bytes32 peginTxid; + address operatorAddress; + bytes16 instanceId; + uint256 lockAmount; + uint256 btcBlockHeightAtWithdraw; + } + + struct GraphData { + bytes1 operatorPubkeyPrefix; + bytes32 operatorPubkey; + bytes32 peginTxid; + bytes32 kickoffTxid; + bytes32 take1Txid; + bytes32 take2Txid; + bytes32 commitTimoutTxid; + bytes32[] assertTimoutTxids; + bytes32[] NackTxids; + } + + // ===== Events ===== + event BridgeInRequest( + bytes16 indexed instanceId, + address indexed depositorAddress, + uint64 peginAmountSats, + uint64[3] txnFees, + Utxo[] userInputs, + bytes32 userXonlyPubkey, + string userChangeAddress, + string userRefundAddress + ); + event CommitteeResponse( + bytes16 indexed instanceId, + address indexed committeeAddress, + bytes committeePubkey + ); + event BridgeIn( + address indexed depositorAddress, + bytes16 indexed instanceId, + uint64 indexed peginAmountSats, + uint64 feeAmountSats + ); + event PostGraphData(bytes16 indexed instanceId, bytes16 indexed graphId); + event InitWithdraw( + bytes16 indexed instanceId, + bytes16 indexed graphId, + address indexed operatorAddress, + uint64 withdrawAmountSats + ); + event CancelWithdraw( + bytes16 indexed instanceId, + bytes16 indexed graphId, + address indexed triggerAddress + ); + event ProceedWithdraw( + bytes16 indexed instanceId, + bytes16 indexed graphId, + bytes32 kickoffTxid + ); + event WithdrawHappyPath( + bytes16 indexed instanceId, + bytes16 indexed graphId, + bytes32 take1Txid, + address indexed operatorAddress, + uint64 rewardAmountSats + ); + event WithdrawUnhappyPath( + bytes16 indexed instanceId, + bytes16 indexed graphId, + bytes32 take2Txid, + address indexed operatorAddress, + uint64 rewardAmountSats + ); + event WithdrawDisproved( + bytes16 indexed instanceId, + bytes16 indexed graphId, + DisproveTxType disproveTxType, + uint256 txnIndex, + bytes32 challengeStartTxid, + bytes32 challengeFinishTxid, + address challengerAddress, + address disproverAddress, + uint256 challengerRewardAmount, + uint256 disproverRewardAmount + ); +} diff --git a/src/libraries/BitvmTxParser.sol b/src/libraries/BitvmTxParser.sol index 50674be..416416a 100644 --- a/src/libraries/BitvmTxParser.sol +++ b/src/libraries/BitvmTxParser.sol @@ -15,50 +15,50 @@ library BitvmTxParser { uint32 constant DISPROVE_CONNECTOR_VOUT = 3; uint32 constant GUARDIAN_CONNECTOR_VOUT = 4; - function parsePegin(BitcoinTx memory bitcoinTx) + function _parsePegin(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 peginTxid, uint64 peginAmountSats, address depositorAddress, bytes16 instanceId) { - peginTxid = computeTxid(bitcoinTx); + peginTxid = _computeTxid(bitcoinTx); bytes memory txouts = bitcoinTx.outputVector; // memory layout of bitcoinTx.outputVector: // | outputVector.length(32-bytes) | outputcount(compact-size).[amount(8-bytes).scriptpubkeysize(compact-size).scriptpubkey(x-bytes); n] // peginAmountSats is the amount of txout[0] - (, uint256 offset) = parseCompactSize(txouts, 32); - uint64 peginAmountSatsRev = uint64(bytes8(memLoad(txouts, offset))); + (, uint256 offset) = _parseCompactSize(txouts, 32); + uint64 peginAmountSatsRev = uint64(bytes8(_memLoad(txouts, offset))); uint256 scriptpubkeysize; - (scriptpubkeysize, offset) = parseCompactSize(txouts, offset + 8); + (scriptpubkeysize, offset) = _parseCompactSize(txouts, offset + 8); uint256 nextTxoutOffset = scriptpubkeysize + offset; // instance-id & depositorAddress is op_return data of txout[1] // Bitvm pegin OP_RETURN script (46-bytes): // OP_RETURN OP_PUSHBYTES44 {magic-bytes(8-bytes)} {instance-id(16-bytes)} {depositorAddress(20-bytes)} - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, nextTxoutOffset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, nextTxoutOffset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); require(opReturnScriptSize == 46 && firstTwoOpcode == 0x6a2c, "invalid pegin OP_RETURN script"); - require(bytes8(memLoad(txouts, opReturnScriptOffset + 2)) == Constants.magic_bytes, "magic_bytes mismatch"); - instanceId = bytes16(memLoad(txouts, opReturnScriptOffset + 10)); - depositorAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 26))); - peginAmountSats = reverseUint64(peginAmountSatsRev); + require(bytes8(_memLoad(txouts, opReturnScriptOffset + 2)) == Constants.magic_bytes, "magic_bytes mismatch"); + instanceId = bytes16(_memLoad(txouts, opReturnScriptOffset + 10)); + depositorAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 26))); + peginAmountSats = _reverseUint64(peginAmountSatsRev); } - function parseChallengeTx(BitcoinTx memory bitcoinTx) + function _parseChallengeTx(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 challengeTxid, bytes32 kickoffTxid, uint32 kickoffVout, address challengerAddress) { - challengeTxid = computeTxid(bitcoinTx); + challengeTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] // memory layout of bitcoinTx.inputVector: // | inputVector.length(32-bytes) | inputcount(compact-size).input_0_txid(32-bytes).input_0_vout(4-bytes little-endian)... bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[1] // if txout[1] is not op_return, return address(0) @@ -67,32 +67,32 @@ library BitvmTxParser { challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; uint256 outputCount; - (outputCount, offset) = parseCompactSize(txouts, 32); + (outputCount, offset) = _parseCompactSize(txouts, 32); if (outputCount >= 2) { uint256 scriptpubkeysize; - (scriptpubkeysize, offset) = parseCompactSize(txouts, offset + 8); + (scriptpubkeysize, offset) = _parseCompactSize(txouts, offset + 8); uint256 nextTxoutOffset = scriptpubkeysize + offset; - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, nextTxoutOffset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, nextTxoutOffset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } } - function parseDisproveTx(BitcoinTx memory bitcoinTx) + function _parseDisproveTx(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 disproveTxid, bytes32 kickoffTxid, uint32 kickoffVout, address challengerAddress) { - disproveTxid = computeTxid(bitcoinTx); + disproveTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[0] // if txout[0] is not op_return, return address(0) @@ -100,27 +100,27 @@ library BitvmTxParser { // OP_RETURN OP_PUSHBYTES20 {challengerAddress(20-bytes)} challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; - (, offset) = parseCompactSize(txouts, 32); - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, offset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (, offset) = _parseCompactSize(txouts, 32); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, offset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } - function parseQuickChallengeTx(BitcoinTx memory bitcoinTx) + function _parseQuickChallengeTx(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 quickChallengeTxid, bytes32 kickoffTxid, uint32 kickoffVout, address challengerAddress) { - quickChallengeTxid = computeTxid(bitcoinTx); + quickChallengeTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[0] // if txout[0] is not op_return, return address(0) @@ -128,15 +128,15 @@ library BitvmTxParser { // OP_RETURN OP_PUSHBYTES20 {challengerAddress(20-bytes)} challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; - (, offset) = parseCompactSize(txouts, 32); - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, offset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (, offset) = _parseCompactSize(txouts, 32); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, offset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } - function parseChallengeIncompleteKickoffTx(BitcoinTx memory bitcoinTx) + function _parseChallengeIncompleteKickoffTx(BitcoinTx memory bitcoinTx) internal pure returns ( @@ -146,14 +146,14 @@ library BitvmTxParser { address challengerAddress ) { - challengeIncompleteKickoffTxid = computeTxid(bitcoinTx); + challengeIncompleteKickoffTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[0] // if txout[0] is not op_return, return address(0) @@ -161,31 +161,31 @@ library BitvmTxParser { // OP_RETURN OP_PUSHBYTES20 {challengerAddress(20-bytes)} challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; - (, offset) = parseCompactSize(txouts, 32); - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, offset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (, offset) = _parseCompactSize(txouts, 32); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, offset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } - function computeTxid(BitcoinTx memory bitcoinTx) internal pure returns (bytes32) { + function _computeTxid(BitcoinTx memory bitcoinTx) internal pure returns (bytes32) { bytes memory rawTx = abi.encodePacked(bitcoinTx.version, bitcoinTx.inputVector, bitcoinTx.outputVector, bitcoinTx.locktime); - return hash256(rawTx); + return _hash256(rawTx); } - function hash256(bytes memory raw) internal pure returns (bytes32) { + function _hash256(bytes memory raw) internal pure returns (bytes32) { return sha256(abi.encodePacked(sha256(raw))); } - function memLoad(bytes memory data, uint256 offset) internal pure returns (bytes32 res) { + function _memLoad(bytes memory data, uint256 offset) internal pure returns (bytes32 res) { assembly { res := mload(add(data, offset)) } } - function reverseUint64(uint64 _b) internal pure returns (uint64 v) { + function _reverseUint64(uint64 _b) internal pure returns (uint64 v) { v = _b; // swap bytes v = ((v >> 8) & 0x00FF00FF00FF00FF) | ((v & 0x00FF00FF00FF00FF) << 8); @@ -195,7 +195,7 @@ library BitvmTxParser { v = (v >> 32) | (v << 32); } - function reverseUint32(uint32 _b) internal pure returns (uint32 v) { + function _reverseUint32(uint32 _b) internal pure returns (uint32 v) { v = _b; // swap bytes @@ -204,11 +204,11 @@ library BitvmTxParser { v = (v >> 16) | (v << 16); } - function reverseUint16(uint16 _b) internal pure returns (uint16 v) { + function _reverseUint16(uint16 _b) internal pure returns (uint16 v) { v = (_b << 8) | (_b >> 8); } - function parseCompactSize(bytes memory data, uint256 offset) + function _parseCompactSize(bytes memory data, uint256 offset) internal pure returns (uint256 size, uint256 nextOffset) @@ -221,7 +221,7 @@ library BitvmTxParser { assembly { sizeRev := mload(sub(add(data, offset), 23)) // -23 = 1 + 8 - 32 } - size = reverseUint64(sizeRev); + size = _reverseUint64(sizeRev); } if (uint8(data[offset - 32]) == 0xfe) { nextOffset = offset + 5; // one-byte flag, 4 bytes data @@ -229,7 +229,7 @@ library BitvmTxParser { assembly { sizeRev := mload(sub(add(data, offset), 27)) // -27 = 1 + 4 - 32 } - size = reverseUint32(sizeRev); + size = _reverseUint32(sizeRev); } if (uint8(data[offset - 32]) == 0xfd) { nextOffset = offset + 3; // one-byte flag, 2 bytes data @@ -237,7 +237,7 @@ library BitvmTxParser { assembly { sizeRev := mload(sub(add(data, offset), 29)) // -29 = 1 + 2 - 32 } - size = reverseUint16(sizeRev); + size = _reverseUint16(sizeRev); } nextOffset = offset + 1; // one-byte flag, 0 bytes data size = uint8(data[offset - 32]); diff --git a/src/libraries/Converter.sol b/src/libraries/Converter.sol index 4f67b16..c7e74a9 100644 --- a/src/libraries/Converter.sol +++ b/src/libraries/Converter.sol @@ -6,7 +6,7 @@ import {Constants} from "../Constants.sol"; library Converter { uint8 constant BtcDecimals = 8; - function amountFromSats(uint64 amountSats) internal pure returns (uint256) { + function _amountFromSats(uint64 amountSats) internal pure returns (uint256) { uint8 TokenDecimals = Constants.TokenDecimals; if (TokenDecimals >= BtcDecimals) { return uint256(amountSats * uint64(10 ** (TokenDecimals - BtcDecimals))); @@ -15,7 +15,7 @@ library Converter { } } - function amountToSats(uint256 amount) internal pure returns (uint64) { + function _amountToSats(uint256 amount) internal pure returns (uint64) { uint8 TokenDecimals = Constants.TokenDecimals; if (TokenDecimals >= BtcDecimals) { return uint64(amount / uint256(10 ** (TokenDecimals - BtcDecimals))); diff --git a/src/libraries/MerkleProof.sol b/src/libraries/MerkleProof.sol index 4367ee1..d3b668f 100644 --- a/src/libraries/MerkleProof.sol +++ b/src/libraries/MerkleProof.sol @@ -16,8 +16,8 @@ library MerkleProof { pure returns (bytes32 blockHash, bytes32 merkleRoot) { - blockHash = BitvmTxParser.hash256(rawHeader); - merkleRoot = BitvmTxParser.memLoad(rawHeader, 0x44); + blockHash = BitvmTxParser._hash256(rawHeader); + merkleRoot = BitvmTxParser._memLoad(rawHeader, 0x44); } function verifyMerkleProof(bytes32 root, bytes32[] memory proof, bytes32 leaf, uint256 index) diff --git a/test/MultiSigVerifier.t.sol b/test/MultiSigVerifier.t.sol index 63c74b1..2b3aae6 100644 --- a/test/MultiSigVerifier.t.sol +++ b/test/MultiSigVerifier.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "forge-std/Test.sol"; -import "../src/MultiSigVerifier.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Test} from "forge-std/Test.sol"; +import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; contract MultiSigVerifierTest is Test { using ECDSA for bytes32; @@ -32,7 +34,8 @@ contract MultiSigVerifierTest is Test { message = keccak256("hello world").toEthSignedMessageHash(); // Require at least 2 signatures - verifier = new MultiSigVerifier(owners, 2); + verifier = new MultiSigVerifier(); + verifier.initialize(owners, 2); } function testVerifyWithEnoughSignatures() public view { @@ -74,12 +77,15 @@ contract MultiSigVerifierTest is Test { assertFalse(ok, "Non-owner signature should not be valid"); } - function _signUpdate(uint256 privKey, address[] memory newOwners, uint256 newRequired, uint256 nonce) - internal - pure - returns (bytes memory sig) - { - bytes32 digest = keccak256(abi.encodePacked(nonce, newOwners, newRequired)); + function _signUpdate( + uint256 privKey, + address[] memory newOwners, + uint256 newRequired, + uint256 nonce + ) internal pure returns (bytes memory sig) { + bytes32 digest = keccak256( + abi.encodePacked(nonce, newOwners, newRequired) + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); sig = abi.encodePacked(r, s, v); diff --git a/test/SequencerSetPublisher.t.sol b/test/SequencerSetPublisher.t.sol index f46eeae..78a9183 100644 --- a/test/SequencerSetPublisher.t.sol +++ b/test/SequencerSetPublisher.t.sol @@ -2,13 +2,17 @@ pragma solidity ^0.8.28; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import "forge-std/Test.sol"; -import "../src/SequencerSetPublisher.sol"; -import "../src/interfaces/ISequencerSetPublisher.sol"; -import "../src/MultiSigVerifier.sol"; -import "../src/interfaces/ISequencerSetPublisher.sol"; -import "forge-std/console.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Test} from "forge-std/Test.sol"; +import {StdStorage, stdStorage} from "forge-std/StdStorage.sol"; +import {SequencerSetPublisher} from "../src/SequencerSetPublisher.sol"; +import { + ISequencerSetPublisher +} from "../src/interfaces/ISequencerSetPublisher.sol"; +import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; +import {console} from "forge-std/console.sol"; contract SequencerSetPublisherTest is Test { using ECDSA for bytes32; @@ -23,13 +27,25 @@ contract SequencerSetPublisherTest is Test { uint256[] batch1; uint256[] batch2; - function _get_pubkey_from_prvkey(uint256 number) internal pure returns (bytes[] memory) { + function _get_pubkey_from_prvkey( + uint256 number + ) internal pure returns (bytes[] memory) { bytes[5] memory newPublisherPubkeysConstant; - newPublisherPubkeysConstant[0] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; - newPublisherPubkeysConstant[1] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; - newPublisherPubkeysConstant[2] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; - newPublisherPubkeysConstant[3] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; - newPublisherPubkeysConstant[4] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; + newPublisherPubkeysConstant[ + 0 + ] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; + newPublisherPubkeysConstant[ + 1 + ] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; + newPublisherPubkeysConstant[ + 2 + ] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; + newPublisherPubkeysConstant[ + 3 + ] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; + newPublisherPubkeysConstant[ + 4 + ] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; require(number <= newPublisherPubkeysConstant.length); bytes[] memory newPublisherPubkeys = new bytes[](number); @@ -66,8 +82,14 @@ contract SequencerSetPublisherTest is Test { initPublishers[2] = vm.addr(batch[2]); sspublisher = new SequencerSetPublisher(); + MultiSigVerifier verifier = new MultiSigVerifier(); - sspublisher.initialize(owner, initPublishers, _get_pubkey_from_prvkey(initPublishers.length)); + sspublisher.initialize( + owner, + address(verifier), + initPublishers, + _get_pubkey_from_prvkey(initPublishers.length) + ); } function testInitialize() public view { @@ -95,19 +117,31 @@ contract SequencerSetPublisherTest is Test { uint256 nonce = sspublisher.multiSigVerifier().nonce(); uint256 newRequired = (newPublishers.length * 2 + 2) / 3; - bytes32 digest = keccak256(abi.encodePacked(nonce, newPublishers, newRequired)); + bytes32 digest = keccak256( + abi.encodePacked(nonce, newPublishers, newRequired) + ); - bytes[] memory newPublisherPubkeys = _get_pubkey_from_prvkey(newPublishers.length); + bytes[] memory newPublisherPubkeys = _get_pubkey_from_prvkey( + newPublishers.length + ); uint256 oldRequired = (oldPublishers.length * 2 + 2) / 3; bytes[] memory sigs = new bytes[](oldRequired); for (uint256 j = 0; j < oldRequired; j++) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(oldPublisherKeys[j], digest); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + oldPublisherKeys[j], + digest + ); sigs[j] = abi.encodePacked(r, s, v); } console.log("height: ", height); vm.startPrank(oldPublishers[1]); - sspublisher.updatePublisherSet(newPublishers, newPublisherPubkeys, sigs, height); + sspublisher.updatePublisherSet( + newPublishers, + newPublisherPubkeys, + sigs, + height + ); vm.stopPrank(); MultiSigVerifier verifier = sspublisher.multiSigVerifier(); @@ -117,26 +151,68 @@ contract SequencerSetPublisherTest is Test { function testUpdatePublisherSet() public { // genesis sequencer set commit, publisher is not changed - run_sequencer_update_test(batch, batch, 10, keccak256("commit1"), keccak256("set1"), keccak256("set2")); + run_sequencer_update_test( + batch, + batch, + 10, + keccak256("commit1"), + keccak256("set1"), + keccak256("set2") + ); // publisher commit, sequencer set is not changed - run_sequencer_update_test(batch, batch1, 11, keccak256("commit2"), keccak256("set2"), keccak256("set2")); + run_sequencer_update_test( + batch, + batch1, + 11, + keccak256("commit2"), + keccak256("set2"), + keccak256("set2") + ); // apply publisher update assert(sspublisher.latestConfirmedHeight() == 0); run_publisher_update_test(batch, batch1, 11); assert(sspublisher.latestConfirmedHeight() == 11); // sequencer set commit, publisher is not changed - run_sequencer_update_test(batch1, batch1, 12, keccak256("commit3"), keccak256("set2"), keccak256("set22")); - run_sequencer_update_test(batch1, batch1, 13, keccak256("commit3"), keccak256("set22"), keccak256("set3")); + run_sequencer_update_test( + batch1, + batch1, + 12, + keccak256("commit3"), + keccak256("set2"), + keccak256("set22") + ); + run_sequencer_update_test( + batch1, + batch1, + 13, + keccak256("commit3"), + keccak256("set22"), + keccak256("set3") + ); // publisher commit, sequencer set is not changed - run_sequencer_update_test(batch1, batch2, 17, keccak256("commit4"), keccak256("set3"), keccak256("set3")); + run_sequencer_update_test( + batch1, + batch2, + 17, + keccak256("commit4"), + keccak256("set3"), + keccak256("set3") + ); // apply publisher update assert(sspublisher.latestConfirmedHeight() == 11); run_publisher_update_test(batch1, batch2, 17); assert(sspublisher.latestConfirmedHeight() == 17); // sequencer set commit, publisher is not changed - run_sequencer_update_test(batch2, batch2, 20, keccak256("commit5"), keccak256("set3"), keccak256("set4")); + run_sequencer_update_test( + batch2, + batch2, + 20, + keccak256("commit5"), + keccak256("set3"), + keccak256("set4") + ); } function run_sequencer_update_test( @@ -151,23 +227,29 @@ contract SequencerSetPublisherTest is Test { for (uint256 i = 0; i < publisherKeys.length; i++) { publishers[i] = vm.addr(publisherKeys[i]); } - address[] memory nextPublishers = new address[](nextPublisherKeys.length); + address[] memory nextPublishers = new address[]( + nextPublisherKeys.length + ); for (uint256 i = 0; i < nextPublisherKeys.length; i++) { nextPublishers[i] = vm.addr(nextPublisherKeys[i]); } - ISequencerSetPublisher.SequencerSet memory ss = ISequencerSetPublisher.SequencerSet({ - sequencerSetHash: sequencerSetHash, - publishersHash: keccak256(abi.encodePacked(publishers)), - nextPublishersHash: keccak256(abi.encodePacked(nextPublishers)), - p2wshSigHash: p2wshSigHash.toEthSignedMessageHash(), - goatBlockNumber: height - }); + ISequencerSetPublisher.SequencerSet memory ss = ISequencerSetPublisher + .SequencerSet({ + sequencerSetHash: sequencerSetHash, + publishersHash: keccak256(abi.encodePacked(publishers)), + nextPublishersHash: keccak256(abi.encodePacked(nextPublishers)), + p2wshSigHash: p2wshSigHash.toEthSignedMessageHash(), + goatBlockNumber: height + }); uint256 oldRequired = (publishers.length * 2 + 2) / 3; for (uint256 i = 0; i < oldRequired; i++) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(publisherKeys[i], ss.p2wshSigHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + publisherKeys[i], + ss.p2wshSigHash + ); bytes memory sig = abi.encodePacked(r, s, v); vm.startPrank(publishers[i]); sspublisher.updateSequencerSet(ss, sig); @@ -177,9 +259,37 @@ contract SequencerSetPublisherTest is Test { function testUpdateSequencerSet() public { // New publishers - run_sequencer_update_test(batch, batch, 10, keccak256("commit1"), keccak256("set1"), keccak256("set2")); - run_sequencer_update_test(batch, batch, 11, keccak256("commit2"), keccak256("set2"), keccak256("set3")); - run_sequencer_update_test(batch, batch, 12, keccak256("commit3"), keccak256("set3"), keccak256("set4")); - run_sequencer_update_test(batch, batch, 13, keccak256("commit4"), keccak256("set4"), keccak256("set5")); + run_sequencer_update_test( + batch, + batch, + 10, + keccak256("commit1"), + keccak256("set1"), + keccak256("set2") + ); + run_sequencer_update_test( + batch, + batch, + 11, + keccak256("commit2"), + keccak256("set2"), + keccak256("set3") + ); + run_sequencer_update_test( + batch, + batch, + 12, + keccak256("commit3"), + keccak256("set3"), + keccak256("set4") + ); + run_sequencer_update_test( + batch, + batch, + 13, + keccak256("commit4"), + keccak256("set4"), + keccak256("set5") + ); } }